Authentication
All API requests require authentication using an API key. Pass the key in the Authorization header as a Bearer token. The API key must start with the nxd_ prefix.
Plan requirement: the REST API, the ISPmanager-compatible API and webhooks are available on Pro plans and above, as are the two capabilities this reference also covers: DNSSEC and secondary (slave) zones. A key on a plan without them gets a 403.
Authorization: Bearer nxd_your_api_key_here
The API authentication layer is stateless – each request is authenticated independently. There are no sessions or cookies.
Keep your API key secret. Do not share it in client-side code, public repositories, or URLs. If a key is compromised, revoke it immediately and create a new one.
Authentication Errors
| Status | Cause |
|---|---|
401 |
Missing or invalid API key, expired key, or cancelled account |
403 |
The key lacks the scope the endpoint needs, or the account's plan does not include API access - the plan gate answers 403, not 401, on every path. |
Response Format
All responses are JSON. Successful responses have the following structure:
Single Resource
{
"status": "success",
"data": {
"id": "xK9mQ2",
"name": "example.com",
...
}
}
Paginated List
{
"status": "success",
"data": [ ... ],
"meta": {
"total": 150,
"page": 1,
"per_page": 25,
"last_page": 6
}
}
Error Response
{
"status": "error",
"error": {
"code": "validation_error",
"message": "Validation failed.",
"details": {
"name": ["Domain name is required."]
}
}
}
Errors raised deeper in the platform - a quota, a blocked domain, a nameserver failure - carry the same error object but no status field. Branch on error.code, which is stable, rather than on the presence of status. Messages are English on every instance and in every locale by contract; error.code is the machine-readable part.
Public IDs
Every resource is identified by an opaque id (for example xK9mQ2), used in URL paths. Raw numeric database IDs are never exposed or accepted.
Pagination
List endpoints that return paginated results accept the following query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 | Page number (minimum 1) |
per_page |
integer | 25 | Items per page (1–100) |
Rate Limits
Requests are counted per account, not per key, in a sliding one-minute window, and the budget comes from your plan - see the plan comparison on the pricing page for the figure. Unauthenticated requests are counted per IP address. Every response carries the current state of your budget, so you never have to guess:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the window. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Unix timestamp at which the window rolls over. |
Retry-After | Seconds to wait, sent on a 429. |
For bulk work - importing a large zone, reconciling hundreds of records - read X-RateLimit-Remaining and pause before it reaches zero rather than retrying after a 429. The CLI does this for you.
A few operations carry their own, longer window on top of the request budget: one zone may be moved to another nameserver group three times a day. The response says which limit was hit.
Zones
Manage DNS zones. Requires zones.read for read operations and zones.write for write operations.
GET /v1/zones
List all zones for the authenticated user.
Query parameters
search– filter zones by namepage,per_page– pagination
Response fields
id, name, type (master/slave), status, ns_group, created_at, updated_at
GET /v1/zones/{id}
Get detailed information about a specific zone, including SOA data, nameservers, and record count.
Additional response fields
records_count, soa (primary_ns, admin_email, serial, refresh, retry, expire, minimum), nameservers (array), ns_group (id, slug, name)
POST /v1/zones
Create a new DNS zone. Refused with 409 when the domain already exists or overlaps another account's zone, and with 422 when the plan's zone limit is reached or the domain is blocked.
Request body (JSON)
{
"name": "example.com",
"type": "master",
"ns_group": "eu"
}
A secondary (slave) zone instead:
{
"name": "example.com",
"type": "slave",
"master_ip": "203.0.113.10"
}
name(required) – domain nametype–"master"(default) or"slave"ns_group– NS group to assign the zone to (optional, uses default if omitted)master_ip- required for secondary zones; must be a valid public IP address
Returns 201 Created with the zone object.
PATCH /v1/zones/{id}
Move the zone into another nameserver group. This is the only PATCH on the API. Scope: zones.write.
Request Body (JSON)
{
"ns_group": "eu"
}
Returns the zone as it stands after the move, in the same shape as GET /v1/zones/{id}. The group is named by its slug - the values GET /v1/ns-groups lists for your account; anything else is rejected with a 400.
The zone keeps answering throughout: the new nameservers are provisioned before the response, and the previous ones keep serving while resolvers refresh. Update the delegation at your registrar afterwards. A zone may be moved three times a day; beyond that the call returns 429.
DELETE /v1/zones/{id}
Delete a zone and all its records.
Returns 204 No Content on success.
GET /v1/zones/{id}/export
Export a zone in BIND format or as structured JSON.
Query parameters
format–"bind"(default) returns BIND zone file text;"json"returns a structured array of records with name, type, content, and ttl
Records
Manage DNS records within a zone. All record endpoints are nested under a zone. Requires records.read for read operations and records.write for write operations.
GET /v1/zones/{zoneId}/records
List all records in a zone.
Query parameters
type– filter by record type (e.g.A,CNAME,MX)name– filter by record name (substring match)search– search in both name and content
Response fields
id, name, type, content, ttl, disabled, fields (type-specific parsed fields)
GET /v1/zones/{zoneId}/records/{recordId}
Get a single record by its ID.
POST /v1/zones/{zoneId}/records
Create a new DNS record.
Request body (JSON)
{
"type": "A",
"name": "www",
"ttl": 3600,
"content": "93.184.216.34"
}
type(required) – record type (A, AAAA, CNAME, MX, TXT, SRV, CAA, NS, PTR, ALIAS, DNAME, DS, TLSA)name– record name relative to the zone (default:@for zone apex)ttl– time to live in seconds (default: 3600)content– record value (IP for A/AAAA, hostname for CNAME/NS/PTR, text for TXT, mail server for MX)
Type-specific fields
- MX:
priority(default: 10) - SRV:
priority,weight,port - CAA:
flags(default: 0),tag(default: "issue") - DS:
keytag,algorithm,digest_type - TLSA:
usage,selector,matching_type
For MX, SRV, CAA, DS and TLSA, content carries the primary value only - the mail host, the SRV target, the CA domain, the bare hex digest - and everything else goes in the fields above. Sending assembled record data (for example 0 issue "letsencrypt.org" as a CAA content) is rejected with a 400 naming the field.
TTL belongs to the record set. Omit ttl when adding another value to a name that already exists and the existing TTL is kept; send one and every value at that name is retimed. A brand-new name defaults to 3600 seconds.
Returns 201 Created with the record object.
PUT /v1/zones/{zoneId}/records/{recordId}
Update an existing record. Only include the fields you want to change; omitted fields retain their current values.
{
"content": "93.184.216.35",
"ttl": 7200
}
Returns 200 OK with the updated record object. Note: the record ID may change after an update since it is computed from the record’s name, type, and content.
DELETE /v1/zones/{zoneId}/records/{recordId}
Delete a record from the zone.
Returns 204 No Content on success.
DNSSEC
Manage DNSSEC for your zones. Requires zones.read to view status and zones.write to enable or disable.
GET /v1/zones/{id}/dnssec
Get DNSSEC status for a zone, including keys and DS records.
Response fields
enabled (boolean), keys (array of DNSKEY records), ds_records (array of DS records to set at registrar)
POST /v1/zones/{id}/dnssec/enable
Enable DNSSEC for a zone. Generates signing keys automatically.
Returns DNSSEC status with generated keys and DS records.
POST /v1/zones/{id}/dnssec/disable
Disable DNSSEC for a zone. Removes all signing keys.
Returns {"enabled": false, "keys": [], "ds_records": []}.
NS Groups
List the available nameserver groups. Use a group's id as ns_group when creating a zone. Any valid API key can read this endpoint.
GET /v1/ns-groups
List the active nameserver groups.
Response fields per group
id, name, slug
Account
View account information and manage API keys.
GET /v1/account
Get current account information, including subscription details.
Response fields
id (UUID), email, name, role, status, language, timezone, created_at
subscription – object with plan, billing_cycle, status, current_period_start, current_period_end (or null if no subscription)
GET /v1/account/api-keys
List all API keys for the authenticated user.
Response fields per key
id, name, key_prefix (first 8 chars), permissions (array), last_used_at, expires_at, created_at
POST /v1/account/api-keys
Create a new API key.
Request body (JSON)
{
"name": "CI/CD Pipeline",
"permissions": ["zones.read", "records.read", "records.write"],
"expires_at": "2027-01-01"
}
name(required) – human-readable name (max 255 characters)permissions(required) – array of permissions (at least one required):zones.read,zones.write,records.read,records.write,webhooks.read,webhooks.writeexpires_at– optional expiration date (ISO 8601 orYYYY-MM-DD); must be in the future
The response includes the full API key in the
keyfield. This is the only time the full key is returned. Store it securely.
Returns 201 Created with the key details including the full key value.
DELETE /v1/account/api-keys/{id}
Revoke (permanently delete) an API key.
Returns 204 No Content on success.
Billing
View subscription, plans, and invoices. These endpoints are read-only.
GET /v1/billing/subscription
Get current subscription details. Returns null if no active subscription.
Response fields
id, plan, billing_cycle, status, current_period_start, current_period_end, created_at
GET /v1/billing/plans
List all available plans with pricing and features.
Response fields per plan
id, name, slug, description, price_monthly, price_yearly, currency, max_domains, max_records, features (array)
GET /v1/billing/invoices
List the authenticated user's invoices, newest first.
Query parameters
status– filter by status (draft,issued,sent,void)page,per_page– pagination
GET /v1/billing/invoices/{id}
Get a single invoice by its <code>id</code>.
Response fields
id, number, status, amount (gross), net_amount, tax_amount, tax_rate, currency, issued_at, created_at. Monetary values are plain decimal strings.
Webhooks
Manage outgoing webhook subscriptions to receive real-time notifications about zone and record changes. Requires webhooks.read for read operations and webhooks.write to create, update, or delete.
GET /v1/webhooks
List all webhook subscriptions for the authenticated user.
Response fields per webhook
id, url, events (array), description, is_active, failure_count, last_triggered_at, created_at
POST /v1/webhooks
Create a webhook subscription.
Request body (JSON)
{
"url": "https://example.com/webhook",
"events": ["zone.created", "record.created"],
"description": "Production webhook"
}
url(required) – HTTPS endpoint that will receive the event payloadsevents(required) – array of event types to subscribe todescription– optional human-readable label
The response includes a
secretused to verify webhook signatures (HMAC). This is the only time the secret is returned. Store it securely.
Returns 201 Created with the webhook id and secret.
GET /v1/webhooks/{id}
Get a single webhook subscription together with its most recent deliveries.
PUT /v1/webhooks/{id}
Update a webhook subscription. Include only the fields you want to change.
Returns 200 OK with the updated webhook object.
DELETE /v1/webhooks/{id}
Delete a webhook subscription.
Returns 204 No Content on success.
POST /v1/webhooks/{id}/test
Send a test event to the webhook endpoint to verify it is reachable.
Queues a test delivery with a "type": "test" payload.
Verifying a Delivery
Every delivery is signed with the secret returned when the webhook was created, and carries four headers:
X-NexDNS-Signature: sha256=<hmac>
X-NexDNS-Timestamp: 1785370265
X-NexDNS-Event: record.created
X-NexDNS-Delivery: 42
Recompute HMAC-SHA256 over the exact raw request body using your secret, and compare it with the hex digest after sha256= in a constant-time comparison. A mismatch means the request did not come from us. X-NexDNS-Delivery identifies the attempt, so retries of the same event share an event id but not a delivery number.
Delivery Payload
{
"id": "evt_szpj9u04z8u0",
"type": "record.created",
"created_at": "2026-07-30 00:31:05",
"data": {
"zone": { "name": "example.com" },
"record": { "name": "www.example.com.", "type": "A", "content": "203.0.113.10", "ttl": 3600 }
}
}
Available event types
Subscribe to any combination of these event types:
zone.created, zone.updated, zone.deleted, record.created, record.updated, record.deleted, dnssec.enabled, dnssec.disabled, zone.health.problem, zone.health.resolved
Error Codes
All errors follow a consistent format with an error code string and a human-readable message.
| HTTP Status | Error Code | Description |
|---|---|---|
400 |
validation_error |
Request body failed validation. Check details for field-specific errors. |
401 |
unauthorized |
Missing, invalid, or expired API key. |
403 |
forbidden |
The API key lacks the required scope, the account's plan does not include API access, or the plan does not include the capability being used (for example secondary zones or DNSSEC). |
404 |
not_found |
The requested resource does not exist or is not accessible by the authenticated user. |
409 |
conflict |
Resource already exists (e.g. duplicate zone name). |
422 |
quota_exceeded |
Your plan's zone or record limit is reached. |
422 |
domain_blacklisted |
The domain is on the blocked list and cannot be added. |
429 |
rate_limit_exceeded |
Too many requests. Check Retry-After header. |
500 |
server_error |
An unexpected internal error occurred. Please try again, or contact support if it persists. |
502 |
dns_server_error |
The nameservers are temporarily unavailable. The request was not applied; retry it. |
Code Examples
All examples use curl. Replace nxd_your_api_key with your actual API key.
List your zones
curl -s "https://api.nexdns.tech/v1/zones" \
-H "Authorization: Bearer nxd_your_api_key"
Create a zone
curl -s -X POST "https://api.nexdns.tech/v1/zones" \
-H "Authorization: Bearer nxd_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "example.com"}'
Add an A record
curl -s -X POST "https://api.nexdns.tech/v1/zones/{zoneId}/records" \
-H "Authorization: Bearer nxd_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"type": "A",
"name": "www",
"ttl": 3600,
"content": "93.184.216.34"
}'
Add an MX record
curl -s -X POST "https://api.nexdns.tech/v1/zones/{zoneId}/records" \
-H "Authorization: Bearer nxd_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"type": "MX",
"name": "@",
"ttl": 3600,
"content": "mail.example.com",
"priority": 10
}'
Update a record
curl -s -X PUT "https://api.nexdns.tech/v1/zones/{zoneId}/records/{recordId}" \
-H "Authorization: Bearer nxd_your_api_key" \
-H "Content-Type: application/json" \
-d '{"content": "93.184.216.35", "ttl": 7200}'
Delete a record
curl -s -X DELETE "https://api.nexdns.tech/v1/zones/{zoneId}/records/{recordId}" \
-H "Authorization: Bearer nxd_your_api_key"
Export a zone (BIND format)
curl -s "https://api.nexdns.tech/v1/zones/{zoneId}/export" \
-H "Authorization: Bearer nxd_your_api_key"
Enable DNSSEC
curl -s -X POST "https://api.nexdns.tech/v1/zones/{zoneId}/dnssec/enable" \
-H "Authorization: Bearer nxd_your_api_key"
Create an API key
curl -s -X POST "https://api.nexdns.tech/v1/account/api-keys" \
-H "Authorization: Bearer nxd_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Read-only key",
"permissions": ["zones.read", "records.read"],
"expires_at": "2027-12-31"
}'
Get account info
curl -s "https://api.nexdns.tech/v1/account" \
-H "Authorization: Bearer nxd_your_api_key"