Skip to content

Domain Registration

Registrar operations — registering, transferring, and managing TLD registrations and their contacts. This is distinct from Domains, which manages the hostnames attached to WordPress sites.

Feature-flagged

These endpoints are gated by the domain-registration feature flag. When it is off, every endpoint here returns 503 {"errors":[...],"code":"feature_disabled"}.

Account Scope Required (X-Auth-Account) — a missing header returns 400. OAuth scopes: reads domains:read, writes domains:write. Mutations require the can_edit role. A registration with an RTR process already in flight rejects further mutations with 409 registration_busy. Mutations may complete synchronously (200) or open an async registrar process (202 with pending_process). TLDs with no configured registrar return 503 registrar_unavailable; upstream registrar failures return 502.

Registering / transferring a domain is an order

The endpoints here check availability (check / suggestions) and manage existing registrations. To actually register or transfer a domain, place a paid order via POST /api/orders/domain (see the full param surface in Orders). To change a registration's registrant as a paid order, use POST /api/domain_registrations/:id/registrant_change (the billable counterpart to PATCH .../holder).


Domain Contacts

Reusable owner/admin/tech/billing contacts that back domain registrations. A contact is pinned to a single TLD on create because each registry validates a different set of fields. :id is the numeric contact ID.

Domain Contact shape
  • id: Integer
  • handle: String
  • first_name: String
  • last_name: String
  • organization: String
  • street_address: String
  • post_code: String
  • city: String
  • state: String
  • country: String | ISO-2
  • email: String
  • phone: String | E.164, +CC.NUMBER
  • extra_properties: Object | TLD-specific registry fields
  • validations: Array
  • split_from_id: Integer | null
  • created_at: DateTime
  • updated_at: DateTime

List Domain Contacts

GET /api/domain_contacts

Params (optional)
  • q: String | substring match against handle, organization, full name, or registration name

Returns { "domain_contacts": [ ... ] }.

View a Domain Contact

GET /api/domain_contacts/:id

Create a Domain Contact

POST /api/domain_contacts

Creates the contact at the registrar AND locally. Returns 201. You must pass tld_id or tld_name so the right registry rules apply. An unknown TLD returns 404 tld_not_found.

Params
  • tld_id: Integer | one of tld_id / tld_name required
  • tld_name: String | e.g. ".uk", ".com"
  • domain_contact: Object (required)
    • first_name, last_name, organization, street_address, post_code, city, state: String
    • country: String | ISO-2
    • email: String
    • phone: String | E.164 (+CC.NUMBER)
    • extra_properties: Object | TLD-specific registry fields

Update a Domain Contact

PATCH /api/domain_contacts/:id

Returns 200, or 202 with pending_process when the registry processes the update asynchronously. If any currently-associated domain rejects the change, the contact is transparently split (the original keeps the failing domains, a new contact is created for the rest). Pass tld_id/tld_name to override which TLD's validation rules apply (defaults to the first linked registration's TLD).

Params
  • tld_id | tld_name: see create
  • domain_contact: Object | same fields as create (all optional)

Delete a Domain Contact

DELETE /api/domain_contacts/:id

Only allowed when no registration uses this contact. Returns 204.

Errors
  • 422 contact_in_use | still linked to a registration
  • 422 tld_required_for_orphan | orphan contact + no tld_id/tld_name provided

Resend Verification Email

POST /api/domain_contacts/:id/resend_verification

For contacts on TLDs that require registrant validation (.au, .it, …). Returns { "status": "queued" }.


Domain Registrations

:id is the registration guid.

Domain Registration shape
  • id: String | guid
  • name: String
  • status: String
  • expires_at: DateTime
  • auto_renew: Boolean
  • locked: Boolean
  • privacy_protect: Boolean
  • dnssec_enabled: Boolean
  • server_transfer_prohibited: Boolean
  • local_contact_active: Boolean
  • cancellation_deadline_at: DateTime | when applicable
  • within_cancellation_window: Boolean | when applicable
  • nameservers: Array<String>
  • tld: Object | { id, name, registrar }
  • owner_contact / admin_contact / tech_contact / billing_contact: Object | see Domain Contact shape, or null
  • pending_async: Boolean
  • rtr_process_id: String
  • rtr_process_status: String
  • transfer_out_pending: Boolean
  • push_transfer_pending: Boolean
  • subscription_id: String
  • feature_flags: Object | { supports_privacy, supports_transfer_lock, supports_push_transfer, supports_dnssec, requires_contact_validation, authcode_behavior }
  • fees: Object | { privacy_enable, registrant_change } — each { amount_cents, currency } or null
  • created_at: DateTime
  • updated_at: DateTime

List Domain Registrations

GET /api/domain_registrations

Params (optional)
  • q: String | case-insensitive substring match against name

Returns { "domain_registrations": [ ... ] }.

View a Domain Registration

GET /api/domain_registrations/:id

Adds, when applicable:

Returned Params
  • domain_registration: Object | see shape above
  • dns_zone: Object | { id, name } if the account has a DnsZone for this name; else null
  • linked_site: Object | { id, name } if a Site uses this name as its hostname; else null
  • transfer_out: Object | live registry view, only while transfer_out_pending is true

Check Availability

POST /api/domain_registrations/check

Params
  • domain: String (required) | fully-qualified, e.g. "example.com"
Returned Params
  • domain: String | normalized lookup
  • available: Boolean | true only when the registry says it's available AND a create_price exists for this account
  • premium: Boolean
  • owned_by_account: Boolean
  • reason: String | null | registrar-supplied reason when unavailable
  • create_price: Object | null | { amount_cents, currency } — current orderable registration price
  • transfer_price: Object | null | orderable transfer-in price (set when unavailable)
Errors
  • 422 missing_domain / invalid_domain / unknown_tld
  • 502 registrar_unavailable

Domain Suggestions

POST /api/domain_registrations/suggestions

Params
  • domain: String (required) | base name or fully-qualified
Returned Params
  • domain: String | normalized lookup
  • suggestions: Array<Object>
    • domain: String
    • tld: String
    • source: String | ADAC categorization (e.g. "spinner", "premium")
    • available: Boolean
    • price: Object | { amount_cents, currency }
Errors
  • 422 missing_domain
  • 503 missing_api_key
  • 502 registrar_unavailable

Update a Registration

PATCH /api/domain_registrations/:id

Currently the only supported field is auto_renew. Toggling it keeps Stripe billing in sync and refunds within the TLD's cancellation window where applicable.

Params
  • domain_registration: Object
    • auto_renew: Boolean
Returned Params
  • domain_registration: Object | refreshed
  • scenario: String | enabled, clean_cancel, refund_cancel, or late_cancel
Errors
  • 422 cancellation_window_passed | cannot disable auto-renew after the window
  • 400 no_op | no supported fields in the update

Update Nameservers

PATCH /api/domain_registrations/:id/nameservers

Params
  • nameservers: Array<String> (required)

Returns 200 or 202 (with pending_process).

Errors
  • 422 nameservers_required | the list is empty / all blank

Toggle Transfer Lock

PATCH /api/domain_registrations/:id/lock

Toggles CLIENT_TRANSFER_PROHIBITED at the registry.

Params
  • locked: Boolean (required)
Errors
  • 422 lock_unsupported | TLD does not support a transfer lock
  • 422 server_transfer_prohibited | registry has prohibited transfers

Toggle WHOIS Privacy

PATCH /api/domain_registrations/:id/privacy

Params
  • privacy_protect: Boolean (required)

Disabling privacy is always free. Enabling on a metered TLD returns 402:

Fee-bearing response (402 Payment Required)
  • errors: Array
  • code: String | payment_required
  • price: Object | { amount_cents, currency, role: "privacy" }
  • portal_url: String
  • remediation: String
Errors
  • 422 privacy_unsupported | TLD does not support WHOIS privacy

Manage DNSSEC

PATCH /api/domain_registrations/:id/dnssec

Params
  • mode: String (required) | enable, disable, or add_keys
  • key_data: Array<Object> | required for enable / add_keys
    • flags: Integer
    • algorithm: Integer
    • publicKey: String | base64
Errors
  • 422 invalid_mode | mode missing or unknown
  • 422 dnssec_no_keys | enable/add_keys called without key_data

Change Registrant / Holder

PATCH /api/domain_registrations/:id/holder

Either edit the existing owner contact in place, or replace it.

Params
  • holder_contact: Object (required)
    • action_mode: String | edit_current or change
    • first_name, last_name, organization, street_address, post_code, city, state: String
    • country: String | ISO-2
    • email: String
    • phone: String | E.164
    • extra_properties: Object
    • confirm_reverification: "1" | required when edits touch a validated field
  • source_domain_contact_id: Integer | with change, reuse this existing contact
  • new_contact: Object | with change, create a new contact

The fee gate fires only on action_mode: change on a metered TLD (returns 402 payment_required with a price block whose role is registrant_change + portal_url); edit_current edits are always free. To perform the billable change directly (rather than being bounced to the portal), use POST .../registrant_change.

Errors
  • 402 payment_required | action_mode: change on a TLD that meters registrant changes (price role: "registrant_change", fee block + portal_url)
  • 409 reverification_required | validated fields changed without confirm_reverification: "1"
  • 404 source_contact_not_found | source_domain_contact_id not found or unused in this account

Order a Registrant (Holder) Change

POST /api/domain_registrations/:id/registrant_change

Places a paid order to change the registration's registrant (owner) contact — for TLDs that bill registrant changes (e.g. .com / .net via Verisign IRTP). This is the billable counterpart to PATCH .../holder: that endpoint makes free edits and bounces fee-bearing changes to the portal with 402; this one charges the account's default payment method off-session and applies the change asynchronously. Returns the same 202 Accepted order/cart envelope as POST /api/orders — poll the returned order via /api/orders/:id.

OAuth-blocked; needs an order-create user

Not in the domains:write allowlist — OAuth access tokens cannot reach it. Requires a user-scoped credential (orders are owned by a User): a system Account-bearer key returns 401 user_required, and a user lacking order-create permission returns 403 forbidden. Like other mutations, an in-flight RTR process rejects with 409 registration_busy.

Params (provide exactly ONE new-owner source)
  • source_domain_contact_id: Integer | reuse an existing contact owned by the account
  • new_contact: Object | create a new owner contact
    • first_name, last_name, country (ISO-2), email, phone (E.164 +CC.NUMBER): required
    • organization, street_address, post_code, city, state: String | optional
    • extra_properties: Object | optional, TLD-specific registry fields

Returns 202 with the order/cart envelope.

Errors
  • 401 user_required | system Account-bearer key (no user)
  • 403 forbidden | user lacks order-create permission
  • 403 trial_account | trial accounts cannot order
  • 409 registration_busy | an RTR process is already in flight
  • 422 source_contact_not_found | source_domain_contact_id not found (note: 422 here vs 404 on /holder)
  • 422 contact_invalid | new_contact missing required fields or registry-rejected (includes violations)
  • 422 no_registrant_change_price | no registrant-change price configured for this account/TLD
  • 422 cart_pay_failed | payment could not be initiated (any contact created for the order is torn down)

Update Role Contacts

PATCH /api/domain_registrations/:id/contacts

Set admin / tech / billing role contacts (the holder is changed via /holder). Each role is independently optional. For each present role choose one source:

  • {role}_contact: { source: "same_as_owner" }
  • {role}_contact: { source: "existing" } + {role}_source_domain_contact_id: <id>
  • {role}_contact: { source: "new" } + {role}_new_contact: { ...fields... }
Errors
  • 422 owner_contact_missing | same_as_owner used with no owner contact set

Retrieve the EPP / Auth Code

GET /api/domain_registrations/:id/epp_code

Requires the domains:write scope despite being a GET. Returns { "authcode": "..." }.

Errors
  • 422 server_transfer_prohibited | registry has prohibited transfers
  • 422 authcode_unavailable | TLD hides the authcode (authcode_behavior: hidden)
  • 502 authcode_unavailable | registrar failed to return the authcode

Force Re-Sync

POST /api/domain_registrations/:id/force_sync

Re-reads the domain from the registrar and syncs local state.

Returned Params
  • domain_registration: Object | refreshed
  • changed: Array<String> | field names that changed

Resolve a Pending Outbound Transfer

POST /api/domain_registrations/:id/transfer_out

Params
  • decision: String (required) | approve or reject
Errors
  • 422 no_pending_transfer_out
  • 422 invalid_decision

Push Transfer to Another Provider

POST /api/domain_registrations/:id/push_transfer

Registrar-to-registrar push transfer (no authcode). Returns 200 or 202.

Params
  • recipient: String (required) | destination registrar handle (e.g. "REG-IDS")
Errors
  • 422 push_transfer_unsupported / push_transfer_in_progress / domain_not_transferable

Resend Holder Contact Verification

POST /api/domain_registrations/:id/resend_contact_verification

For TLDs requiring registrant contact verification. Returns { "status": "queued" }.

Errors
  • 422 verification_not_applicable | TLD doesn't require validation, or no owner contact

Hosts (Glue Records)

Glue records for a registration. Nested under /api/domain_registrations/:domain_registration_id/hosts. The route param is :host_name.

List Hosts

GET .../hosts

Returned Params
  • hosts: Array<Object>
    • host_name: String | e.g. "ns1.example.com"
    • addresses: Array<Object>
      • address: String | IP literal
      • version: Integer | 4 or 6

Create a Host

POST .../hosts

The hostname must be a subdomain of the registration. Each address is auto-classified as IPv4/IPv6. Returns 201 { "host": { ... } }.

Params
  • host_name: String (required) | fully-qualified subdomain
  • addresses: Array<Object> (required, at least one)
    • address: String | IP literal (no CIDR)

Update a Host's Addresses

PATCH .../hosts/:host_name

Replaces the address set. Returns 200.

Params
  • addresses: Array<Object> (required) | same shape as create

Delete a Host

DELETE .../hosts/:host_name

Returns 204.


Processes

RTR processes attached to a registration — every async registrar operation opens one. Poll these after a 202. Nested under /api/domain_registrations/:domain_registration_id/processes.

List Processes

GET .../processes

Returns { "processes": [ ... ] }, newest first (raw RTR process records).

Get a Process

GET .../processes/:id

Returned Params
  • process: Object | raw RTR process body
  • log: Array<Object>

A process whose identifier doesn't belong to the registration returns 404.

Resume a Suspended Process

POST .../processes/:id/resend

Returns { "status": "queued" }.

Cancel a Process

DELETE .../processes/:id/cancel

Returns { "status": "cancelled" }.