SatsDNS

API reference

Every route, field and answer is also in the machine-readable OpenAPI 3.1 file. Prices are on the pricing page.

Basics

NameWhat
https://api.satsdns.comThe API, for agents and scripts
https://app.satsdns.comThe dashboard (Nostr login); it uses the same API under /v1/
https://pay.satsdns.comLightning addresses for renewals (LNURL-pay) only
<label>.ns1.satsdns.com, <label>.ns2.satsdns.comThe nameservers your domain is delegated to: each zone gets its own pair (see delegation)
dynkite.comThe domain for dynamic-DNS hostnames (myhost.dynkite.com)

All are POST with a JSON body, on https://api.satsdns.com.

RouteBodySatsResult
/v1/zones/create/30d{"zone": "example.org", "ns_ticket": "nst_..."} (see delegation)500201: zone status with its nameservers, token (shown once), DNSSEC DS data
/v1/zones/create/1ysame5,000
/v1/zones/renew/30d{"zone": "example.org"}500200: status with the new expires_at (old end date + period). No token needed. Works for hosted and secondary zones.
/v1/zones/renew/1ysame5,000
/v1/zones/queries/1m{"zone": "example.org"}200200: 1,000,000 more queries for the current UTC month
/v1/secondary/create/30d{"zone": "example.net", "primaries": ["198.51.100.7"], "ns_ticket": "nst_..."}500201: status, token and the TSIG key (each shown once); see secondary DNS
/v1/secondary/create/1ysame5,000
/v1/ddns/create/30d{"hostname": "myhost"}200201: hostname status, token (shown once)
/v1/ddns/create/1ysame2,000
/v1/ddns/renew/30d{"hostname": "myhost"}200200: status with the new expires_at. No token needed.
/v1/ddns/renew/1ysame2,000

The body must hold exactly the fields shown (ns_ticket and previous_token are optional and only for the create routes). HEAD or GET on /v1/zones/create/30d, /v1/zones/renew/30d, /v1/ddns/create/30d and /v1/secondary/create/30d returns a shared 402 that only shows the price (for directories); to buy, POST your body and pay your own 402. HEAD and GET on the other paid routes are 405.

The free check before payment

Before any invoice is made, the request is checked. A request that would fail gets its error and no invoice. The same checks run again after payment.

CheckError
JSON body with exactly the expected fields; Content-Type: application/json400 missing_parameter, unknown_parameter, invalid_body; 415
Zone name: letters, digits and hyphens per label, at least 2 labels, at most 253 characters400 invalid_zone_name
Not a reserved or special-use name (.arpa, .example, .invalid, .local, .localhost, .onion, .test, .alt, .internal, example.com/.net/.org, common private suffixes), and not one of our own domains403 reserved_name
Not blocked, not on a public phishing list, not below a blocked or listed name403 name_blocked (zones), hostname_refused (hostnames)
Not hosted here already, and no hosted parent or child zone409 zone_exists, zone_clash
Delegation: the parent zone's servers list both of the zone's own nameservers (other nameservers may be listed too); see delegation409 delegation_missing, delegation_mismatch with detail.expected_ns and detail.found_ns; ns_ticket_invalid, ns_offers_exhausted; 429 ns_offer_limit
Not a domain registered through SatsDNS by someone else, and not a recently removed zone of someone else409 domain_owner_only, zone_reserved
Renewals: exists, not suspended, not past grace, and not more than 1,125 days ahead afterwards404, 403, 409 too_far_ahead
Hostname: one label, 4 to 63 characters, a-z, 0-9 and hyphens, no -- in positions 3-4 (so no xn--), not reserved (www, mail, api, admin and similar), no brand or lure words400 invalid_hostname, 403 reserved_name, hostname_refused
Hostname not taken409 hostname_taken
Secondary: 1 to 4 public IPv4 primaries, at most 20 zones per primary address400 bad_primary, ipv6_primary_unsupported; 409 limit_reached
Our DNS servers reachable503 dns_backend_unavailable with Retry-After

The delegation check asks the parent zone's authoritative servers directly (no resolver cache), so you can order as soon as your registry publishes the new NS records. It gives up after 8 seconds (503 delegation_check_unavailable, retry).

Delegation: your zone's own nameservers

Each zone is served under its own pair of nameserver names, for example k7qd.ns1.satsdns.com and m3x9.ns2.satsdns.com. Pointing a domain at us therefore cannot let anyone else take it over: only the party that got the pair, and made the registry list it, can create the zone.

  1. Send the create request without payment: {"zone": "example.org"}. The free check answers 409 delegation_missing with detail.expected_ns (your pair), detail.ns_ticket (a secret, shown once) and detail.ns_offer_expires_at (48 hours; an account's pair 7 days). Register the domain first: for a name its registry does not know, the answer has no expected_ns (detail.registered false). At most two open pairs per network and zone, and 20 new pairs a day per network (429 ns_offer_limit).
  2. At your registrar, set exactly those two names as the domain's nameservers (other providers' nameservers may stay listed). Wait until the registry publishes them.
  3. Send the request again with the ticket: {"zone": "example.org", "ns_ticket": "nst_..."}. Now you get the 402; pay as below. The create answer lists the zone's nameservers.

Keep the ticket: asking again without it gives a new pair (at most two per network and zone), and a pair the registry already lists is never given to anyone else. With an account, the pair belongs to the account and no ticket is needed (the order check, pay: "check", shows it). Domains registered through SatsDNS can be hosted only by their owner's account and keep ns1.satsdns.com and ns2.satsdns.com.

When a zone is removed (deleted or expired), first remove its NS records at your registrar. For 90 days, and for as long as the registry still lists the zone's nameservers, only its last owner can host the name again: the same account, or the old zone token sent as "previous_token" in the create body; it gets the same nameservers back.

Paying without an account

A valid request without payment gets 402 Payment Required with two ways to pay in the same answer.

L402 (Lightning)

  1. The WWW-Authenticate header holds L402 macaroon="...", invoice="lnbc...".
  2. Pay the invoice with any Lightning wallet and keep the preimage (64 hex characters).
  3. Repeat the same request with Authorization: L402 <macaroon>:<preimage>.

The invoice and macaroon are valid for 1 hour and pay one request on that route and method. When the service's own Lightning node cannot make an invoice, an accepted Cashu mint issues it and X-L402-Invoice-Issuer names the mint; pay it the same way. If no invoice can be made at all, the 402 offers Cashu only, with Retry-After.

curl -i -X POST https://api.satsdns.com/v1/ddns/create/30d \
  -H "Content-Type: application/json" -d '{"hostname": "myhost"}'
# pay the invoice from www-authenticate, then:
curl -X POST https://api.satsdns.com/v1/ddns/create/30d \
  -H "Authorization: L402 <macaroon>:<preimage>" \
  -H "Content-Type: application/json" -d '{"hostname": "myhost"}'

Cashu (NUT-24)

The 402 also carries X-Cashu: creqA..., a payment request with the amount, the unit (sat) and the accepted mints. Repeat the request with a cashuB... token in the X-Cashu header.

MintPrice
https://mint.minibits.cash/Bitcoin, https://21mint.me, https://mint.routstr.com, https://forge.flashapp.me, https://mint.cashu.chatList price
https://mint.coinos.io, https://mint.macadamia.cashList price plus the mint's swap fee, ceil(sum of input_fee_ppk over the token's proofs / 1000) sats: send with your wallet's "include fees" option

A token that is short, or from a mint that is not accepted right now, gets 400 with an X-Cashu-Error header and is not spent. No change is returned on overpayment. The current list is always in /.well-known/l402-services.

Failures after payment

If a paid request is refused after payment (for example the zone was taken by someone else in between, or the delegation changed), the answer is 422 with the reason, or 503 if our side failed. Either way:

At most one retry or refund per payment and 10 per IP address per hour, within a service-wide hourly budget. If no automatic refund could be made, ask through the contact page.

If the answer to a paid zone create is lost (a timeout), send the identical request again with the same L402 proof within 15 minutes: you get the zone back with a new token (an earlier one, if it arrived, stops working). A create whose checks run too long is refused with 422 create_timeout rather than finished late.

Authentication

Management routes are free per call. Each accepts any of these credentials:

CredentialHeaderGrants
Zone token jdz_...Authorization: Bearer jdz_...Everything for that zone
Hostname token jdh_...Authorization: Bearer jdh_..., or Basic on /nic/updateEverything for that hostname
NIP-98 signed eventAuthorization: Nostr <base64 event>Everything the account owns, and the account itself
API key jdk_...Authorization: Bearer jdk_...What its scope says; never sensitive actions
Dashboard sessionCookie, on app.satsdns.com onlyThe account, except sensitive actions

NIP-98 signed requests

An account is a Nostr public key. It is created when it first orders, claims or sets something; reading GET /v1/account creates nothing. To sign a request, make a kind 27235 event with empty content and these tags, sign it, base64-encode the JSON and send Authorization: Nostr <base64>:

created_at must be within 60 seconds of our clock, and each event id is accepted once. A failed check is 401 with a code such as auth_expired, auth_replayed, auth_url_mismatch, auth_method_mismatch, auth_payload_mismatch or bad_signature, plus a Date header and detail.server_time so you can correct your clock. In JavaScript, nostr-tools' nip98.getToken(url, method, sign, true, payload) makes the header; send exactly the body string it hashed.

API keys

POST /v1/account/keys        (NIP-98)
{"label": "ci", "scope": {"objects": ["zone:example.org", "host:myhost"], "access": "write"},
 "expires_at": 1798761600}

Orders with an account

POST /v1/account/orders      (NIP-98, API key for renewals, or the dashboard)
Idempotency-Key: 6f3c0f0e-...          (optional; same key + same body within 24 h = same answer;
                                        409 idempotency_in_progress while the first is still running)
{"product": "zone_create", "period": "1y", "zone": "example.org", "pay": "onchain"}
FieldValues
productzone_create, zone_renew, zone_queries, secondary_create (with primaries), ddns_create, ddns_renew; for domains domain_register, domain_renew, domain_transfer (see domain registration)
period30d or 1y (not for zone_queries)
zone or hostnameThe name
paylightning (default), onchain (orders of at least 5,000 sats), both (either rail, for orders of at least 5,000 sats), or check (run the free checks only: 204 or the error, nothing created)

The answer is 201 {"invoice": {...}} with lightning.bolt11 (valid 15 minutes) and/or onchain.address and onchain.bip21 (valid 24 hours). Poll GET /v1/invoices/{id}: when the state is fulfilled, result holds the zone or hostname status and, once, its token.

Zones and records

RouteWhat
GET /v1/zones/{zone}Status: status (active, grace, suspended), expires_at, grace_ends_at, days_left, nameservers, record count, limits, writes_today, query use
GET /v1/zones/{zone}/records[?name=&type=]All record sets; SOA and apex NS are marked managed; DNSSEC records are not listed
POST /v1/zones/{zone}/recordsOne record set {"name", "type", "ttl", "records": [...]}; replaces that name and type
PUT /v1/zones/{zone}/records/{name}/{type}{"ttl", "records"}, replace
DELETE /v1/zones/{zone}/records/{name}/{type}Delete a record set
PATCH /v1/zones/{zone}/records{"rrsets": [{..., "changetype": "REPLACE" | "DELETE"}]}, up to 100, applied together or not at all
GET /v1/zones/{zone}/dnssecDS, CDS, CDNSKEY and DNSKEY data; see DNSSEC
POST /v1/zones/{zone}/tokenNew token; the old one stops working
DELETE /v1/zones/{zone}Remove the zone now (no refund of the rest of the period). Remove its NS records at your registrar too; the name stays reserved for you (see delegation)
curl -X PATCH https://api.satsdns.com/v1/zones/example.org/records \
  -H "Authorization: Bearer jdz_..." -H "Content-Type: application/json" -d '{"rrsets": [
    {"name": "@",   "type": "MX",  "ttl": 3600, "records": ["10 mail.example.org"], "changetype": "REPLACE"},
    {"name": "@",   "type": "TXT", "ttl": 3600, "records": ["v=spf1 mx -all"],      "changetype": "REPLACE"},
    {"name": "old", "type": "A",                                                     "changetype": "DELETE"}]}'

DNSSEC

Dynamic DNS

A hostname myhost.dynkite.com carries A, AAAA, TXT and HTTPS records, and a TXT record at _acme-challenge.myhost.dynkite.com for ACME DNS-01. All have a 60-second TTL.

Hostnames and their addresses are public: anyone who knows or guesses a hostname can look up its addresses, and dynkite.com's DNSSEC (NSEC3, no salt, no extra iterations) gives out a hash of every hostname, from which short or common ones can be guessed. Use a long random hostname if it should be hard to find.

Buy

curl -i -X POST https://api.satsdns.com/v1/ddns/create/30d \
  -H "Content-Type: application/json" -d '{"hostname": "myhost"}'
# 402: pay 200 sats (L402 or Cashu) and repeat. The 201 answer holds "token": "jdh_..." (shown once).

Update with JSON

# Point A at the address this request comes from, and set an IPv6 address:
curl -X POST https://api.satsdns.com/v1/ddns/myhost/update \
  -H "Authorization: Bearer jdh_..." -H "Content-Type: application/json" \
  -d '{"a": "auto", "aaaa": "2001:db8::10"}'
#   {"changed": true, "records": {...}, "hostname": "myhost.dynkite.com.", "ttl": 60}

ACME DNS-01

curl -X POST https://api.satsdns.com/v1/ddns/myhost/update \
  -H "Authorization: Bearer jdh_..." -H "Content-Type: application/json" \
  -d '{"acme_txt": "gfj9Xq...Rg85nM"}'
# ...let the CA validate, then remove it:
curl -X POST https://api.satsdns.com/v1/ddns/myhost/update \
  -H "Authorization: Bearer jdh_..." -H "Content-Type: application/json" -d '{"acme_txt": null}'

Routers, ddclient and inadyn (dyndns2)

GET https://api.satsdns.com/nic/update?hostname=myhost&myip=203.0.113.7 with HTTP Basic authentication: any user name, the hostname token as the password. Leave out myip to use the caller's address; give IPv4 and IPv6 separated by a comma. Answers are plain text: good <ip>, nochg <ip>, badauth, dnserr (not an IP address, or not a public one), abuse (suspended, over the daily limit, or the owning account is banned) or 911.

curl -u "user:jdh_..." "https://api.satsdns.com/nic/update?hostname=myhost"
#   good 203.0.113.7

# /etc/ddclient.conf
protocol=dyndns2
ssl=yes
server=api.satsdns.com
login=myhost
password='jdh_...'
myhost.dynkite.com

HTTPS only: the token travels in the request. Plain http gets badauth (401) and no redirect; a client set to http has already sent its token in clear, so switch it to https and rotate the token (POST /v1/ddns/{hostname}/token). Updates are limited to 12 a minute per address.

Secondary DNS

You run the primary. Our ns1 transfers the zone from it with a TSIG key; ns2 transfers from ns1. Delegate the domain to the zone's own pair of nameservers first, as for a hosted zone (delegation; your own servers may be listed too).

Create

POST /v1/secondary/create/30d      (paid route, or an order with "product": "secondary_create")
{"zone": "example.net", "primaries": ["198.51.100.7"]}

Set up your primary

  1. Install the TSIG key and allow zone transfers (AXFR and IXFR) to that key from the transfer_source_ips.
  2. Send NOTIFY to the notify_to address, from the primary address you registered. NOTIFY from any other address is refused, and at most 10 a minute per zone are accepted.
  3. The first transfer starts within about a minute. Until then the status shows health: pending.
# BIND (named.conf), with the values from the answer
key "jd-123-abcdef" { algorithm hmac-sha256; secret "<secret>"; };
zone "example.net" {
    type primary;
    file "example.net.zone";
    notify explicit;
    also-notify { <notify_to>; };
    allow-transfer { key "jd-123-abcdef"; };
};

Run it

Outside secondaries for hosted zones

A hosted zone may also be served by up to 2 other providers' secondaries, listed as extra nameservers at your registrar:

PUT /v1/zones/{zone}/xfr-peers
{"peers": [{"address": "192.0.2.25", "tsig": true}, {"address": "198.51.100.40", "tsig": false}]}

With "tsig": true a key is made for that peer and shown once; with false the peer may transfer the zone from its address without a key (for providers that cannot use TSIG). The peers get NOTIFY on every change. The same address rules as for primaries apply. GET on the same route lists them.

LNURL renewals

POST /v1/zones/{zone}/lnurl      (token, owner or write key; also /v1/ddns/{hostname}/lnurl)
#   {"pay_id": "abcdefghijkm", "lnurl": "LNURL1...", "lightning_address": "abcdefghijkm@pay.satsdns.com",
#    "rate": {"sat_per_30_days": 500, "min_sat": 100, "max_sat": 100000}, ...}

Expiry, grace and reminders

Account features

RouteWhat
GET /v1/accountState, tier, limits, usage, recovery key, message settings
GET /v1/account/zones, /v1/account/hostnamesWhat you own, with status
POST /v1/account/claimClaim a token-bought zone or hostname ({"zone" | "hostname", "token"})
PUT /v1/account/recovery {"recovery_npub": "npub1..."}A second key that can recover the whole account. It takes effect 72 hours after it is set.
POST /v1/account/recoverSigned by the recovery key: moves a lost account's zones to a new key after 72 hours. The lost account is told and can cancel.
POST /v1/zones/{zone}/transfer {"to_npub": "npub1..."}Offer a zone (or /v1/ddns/{hostname}/transfer) to another account; it accepts with POST /v1/account/transfers/{id}/accept within 72 hours. The token is retired on transfer (and on recovery); the new owner issues a new one with POST .../token (re-enabling the old one gives 409 token_reissue_required). The recipient gets one DM per object per offer period and at most 5 offer DMs a day; an account can make 10 offers a day.
DELETE /v1/accountDelete the account (only when it owns nothing live; registered domains or a domain order in progress give 409 account_has_domains)

Recovery by token: if someone else's account owns a zone and you hold its token, a claim starts a 72-hour recovery; the owner is told and can cancel it (and should then rotate or disable the token). No key, no token and no recovery key: the zone keeps running until it expires; we cannot hand it over.

Domain registration

.com, .net and .org names, registered with our sponsoring registrar, Global Domain Group LLC (IANA ID 3956), under the registration agreement. Domains are ordered only with an account, in the dashboard (Register domain) or with POST /v1/account/orders; they are not sold on the L402/Cashu routes. The dashboard's Domains screens also cover renewal, nameservers, DNSSEC, lock, transfer codes and registrant changes. WHOIS privacy is always on. Prices: pricing.

Check and quote (free)

GET /v1/domains/check?domain=example.com&years=1

Answers available, premium, prices_sat (register, or transfer when the name is taken, and renew) and hosting_addon_sat. The answer is cached for 60 seconds. 6 checks a minute per address.

Order

POST /v1/account/orders      (NIP-98 or an API key)
{"product": "domain_register", "period": "1y", "domain": "example.com",
 "registrant": {"name": "Ada Lovelace", "email": "ada@example.net", "phone": "+44.2071234567",
                "address1": "1 Example Street", "city": "London", "postal_code": "N1 1AA", "country": "GB"},
 "attestation": true, "nameservers": ["ns1.example.net", "ns2.example.net"], "host_zone": false,
 "pay": "lightning"}
FieldValues
productdomain_register, domain_renew, domain_transfer
period1y to 10y (default 1y); domain_transfer only 1y
domainA second-level name under .com, .net or .org, ASCII only
registrantRegistrations and transfers. Required: name (real full name; for an organization, a person authorized to act for it), email (must accept mail), phone (+CC.NUMBER, for example +1.6045551234), address1, city, postal_code, country (ISO 3166 two letters). Optional: organization, address2, state. Plain Latin letters only; no placeholders
attestationtrue for registrations and transfers: "I am authorised by the named registrant to register this domain for them, and the registrant accepts the registration agreement."
nameservers"ours" (the default for a registration) or 2 to 13 host names. A transfer needs this or host_zone
host_zonetrue adds a hosted zone here for a year (the hosting add-on); its DS record is set at the registry for you
auth_codedomain_transfer only: the transfer code from the current registrar; never shown back
paylightning, onchain, both, or check (the free checks: 200 with the quote, no invoice)

Manage

Management routes accept the domain token (Authorization: Bearer jdd_...), NIP-98 by the owning account, an API key with domain: scope, or a dashboard session. Actions marked "fresh signature" need a new NIP-98 signature (401 step_up_required) when you use an account. The domain token is as strong as the owner's key: whoever holds it can change nameservers and DNSSEC, unlock the domain, change the registrant and replace the token, so keep it as safe as your key. The one exception: when an account owns the domain, only the owner's NIP-98 signature gets the transfer code.

RouteWhat
GET /v1/domains/{domain}Status: status, expires_at, days_left, renew_until, nameservers, hold, hosted zone, DNSSEC, lock, registrant (read from the registrar), verification, transfer_lock_until, pending orders
GET /v1/account/domainsThe account's domains
PUT /v1/domains/{domain}/nameservers{"nameservers": "ours"} or 2 to 13 host names; queued during the hold
PUT /v1/domains/{domain}/dnssec, DELETESet the DS record at the registry (your own nameservers only); remove every DS (fresh signature). POST /v1/domains/{domain}/dnssec/sync pushes the DS of a zone hosted here again
PUT /v1/domains/{domain}/lockRegistrar lock on or off (off: fresh signature)
POST /v1/domains/{domain}/auth-codeTransfer code, at once; unlocks the domain (fresh signature; the domain token is enough only when no account owns the domain). Not within 60 days of a registration or transfer in; 5 a day
PUT /v1/domains/{domain}/registrantChange the registrant ({"registrant": {...}, "attestation": true}, fresh signature); free; the registrar may ask for email confirmation
PUT /v1/domains/{domain}/transferCorrect the transfer code of a transfer in progress (fresh signature)
POST /v1/domains/{domain}/tokenReplace the domain token (fresh signature)
PUT /v1/domains/{domain}/glue/{host}Glue addresses for nameservers under the domain itself

Renewal and expiry

Domain errors

StatusCodes
400invalid_domain, tld_not_offered, idn_not_offered, invalid_period, invalid_registrant (with detail.field), attestation_required, invalid_nameservers, invalid_auth_code, invalid_ds, invalid_host
403registrant_refused (refused country), name_blocked, domain_suspended
404not_offered (registration not open), not_found
409domain_taken, domain_exists, price_not_offered (premium or too expensive), limit_reached, domain_order_in_progress, domain_not_active, not_renewable, renew_window_closed, too_far_ahead, transfer_locked_60d, abuse_hold, change_pending, dnssec_managed, dnssec_not_managed, registrar_refused
503registration_unavailable, price_unavailable, price_over_cap (prices paused for a check), email_check_unavailable, waiting_for_funding, with Retry-After where known

Abuse reports

POST /v1/abuse/reports (no account) with {"domain", "category", "details", "contact"} reports abuse of a domain, zone or hostname served or sold here now; any other name (also one removed, transferred away or deleted) gets 404 not_ours. See the abuse page.

Limits and quotas

LimitValue
Records per zone1,000 (records, not record sets; SOA, apex NS and DNSSEC records not counted)
Records per record set; record sets per PATCH100; 100
Record writes per zone1,000 per UTC day (each record written and each record set deleted counts 1; reads are free). Over it: 429 write_quota_exceeded until 00:00 UTC
TTL60 to 86,400 seconds (dynamic DNS: 60)
Queries per zone1,000,000 per calendar month (UTC), counted on both nameservers, plus any extra bought for that month
Over the query quotaThe zone is flagged and its owner told at once; its records keep answering. Only answers with data count (not NXDOMAIN or empty answers, not traffic our rate limits drop or truncate). When use passes 120% of the quota, or 3 days after the flag, the operator reviews the zone and decides; records are never withdrawn automatically because of query volume. If the operator withdraws them, buying extra queries, or the next month, brings them back.
Dynamic DNSUp to 10 values per record type; 1,000 changes per hostname per UTC day (429 update_quota_exceeded)
Paid aheadAt most 1,125 days (3 years and 30 days) from now
Grace after expiry7 days
Secondary4 primaries per zone, 20 zones per primary address, 20,000 records, 16 MiB per transfer
Outside secondaries of a hosted zone2
Accounts: new zones and hostnames per UTC day3 (established accounts: 20)
Accounts: live zones and hostnames10 (established: 100)
Accounts: open Lightning invoices / open on-chain orders5 / 2 (established: 10 / 5)
Accounts: API keys / dashboard sessions / open transfer offers / new transfer offers a day20 / 10 / 3 / 10
Accounts: requests120 a minute, of which 60 writes; 10 invoices a minute (429 account_rate_limited)
Accounts: new accounts per network30 a day per IPv4 /24 or IPv6 /48 (429 rate_limited); 500 a day in all (503 busy_try_later)
Invoices per network100 an hour per IPv4 /24 or IPv6 /48 (429 rate_limited), 600 an hour in all (429 busy_try_later); Lightning-address invoices: 30 an hour per Lightning address, 200 per network, 600 in all
Domain checks (GET /v1/domains/check and orders with "pay": "check")Answers cached for 60 seconds; a name not in the cache counts against 6 a minute per address, 10 per network and 20 in all

An account becomes established by itself after 30 days and 10,000 sats paid in total. If you need more, ask through the contact page.

Rate limits

Per IPv4 address or IPv6 /64. Over the limit: 429 rate_limited with Retry-After.

RequestsRate
Paid routes without a payment proof (including HEAD/GET there)20 a minute
Paid routes with a payment proof120 a minute
Management and account reads (GET)120 a minute, burst 60
Management and account writes60 a minute, burst 30
Invoice status polls (GET /v1/invoices/{id})120 a minute
Dynamic-DNS updates (/v1/ddns/{hostname}/update, /nic/update)12 a minute, burst 10
Dashboard login (/v1/auth/)30 a minute
LNURL routes20 a minute
/v1/info, /healthz, discovery files60 a minute

Service-wide limits also apply. Limits per address and per network are checked first, and a refused request counts against none of the others. Do not spread requests over many addresses to get around these.

Errors

StatusCommon codes
400invalid_body, missing_parameter, unknown_parameter, invalid_zone_name, invalid_hostname, invalid_rrset, invalid_record, invalid_ttl, record_type_not_allowed, cname_conflict, cname_at_apex, ds_without_ns, occluded_name, bad_primary, invalid_address, address_not_public, onchain_minimum, amount_out_of_range, invalid_scope
401missing_token, auth_required, invalid_token, invalid_auth, auth_expired, auth_replayed, auth_url_mismatch, auth_method_mismatch, auth_payload_required, auth_payload_mismatch, bad_signature, step_up_required, session_expired
402Payment required (paid routes)
403reserved_name, name_blocked, hostname_refused, hostname_suspended, forbidden_scope, account_banned, account_deleted
404not_found, invoice_not_found
405method_not_allowed, with Allow
409zone_exists, zone_clash, hostname_taken, delegation_missing, delegation_mismatch, ns_ticket_invalid, ns_offers_exhausted, zone_reserved, domain_owner_only, too_far_ahead, zone_is_secondary, limit_reached, idempotency_conflict, idempotency_in_progress, transfer_pending, recovery_pending, token_reissue_required, account_has_domains, account_has_objects
413 / 415Body too large / not JSON
422Refused after payment (see failures after payment); invalid_request for a query parameter of the wrong type (for example years=abc)
429rate_limited, account_rate_limited, busy_try_later, write_quota_exceeded, update_quota_exceeded, ns_offer_limit, all with Retry-After
500internal_error (an unexpected fault on our side)
503dns_backend_unavailable, lightning_unavailable, onchain_unavailable, busy_try_later, delegation_check_unavailable, delegation_check_busy, with Retry-After where known

LNURL routes answer in the LNURL format ({"status": "ERROR", "reason": "..."} with HTTP 200), as wallets expect.

Status and discovery