Add a custom domain
POST /v1/custom-domains · permission custom-domains:write
Adds a custom login domain to the environment. The name is converted to its IDNA2008 A-label; public suffixes, single-label names, IP addresses and platform domains are rejected. A name is unique across the platform: if another environment uses it, or it was released less than 30 days ago (quarantine), the request fails with 409 custom_domain_taken. The domain starts in pending_dns; publish the returned dns_records (a permanent TXT ownership record and a CNAME to the environment's edge host) at your DNS provider, then call :verify or wait for the automatic check.
Request
JSON body (unknown fields are rejected with 400 unknown_field):
| Field | Type | Required | Description |
|---|---|---|---|
Idempotency-Key |
header | yes | Required. 16-64 characters [A-Za-z0-9_-]; repeating the same key with the same name returns the same domain. |
hostname |
string | yes | The domain to add (Unicode allowed), at most 253 bytes, for example login.example.com. |
Response
201 Created with the domain and an ETag header.
{
"id": "cdom_01j9xk3m7q8r2s4t6v8w0y2z4a",
"hostname": "login.example.com",
"is_primary": false,
"status": "pending_dns",
"failure_reason": null,
"dns_records": [
{
"type": "TXT",
"name": "_akp-verify.login.example.com",
"value": "idp-verify=3q2-7wEXAMPLEtokenQ9"
},
{
"type": "CNAME",
"name": "login.example.com",
"value": "acme.eu.edge.keenauth.app"
}
],
"certificate_expires_at": null,
"last_checked_at": null,
"next_check_at": "2026-10-05T09:36:00Z",
"activated_at": null,
"degraded_since": null,
"status_changed_at": "2026-10-05T09:30:00.123456Z",
"created_at": "2026-10-05T09:30:00.123456Z",
"updated_at": "2026-10-05T09:30:00.123456Z"
}
Errors
| HTTP | code | When |
|---|---|---|
| 400 | malformed_request |
Idempotency-Key is missing or malformed. |
| 400 | unknown_field |
The body contains an unknown field. |
| 403 | quota_exceeded |
The environment's custom domain quota is full, or the environment is a development environment. |
| 409 | custom_domain_taken |
The name is used by another environment or is in its 30-day quarantine. |
| 409 | conflict |
The account or the environment is not active. |
| 415 | unsupported_media_type |
The content type is not application/json. |
| 422 | validation_failed |
hostname is not a valid domain name for a custom domain. |
| 422 | idempotency_key_reuse |
The Idempotency-Key was already used with a different body. |
| 429 | rate_limited |
Too many custom domain add attempts for the account today; retry after the Retry-After period. |
| 503 | service_unavailable |
The platform domain registry or the environment's account information is temporarily unavailable. |
| 401 | unauthenticated |
The credential is missing, invalid or revoked. |
| 403 | insufficient_permissions |
The credential lacks the required permission. |
Example
curl -X POST https://api.eu.keenauth.com/v1/custom-domains \
-H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"hostname":"login.example.com"}'