v2026-10-07-204706
OpenAPI 3.1.0

OpusDNS API

Authentication

OpusDNS supports API authentication in two ways:

  • Direct API key authentication using the X-Api-Key header
  • OAuth token authentication using the /v1/auth/token endpoint

Creating an API key in the Dashboard

  • Create a new API key from the OpusDNS Dashboard under API Credentials
  • Store the generated key securely when it is shown to you.

Using the API key

Option 1: X-Api-Key header

Send your full OpusDNS API key in the X-Api-Key header on each request:

GET /v1/domains HTTP/1.1
Host: sandbox.opusdns.com
X-Api-Key: opk_your_full_api_key_here

This is the most direct way to authenticate.

Option 2: OAuth token flow

OpusDNS also supports retrieving a bearer token from the token endpoint. The endpoint accepts both application/x-www-form-urlencoded (per the OAuth 2.0 spec) and application/json:

POST /v1/auth/token HTTP/1.1
Host: sandbox.opusdns.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=organization_...&client_secret=...

Or with JSON:

POST /v1/auth/token HTTP/1.1
Host: sandbox.opusdns.com
Content-Type: application/json

{"grant_type": "client_credentials", "client_id": "organization_...", "client_secret": "..."}

Successful responses return a bearer token and expiry:

{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600
}

You can then use that token on subsequent requests:

GET /v1/domains HTTP/1.1
Host: sandbox.opusdns.com
Authorization: Bearer eyJ...

You can use this method when:

  • Your platform prefers standard OAuth-style bearer tokens
  • You want short-lived access tokens instead of sending the API key on every request
  • Your HTTP tooling already expects Authorization: Bearer ...

Resource IDs

The API uses extensively Type IDs: type-safe, K-sortable, globally unique identifier inspired by Stripe IDs. They can be easily identified with a format like prefix_01jxe1nzrmf78scaqbkjx0va0f. The prefix gives context to the ID - some examples include user, organization, domain. The rest of the ID is a 128-bit UUIDv7 encoded as a 26-character string using a modified base32 encoding. See formal specification for details.

This approach allows using unique IDs (UUIDv7), preventing iteration attacks, while also easily identifying the "namespace" of the ID. 01975c1f-a15a-7f6c-a5ce-b75fe33de079 is hardly distuingishable from 01975c1f-f120-7874-8dc2-de7d728bf261 by humans on first glance. However, when represented as Type IDs, they could be represented as user_01jxe1z8atfxpabknqbzhkvr3s and domain_01jxe1zw90f1t8vgpyfns8qwk1, immediately making it easier to differentiate between them.

Additionally, this gives type safety and additional validation that can be done. There's libraries available for many languages to make handling Type IDs easier. We're using them ourselves on the backend to quickly catch mistakes like passing the wrong Type ID (passing a user ID like user_01jxe1z8atfxpabknqbzhkvr3s where a domain ID domain_01jxe1zw90f1t8vgpyfns8qwk1 was expected).

Sandbox Environment

We provide a free sandbox environment where you can test and make sure your integration with OpusDNS runs smoothly. The sandbox is completely isolated, meaning all domains, configurations, and actions inside this environment have no effect on your live data or production systems. It is a full test system for safe experimentation and completely free to use.

Please register a new sandbox account at https://app.sandbox.opusdns.com and create a new API key there.

This sandbox provides a separate dashboard at app.sandbox.opusdns.com.

The sandbox API Base URL is https://sandbox.opusdns.com.

If you have any questions about the API or run into issues, please open a ticket at support@opusdns.com. We are happy to help!

Open-source SDKs and libraries

We're constantly building new features and services from SDKs and plugins to other exciting tools. Check out our GitHub to see the latest developments and try them out yourself!

OpusDNS Go Client

We offer a client implementation for go that supports all the features of our API.
Check out the client here!

Production environment

No authentication selected
Client Libraries

Authentication (Collapsed)

​

Endpoints for authentication.

Users (Collapsed)

​

Some requirements for users:

  • A user must belong to one and only one organization.
  • Both their email address and username must be unique within the whole system.

Product waitlist (Collapsed)

​

Endpoints for products that are not generally available yet and are reached through a waitlist.

A waitlisted product is invitation-only: OpusDNS invites an organization, its members apply, and OpusDNS decides on each application. Until your organization is invited, the product does not exist as far as this API is concerned - it is left out of the listing, and applying for it answers 404. Once you have applied, the product stays in your listing whatever happens to the invite afterwards.

Endpoints

Method Path Description
GET /v1/organizations/product-waitlist The waitlisted products you may see, and where your own application stands on each
POST /v1/organizations/product-waitlist/{product}/apply Apply for one product's waitlist

Both routes act on your own organization - the one you authenticated as - even when an X-Organization-Context header names one of its sub-organizations. Neither takes an organization in the path.

Product state

Each entry in the listing describes one product and your own standing with it. Applications are per user, so two members of one organization can see different states for the same product:

Field Description
product The product key, used as {product} in the apply path
display_name Human-readable product name
description What the product does
status pending, granted or rejected, or null while you have not applied
applied_on When you applied, or null
decided_on When OpusDNS decided, or null
can_apply Whether an apply from you would be accepted right now

An API key has no application of its own: it sees the products your organization is invited to, with status null and can_apply false.

Applying

POST /v1/organizations/product-waitlist/{product}/apply records your application as pending. Any member of an invited organization may apply. It requires a user token: an API-key token has no user to record.

The body is optional. Send {"note": "..."} to say what you would use the product for - it is the one thing OpusDNS has to go on when deciding, so it is worth filling in. A note is trimmed and may be up to 1000 characters; a blank or whitespace-only one is rejected rather than stored. Posting no body at all applies without a note.

One application per user per product. A second apply answers 409, whatever state the first one reached - a rejection is reconsidered by OpusDNS approving it after all, not by applying again. A colleague's application neither stands in for yours nor blocks it. can_apply in the listing tells you in advance whether an apply would be accepted.

Being granted a product means you, the user who applied, gain access to it. Your colleagues apply on their own, or ask OpusDNS if the whole organization should have it.

A decision is not final: OpusDNS can approve an application it rejected, and can withdraw a product it granted, which puts the application back to rejected.

Usage (Collapsed)

​

Endpoints for reporting your organization's usage of metered products.

These endpoints expose the usage your organization has incurred for a metered product, so you can monitor consumption over time and in total. Usage is reported as quantities only; billing costs are never included.

Endpoints

Method Path Description
GET /v1/organizations/{organization_id}/usage/{product} Usage as a time series, bucketed by granularity and grouped per sub-metric
GET /v1/organizations/{organization_id}/usage/{product}/summary Usage totals over a date range, grouped per sub-metric

{product} selects the metered product. Currently supported: ai_inference (AI token consumption from OpusDNS AI features). The response carries a top-level product field identifying the reported product.

Query Parameters

Parameter Description
start_date Inclusive start date (YYYY-MM-DD)
end_date Inclusive end date (YYYY-MM-DD)
granularity Time-bucket size for the series endpoint: day (default), week, or month

start_date must be on or before end_date, otherwise the request is rejected with 422 Unprocessable Content.

Reported Metrics

ai_inference

Each group reports token counts and request volume for one AI model:

Field Description
model The AI model the usage was recorded against
input_tokens Prompt (input) tokens consumed
output_tokens Generated (output) tokens
cache_read_tokens Tokens served from prompt cache
cache_write_tokens Tokens written to prompt cache
request_count Number of requests

Usage is scoped to your billing organization and requires the organization view permission.

Domain management (Collapsed)

​

Endpoints for creating and managing domains.

Domain References

Most domain endpoints accept a domain_reference path parameter. This can be either the domain ID (e.g., domain_01jt7deb8mftf8261a54v6m3ey) or the domain name (e.g., example.com). Both are interchangeable wherever domain_reference appears.

Transfer Lock

The transfer_lock field on a domain response reflects whether the domain has the clientTransferProhibited status. Setting or removing clientTransferProhibited via the statuses or status_changes fields will automatically update transfer_lock accordingly. The transfer_lock field itself is read-only.

DNS Zone Creation

When creating or transferring a domain, you can set create_zone to true to automatically provision a DNS zone on OpusDNS nameserver infrastructure. Zone creation and nameserver assignment are handled asynchronously after the domain operation completes, so there may be a short delay before the zone is created and nameservers are active.

Updating Domain Statuses

PATCH /v1/domains/{domain_reference} supports two mutually exclusive approaches for modifying client statuses. You cannot use both in the same request.

Option 1: statuses (absolute / declarative)

Provide the complete list of client statuses the domain should have. The API computes the diff against the current state and adds or removes statuses accordingly.

{
  "statuses": ["clientTransferProhibited", "clientDeleteProhibited"]
}

Use this when you know exactly which statuses the domain should end up with. Any current client statuses not in the list will be removed.

Option 2: status_changes (relative / delta)

Specify which statuses to add and/or remove relative to the domain's current state. Statuses not mentioned are left unchanged.

{
  "status_changes": {
    "add": ["clientTransferProhibited"],
    "remove": ["clientHold"]
  }
}

Use this when you want to make a targeted change without needing to know the domain's full current status set. This is especially useful in bulk operations where different domains may have different existing statuses.

Validation rules

  • At least one of add or remove must contain a value
  • A status cannot appear in both add and remove
  • Only client statuses are accepted — server statuses cannot be modified

When to use which

Scenario Recommended approach
Setting up a domain's statuses for the first time statuses
Locking many domains for transfer without touching other statuses status_changes with add
Releasing a hold on a specific domain status_changes with remove
Replacing all statuses as part of a known workflow statuses
Applying the same change across a batch of domains with varying existing statuses status_changes via domain_update_bulk

Blocking statuses

Some statuses prevent domain updates entirely. If the domain has any of the following, the update will be rejected:

  • clientUpdateProhibited
  • serverUpdateProhibited
  • pendingTransfer
  • pendingRestore
  • pendingDelete
  • redemptionPeriod

To update a domain that has clientUpdateProhibited, you can remove that status in a request that makes no other changes, or remove it first and then make additional changes in a follow-up request.

Domain management Operations

TLD specification (Collapsed)

​

Endpoints for retrieving TLD specifications.

Hosts (Collapsed)

​

Endpoints for managing host objects (nameserver glue records).

What is a host object?

A host object represents a nameserver hostname registered at a registry. When a nameserver hostname is subordinate to a domain in your portfolio (e.g. ns1.example.com under example.com), the registry requires a host object carrying one or more IP addresses — commonly known as glue records — so that resolvers can reach the nameserver even though its address lives inside the zone it serves.

These endpoints manage subordinate host objects: the parent domain of the hostname must be managed in your OpusDNS account. Hostnames whose parent domain is not in your account are rejected with ERROR_PARENT_DOMAIN_NOT_FOUND. External nameservers (e.g. your DNS provider's hostnames) do not need host objects — simply reference them by hostname when setting a domain's nameservers.

Referencing a host

GET, PUT and DELETE accept either the host ID (host_...) or the hostname as the {host_reference} path parameter.

Lifecycle

  • Create — POST /v1/hosts with the hostname and at least one IP address. The host object is created at the registry of the parent domain. Creating a host that already exists returns ERROR_HOST_ALREADY_EXISTS.
  • Update — PUT /v1/hosts/{host_reference} replaces the host's IP addresses with the provided list. If the provided addresses match the current state at the registry, the request succeeds without performing a registry update.
  • Delete — DELETE /v1/hosts/{host_reference} removes the host object. A host that is still in use as a nameserver by one or more domains cannot be deleted and returns ERROR_HOST_IN_USE.

Not all TLDs use host objects; for TLDs without host object support, creation is rejected with ERROR_HOST_OBJECTS_NOT_SUPPORTED.

Contacts (Collapsed)

​

Endpoints for creating and managing contacts.

Contact Verification Attestation

POST /v1/contacts/{contact_id}/verifications/attest submits attestations for a contact. The request is forwarded synchronously to the contact-verification service, which is the source of truth for the response. When DENIC is among the registries that flagged the contact for verification, OpusDNS additionally queues a registry-side attest job behind the scenes.

Workflow

When a registry signals that a contact requires verification (e.g. DENIC's contactVerificationRequired notification), OpusDNS marks each affected domain with the VERIFICATION_REQUIRED status tag. Submit attestations via this endpoint and the response reports the resulting per-claim verification state.

Request format

The request body wraps the attestations under attestations. Fields are snake_case and enum values are UPPERCASE.

{
  "attestations": [
    {
      "claim": "NAME",
      "method": "AUTH",
      "proof": "IDCARD",
      "attestation_reference": "ticket-12345"
    }
  ]
}
Field Type Notes
claim enum NAME, ADDRESS, EMAIL, PHONE, LEGAL_ENTITY
method enum AUTH, VDIG, ELECTRONIC_DOCUMENT, PHYSICAL_DOCUMENT, BVR, PVR, DATA, REACHABILITY
proof enum IDCARD, PASSPORT, POPULATION_REGISTER, RESIDENCE_PERMIT, PROOF_OF_ARRIVAL, DRIVERS_LICENCE, COMPANY_REGISTER, COMPANY_STATEMENT, BANK_ACCOUNT, ONLINE_PAYMENT_ACCOUNT, UTILITY_ACCOUNT, BANK_STATEMENT, TAX_STATEMENT, WRITTEN_ATTESTATION, DIGITAL_ATTESTATION, POSTAL_VER_TRANSACTION_LOG, EMAIL_VER_TRANSACTION_LOG, PHONE_VER_TRANSACTION_LOG, ADDRESS_DATABASE
attestation_reference string Caller-supplied reference (e.g. internal ticket ID, document hash). Up to 255 chars.

DENIC-only constraint: when the contact has DENIC verification pending, all items in a single request must share the same method, proof, and attestation_reference. DENIC's contactUPDATE carries one verification block per call; the API enforces this only when DENIC is in scope so callers see a synchronous 400 instead of a partial-success. Contacts not pending DENIC verification accept heterogeneous batches.

Response

The endpoint returns HTTP 200 OK with an object containing the per-claim verification state from the contact-verification service:

{
  "verifications": [
    {
      "claim": "NAME",
      "state": "VERIFIED",
      "method": "AUTH",
      "proof": "IDCARD",
      "attestation_reference": "ticket-12345",
      "verified_on": "2026-05-15T12:00:00Z",
      "expires_on": null
    }
  ]
}
Field Type Notes
claim enum The claim this entry describes.
state enum UNVERIFIED, VERIFIED, IN_PROGRESS, EXPIRED.
method enum or null The method that produced the current state.
proof enum or null The proof that produced the current state.
attestation_reference string or null Reference recorded with the current state.
verified_on RFC3339 datetime or null When the claim entered the VERIFIED state.
expires_on RFC3339 datetime or null When the current verification expires.

When DENIC verification was pending for the contact, the registry-side contactUPDATE is queued behind the scenes and emits a CONTACT/VERIFICATION/SUCCESS or CONTACT/VERIFICATION/FAILURE poll-message event keyed on contact_id. The VERIFICATION_REQUIRED status tag on the affected domains is cleared on success.

Error codes

Code Status Meaning
ERROR_DOMAIN_VERIFICATION_INCONSISTENT_METHOD_PROOF 400 DENIC verification is pending for this contact and items in the request disagree on method, proof, or attestation_reference.
ERROR_DOMAIN_VERIFICATION_INVALID_COMBINATION (async) The registry rejected the DENIC attestation as an invalid method/proof/reference combination. Surfaces as a CONTACT/VERIFICATION/FAILURE poll-message event.
ERROR_DOMAIN_VERIFICATION_REGISTRY_REJECTED (async) DENIC accepted the submission at the protocol level but indicated the verification attempt itself was not successful. Surfaces as a CONTACT/VERIFICATION/FAILURE poll-message event.

Event handling (Collapsed)

​

Endpoints for interacting with events.

Availability (Collapsed)

​

Endpoints for checking domain availability.

DNS Management (Collapsed)

​

Endpoints for managing DNS zones and records.

Zone, RRset, and Record

OpusDNS uses standard DNS terminology, and the distinction matters when choosing which endpoint to call:

  • Zone — a DNS zone (e.g., example.com). Contains many rrsets.
  • RRset (Resource Record Set) — all records that share the same (name, type) tuple within a zone (e.g., the set of all A records for www.example.com). An rrset is the smallest unit DNS resolvers return in a query response.
  • Record — a single rdata value inside an rrset (e.g., 203.0.113.10). An rrset contains one or more records.

Three different update endpoints let you operate at different levels of granularity:

Endpoint Scope Semantics
PUT /v1/dns/{zone_name}/rrsets Whole zone Replaces all user-managed rrsets in the zone
PATCH /v1/dns/{zone_name}/rrsets RRset-level Upserts or removes entire rrsets by (name, type)
PATCH /v1/dns/{zone_name}/records Record-level Upserts or removes individual rdata values inside an rrset

All three endpoints refuse writes targeting rrsets you cannot manage. SOA, DNSKEY, DS, and the NS rrset at the zone apex are system-managed by OpusDNS: a payload rrset or patch op targeting one — upsert or remove — is rejected with a 409 protected-rrset error (these rrsets are reported with "protected": true in zone responses). To change the nameservers a domain delegates to, update the nameservers on the domain instead.

NS rrsets below the apex delegate a subdomain to other nameservers and are managed like any other record type, on all three endpoints and at zone creation. Two caveats: records (including domain/email forwards and parking) at or below a delegated name stop resolving publicly, because the delegated nameservers are authoritative for that subtree; and delegations from DNSSEC-signed zones are insecure delegations — child DS records are not supported, so the delegated subtree is not covered by the parent's chain of trust.

PUT /v1/dns/{zone_name}/rrsets — Replace all rrsets

Supply the complete set of rrsets the zone should contain. Any existing user-managed rrset not in the request is removed. System-managed rrsets cannot be set or removed via this endpoint; omit them from the payload.

Use this when you have authoritative, end-state data for a zone and want to reset it to that state.

PATCH /v1/dns/{zone_name}/rrsets — Upsert or remove whole rrsets

Submit an ops[] array where each op targets an rrset by (name, type):

  • op: "upsert" — create or replace the rrset at (name, type) with the records you provide. Any records that previously existed at that (name, type) are discarded.
  • op: "remove" — delete the entire rrset at (name, type) (pass records: []).
{
  "ops": [
    {
      "op": "upsert",
      "rrset": {
        "name": "www",
        "type": "A",
        "ttl": 300,
        "records": [
          {"rdata": "203.0.113.10"},
          {"rdata": "203.0.113.11"}
        ]
      }
    },
    {
      "op": "remove",
      "rrset": {"name": "old", "type": "CNAME", "ttl": 300, "records": []}
    }
  ]
}

Use this when you want to define the complete set of records for a given (name, type) — e.g., "www should have exactly these two A records". Any other A records for www that currently exist will be removed.

PATCH /v1/dns/{zone_name}/records — Upsert or remove individual records

Submit an ops[] array where each op targets a single rdata value inside an rrset. Note the op payload uses a single rdata field, not records[].

  • op: "upsert" — add or update that one rdata value. The containing rrset is created if it doesn't exist; sibling records in the rrset are left alone.
  • op: "remove" — remove just that one rdata value. Other records in the same rrset remain.
{
  "ops": [
    {
      "op": "upsert",
      "record": {
        "name": "@", "type": "MX", "ttl": 300,
        "rdata": "5 new-mail.example.com."
      }
    },
    {
      "op": "remove",
      "record": {
        "name": "@", "type": "MX", "ttl": 300,
        "rdata": "10 old-mail.example.com."
      }
    }
  ]
}

Use this when you need to surgically add or remove an individual record without disturbing sibling records in the same rrset — e.g., "add one more MX target", "rotate one A record IP out of a pool".

Choosing the right endpoint

Scenario Endpoint
Migrating a zone from another provider; want to set it to a known state PUT /rrsets
Replacing all records of a given name+type (e.g., the full MX set) PATCH /rrsets with upsert
Adding or removing a single record without touching its siblings PATCH /records
Swapping a name from CNAME to A PATCH /rrsets with remove + upsert

For bulk variants that operate across many zones in a single request, see the Jobs documentation.

Whitelabel Branding (Collapsed)

​

Endpoints for managing an organization's whitelabel branding configuration.

An organization has one whitelabel config, on one tier at a time. GET /v1/whitelabel-branding returns it, or 404 when nothing is set up.

The base tier serves the customer on a subdomain of a zone OpusDNS owns, so it needs nothing from them but a name. The plus tier serves them on their own domain for the fully unbranded experience.

POST /v1/whitelabel-branding buys a whitelabel on the chosen tier: {"tier": "base", "label": "acme", "period": {"value": 1, "unit": "y"}}, or for plus {"tier": "plus", "label": "acme", "hostname": "customer.com", "dashboard_subdomain": "dash", "auth_subdomain": "auth", "period": {...}}. A label is required for both tiers: every whitelabel keeps a managed subdomain, composed as app.<label>.<base-tier suffix> (dashboard) and auth.<label>.<base-tier suffix> (login). For base, that composed pair is what gets served: onboarding skips domain verification (the zone is OpusDNS-owned) but still points each host at the whitelabel edge, which issues a per-host certificate on demand. For plus, the customer's own domain is served instead (omitting dashboard_subdomain serves the dashboard on the apex); the domain must be an OpusDNS-hosted zone owned by the organization, which onboarding verifies before pointing both hosts at the whitelabel edge. Set create_zone: true to have onboarding stand the zone up if it does not already exist. The managed base pair is published alongside the custom domain as a backup address. Labels are validated on the way in, and branding-service additionally rejects reserved ones (that rejection surfaces as a 400). Because every whitelabel composes its label, both tiers are only purchasable in environments configured with a base-tier zone; elsewhere create answers 403.

Both tiers are paid, and a whitelabel is a single subscription: the tiers are alternatives, not add-ons.

POST /v1/whitelabel-branding/tier upgrades a base whitelabel to plus in place, with the plus create's domain fields (hostname, dashboard_subdomain, auth_subdomain - no tier, no period). The subscription keeps its term and renewal date and simply renews at the plus price; a yearly term additionally settles a one-time prorated fee for the remaining whole months now, a monthly term pays nothing now. The managed subdomain label is kept. Downgrades are not offered (cancel and rebook instead), so a whitelabel already on plus answers 409.

POST /v1/whitelabel-branding/recheck re-runs onboarding - the customer's retry after fixing their DNS setup. A plus recheck may also carry a corrected hostname plus subdomains to re-point the config before re-running, which is accepted only before provisioning has started. A base whitelabel takes no repoint: its hostnames come from its label, so it is moved with PATCH instead.

A disabled whitelabel answers 409 to both /recheck and /tier: re-enable it first (PATCH {"enabled": true} alone re-runs onboarding).

PATCH /v1/whitelabel-branding changes the config without touching its subscription or price. {"label": "newname"} moves the managed subdomain to a different label at any time, including while the whitelabel is active. On base this changes the served host; on plus it moves the backup pair, leaving the custom domain unchanged (re-point that through recheck). {"enabled": false} stops serving the whitelabel: its login client is disabled and OpusDNS withdraws its routing - the edge records for the served hosts are removed and the managed subdomain unpublished (a plus host whose DNS is still pointed at the edge keeps reaching the branded dashboard, but its login stays disabled). The configuration, the branding document and the provisioning history are kept, so {"enabled": true} brings it back without re-provisioning. Like the other write routes this is asynchronous: the response is 202 once the change is recorded, and a job converges the login client and the routing to it shortly after. Disabling a whitelabel does not cancel its subscription; billing continues until the subscription itself is cancelled.

Domain parking (Collapsed)

​

Reports (Collapsed)

​

Endpoints for generating and downloading reports.

Reports provide exportable snapshots of your domain, DNS, and billing data. Reports are generated asynchronously and can be downloaded as ZIP files once complete.

Report Types

Type Description
domain_inventory Full inventory of all domains in the organization
dns_zone_summary Summary of all DNS zones and their configuration
dns_zone_records Detailed export of all DNS records across zones
billing_transactions Billing transactions for the current month to date
billing_transactions_monthly Billing transactions for the previous calendar month
registrar_portfolio_pdf Credential-scoped registrar portfolio report as a PDF inside the ZIP download

Billing Transaction Reports

Billing transaction reports export all completed transactions as a CSV with the following columns: product_reference, action, period, product_type, amount, currency, completed_on.

There are two billing report types:

  • billing_transactions — On-demand report covering the current month to date (1st of the month through the time of the request). Request this via the API like any other report type.
  • billing_transactions_monthly — Automatically generated on the 1st of each month at 5 AM UTC, covering the full previous calendar month. These are scheduled reports and do not need to be manually requested.

Both types maintain their own retention of 30 reports each, so requesting on-demand reports will not displace the monthly archive.

Registrar Portfolio PDF Reports

Registrar portfolio reports are requested with POST /v1/reports and require registrar_credential_id:

{
  "report_type": "registrar_portfolio_pdf",
  "registrar_credential_id": "registrar_credential_..."
}

The report uses registrar data from the registrar-elements OpenSearch index, populated by the scheduled daily registrar syncs. Report generation does not trigger a sync; it uses the most recently synced data as-is. If the credential has no portfolio data yet (for example, a credential created since the last sync), the report ends as empty rather than failed: there is nothing to report on yet, and the next run after a sync produces the real report.

The downloaded ZIP contains registrar-portfolio-{report_typeid}.pdf. Before each PDF generation, the report service loads the organization's current available TLD list from the backend and uses it to mark TLDs in the portfolio as available or unavailable. The first version includes the sections backed by indexed data: portfolio overview, domains/statuses/TLDs/nameservers, TLD availability, expiry summaries, contact validation summary, and DNS record summaries. Additional sections that require new OpenSearch fields will be added explicitly when those fields are indexed.

Report Lifecycle

A report transitions through the following statuses:

  1. pending — Report has been queued for generation
  2. generating — Report is being built
  3. completed — Report is ready for download
  4. failed — Generation failed
  5. empty — The organization had no data for this report type (for example no domains, zones or forwards). No file is produced and download returns 409 Conflict. record_count is 0.

completed, failed and empty are terminal.

Usage Pattern

  1. Request a report — POST /v1/reports with the desired report_type
  2. Poll for completion — GET /v1/reports/{report_id} until status is terminal (completed, empty or failed)
  3. Download — GET /v1/reports/{report_id}/download returns the report as a streamed ZIP file

Downloading Reports

GET /v1/reports/{report_id}/download streams the report file directly to the client. The report must have status completed before it can be downloaded — calling this endpoint on a report that is still pending or generating, or that ended empty, returns 409 Conflict.

Response details:

Header Value
Content-Type application/zip
Content-Disposition attachment; filename={report_type}-{report_id}.zip
Content-Length File size in bytes (included when known)

The response body is streamed in chunks, so clients should read the body incrementally rather than buffering it entirely in memory. When Content-Length is present, it can be used to display download progress.

Trigger Types

Type Description
on_demand Manually requested via the API
scheduled Automatically generated on a recurring schedule

Rate Limiting

Report creation is rate-limited at two levels:

  • Cooldown — After creating a report, you must wait 5 minutes before requesting another report of the same type. Requests during the cooldown return 429 Too Many Requests with a Retry-After header.
  • Hourly limit — Only one report per type per organization per clock hour (UTC) can be generated. Duplicate requests within the same hour will not produce an additional report.

Retention

The 30 most recent reports of each type are kept per organization. Older reports and their associated files are automatically deleted when a new report is generated. Of the empty reports of a type, only the most recent one is kept, so scheduled runs for an organization without data do not fill the list with identical entries. registrar_portfolio_pdf is exempt from that rule, because its reports are per registrar credential and an empty result for one credential says nothing about another.

Listing Reports

Use GET /v1/reports to list reports with optional filters:

Parameter Description
report_type Filter by report type (repeatable)
status Filter by report status (repeatable)
trigger_type Filter by trigger type
created_after Only reports created after this timestamp
created_before Only reports created before this timestamp

Jobs (Collapsed)

​

Endpoints for submitting and tracking batch command execution.

The Jobs API enables asynchronous execution of bulk operations through batches. Submit multiple commands in a single request and poll for completion status.

Supported Commands

Single-resource commands

Command Description
domain_create Register a new domain
domain_update Modify domain settings (contacts, nameservers, statuses, renewal mode)
domain_transfer Initiate an inbound domain transfer
dns_zone_create Create a new DNS zone with optional records
dns_zone_update Update an existing DNS zone
contact_create Create a new contact

Bulk commands (template + instances)

Bulk commands use a template + instances pattern. The template defines shared settings applied to all items, while each instance specifies a target resource and optional per-resource overrides.

Command Description
domain_create_bulk Register multiple domains
domain_update_bulk Update multiple domains (statuses, nameservers, contacts, renewal mode)
domain_transfer_bulk Transfer multiple domains
dns_zone_create_bulk Create multiple DNS zones
dns_zone_update_bulk Replace rrsets (and zone attributes like DNSSEC) across multiple DNS zones
dns_zone_patch_rrsets_bulk Upsert or remove entire rrsets (by name + type) across multiple zones
dns_zone_patch_records_bulk Upsert or remove individual rdata values across multiple zones
contact_create_bulk Create multiple contacts
parking_create_bulk Create parking pages for multiple domains
parking_enable_bulk Enable parking on multiple domains
parking_disable_bulk Disable parking on multiple domains
parking_delete_bulk Delete parking pages for multiple domains

Batch Lifecycle

Each job within a batch transitions through states:

  1. blocked — Waiting for eligibility (scheduled via not_before, or awaiting capacity)
  2. queued — Eligible and awaiting processing
  3. paused — Paused; must be explicitly resumed to continue
  4. running — Currently being executed
  5. succeeded — Completed successfully
  6. failed — Execution failed (check error_class and error_message)
  7. canceled — Job was canceled before completion
  8. dead_letter — Permanently failed after exhausting retries

A batch itself has a status field that is either pending (jobs are still in progress) or complete (all jobs have reached a terminal state).

Usage Pattern

  1. Submit a batch — POST /v1/jobs with an array of commands
  2. Poll for status — GET /v1/jobs/{batch_id} to check progress counts and progress_percentage
  3. Review results — GET /v1/jobs/{batch_id}/jobs to see individual job outcomes

Managing Batches

  • Pause — POST /v1/jobs/{batch_id}/pause pauses all eligible jobs in the batch
  • Resume — POST /v1/jobs/{batch_id}/resume resumes all paused jobs
  • Retry — POST /v1/jobs/{batch_id}/retry retries failed and dead-lettered jobs in the batch (see Retrying Failed Jobs)
  • Cancel — DELETE /v1/jobs/{batch_id} cancels all pending jobs in the batch

Individual jobs can also be paused, resumed, retried, or canceled via the /v1/job/{job_id} endpoints.

Retrying Failed Jobs

If jobs in a batch end up in failed or dead_letter state — for example because the account had insufficient funds at the time the batch was processed — you can re-attempt them without rebuilding the batch from scratch.

  • Single job: POST /v1/job/{job_id}/retry
  • Whole batch: POST /v1/jobs/{batch_id}/retry

What gets retried

Only jobs in failed or dead_letter status are eligible. Jobs in any other status are left unchanged:

  • succeeded — already complete; retrying returns 409 Conflict
  • canceled — you explicitly canceled these, so the retry won't undo that; returns 409 Conflict (single) or is silently skipped (batch)
  • queued, blocked, paused, running — still in progress; not touched

Calling batch retry on a mixed-state batch returns retried_count (the number of failed / dead_letter jobs that were reset for retry); everything else is left alone. If the batch has no retryable jobs, retried_count is 0. The response also splits that total into queued_count (jobs dispatched immediately) and blocked_count (jobs held behind the topic's rate limit or backlog — these are released automatically as capacity frees up).

What happens to a retried job

Each retried job is reset to a fresh attempt:

  • status → queued, or blocked if the job's topic is rate-limited and has no capacity (or a higher-priority backlog is waiting) — a blocked job is released to queued automatically as capacity frees up
  • attempts → 0
  • not_before → now
  • error_class and error_message → cleared
  • The job is republished for worker pickup once it is queued

The original payload, idempotency_key, correlation_id (batch id), max_attempts, and backoff configuration are preserved. Each retry is processed under the same worker idempotency guarantees as the original — for billing operations specifically, a retried job will not double-charge an account that was already debited.

Filtering by error type

A batch may contain a mix of failures that you do and don't want to retry. For example, after adding funds to your account, you may want to retry only the jobs that failed with BillingInsufficientFundsError, while leaving alone jobs that failed for other reasons (e.g. an invalid domain name that should not be re-attempted).

Pass one or more error_class query parameters to filter:

POST /v1/jobs/{batch_id}/retry?error_class=BillingInsufficientFundsError
POST /v1/jobs/{batch_id}/retry?error_class=BillingInsufficientFundsError&error_class=DomainRegistryTemporaryError

Multiple values are OR'd — a job is retried if its error_class matches any of the supplied values. Omitting the filter retries all failed and dead_letter jobs in the batch.

The error_class for each failed job is visible on the individual job response (GET /v1/jobs/{batch_id}/jobs), so the typical flow is:

  1. List the failed jobs in the batch and group by error_class.
  2. Determine which failures are recoverable.
  3. Call retry with the relevant error_class filter.

Limits

  • Maximum 50,000 commands per batch
  • Bulk commands support up to 1,000 instances per command

Scheduling

Use not_before to schedule batch execution for a future time (UTC timestamp). If not provided, processing begins as soon as a worker is available.

Idempotency

Each command can include an optional idempotency_key to prevent duplicate execution. If a command with the same idempotency key has already been processed, it will be skipped.

Domain Status Updates in Batches

The domain_update and domain_update_bulk commands support two mutually exclusive approaches for modifying domain statuses. These work the same way as the PATCH /v1/domains/{domain_reference} endpoint — see the Domain management documentation for full details on statuses vs status_changes.

Using status_changes in bulk templates

The domain_update_bulk command is particularly useful with status_changes when you need to apply the same relative status change across many domains. Set status_changes in the template and list the target domains as instances. Each instance identifies a domain by either name or domain_id (but not both):

{
  "command": "domain_update_bulk",
  "payload": {
    "template": {
      "status_changes": {
        "add": ["clientTransferProhibited"]
      }
    },
    "instances": [
      { "name": "example.com" },
      { "name": "example.net" },
      { "name": "example.org" }
    ]
  }
}

Per-domain overrides

If an instance provides its own statuses or status_changes field, it completely overrides the template's status settings for that domain. This lets you apply a default change to most domains while handling exceptions individually:

{
  "command": "domain_update_bulk",
  "payload": {
    "template": {
      "status_changes": {
        "add": ["clientTransferProhibited"]
      }
    },
    "instances": [
      { "name": "example.com" },
      { "name": "special-case.com", "status_changes": { "remove": ["clientHold"] } }
    ]
  }
}

DNS Zone Bulk Commands

The three dns_zone_*_bulk commands operate at different levels of granularity. Picking the right one depends on how much of the zone's existing state you want to preserve.

Command Operates on Use when
dns_zone_update_bulk Whole zone Resetting rrsets (or zone attributes like DNSSEC) across many zones
dns_zone_patch_rrsets_bulk Whole rrsets by (name, type) Replacing all records of a given type at a given name
dns_zone_patch_records_bulk Individual rdata values Adding or removing a single record without touching siblings

These are the bulk equivalents of the single-zone PUT and PATCH DNS endpoints — see the DNS Management documentation for the underlying semantics. The bulk variants accept up to 100 ops per instance for the two patch commands.

dns_zone_update_bulk

Bulk equivalent of PUT /v1/dns/{zone_name}/rrsets plus zone-level attribute updates. For each zone, whatever rrsets you supply become the zone's rrsets — anything not listed is removed. System-managed record types (SOA, DNSKEY, DS) cannot be set via this command. This is also the command to flip DNSSEC on or off across many zones.

Both rrsets and dnssec_status are partial-update fields:

Value Meaning
omitted (in both template and instance) leave that field unchanged on the zone
[] (rrsets only) delete all records
[...] / a status value replace

An instance with neither rrsets nor dnssec_status (after the template is merged in) is rejected as a no-op.

Payload: template (shared rrsets and dnssec_status) + instances[] (per-zone overrides).

{
  "command": "dns_zone_update_bulk",
  "payload": {
    "template": {
      "rrsets": [
        {"name": "@", "type": "A", "ttl": 300, "records": [{"rdata": "203.0.113.10"}]}
      ],
      "dnssec_status": "enabled"
    },
    "instances": [
      { "name": "example.com" },
      {
        "name": "example.net",
        "rrsets": [
          {"name": "@", "type": "A", "ttl": 300, "records": [{"rdata": "203.0.113.20"}]}
        ]
      }
    ]
  }
}

Bulk-toggle DNSSEC across many zones without touching their records — omit rrsets everywhere:

{
  "command": "dns_zone_update_bulk",
  "payload": {
    "template": { "dnssec_status": "enabled" },
    "instances": [
      { "name": "example.com" },
      { "name": "example.net" },
      { "name": "example.org" }
    ]
  }
}

dns_zone_patch_rrsets_bulk

Bulk equivalent of PATCH /v1/dns/{zone_name}/rrsets. Each instance is { zone_name, ops[] }, where each op targets an rrset by (name, type):

  • op: "upsert" — create or replace the rrset with the records you provide. Any prior records for that (name, type) are discarded.
  • op: "remove" — delete the entire rrset (pass records: []).
{
  "command": "dns_zone_patch_rrsets_bulk",
  "payload": {
    "instances": [
      {
        "zone_name": "example.com",
        "ops": [
          {
            "op": "upsert",
            "rrset": {
              "name": "www", "type": "A", "ttl": 300,
              "records": [
                {"rdata": "203.0.113.10"},
                {"rdata": "203.0.113.11"}
              ]
            }
          },
          {
            "op": "remove",
            "rrset": {"name": "old", "type": "CNAME", "ttl": 300, "records": []}
          }
        ]
      }
    ]
  }
}

dns_zone_patch_records_bulk

Bulk equivalent of PATCH /v1/dns/{zone_name}/records. Each instance is { zone_name, ops[] }, where each op targets a single rdata value inside an rrset. Note the op payload uses a single rdata field, not records[].

  • op: "upsert" — add or update that one rdata value (creates the rrset if it doesn't exist). Sibling records in the rrset are untouched.
  • op: "remove" — remove just that one rdata value. Other records in the rrset remain.
{
  "command": "dns_zone_patch_records_bulk",
  "payload": {
    "instances": [
      {
        "zone_name": "example.com",
        "ops": [
          {
            "op": "upsert",
            "record": {
              "name": "@", "type": "MX", "ttl": 300,
              "rdata": "5 new-mail.example.com."
            }
          },
          {
            "op": "remove",
            "record": {
              "name": "@", "type": "MX", "ttl": 300,
              "rdata": "10 old-mail.example.com."
            }
          }
        ]
      }
    ]
  }
}

Models