
# 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.

```json
{
  "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

```bash
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"}'
```
