# Domains

> Every domains endpoint, generated from the API's own schemas.

Base URL `https://pidgeon.ai/api/v1`. Every request carries `Authorization: Bearer pgn_live_…`. The machine-readable version of this page is https://pidgeon.ai/openapi.json.

## GET /domains

List domains

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | no |  |
| `cursor` | string | no |  |

**Responses**

- `200` A page of domains.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## POST /domains

Connect a domain

Returns the domain **and the records to publish**, because showing them to your own customer is the one thing every caller does next. DNS is the step this API cannot perform — we cannot write in somebody else’s zone — so the job is to make the human step delegable. Watch `dns_complete`: it is false while the DKIM records do not exist yet, and a list published in that state verifies and cannot sign mail.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Any string unique to this attempt, reused on its retries. A repeat with the same key and the same request returns the first answer with `Idempotent-Replay: true` and does nothing — no second email. The same key with a different request is a `409`. Kept for 24 hours; a failure is not kept, so a retry after one runs. |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | yes | A domain name you own, or the shared one. An alternative to `domain_id`. |
| `inbound_enabled` | boolean | no |  |

**Responses**

- `201` The domain and its records.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `402` Your plan does not include this. `error.message` says what does.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` This `Idempotency-Key` was used with a different request, or that request is still running. Nothing was sent by this call.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /domains/{id}

Fetch one domain, with the state of each record

`dns_records[].status` says which of ownership, MX, SPF, DKIM and DMARC is missing or wrong, and `found` says what is there instead — which is the difference between your support queue and your customer fixing one TXT record.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` The domain.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## PATCH /domains/{id}

Turn the catch-all on or off

The one thing about a domain that can change. Catch-all is two gates: this switch, and a `catch_all` routing rule saying where the mail goes — so turning it on without a rule does nothing, and turning it off later does not throw away where the mail was going.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `catch_all_enabled` | boolean | yes |  |

**Responses**

- `200` The domain.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## DELETE /domains/{id}

Disconnect a domain

Takes its addresses, their mail, their routing rules and their webhooks with it. There is no undo.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` Deleted.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /domains/{id}/zone-file

The records as a zone file

BIND format, which Route 53, Cloudflare and most registrars import and our JSON is not. TTLs are 300: these records change twice, and a long TTL on one you are getting right is an hour of somebody else’s cache.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` A zone file.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /domains/{id}/setup-link

One-click DNS setup, when the provider supports it

The **Domain Connect** standard: the URL returned is signed by and points at the domain’s own DNS provider, where the owner signs in, reviews the records and approves — the provider writes them. Nothing here is a credential of ours.

Answers `200` either way. `supported: false` is a fact about somebody else’s DNS provider rather than a failure of this request, and `reason` says whose problem it is: `provider` means that zone cannot do it, `unconfigured` means this deployment is not registered. Either way, fall back to `GET /domains/{id}/zone-file` or the `records` on the domain.

`template` is `mail` (everything: ownership, DKIM and MX) or `mx` (the MX alone, for a domain already sending through us that is now going to receive).

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `template` | enum | no | Which records the link should publish. One of: `mail`, `mx`. |

**Responses**

- `200` Whether it can be done, and where.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## POST /domains/{id}/verify

Check the zone again

Idempotent and safe to poll — calling it on a verified domain re-checks and says so rather than erroring. Both halves in one answer: ownership, and whether mail is configured. The floor on how often this is worth calling is the zone’s TTL. Better still, subscribe to `domain.verified` and do not poll.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` Where it stands.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.
