Docs / API reference
API reference
The Custom Domain API v1 reference. Register customer hostnames, read their DNS records and checks, re-check and delete them, with idempotency, pagination, errors and limits.
Base URL: https://edge.customdomainapi.com/v1 on Free; on a paid or
self-hosted edge, https://<your edge name>/v1.
Authentication: every request sends your API key:
Authorization: Bearer cd_...
The application is derived from the key, never from a request field. Keys are not accepted in query strings or cookies.
Machine-readable contract: the OpenAPI document is at
/v1/openapi.json on
every edge, and in the repository as
docs/openapi.json.
Endpoints
| Method and path | Purpose | Success |
|---|---|---|
POST /v1/domains |
Register a hostname for a workspace. | 201, or 200 on an idempotent replay |
GET /v1/domains |
List domains. Filters: reference, status, include_deleted. Pages: limit (1 to 200, default 50), offset. |
200 |
GET /v1/domains/{id} |
One domain with its current checks. | 200 |
POST /v1/domains/{id}/checks |
Re-run all checks now. | 202 |
DELETE /v1/domains/{id} |
Stop serving and delete. | 202 |
POST /v1/webhooks |
Subscribe to events (Webhooks). | 201 |
GET /v1/webhooks |
List subscriptions. | 200 |
POST /v1/webhooks/{id}/rotate |
New signing secret. | 200 |
DELETE /v1/webhooks/{id} |
Revoke a subscription. | 200 |
Register a domain
POST /v1/domains
Authorization: Bearer cd_...
Idempotency-Key: signup-ws_8f3a1c
Content-Type: application/json
{"hostname": "forms.acme.com", "reference": "ws_8f3a1c", "metadata": {"plan": "pro"}}
hostname: the customer’s exact subdomain. It’s normalized to lowercase punycode. Apex domains (acme.com), wildcards and IP addresses are refused.reference: your opaque workspace id (up to 255 characters). It is returned verbatim and delivered to your origin in the assertion.metadata: optional, up to 32 scalar values, echoed back.
Response 201:
{
"id": "6f1c2d3e-4b5a-4c6d-8e9f-0a1b2c3d4e5f",
"hostname": "forms.acme.com",
"reference": "ws_8f3a1c",
"status": "pending_dns",
"dns_records": [
{"name": "_custom-domain-challenge.forms.acme.com", "type": "TXT",
"value": "custom-domain-verify=…", "purpose": "ownership", "help": "…"},
{"name": "forms.acme.com", "type": "CNAME",
"value": "edge.customdomainapi.com", "purpose": "routing", "help": "…"}
],
"checks": [
{"type": "ownership", "status": "pending", "error_code": null, "message": null,
"observed_at": null, "next_check_at": null},
{"type": "routing", "status": "pending"},
{"type": "certificate", "status": "pending"},
{"type": "origin", "status": "pending"}
],
"metadata": {"plan": "pro"},
"created_at": "2026-10-04T14:00:00Z",
"updated_at": "2026-10-04T14:00:00Z",
"deleted_at": null
}
Show both dns_records to your customer exactly as returned. Statuses and
checks are explained in How it works.
Idempotency
Send Idempotency-Key (up to 255 characters, unique per application) on
POST /v1/domains, and retries are safe:
- the same key and body returns the first result with
200andIdempotent-Replayed: true; - the same key with a different body is
422 idempotency_key_reused; - a retry while the first is still running is
409 idempotency_request_in_progress; - keys expire after 24 hours.
Errors
Every error has the same shape:
{"error": {"code": "hostname_already_claimed", "message": "forms.acme.com is already claimed", "details": {}}}
| Status | code |
When |
|---|---|---|
| 401 | unauthorized |
Missing, unknown, revoked or expired key. |
| 403 | application_suspended |
Your application is suspended. |
| 404 | domain_not_found |
No such domain in your application. Another application’s domains are never visible. |
| 409 | hostname_already_claimed |
The hostname is live in this or another application. |
| 409 | domain_limit_reached |
Your application is at its domain limit (25 on Free). Existing domains keep working. |
| 409 | invalid_status_transition |
Not possible in the domain’s status, such as re-checking a deleted domain. |
| 409 | idempotency_request_in_progress |
The first request with this key hasn’t finished. |
| 422 | validation_error |
The body or query doesn’t match the schema; details.errors lists where. |
| 422 | apex_not_supported, wildcard_not_supported, ip_literal_not_supported, invalid_hostname, invalid_label, hostname_too_long, empty_hostname |
The hostname can’t be registered. |
| 422 | invalid_reference |
The reference is empty or longer than 255 characters. |
| 422 | idempotency_key_reused |
The key was used with a different body. |
| 429 | rate_limited |
Too many requests; wait Retry-After seconds. |
Limits on Free
| Limit | Value |
|---|---|
| Customer hostnames | 25 |
| API keys | 1 (two during a rotation’s 24-hour overlap) |
| API requests | 60 a minute per key |
| Proxied requests | 600 a minute and 100 a second, across all your hostnames; over either, visitors get 429 with Retry-After |
| Manual re-checks | 1 per domain a minute, 60 per application an hour |
| Fair use | 10 GB of responses served a month: above it we email you; nothing is switched off |
Paid plans have their own edge, with more domains and no proxied-request limit of ours.
SDK
The Python SDK wraps all of the above, with typed errors and safe retries:
pip install custom-domain-sdk
See the SDK README.