# Domains

Add your customers' hostnames, read the record each one needs and where it stands, re-check it, remove it.

Base URL `https://api.apexly.dev`. Send your key as `Authorization: Bearer sk_live_…` or `sk_test_…`; the examples read it from `APEXLY_API_KEY`.

## Add a domain

`POST /v1/domains`

201 with the new domain in pending_dns, or 200 with the existing one when the app already has this hostname (idempotent on app + hostname; an expired one is re-added: back to pending_dns with the same connect link and a fresh 72 h window). The mode comes from the key: test keys create test domains, which never contact a CA. 422 invalid_hostname, hostname_is_apexly, hostname_is_public_suffix, hostname_held, domain_limit_reached (live domains at the plan hard cap: 2× included, 3 on Free, Growth during the 14-day trial; error.status_detail lists \{ plan, trial, used, limit, hostname, plans[] \} that fit one more. New accounts: 10 in the first 24 hours), origin_not_allowed, validation_failed (metadata over 4 KB, apex_redirect on a subdomain, hostname already in the app in the other mode). origin needs the Scale plan; other plans ignore it and the response carries warnings.

### Body

| Field | Type | Description |
| --- | --- | --- |
| `hostname` | `string` | **Required.** The customer's hostname, e.g. careers.acme.com (Unicode is encoded). At most 1024 characters. |
| `origin` | `string \| null` | Per-domain origin override (Scale and Enterprise plans; other plans: ignored with a warning). At most 2048 characters. |
| `metadata` | `object \| null` | Any JSON object, at most 4 KB; returned on every domain object and event. |
| `apex_redirect` | `string` | Apex domains only: also serve `www.<apex>` and redirect it (default: the app's apex_redirect_default). One of `none`, `www`. |

### Response

`200 OK`: the hostname already exists in this app · `201 Created`. Errors use the [error envelope](/docs/api/errors).

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | dom_… |
| `hostname` | `string` | Lowercase ASCII (punycode). |
| `status` | `string` | One of `pending_dns`, `verifying`, `issuing`, `active`, `dns_broken`, `cert_failed`, `expired`, `held`, `deleted`. |
| `status_reason` | `string \| null` | Machine code when status is not active, e.g. dns_record_mismatch. |
| `status_detail` | `object \| null` | \{ expected, found, hint \}. |
| `is_apex` | `boolean` |  |
| `mode` | `string` | From the key that created it; test domains never contact a CA. One of `live`, `test`. |
| `dns` | `object` | Record names are relative to the registrable domain (e.g. "careers" in acme.com). |
| `dns.cname` | `object` | Subdomains: the one record to add. |
| `dns.cname.name` | `string` |  |
| `dns.cname.value` | `string` |  |
| `dns.apex` | `object` | Apex domains: A (and AAAA) records plus the _apexly TXT record. |
| `dns.apex.a` | `string[]` |  |
| `dns.apex.aaaa` | `string[]` |  |
| `dns.apex.txt` | `object` |  |
| `dns.apex.txt.name` | `string` |  |
| `dns.apex.txt.value` | `string` |  |
| `origin` | `string \| null` | Origin override, or null for the app origin. |
| `apex_redirect` | `string` | One of `none`, `www`. |
| `connect_url` | `string` | The page the customer opens to connect the domain. |
| `certificate` | `object \| null` |  |
| `certificate.issuer` | `string` |  |
| `certificate.not_before` | `string` |  |
| `certificate.not_after` | `string` |  |
| `certificate.renews_at` | `string` |  |
| `metadata` | `object \| null` |  |
| `created_at` | `string` |  |
| `updated_at` | `string` |  |
| `last_checked_at` | `string \| null` |  |
| `next_check_at` | `string \| null` |  |
| `warnings` | `object[]` | Present when part of the request was ignored, e.g. origin on a plan without per-domain origins. |
| `warnings[].code` | `string` |  |
| `warnings[].message` | `string` |  |

### Example

```bash tab="curl"
curl -X POST https://api.apexly.dev/v1/domains \
  -H "Authorization: Bearer $APEXLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"careers.acme.com"}'
```

```js tab="Node"
const res = await fetch("https://api.apexly.dev/v1/domains", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.APEXLY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ hostname: "careers.acme.com" }),
});
const body = await res.json();
```

```python tab="Python"
import os
import requests

res = requests.post(
    "https://api.apexly.dev/v1/domains",
    headers={"Authorization": f"Bearer {os.environ['APEXLY_API_KEY']}"},
    json={"hostname": "careers.acme.com"},
)
body = res.json()
```

## List domains

`GET /v1/domains`

Newest first (created_at desc), cursor-paginated: pass next_cursor as cursor. Filters: status, hostname (prefix), `metadata.<key>=<value>` (string equality on a top-level key, up to 5).

### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `status` | `string` | One of `pending_dns`, `verifying`, `issuing`, `active`, `dns_broken`, `cert_failed`, `expired`, `held`, `deleted`. |
| `hostname` | `string` | Hostname prefix. At most 253 characters. |
| `limit` | `integer` | Default `50`. 1 to 100. |
| `cursor` | `string` | next_cursor of the previous page. At most 200 characters. |

### Response

`200 OK`. Errors use the [error envelope](/docs/api/errors).

| Field | Type | Description |
| --- | --- | --- |
| `data` | `object[]` |  |
| `data[].id` | `string` | dom_… |
| `data[].hostname` | `string` | Lowercase ASCII (punycode). |
| `data[].status` | `string` | One of `pending_dns`, `verifying`, `issuing`, `active`, `dns_broken`, `cert_failed`, `expired`, `held`, `deleted`. |
| `data[].status_reason` | `string \| null` | Machine code when status is not active, e.g. dns_record_mismatch. |
| `data[].status_detail` | `object \| null` | \{ expected, found, hint \}. |
| `data[].is_apex` | `boolean` |  |
| `data[].mode` | `string` | From the key that created it; test domains never contact a CA. One of `live`, `test`. |
| `data[].dns` | `object` | Record names are relative to the registrable domain (e.g. "careers" in acme.com). |
| `data[].dns.cname` | `object` | Subdomains: the one record to add. |
| `data[].dns.cname.name` | `string` |  |
| `data[].dns.cname.value` | `string` |  |
| `data[].dns.apex` | `object` | Apex domains: A (and AAAA) records plus the _apexly TXT record. |
| `data[].dns.apex.a` | `string[]` |  |
| `data[].dns.apex.aaaa` | `string[]` |  |
| `data[].dns.apex.txt` | `object` |  |
| `data[].origin` | `string \| null` | Origin override, or null for the app origin. |
| `data[].apex_redirect` | `string` | One of `none`, `www`. |
| `data[].connect_url` | `string` | The page the customer opens to connect the domain. |
| `data[].certificate` | `object \| null` |  |
| `data[].certificate.issuer` | `string` |  |
| `data[].certificate.not_before` | `string` |  |
| `data[].certificate.not_after` | `string` |  |
| `data[].certificate.renews_at` | `string` |  |
| `data[].metadata` | `object \| null` |  |
| `data[].created_at` | `string` |  |
| `data[].updated_at` | `string` |  |
| `data[].last_checked_at` | `string \| null` |  |
| `data[].next_check_at` | `string \| null` |  |
| `next_cursor` | `string \| null` |  |

### Example

```bash tab="curl"
curl https://api.apexly.dev/v1/domains \
  -H "Authorization: Bearer $APEXLY_API_KEY"
```

```js tab="Node"
const res = await fetch("https://api.apexly.dev/v1/domains", {
  headers: {
    Authorization: `Bearer ${process.env.APEXLY_API_KEY}`,
  },
});
const body = await res.json();
```

```python tab="Python"
import os
import requests

res = requests.get(
    "https://api.apexly.dev/v1/domains",
    headers={"Authorization": f"Bearer {os.environ['APEXLY_API_KEY']}"},
)
body = res.json()
```

## Get a domain

`GET /v1/domains/{id}`

Another app’s domain, or a deleted one, is 404.

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` |  |

### Response

`200 OK`. Errors use the [error envelope](/docs/api/errors).

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | dom_… |
| `hostname` | `string` | Lowercase ASCII (punycode). |
| `status` | `string` | One of `pending_dns`, `verifying`, `issuing`, `active`, `dns_broken`, `cert_failed`, `expired`, `held`, `deleted`. |
| `status_reason` | `string \| null` | Machine code when status is not active, e.g. dns_record_mismatch. |
| `status_detail` | `object \| null` | \{ expected, found, hint \}. |
| `is_apex` | `boolean` |  |
| `mode` | `string` | From the key that created it; test domains never contact a CA. One of `live`, `test`. |
| `dns` | `object` | Record names are relative to the registrable domain (e.g. "careers" in acme.com). |
| `dns.cname` | `object` | Subdomains: the one record to add. |
| `dns.cname.name` | `string` |  |
| `dns.cname.value` | `string` |  |
| `dns.apex` | `object` | Apex domains: A (and AAAA) records plus the _apexly TXT record. |
| `dns.apex.a` | `string[]` |  |
| `dns.apex.aaaa` | `string[]` |  |
| `dns.apex.txt` | `object` |  |
| `dns.apex.txt.name` | `string` |  |
| `dns.apex.txt.value` | `string` |  |
| `origin` | `string \| null` | Origin override, or null for the app origin. |
| `apex_redirect` | `string` | One of `none`, `www`. |
| `connect_url` | `string` | The page the customer opens to connect the domain. |
| `certificate` | `object \| null` |  |
| `certificate.issuer` | `string` |  |
| `certificate.not_before` | `string` |  |
| `certificate.not_after` | `string` |  |
| `certificate.renews_at` | `string` |  |
| `metadata` | `object \| null` |  |
| `created_at` | `string` |  |
| `updated_at` | `string` |  |
| `last_checked_at` | `string \| null` |  |
| `next_check_at` | `string \| null` |  |

### Example

```bash tab="curl"
curl https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3 \
  -H "Authorization: Bearer $APEXLY_API_KEY"
```

```js tab="Node"
const res = await fetch("https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3", {
  headers: {
    Authorization: `Bearer ${process.env.APEXLY_API_KEY}`,
  },
});
const body = await res.json();
```

```python tab="Python"
import os
import requests

res = requests.get(
    "https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3",
    headers={"Authorization": f"Bearer {os.environ['APEXLY_API_KEY']}"},
)
body = res.json()
```

## Update a domain

`PATCH /v1/domains/{id}`

origin (Scale plan; null removes the override; other plans ignore it with warnings), metadata (replaces the object; null removes it), apex_redirect (apex domains only).

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` |  |

### Body

| Field | Type | Description |
| --- | --- | --- |
| `origin` | `string \| null` | Per-domain origin override (Scale and Enterprise plans; other plans: ignored with a warning). At most 2048 characters. |
| `metadata` | `object \| null` | Any JSON object, at most 4 KB; returned on every domain object and event. |
| `apex_redirect` | `string` | Apex domains only. One of `none`, `www`. |

### Response

`200 OK`. Errors use the [error envelope](/docs/api/errors).

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | dom_… |
| `hostname` | `string` | Lowercase ASCII (punycode). |
| `status` | `string` | One of `pending_dns`, `verifying`, `issuing`, `active`, `dns_broken`, `cert_failed`, `expired`, `held`, `deleted`. |
| `status_reason` | `string \| null` | Machine code when status is not active, e.g. dns_record_mismatch. |
| `status_detail` | `object \| null` | \{ expected, found, hint \}. |
| `is_apex` | `boolean` |  |
| `mode` | `string` | From the key that created it; test domains never contact a CA. One of `live`, `test`. |
| `dns` | `object` | Record names are relative to the registrable domain (e.g. "careers" in acme.com). |
| `dns.cname` | `object` | Subdomains: the one record to add. |
| `dns.cname.name` | `string` |  |
| `dns.cname.value` | `string` |  |
| `dns.apex` | `object` | Apex domains: A (and AAAA) records plus the _apexly TXT record. |
| `dns.apex.a` | `string[]` |  |
| `dns.apex.aaaa` | `string[]` |  |
| `dns.apex.txt` | `object` |  |
| `dns.apex.txt.name` | `string` |  |
| `dns.apex.txt.value` | `string` |  |
| `origin` | `string \| null` | Origin override, or null for the app origin. |
| `apex_redirect` | `string` | One of `none`, `www`. |
| `connect_url` | `string` | The page the customer opens to connect the domain. |
| `certificate` | `object \| null` |  |
| `certificate.issuer` | `string` |  |
| `certificate.not_before` | `string` |  |
| `certificate.not_after` | `string` |  |
| `certificate.renews_at` | `string` |  |
| `metadata` | `object \| null` |  |
| `created_at` | `string` |  |
| `updated_at` | `string` |  |
| `last_checked_at` | `string \| null` |  |
| `next_check_at` | `string \| null` |  |
| `warnings` | `object[]` | Present when part of the request was ignored, e.g. origin on a plan without per-domain origins. |
| `warnings[].code` | `string` |  |
| `warnings[].message` | `string` |  |

### Example

```bash tab="curl"
curl -X PATCH https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3 \
  -H "Authorization: Bearer $APEXLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"metadata":{"customer":"acme"}}'
```

```js tab="Node"
const res = await fetch("https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.APEXLY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ metadata: { customer: "acme" } }),
});
const body = await res.json();
```

```python tab="Python"
import os
import requests

res = requests.patch(
    "https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3",
    headers={"Authorization": f"Bearer {os.environ['APEXLY_API_KEY']}"},
    json={"metadata": {"customer": "acme"}},
)
body = res.json()
```

## Delete a domain

`DELETE /v1/domains/{id}`

Emits domain.deleted; routing is dropped within 5 seconds; the hostname can be added again at once.

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` |  |

### Response

`204 No Content`. Errors use the [error envelope](/docs/api/errors).

### Example

```bash tab="curl"
curl -X DELETE https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3 \
  -H "Authorization: Bearer $APEXLY_API_KEY"
```

```js tab="Node"
const res = await fetch("https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.APEXLY_API_KEY}`,
  },
});
```

```python tab="Python"
import os
import requests

res = requests.delete(
    "https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3",
    headers={"Authorization": f"Bearer {os.environ['APEXLY_API_KEY']}"},
)
```

## Check a domain now

`POST /v1/domains/{id}/verify`

Queues a DNS check right away instead of waiting for the next scheduled one; on a cert_failed domain it retries issuance (after re-checking CAA when that was the reason). One per 15 seconds per domain (429 rate_limited with Retry-After). 422 validation_failed when there is nothing to check (issuing, held, expired, or a test domain).

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` |  |

### Response

`202 Accepted`. Errors use the [error envelope](/docs/api/errors).

| Field | Type | Description |
| --- | --- | --- |
| `queued` | `boolean` | One of `true`. |

### Example

```bash tab="curl"
curl -X POST https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3/verify \
  -H "Authorization: Bearer $APEXLY_API_KEY"
```

```js tab="Node"
const res = await fetch("https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3/verify", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.APEXLY_API_KEY}`,
  },
});
const body = await res.json();
```

```python tab="Python"
import os
import requests

res = requests.post(
    "https://api.apexly.dev/v1/domains/dom_01J9X8T4Q5M6N7P8R9S0V1W2X3/verify",
    headers={"Authorization": f"Bearer {os.environ['APEXLY_API_KEY']}"},
)
body = res.json()
```
