Reseller program
Reseller API
Manage your clients' domains and DNS records from your own tools: a client portal, a deploy script or your agency's back office.
Overview
- Base URL:
https://www.bluewhaledns.com/api/v1/reseller. HTTPS only. Usewww:https://bluewhaledns.com/api/…also works, but the site itself lives onwww. - REST: resources are nouns in the path, and the HTTP method says what happens to them.
- Every request and response body is JSON (
Content-Type: application/json), errors, deletes and unknown paths included. - The API works while your reseller plan is active. Keys are made in the reseller dashboard under API.
- Changes to
/api/v1only add fields and endpoints: nothing your code reads is renamed or removed.
Quick start
- Create an API key in the reseller dashboard. It's shown once: keep it on your server.
- Send it as the bearer on every call.
# Your domains
curl https://www.bluewhaledns.com/api/v1/reseller/domains \
-H "Authorization: Bearer bwd_live_6f1c…_9a2b…"
# Add a record
curl -X POST https://www.bluewhaledns.com/api/v1/reseller/domains/example.com/dns-records \
-H "Authorization: Bearer bwd_live_6f1c…_9a2b…" \
-H "Content-Type: application/json" \
-d '{"type":"A","name":"www","value":"203.0.113.10","ttl":3600}'Authentication
Every call sends Authorization: Bearer <credential>. The credential is your API key (bwd_live_…) or, if you'd rather not send the key on every call, a short-lived access token.
/auth/tokenHTTP/1.1 200 OK
{
"token_type": "Bearer",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"expires_in": 3600,
"expires_at": "2026-10-19T16:04:11.000Z"
}- An expired or altered token answers 401
invalid_token: request a new one. - Both are checked against your key and plan on every call: revoking the key, or a plan that lapses, stops them at once (401
key_revoked, 403reseller_inactive). - Test keys (
bwd_test_…) can read, but every change answers 403test_mode_unavailablefor now. - Never put an API key or token in a web page or app your clients can see. Call the API from your server.
Clients and ownership
The API acts for your reseller account only. You buy domains and services for your clients on your account, then put each one on a client in the dashboard (Domains, Services, or the client's page).
- Every domain call checks that the domain in the path is an active registration on your account.
{domain}can be the domain name (example.com) or its id fromGET /domains. - Add
?client_id=<id>to any domain or record call to limit it to one client's domains. A portal you build for a client can pass that client's id on every call, so it can't reach anyone else's domains. - A domain that isn't yours, isn't active, or isn't that client's answers 404
domain_not_found. The API never says whether someone else holds it.
Rate limits
| Plan | Requests a minute |
|---|---|
| Starter | 60 |
| Grow | 180 |
| Pro | 600 |
- One budget per account for the whole API (
/api/v1/reseller/…and/api/v1/domains/…together), counted per minute. - Every answer carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(Unix time, in seconds, when the minute starts again). - Past the limit, calls answer 429
rate_limitedwithRetry-After(seconds). The token endpoint has its own limit of 30 requests a minute from one address. - When our domain Provider is limiting us or down, calls that need it answer 429
upstream_rate_limitedor 503upstream_unavailable, also withRetry-After.
Endpoints
/auth/token/domains/domains/check?domains=…/domains/register/domains/transfer/domains/{domain_name_or_id}/domains/{domain_name_or_id}/renew/domains/{domain_name_or_id}/nameservers/domains/{domain_name_or_id}/nameservers/domains/{domain_name_or_id}/dns-records/domains/{domain_name_or_id}/dns-records/domains/{domain_name_or_id}/dns-records/rrset/domains/{domain_name_or_id}/dns-records/{record_id}/domains/{domain_name_or_id}/dns-records/{record_id}/domains/{domain_name_or_id}/dns-records/{record_id}/domains/{domain_name_or_id}/dns-records/{record_id}/orders/{order_id}/pricing?tlds=…Paths are relative to https://www.bluewhaledns.com/api/v1/reseller. The earlier https://www.bluewhaledns.com/api/v1/domains, /domains/check, /domains/register and /domains/transfer stay available with the same answers.
Domains
The domain object
| Field | Type | |
|---|---|---|
id | string | null | The domain's id; null while it's pending. |
name | string | The domain name, lower case. |
status | string | active, or pending while its registration or transfer is being set up. |
expires_at | string | null | When the registration ends (ISO 8601). |
auto_renew | boolean | |
client_id | string | null | The client it's on, if any. |
order_id | string | Pending domains only: the order to follow with GET /orders/{order_id}. |
/domains?page=1&per_page=25&client_id=…client_id is optional.{
"page": 1,
"per_page": 25,
"total": 3,
"domains": [
{ "id": null, "name": "new-client.com", "status": "pending", "expires_at": null,
"auto_renew": true, "client_id": null, "order_id": "5b1e…" },
{ "id": "d6a1…", "name": "client-a.com", "status": "active",
"expires_at": "2027-03-02T00:00:00Z", "auto_renew": true, "client_id": "c41f…" },
{ "id": "8b02…", "name": "client-b.net", "status": "active",
"expires_at": "2026-12-18T00:00:00Z", "auto_renew": true, "client_id": null }
]
}/domains/{domain_name_or_id}{ "domain": { … } }. A pending domain is found by its name.The earlier GET /api/v1/domains lists every registration on the account, newest first, with the same object plus domain, years and created_at.
Orders
Orders are priced at your plan, charged at once to the card saved under API in the reseller dashboard, and set up in the background: they answer 202 with an order_id to follow.
/domains/check?domains=acme.com,acme.io{ "results": [
{ "domain": "acme.com", "available": true, "price_cents": 1499, "currency": "USD" },
{ "domain": "acme.io", "available": false, "price_cents": null, "currency": "USD" }
] }/domains/register{ "domain": "acme.com", "years": 1, "nameservers": ["ns1.example.net", "ns2.example.net"] }. nameservers is optional (2 to 8); without it the domain gets our DNS, which you manage with the DNS record endpoints.HTTP/1.1 202 Accepted
{
"order_id": "5b1e…", "job_id": "9c07…", "domain": "acme.com", "years": 1,
"price_cents": 1499, "currency": "USD", "status": "queued",
"registration": { "id": null, "name": "acme.com", "status": "pending", "expires_at": null,
"auto_renew": true, "client_id": null, "order_id": "5b1e…" }
}/domains/transfer{ "domain": "acme.com", "auth_code": "…", "years": 1 }. The domain keeps its current nameservers, so its DNS keeps working while it moves (usually up to 5 days)./domains/{domain}/renew{ "years": 1 }. Adds years to an active domain, at most up to 10 years from today. The answer also has renews_to (the new expiry date). A domain bought in the store, with a plan in the dashboard, renews through its plan instead (409 renews_with_billing)./orders/{order_id}{
"id": "5b1e…", "kind": "register", "domain": "acme.com", "years": 1,
"status": "completed", "price_cents": 1499, "currency": "USD",
"created_at": "2026-10-20T09:00:00Z", "completed_at": "2026-10-20T09:41:12Z"
}kind:register,transferorrenew.status:queued,processing,completedorfailed(then witherror:codeandmessage). A transfer staysprocessinguntil the registry completes it.warningsappears when something needs you, e.g.nameservers_not_set: the registry didn't take the nameservers sent with the registration.
/pricing?tlds=com,net,co.uk{ "prices": [
{ "tld": "com", "register_cents": 1499, "renew_cents": 1699, "transfer_cents": 1499, "currency": "USD" },
{ "tld": "co.uk", "register_cents": 899, "renew_cents": 899, "transfer_cents": null, "currency": "USD" }
] }Prices are for a standard name; a premium or sale-priced name differs, and /domains/check gives its exact price. null means that operation isn't available for the extension.
Retrying safely: Idempotency-Key
Register, transfer and renew charge your card, so a retry after a timeout must not order twice. Send an Idempotency-Key header (1 to 255 characters, e.g. your own order id or a UUID) and send the same key, with the same body, on every retry of that order.
curl -X POST https://www.bluewhaledns.com/api/v1/reseller/domains/register \
-H "Authorization: Bearer bwd_live_6f1c…_9a2b…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-81723" \
-d '{"domain":"acme.com","years":1}'- The same key and body within 24 hours answers with the first answer (same status and body, plus
Idempotent-Replayed: true). Your card is charged once. - Answers from before the charge (a validation error, the domain taken, the Provider busy) don't use the key up: fix the problem and retry with the same key. Once the card was charged or declined, the answer is final for that key; after a declined card, use a new key.
- The same key with a different body, or on another endpoint or domain, answers 422
idempotency_key_reused. - A retry while the first request is still running answers 409
request_in_progress: wait a few seconds and retry. - Without a key, a second order for a domain whose first order is still being set up answers 409
order_pendingwithdetails.order_id.
Nameservers
/domains/{domain}/nameservers{ "domain": { "id", "name", "client_id" }, "nameservers": ["…", "…"] }/domains/{domain}/nameservers{ "nameservers": ["ns1.example.net", "ns2.example.net"] }: 2 to 8 host names. The answer has the new list and changed (false when they were already set).- With other nameservers, the domain's records are managed there: the DNS record endpoints then answer 409
dns_not_managed. - Nameservers inside the domain itself (e.g.
ns1.acme.comforacme.com) need glue records: 400nameservers_need_glue, contact support. - To move a domain to BlueWhale DNS, use its Nameservers tab in the dashboard, which copies the records into a zone first (400
bluewhale_nameservershere).
DNS records
The record object
| Field | Type | |
|---|---|---|
id | string | The record's id (see Record ids). |
type | string | A, AAAA, CNAME, MX, TXT, SRV, NS, CAA, ALIAS |
name | string | Relative to the domain; @ is the domain itself. |
fqdn | string | The full name, for reading only. |
value | string | Depends on the type (see Validation rules). |
ttl | integer | Seconds, as the zone really serves it. |
priority | integer | null | MX and SRV only. |
weight | integer | null | SRV only. |
port | integer | null | SRV only. |
read_only | boolean | True for records the API can't change, such as the domain's own NS records. |
managed_by | string | null | alias for the A/AAAA records an ALIAS record keeps up to date (they're read-only); otherwise null. |
List records
/domains/{domain}/dns-records?type=MX&name=@type and name narrow the list.{
"domain": { "id": "d6a1…", "name": "client-a.com", "client_id": "c41f…" },
"dns": {
"host": "bluewhale_dns",
"nameservers": ["ns1.bluewhaledns.net", "ns2.bluewhaledns.net", "ns3.bluewhaledns.net", "ns4.bluewhaledns.net"],
"record_types": ["A", "AAAA", "CNAME", "MX", "TXT", "SRV", "ALIAS"],
"ttl": { "fixed": 3600 }
},
"records": [
{ "id": "rec_3f9a0c1e2b7d4a5f6e8c9b0a", "type": "A", "name": "@", "fqdn": "client-a.com",
"value": "203.0.113.10", "ttl": 3600, "priority": null, "weight": null, "port": null,
"read_only": false, "managed_by": null },
{ "id": "rec_9c2e4b6a8d0f1e3c5a7b9d1f", "type": "MX", "name": "@", "fqdn": "client-a.com",
"value": "mx1.example-mail.net", "ttl": 3600, "priority": 10, "weight": null, "port": null,
"read_only": false, "managed_by": null }
],
"total": 2
}Get one record
/domains/{domain}/dns-records/{record_id}{ "domain", "dns", "record" }Create a record
/domains/{domain}/dns-recordsLocation header. warnings says when the zone serves a different TTL from the one you sent.POST /domains/client-a.com/dns-records
{ "type": "MX", "name": "@", "value": "mx2.example-mail.net", "priority": 20, "ttl": 300 }
HTTP/1.1 201 Created
Location: /api/v1/reseller/domains/d6a1…/dns-records/rec_1d7f…
{
"domain": { … },
"dns": { … },
"record": { "id": "rec_1d7f…", "type": "MX", "name": "@", "fqdn": "client-a.com",
"value": "mx2.example-mail.net", "ttl": 3600, "priority": 20, "weight": null, "port": null, "read_only": false },
"warnings": [ { "code": "ttl_adjusted", "message": "BlueWhale DNS serves every record with a 3600-second TTL." } ]
}Update a record
/domains/{domain}/dns-records/{record_id}/domains/{domain}/dns-records/{record_id}PATCH /domains/client-a.com/dns-records/rec_9c2e4b6a8d0f1e3c5a7b9d1f
{ "priority": 5 }
HTTP/1.1 200 OK
{ "domain": { … }, "dns": { … }, "record": { "id": "rec_77b0…", "type": "MX", "priority": 5, … }, "warnings": [] }The answer carries the record's new id.
Delete a record
/domains/{domain}/dns-records/{record_id}HTTP/1.1 200 OK
{ "deleted": true, "id": "rec_77b0…", "domain": { "id": "d6a1…", "name": "client-a.com", "client_id": "c41f…" } }Record sets
Say what a name's records of one type should be, and we make the zone match in one change: records already right are left alone, the rest are added or removed. Sending the same set again changes nothing, so it's safe to call on every deploy instead of listing, comparing, deleting and adding records one at a time.
/domains/{domain}/dns-records/rrset{ "name", "type", "values": [...], "ttl"?, "priority"? }| Field | Rule |
|---|---|
name | As for a record (@ for the domain itself). |
type | One of A, AAAA, CNAME, MX, TXT, SRV, NS, CAA, ALIAS. |
values | Required: the record values, each checked as a record's value is; at most 50. Duplicates are dropped. [] deletes every record of that name and type. CNAME and ALIAS take one value. |
ttl | Optional, as for a record; the same for every value. |
priority | MX and SRV. A value may carry its own instead: "10 mx1.example.net" for MX, "10 5 5060 sip.example.net" (priority, weight, port, target) for SRV. |
PUT /domains/client-a.com/dns-records/rrset
{ "name": "@", "type": "MX", "values": ["10 mx1.example-mail.net", "20 mx2.example-mail.net"], "ttl": 3600 }
HTTP/1.1 200 OK
{
"domain": { … },
"dns": { … },
"name": "@",
"type": "MX",
"records": [
{ "id": "rec_9c2e…", "type": "MX", "name": "@", "value": "mx1.example-mail.net", "priority": 10, … },
{ "id": "rec_51ad…", "type": "MX", "name": "@", "value": "mx2.example-mail.net", "priority": 20, … }
],
"changed": { "added": 1, "removed": 0, "updated": 0, "kept": 1 },
"warnings": []
}recordsis the set after the change.changedcounts recordsadded,removed,updated(only their TTL changed) andkeptas they were.- The same conflicts as single records apply (409
cname_conflict,alias_conflict). Records in the set the API can't change answer 409record_read_only. - With
"type": "ALIAS"it sets or deletes the domain's ALIAS record, replacing any A and AAAA records on the domain itself (see ALIAS records).
ALIAS records
A CNAME can't be on the domain itself, so pointing client-a.com (not just www) at your app usually means copying your app's IP addresses into A records, which go stale when they change. An ALIAS record does it for you: give it a host name and we keep the domain's A and AAAA records set to that host's addresses.
POST /domains/client-a.com/dns-records
{ "type": "ALIAS", "name": "@", "value": "saas-target.example.net" }
# or, replacing any A/AAAA records the domain has:
PUT /domains/client-a.com/dns-records/rrset
{ "name": "@", "type": "ALIAS", "values": ["saas-target.example.net"] }- Only on
@, with a fully qualified host name as the value (not the domain itself).ttldefaults to 900. - We look up the host's IPv4 and IPv6 addresses and write them as the domain's A and AAAA records in the same change. A host with no addresses answers 422 (
alias_target_unresolvable). - Every 10 minutes we look the host up again and change only the records that differ. If the host can't be looked up, the records are kept as they are.
- Lists show one
ALIASrecord, and the A/AAAA records it keeps with"read_only": trueand"managed_by": "alias". Change or delete the ALIAS, not those records. - While a domain has an ALIAS it can't have other A/AAAA records on
@(409alias_conflict); a plain create is refused when it already has some (use the record set, which replaces them). Deleting the ALIAS removes its A/AAAA records. - Check for
ALIASindns.record_typesbefore relying on it.
How soon a change of address is seen
dns.host | Longest wait after your host's addresses change |
|---|---|
bluewhale_dns | 10 minutes, plus up to 3600 seconds (1 hour) for resolvers' caches. |
provider_dns | 10 minutes, plus the ALIAS's TTL (at least 900 seconds) for resolvers' caches. |
When you move your app to new addresses, keep the old ones answering for that long.
Validation rules
A record that breaks a rule answers 422 validation_failed, with every problem listed in error.details.errors (each has field, code and message). Unknown fields are refused too, so a typo like prio doesn't pass silently.
| Field | Rule |
|---|---|
type | Required. One of A, AAAA, CNAME, MX, TXT, SRV, NS, CAA, ALIAS (case doesn't matter). |
name | Required. A subdomain such as www, shop.eu, _dmarc or *.dev, or @ for the domain itself. Letters, digits, dashes and underscores; each part at most 63 characters; * only first. A full name is shortened for you: www.example.com (or www.example.com.) becomes www, and example.com becomes @. |
value | Required. Checked against the type (below). |
ttl | Optional, default 3600 (900 for ALIAS). A whole number of seconds from 60 to 86400. |
priority | Required for MX and SRV: a whole number from 0 to 65535. Must be null or left out for every other type. |
weight, port | SRV only, each a whole number from 0 to 65535. Leave them out for other types. |
| Type | Value |
|---|---|
| A | An IPv4 address, e.g. 203.0.113.10. |
| AAAA | An IPv6 address, e.g. 2001:db8::10 (stored in its standard short form). |
| CNAME | A fully qualified host name, e.g. client-a.netlify.app. Not on @, and not on a name that has other records. |
| MX | The mail server's host name, with a priority. |
| TXT | Text up to 2048 characters, printable ASCII. Surrounding quotes are optional; long values are split for you. |
| SRV | The target host name, with priority, weight and port ("5 5060 sip.example.net" as the value also works). The name starts with _service._protocol, e.g. _sip._tcp. |
Host names (CNAME, MX, NS and SRV targets) must be fully qualified: at least two labels ending in a real extension. A final dot is removed. An exact copy of an existing record answers 409 duplicate_record; a CNAME sharing a name with other records answers 409 cname_conflict.
Where records are served
The domain's nameservers decide where its records live, and so what they can hold. dns.host in every answer says which.
dns.host | Nameservers | Types | TTL |
|---|---|---|---|
bluewhale_dns | ns1–ns4.bluewhaledns.net | A, AAAA, CNAME, MX, TXT, SRV, ALIAS | Always 3600 seconds; any TTL you send is accepted and noted in warnings. |
provider_dns | The registrar's own | A, AAAA, CNAME, MX, TXT, SRV, NS, CAA, ALIAS | One of 900, 3600, 10800, 21600, 43200, 86400; others are raised to the next one up, and noted in warnings. |
A type the zone doesn't take answers 422 with unsupported_record_type. A domain on other nameservers (Cloudflare, another host…) answers 409 dns_not_managed: its records are managed there. Both zones take ALIAS: it's written as A and AAAA records.
Record ids
A record's id comes from what it holds (type, name, value, priority, weight and port), so it stays the same however often you read the zone, and changes when the record changes. A TTL-only change keeps it. After an update use the id in the answer; an old id answers 404 record_not_found. If in doubt, list the zone again.
Webhooks
Instead of polling orders, let us tell you. Add up to 5 HTTPS endpoints in the reseller dashboard under API → Webhooks, for every event or the ones you pick. Each endpoint gets a signing secret (whsec_…), shown once when you add it.
| Event | When |
|---|---|
domain.registered | A registration finished: the domain is yours. |
domain.registration_failed | A registration failed for good (support refunds or finishes it). |
domain.transfer_completed | A transfer in finished at the registry. |
domain.transfer_failed | A transfer in was refused or failed at the registry. |
domain.renewed | A domain was renewed. |
domain.expiring | A domain expires in 30 days, and again at 7 days. |
order.failed | Any register, transfer or renew order failed for good. |
Each event is a POST with a JSON body. order.failed comes with domain.registration_failed or domain.transfer_failed where one applies, and on its own for a renewal. The dashboard's "Send test event" sends webhook.test.
POST https://app.example.com/webhooks/bluewhale
content-type: application/json
webhook-id: msg_2c1f9e0b8a7d4c3e9f1a6b5d4c3e2f1a
webhook-timestamp: 1792512000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"type": "domain.registered",
"timestamp": "2026-10-20T14:00:00.000Z",
"data": {
"domain": { "name": "client-a.com", "client_id": "c41f…", "expires_at": "2027-10-20T14:00:00.000Z" },
"order_id": "7b0e…",
"years": 1
}
}| Event | data |
|---|---|
domain.registered | { domain: { name, client_id, expires_at }, order_id, years } |
domain.renewed | { domain: { name, client_id, expires_at }, order_id, years } |
domain.transfer_completed | { domain: { name, client_id, expires_at }, order_id } |
domain.registration_failed, domain.transfer_failed | { domain: { name, client_id }, order_id, error: { code, message } } |
domain.expiring | { domain: { name, client_id, expires_at, auto_renew }, days_left, threshold: 30 | 7 } |
order.failed | { order: { id, kind: "register" | "transfer" | "renew", domain, status: "failed", error } } |
Checking the signature
Events are signed with the Standard Webhooks scheme, so their libraries (e.g. standardwebhooks on npm) verify them with your secret. By hand: HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<raw body>, keyed with the base64-decoded part of the secret after whsec_, base64-encoded, must equal one of the space-separated v1,… signatures. Refuse timestamps more than 5 minutes from now.
import crypto from "node:crypto";
function verify(secret, headers, rawBody) {
const id = headers["webhook-id"], ts = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");
return headers["webhook-signature"].split(" ").some((s) => {
const sig = s.slice(s.indexOf(",") + 1);
return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
});
}Delivery and retries
- Answer with any 2xx within 10 seconds; do slow work afterwards. Redirects aren't followed.
- Anything else is retried about 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours and 10 hours later, with a last try 24 hours after the event. Retries go out with our 10-minute run, so allow a few minutes either way.
- A retry has the same
webhook-id(a new timestamp and signature): use it to ignore an event you've already handled. Events can arrive out of order. - The dashboard shows the latest deliveries and the last error for each endpoint. Turning an endpoint off drops its retries.
Errors
Every error, on every /api/v1 path, has the same shape: error.code for your code to check, error.message for people, and sometimes error.details.
{
"error": {
"code": "validation_failed",
"message": "The record isn't valid.",
"details": {
"errors": [
{ "field": "value", "code": "invalid_ipv4", "message": "An A record's value must be an IPv4 address, e.g. 203.0.113.10." },
{ "field": "ttl", "code": "invalid_ttl", "message": "ttl must be a whole number of seconds from 60 to 86400." }
]
}
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request, invalid_json, invalid_client_id, invalid_name | The body or a query parameter isn't valid. |
| 400 | invalid_idempotency_key | The Idempotency-Key header isn't 1 to 255 printable characters. |
| 400 | invalid_nameservers, nameservers_need_glue, bluewhale_nameservers | See Nameservers. |
| 401 | invalid_auth, invalid_key, key_revoked, key_expired, invalid_secret, invalid_token | No credential, an unknown, revoked or expired key, or an expired or altered token. |
| 402 | no_payment_method, payment_failed | No saved card, or the card was declined. |
| 402 | authentication_required | The card needs the cardholder (3-D Secure); details.payment_intent_client_secret completes it. |
| 403 | no_tier, tier_inactive, reseller_inactive | No active reseller plan or pricing for this key. |
| 403 | test_mode_unavailable | A test key (bwd_test_…) tried to change something. |
| 404 | domain_not_found, record_not_found, order_not_found, not_found | Not on your account (or that client's), no such record or order, or no endpoint at that path. |
| 405 | method_not_allowed | The endpoint doesn't take that method; Allow lists the ones it does. |
| 409 | duplicate_record, cname_conflict, record_read_only, dns_not_managed | The change clashes with the zone, or the zone isn't managed here. |
| 409 | alias_conflict, alias_exists | An ALIAS and the domain's own A/AAAA records can't sit together, and a domain has one ALIAS. |
| 409 | unavailable, already_with_us, not_transferable, too_many_years, renews_with_billing | The domain can't be ordered or renewed this way. |
| 409 | order_pending, request_in_progress | An earlier order or request for it is still running. |
| 409 | premium_dns, nameservers_unavailable | Its nameservers can't be changed here: contact support. |
| 413 | body_too_large | Bodies are at most 64 KB. |
| 422 | validation_failed | See details.errors. |
| 422 | idempotency_key_reused | The key was used for a different request. |
| 429 | rate_limited, upstream_rate_limited | Wait for Retry-After seconds, then retry (same Idempotency-Key). |
| 500 | order_failed, queue_failed | Paid, but the order needs support. Don't retry with a new key: contact support. |
| 502 | dns_rejected, registry_rejected | The DNS server or registry didn't keep the change. Nothing was half-done; try again. |
| 503 | upstream_unavailable, no_price, no_expiry, not_configured, resolver_unavailable | Briefly unavailable: wait for Retry-After (or a minute), then retry. |
Warnings aren't errors: warnings in an answer lists things worth knowing, e.g. ttl_adjusted (the zone serves another TTL) or nameservers_not_set on an order.
Code examples
JavaScript (Node 18+)
const BASE = "https://www.bluewhaledns.com/api/v1/reseller";
const KEY = process.env.BLUEWHALE_API_KEY;
async function api(method, path, data, idempotencyKey) {
const res = await fetch(`${BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
...(idempotencyKey && { "Idempotency-Key": idempotencyKey }),
},
body: data ? JSON.stringify(data) : undefined,
});
const body = await res.json();
if (!res.ok) throw Object.assign(new Error(body.error.message), body.error, { status: res.status });
return body;
}
// Register a domain for one of your orders (safe to retry with the same key).
const order = await api("POST", "/domains/register", { domain: "client-a.com", years: 1 }, "my-order-1042");
console.log(order.order_id, order.registration.status); // "…", "pending"
// Point a client's domain at your app: www by CNAME, the domain itself by ALIAS.
// Safe to run on every deploy: a set that's already right isn't touched.
await api("PUT", "/domains/client-a.com/dns-records/rrset", { name: "www", type: "CNAME", values: ["saas-target.example.net"] });
await api("PUT", "/domains/client-a.com/dns-records/rrset", { name: "@", type: "ALIAS", values: ["saas-target.example.net"] });
// Point www at a new server for one client's domain, one record at a time.
const clientId = "c41f…";
const { records } = await api("GET", `/domains/client-a.com/dns-records?type=A&name=www&client_id=${clientId}`);
if (records[0]) {
await api("PATCH", `/domains/client-a.com/dns-records/${records[0].id}?client_id=${clientId}`, { value: "203.0.113.20" });
} else {
await api("POST", `/domains/client-a.com/dns-records?client_id=${clientId}`, { type: "A", name: "www", value: "203.0.113.20" });
}curl: replace and delete
curl -X PUT https://www.bluewhaledns.com/api/v1/reseller/domains/client-a.com/dns-records/rec_3f9a0c1e2b7d4a5f6e8c9b0a \
-H "Authorization: Bearer $BLUEWHALE_API_KEY" -H "Content-Type: application/json" \
-d '{"type":"CNAME","name":"shop","value":"shops.myshopify.com","ttl":3600}'
curl -X DELETE https://www.bluewhaledns.com/api/v1/reseller/domains/client-a.com/dns-records/rec_3f9a0c1e2b7d4a5f6e8c9b0a \
-H "Authorization: Bearer $BLUEWHALE_API_KEY"