# OrbitFour REST API — integration guide ICANN-accredited registrar (IANA 3873). This document is written for LLM agents and integration authors. The normative reference is the OpenAPI spec at `https://www.orbitfour.com/api/v1/openapi`; everything here is the operational layer on top of it. Base URL: `https://www.orbitfour.com/api/v1` ## Authentication Every request to an authenticated endpoint sends: ``` Authorization: Bearer o4_live_ ``` Keys are created at `https://www.orbitfour.com/account/api-keys` and the secret is shown **once**. A key belongs to one account, carries an explicit scope list, an optional IP allowlist, an optional expiry, and its own per-minute rate limit. Scopes (a key is granted a subset; every endpoint documents what it needs): - `domains:read` - `domains:write` - `dns:read` - `dns:write` - `contacts:read` - `contacts:write` - `account:read` - `billing:write` `billing:write` is required *in addition to* `domains:write` for anything that spends money (register, renew, transfer, deposit). A domain-automation key without `billing:write` can read and manage DNS but cannot charge the account. Verify a key and see its account + scopes: ```bash curl -sS https://www.orbitfour.com/api/v1/me \ -H "Authorization: Bearer $O4_KEY" ``` ### Rate limits Every authenticated response carries the current bucket state, so pace proactively rather than discovering the limit with a 429: ``` x-ratelimit-limit: 600 x-ratelimit-remaining: 594 x-ratelimit-reset: 41 ``` `x-ratelimit-reset` is **seconds until the window rolls**, not a timestamp. A 429 adds `Retry-After` (also seconds). Card payments carry a second, separate anti-card-testing budget per account — see `velocity_limited` in the error catalog; the prepaid balance is not velocity-limited and is the intended path for volume automation. ## How money settles Two settlement paths, chosen per request with the `payment_method` field: | `payment_method` | Behavior | | --- | --- | | omitted | Prepaid balance first, falling back to the account's default card. | | `"balance"` | Prepaid balance only. `402 insufficient_balance` if short — never falls back to a card. | | `"default"` | The account's default card. | | a payment-method UUID | That specific card. | | a last-four, e.g. `"4242"` | The card with that last-four; `409 ambiguous_payment_method` (with candidate UUIDs) if several match. | **Money always settles before registry work, and unwinds if the registry work fails.** A `202` never means the domain exists yet — read the operation's status for that. If fulfillment fails, the charge is refunded (card) or the ledger debit reversed (balance), and the status reflects it. For unattended automation, prefer the prepaid balance: it is a single atomic debit (no 3-D Secure, no `authentication_required`, no card velocity limit). Cards that demand 3-D Secure cannot be completed over the API at all — they need a browser. Amounts are always objects, never bare numbers: ```json { "amount": "12.20", "currency": "USD" } ``` The amount is a decimal **string** to avoid float rounding. The prepaid balance is USD-only; the minimum deposit is $5.00. **Cap what an operation can charge with `max_price`.** Register, renew and transfer accept an optional `max_price`, a USD decimal string compared against the full charge (every year, the ICANN fee included: the same figure the operation reports as `amount`). A premium name is charged at the live registry quote, so set it to the most you'll pay for the whole operation. `GET /domains/check` quotes a per-year price without the ICANN fee: multiply it by the years you're buying and add each year's ICANN fee (`icann_fee` on `GET /pricing`; $0.20 on gTLDs, none on ccTLDs). A higher charge returns `422 max_price_exceeded` with the real price in `details.price`, and nothing is charged. ## Idempotency Every mutating endpoint accepts an `Idempotency-Key` header — the paid ones (`POST /domains`, `POST /domains/:id/renew`, `POST /transfers`, `POST /balance/deposits`) and the free ones (`POST/PATCH/DELETE` on `/domains/:id/dns` and `/contacts`) alike. Any opaque unique string works (a UUID, or something like `reg-2026-07-24-001`). Pick one per logical operation and **reuse it verbatim on every retry of that operation**. The key is claimed atomically *before* the mutation, so: - Retrying the same key with the same payload replays the original outcome. A replay carries the response header `idempotent-replayed: true` — use it to tell a fresh charge from a cached one. - Retrying while the first attempt is still running returns `409 idempotency_key_in_flight` — wait and retry the same key. - Reusing the key with a *different* payload returns `409 idempotency_key_reused`. - Only `2xx` responses are cached. Any other status **releases** the key. That last point matters more than it looks. Because a non-2xx releases the key, a **5xx or a network timeout on a paid operation is ambiguous** — the charge may or may not have landed — and a same-key retry *re-executes* rather than replaying. The key is not what protects you there. What protects you is the duplicate guard: a second attempt at an operation already in flight for the same domain is rejected with `409 duplicate_operation`, and if a charge did slip through it is refunded automatically. So the recovery rule for an ambiguous paid request is: retry the identical request with the same Idempotency-Key (never worse than a fresh key, and it keeps your retry path uniform), and treat `409 duplicate_operation` as "the first attempt won" rather than an error — poll `details.operation_id` when it is present. Never assume failure and never re-issue as a new operation. ## Workflows Integrations fail at the seams between endpoints, not at the endpoints. These are the end-to-end paths. `$O4_KEY` is an API key; `$IDEM` is a fresh key per operation. **Scopes do not imply each other.** `domains:write` does not grant `domains:read`, so a key that can register a domain but not read one gets `403 insufficient_scope` at the polling step — after the money has moved. Grant every scope a whole workflow touches; each step below names its own. ### 1. Register a domain ```bash # 1. Is it available, and what does it cost? (unauthenticated; `names` takes # a comma-separated batch, so check alternates in one call) curl -sS "https://www.orbitfour.com/api/v1/domains/check?names=example.com,example.net&years=1" # 2. Make sure the balance covers it (scope: account:read) curl -sS https://www.orbitfour.com/api/v1/balance \ -H "Authorization: Bearer $O4_KEY" # 3. Top up if needed — synchronous, a 201 means the credit is committed # (scopes: billing:write) curl -sS -X POST https://www.orbitfour.com/api/v1/balance/deposits \ -H "Authorization: Bearer $O4_KEY" \ -H "Idempotency-Key: $IDEM_DEPOSIT" \ -H "Content-Type: application/json" \ -d '{"amount": "250.00", "payment_method": "default"}' # 4. Register. Returns 202 + an operation id; the money has already settled. # (scopes: domains:write + billing:write) curl -sS -X POST https://www.orbitfour.com/api/v1/domains \ -H "Authorization: Bearer $O4_KEY" \ -H "Idempotency-Key: $IDEM" \ -H "Content-Type: application/json" \ -d '{"domain_name": "example.com", "years": 1, "payment_method": "balance", "max_price": "20.00"}' # 5. Poll the operation until status leaves pending_payment/processing. # Honor the `retry_after` field in the response as your polling interval. # (scope: domains:read — NOT implied by domains:write) curl -sS https://www.orbitfour.com/api/v1/operations/ \ -H "Authorization: Bearer $O4_KEY" # 6. On "completed", the operation carries domain_id — fetch the domain. # (scope: domains:read) curl -sS https://www.orbitfour.com/api/v1/domains/ \ -H "Authorization: Bearer $O4_KEY" ``` Minimum key for this whole workflow: `domains:read`, `domains:write`, `billing:write`, and `account:read` for the balance check. Operation statuses and what to do with them: | status | meaning | next | | --- | --- | --- | | `pending_payment` | Card authentication in flight. | Keep polling. | | `processing` | Paid; registry work running. | Keep polling. | | `completed` | Done. `domain_id` is set. | Stop. | | `action_required` | Transfer held on a user-fixable problem (e.g. wrong auth code); the charge is held for a retry window. | Resolve in the web UI. | | `failed_refund_pending` | Fulfillment failed; refund in progress. | Stop; the money is coming back. | | `failed_refunded` | Fulfillment failed; money returned. | Safe to try again as a new operation. | | `payment_failed` | Nothing was charged. | Fix the payment method and retry. | | `canceled` | Payment canceled. | Nothing charged. | `years` in the response is the term **actually purchased** — it is clamped up to the TLD minimum, so always read it back rather than assuming your requested value. ### 2. Transfer a domain in ```bash # 1. Get the auth (EPP) code from the losing registrar, and confirm the domain # is transferable — 60 days must have passed since registration or the last # transfer, and the domain must be unlocked at the current registrar. # 2. Request the transfer. The term is fixed by registry policy per TLD and is # NOT client-selectable. (scopes: domains:write + billing:write) curl -sS -X POST https://www.orbitfour.com/api/v1/transfers \ -H "Authorization: Bearer $O4_KEY" \ -H "Idempotency-Key: $IDEM" \ -H "Content-Type: application/json" \ -d '{"domain_name": "example.com", "auth_code": "aBcD1234!", "payment_method": "balance"}' # 3. Poll the operation (scope: domains:read). NOTE: a transfer settles # asynchronously at the REGISTRY — approval takes up to 5 days. # `completed` here means the transfer REQUEST was accepted, and the domain # then appears with a "Transferring In" status until the registry finishes. curl -sS https://www.orbitfour.com/api/v1/operations/ \ -H "Authorization: Bearer $O4_KEY" ``` Inbound transfers keep the domain's existing nameservers and enable WHOIS privacy. Change either after the transfer completes — not before. ### 3. Manage DNS ```bash # List the zone (scope: dns:read) curl -sS https://www.orbitfour.com/api/v1/domains//dns \ -H "Authorization: Bearer $O4_KEY" # Create a record — name/type/value required; use "@" for the zone root. # (scope: dns:write) curl -sS -X POST https://www.orbitfour.com/api/v1/domains//dns \ -H "Authorization: Bearer $O4_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "www", "type": "A", "value": "203.0.113.10", "ttl": 3600}' # Update (partial) / delete by record id curl -sS -X PATCH https://www.orbitfour.com/api/v1/domains//dns/ \ -H "Authorization: Bearer $O4_KEY" \ -H "Content-Type: application/json" \ -d '{"value": "203.0.113.11"}' curl -sS -X DELETE https://www.orbitfour.com/api/v1/domains//dns/ \ -H "Authorization: Bearer $O4_KEY" ``` `type` is one of A, AAAA, ALIAS, CNAME, MX, TXT, NS, SRV, CAA, WR; MX and SRV also take `priority`. DNS writes are free, so they do not need `billing:write` — but they do honor `Idempotency-Key` if you send one. ### 3b. Contacts (required for nexus TLDs) Registrations for `.us` and other nexus TLDs need a registrant contact that already carries the eligibility data — create it first, then pass its id as `contact_id` on the registration. ```bash # List (scope: contacts:read) curl -sS https://www.orbitfour.com/api/v1/contacts -H "Authorization: Bearer $O4_KEY" # Create (scope: contacts:write). Required: first_name, last_name, address1, # city, zip, country_code, phone, email. The three nexus_* / app_purpose # fields are what a .us registration additionally needs. curl -sS -X POST https://www.orbitfour.com/api/v1/contacts \ -H "Authorization: Bearer $O4_KEY" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com", "phone": "+1.5551234567", "address1": "1 Main St", "city": "Columbus", "state": "OH", "zip": "43004", "country_code": "US", "nexus_category": "C11", "app_purpose": "P1" }' # Update (partial) / delete by contact id curl -sS -X PATCH https://www.orbitfour.com/api/v1/contacts/ \ -H "Authorization: Bearer $O4_KEY" -H "Content-Type: application/json" \ -d '{"nexus_category": "C21"}' curl -sS -X DELETE https://www.orbitfour.com/api/v1/contacts/ \ -H "Authorization: Bearer $O4_KEY" ``` `nexus_category` is one of C11/C12/C21/C31/C32 and `app_purpose` one of P1–P5; C31/C32 additionally need `nexus_country`. If any are missing at registration time you get `422 nexus_required` with `details.missing_fields` naming them — PATCH the contact and retry. Full property list: `ContactCreateInput` in `https://www.orbitfour.com/api/v1/openapi`. ### 4. Fund and audit the prepaid balance ```bash # Current balance (scope: account:read) curl -sS https://www.orbitfour.com/api/v1/balance -H "Authorization: Bearer $O4_KEY" # Deposit — synchronous. 201 = credited. (scope: billing:write) curl -sS -X POST https://www.orbitfour.com/api/v1/balance/deposits \ -H "Authorization: Bearer $O4_KEY" \ -H "Idempotency-Key: $IDEM" \ -H "Content-Type: application/json" \ -d '{"amount": "250.00"}' # Append-only ledger: deposits, draws, reversals, refunds, adjustments curl -sS "https://www.orbitfour.com/api/v1/balance/ledger?page=1&per_page=50" \ -H "Authorization: Bearer $O4_KEY" # Transactions and invoices for accounting reconciliation curl -sS https://www.orbitfour.com/api/v1/transactions -H "Authorization: Bearer $O4_KEY" curl -sS https://www.orbitfour.com/api/v1/invoices -H "Authorization: Bearer $O4_KEY" ``` A `502 deposit_pending_reconciliation` means **the card was charged** but the ledger credit has not committed. It reconciles automatically. Retry the same `Idempotency-Key` to check — do not submit a second deposit. ## Registry constraints to check before you call These are registry and ICANN rules, not OrbitFour preferences. Each one otherwise surfaces as a 422 after you have already built the request. **Read them programmatically instead of hardcoding them.** `GET https://www.orbitfour.com/api/v1/pricing` is unauthenticated and cacheable, and every constraint below is a field in it — per-TLD (`min_years`, `max_years`, `transfer_years`, `transfer_years_fixed`, `requires_nexus`, `whois_privacy.supported`, `eligibility`) and account-wide in the top-level `constraints` object (`transfer_lock_days`, `grace_period_days`, `redemption_period_days`, `balance_currency`, `min_deposit`, `premium_max_retail`). The prose here explains the *why*; the feed is the current truth, so prefer it — TLD policy changes without this document changing. - **Minimum registration term varies by TLD.** Most are 1 year; some (e.g. `.ai`) require 2. `years` is clamped **up** to the minimum rather than rejected, and the response reports the term purchased — read it back and expect to be charged for it. - **Transfer terms are fixed per registry, not chosen by you.** Some registries mandate exactly 1 year (PIR/`.org`), others allow a range, and some require 2. `POST /transfers` takes no `years` field at all. - **60-day transfer lock.** ICANN forbids transferring a domain within 60 days of its initial registration or of a previous completed transfer. The losing registrar may also apply a 60-day lock after a registrant contact change. - **Transfers need the domain unlocked and an auth code** from the losing registrar. Auth codes are at least 8 characters. - **US Nexus TLDs need eligibility data on the contact.** `.us` (and any TLD flagged as requiring nexus) requires NexusCategory + application purpose on the **registrant contact** before the registration can be attempted. Create the contact with that data, pass it as `contact_id`, and expect `422 nexus_required` with `details.missing_fields` if anything is absent. - **WHOIS privacy is forbidden on some TLDs.** `.us` is the notable one — privacy is forced off there regardless of what you request. - **Premium domains are priced by the registry per name.** Always read the price from `/api/v1/domains/check` (or the operation response) rather than the TLD catalog price. Above an automated cap you get `422 premium_requires_review` instead of a charge. - **The prepaid balance is USD-only**, minimum deposit $5.00. - **A domain already inside an OrbitFour account cannot be transferred in** — `422 domain_already_managed`. Human-readable per-TLD legal and eligibility requirements are published at `https://www.orbitfour.com/legal/tld-requirements` (the same copy the feed's `eligibility.summary` carries). There is no separate requirements endpoint — `/api/v1/pricing` is the single catalog, so terms and prices can never disagree. ## Error catalog Every error the API raises deliberately uses one envelope: ```json { "error": { "code": "insufficient_scope", "message": "This API key lacks the required scope: domains:write.", "details": { "required_scope": "domains:write" } } } ``` `code` is the stable, machine-readable identifier — branch on it, never on `message`. `details` is present only when there is structured context to give. Rate limits use it on every endpoint, authenticated or not: a `429 rate_limited` carries `details.retry_after` (seconds) alongside the `Retry-After` header. **Framework-level errors do NOT use it** — an unknown path, an unacceptable `Accept` header, or an unhandled server exception render `{"errors": {"detail": "..."}}` (plural `errors`, no `code`). The catalog's `operation_failed` covers the paid-operation catch-all, not every possible 500. A robust client reads `error.code` and treats anything else as an unmapped failure keyed off the HTTP status alone. Retryability, in three kinds: - **no — fix the request**: deterministic. Retrying the same request always fails the same way. - **yes — with backoff**: transient. Retry with exponential backoff; honor `Retry-After` when present. - **yes — same Idempotency-Key only**: the original attempt may have moved money. Retry the identical request with the identical key. Never mint a new key. | code | HTTP | retryable | meaning | | --- | --- | --- | --- | | `unauthorized` | 401 | no — fix the request | The Authorization header is missing, malformed, or the API key is revoked, expired, or IP-blocked. | | `insufficient_scope` | 403 | no — fix the request | The key authenticated but lacks the scope this endpoint requires. `details.required_scope` names it; re-create the key with that scope. | | `forbidden` | 403 | no — fix the request | The resource exists but belongs to another account. | | `account_suspended` | 403 | no — fix the request | The account is not active, so it cannot be charged. Contact support — retrying will not clear it. | | `identity_verification_required` | 403 | no — fix the request | The domain is premium and the key's acting user has not completed identity verification. Verify once in the web UI (Settings), then retry. Nothing was charged. | | `no_acting_user` | 422 | no — fix the request | The key's creating user is no longer an active member of the account. Re-create the key from an active member. | | `not_found` | 404 | no — fix the request | No such resource on this account. | | `bad_request` | 400 | no — fix the request | A query parameter could not be parsed (e.g. a non-ISO-8601 date filter). `message` names the offending value. | | `validation_failed` | 422 | no — fix the request | Request body or params failed schema validation. `details.errors` (schema violations) or `details` (field → messages) says which. | | `names_required` | 400 | no — fix the request | GET /domains/check needs a comma-separated `names` query parameter. | | `too_many_names` | 400 | no — fix the request | GET /domains/check was given more names than one request allows. Split the batch. | | `query_required` | 400 | no — fix the request | GET /domains/search needs a non-empty `q` query parameter. | | `rate_limited` | 429 | yes — with backoff | Per-key (or per-IP, unauthenticated) request rate exceeded. Honor `Retry-After`; pace with the `x-ratelimit-*` headers on every response. | | `velocity_limited` | 429 | yes — with backoff | Too many CARD payment attempts for this account (shared anti-card-testing budget). Prepaid balance is not velocity-limited — use it for volume. | | `idempotency_key_in_flight` | 409 | yes — same Idempotency-Key only | A request with this Idempotency-Key is still executing. Retry the same key after a short delay to get the original outcome. | | `idempotency_key_reused` | 409 | no — fix the request | This Idempotency-Key was already used with a DIFFERENT payload. Use a fresh key, or resend the original payload byte-for-byte. | | `invalid_domain_name` | 422 | no — fix the request | `domain_name` is not a syntactically valid domain. | | `unsupported_tld` | 422 | no — fix the request | The TLD is not offered (or not offered for this operation). Check GET /api/v1/pricing for what is sellable. | | `invalid_years` | 422 | no — fix the request | `years` must be an integer 1–10. It is clamped UP to the TLD minimum; the response reports the term actually purchased. | | `invalid_auth_code` | 422 | no — fix the request | The transfer `auth_code` is missing or shorter than 8 characters. Get it from the losing registrar. | | `invalid_contact_role` | 422 | no — fix the request | `contacts` keys must be registrant, admin, technical, or billing. | | `contact_not_found` | 422 | no — fix the request | A referenced contact UUID does not exist on this account. | | `nexus_required` | 422 | no — fix the request | The TLD requires US Nexus eligibility data on the registrant contact. `details.missing_fields` lists what to set before retrying. | | `invalid_amount` | 422 | no — fix the request | `amount` must be a positive decimal string, e.g. "250.00". | | `domain_unavailable` | 422 | no — fix the request | The registry says the domain is not available to register. Nothing was charged. | | `domain_already_managed` | 422 | no — fix the request | The domain is already in an OrbitFour account, so it cannot be transferred in. | | `not_renewable` | 422 | no — fix the request | The domain's current status forbids renewal (e.g. mid-transfer, or past the renewal window into redemption). | | `not_priced` | 422 | no — fix the request | The TLD has no retail price configured for this operation. Contact support; retrying will not help. | | `invalid_max_price` | 422 | no — fix the request | max_price was not a positive decimal string in USD, such as "25.00". | | `max_price_exceeded` | 422 | no — fix the request | The operation's full charge is above the max_price sent, so nothing was charged. details.price is the charge; resend with a higher max_price to accept it. | | `premium_requires_review` | 422 | no — fix the request | The registry quoted a premium price above the automated cap. Contact support to buy it — the API will not charge it. | | `transfer_conflict` | 409 | no — fix the request | A previous transfer attempt for this domain is in a conflicting state (possibly in another account). Contact support. | | `duplicate_operation` | 409 | no — fix the request | An operation for this domain is already in flight. Poll `details.operation_id` when present instead of retrying; any duplicate charge is refunded automatically. | | `availability_unknown` | 502 | yes — with backoff | The registry did not answer the availability check. Nothing was charged — retry shortly. | | `price_unconfirmed` | 502 | yes — with backoff | The registry did not confirm the price (premium fee check failed). Nothing was charged — retry shortly. Deliberately fails closed. | | `insufficient_balance` | 402 | no — fix the request | `payment_method: "balance"` was requested and the prepaid balance does not cover the operation. Deposit first, or omit to allow card fallback. | | `no_default_payment_method` | 422 | no — fix the request | Neither prepaid balance nor a default card can cover this. Deposit funds or add a card in the web UI. | | `payment_method_not_found` | 404 | no — fix the request | No card matches the `payment_method` reference given. | | `ambiguous_payment_method` | 409 | no — fix the request | Several cards share that last-four. `details.candidates` lists their UUIDs — resend with one. | | `payment_method_expired` | 422 | no — fix the request | The selected card is past its expiry date. | | `payment_failed` | 402 | no — fix the request | The card was declined. `message` carries the normalized decline reason. Nothing was charged. | | `authentication_required` | 402 | no — fix the request | The card demands 3-D Secure, which needs a browser. Complete it in the web UI, or fund the prepaid balance and pay from there. | | `payment_in_progress` | 409 | yes — with backoff | Another payment for this same operation is already settling. Wait and retry, or poll for the resulting operation. | | `settlement_failed` | 422 | yes — with backoff | The charge or balance debit could not be committed. Nothing is left half-paid. Retry; if it persists, contact support. | | `below_minimum_deposit` | 422 | no — fix the request | Deposits must be at least $5.00 USD. | | `deposit_failed` | 422 | no — fix the request | The deposit could not be started (e.g. the card was rejected before capture). Nothing was charged. | | `deposit_pending_reconciliation` | 502 | yes — same Idempotency-Key only | The card WAS charged but the ledger credit has not committed yet. It reconciles automatically. Retry the same Idempotency-Key to check — do NOT submit a new deposit. | | `pdf_generation_failed` | 502 | yes — with backoff | The invoice PDF could not be rendered. Retry later or use the JSON representation. | | `pdf_generation_busy` | 503 | yes — with backoff | Too many invoice PDFs are rendering at once. Honor the Retry-After header and retry in a few seconds. | | `pdf_generation_unavailable` | 503 | no — fix the request | PDF rendering is not installed on this server — a deployment defect, not a transient. Use the JSON representation and report it. | | `operation_failed` | 500 | yes — same Idempotency-Key only | An unmapped server-side failure; the detail is logged, not returned. Whether anything was charged is unknown — retry with the same Idempotency-Key and expect duplicate_operation if the first attempt did land. | ## MCP server The unauthenticated check and search are also served as Model Context Protocol tools at `https://www.orbitfour.com/mcp`: Streamable HTTP, JSON-RPC 2.0 over POST, with no session and no SSE stream (a GET answers 405). Client setup for Claude, Cursor and others: `https://www.orbitfour.com/developers/mcp`. - `check_domain`: Check domain availability and price. Parameters: `names` (required), `years`. - `search_domains`: Search for available domain names. Parameters: `query` (required), `limit`, `tlds`, `years`. `check_domain` returns the `GET /api/v1/domains/check` payload (at most 10 names per call) and `search_domains` the `GET /api/v1/domains/search` payload. Each result comes back twice: in `structuredContent`, and JSON-encoded as the text content. Requests share the per-IP rate limit of the unauthenticated REST endpoints. Bad input (a blank query, too many names) comes back in-band as `isError: true`, not as a JSON-RPC error. The tools are read-only; registering, renewing and DNS stay on the authenticated REST API. ## Response conventions - **Authenticated** reads are `{"data": ...}`; list endpoints add `{"meta": {...}}` with pagination. - **Unauthenticated** endpoints predate that convention and each have their own top-level key — do NOT expect `data` from them: - `GET /domains/check` → `{"domains": [...]}` - `GET /domains/search` → `{"query": ..., "count": N, "suggestions": [...]}` - `GET /pricing` → `{"registrar": {...}, "currency": "USD", "count": N, "generated_at": "...", "tlds": [...]}` - Pagination is `?page=` / `?per_page=` (default 50, max 100). - Identifiers are UUIDs. - Timestamps are ISO 8601 UTC. - Monetary values are `{"amount": "", "currency": "USD"}`, or `null` when unpriced. - `POST` of a paid operation returns `202 Accepted` with an operation id; poll it. - `POST /balance/deposits` is the one synchronous money path — `201` means credited. A `202` means the request was accepted and settlement was attempted — in the common case the money committed and fulfillment is queued. Two branches are weaker than that: an asynchronously-capturing card leaves the operation in `pending_payment` until the capture settles, and a charge whose fulfillment could not be scheduled is auto-refunded. Both are visible in the operation's `status`, which is why polling is mandatory rather than optional. ### `/domains/check` per-name results A `200` does not mean every name resolved. An entry that failed comes back as `{"name": ..., "error": ...}` with **no** `available` field, so branch on the presence of `error` before reading `available`. Price fields are `null` when we have no price for the name. `min_years` on each entry is the smallest purchasable term (2 for `.ai`) — multiply the per-year price by it for the true minimum. The batch is capped at 10 names per request; over that you get `400 too_many_names`. `pricing_source` is `"registry"` (quoted live for this name), `"baseline"` (the extension's standard rate — no registry quote is being shown) or `"unconfirmed"` (the lookup did not complete; do not trust it for a name that may be premium). It describes REGISTRATION only. `pricing_sources` breaks the same values out per action as `{"registration", "renewal", "transfer"}`, and they can legitimately differ on one name — a registry may quote a registration and not a transfer. ## Getting started safely Do a dry run before spending money: 1. Read `GET /api/v1/pricing` and `GET /api/v1/domains/check` — both unauthenticated, no key needed. 2. Create a **read-only** key (`domains:read`, `dns:read`, `account:read`) and walk `GET /me`, `GET /domains`, `GET /balance`. 3. Only then add `domains:write` + `billing:write`, and register one cheap name end-to-end while polling the operation. 4. Keep the prepaid balance small until your retry handling is proven — the balance is the cap on what a runaway loop can spend. ## Support Ask a question or report an API problem: `https://www.orbitfour.com/contact`.