# Connecting a domain

> Add your own domain to Pidgeon, publish the DNS it needs, and send and receive on your own name instead of the shared one.

A domain is the one thing this API cannot finish for you. We can generate
every record, check them, and tell you the moment they land — but we cannot
write in somebody else's zone, and a product that asked you to move your
nameservers so it could would be a different product.

So the job here is to make that step **delegable**. You get the records in the
same response as the domain, you show them to your own customer in your own
words, and nobody ever sees a Pidgeon screen.

## Connect one

```bash
curl https://pidgeon.ai/api/v1/domains \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "domain": "acme.com" }'
```

```ts
const { data: domain } = await domainsCreate({
body: { domain: 'acme.com' },
throwOnError: true,
});

for (const record of domain.data.dns_records) {
console.log(record.record_type, record.name, record.value);
}
```

```python
domain = httpx.post(
  "https://pidgeon.ai/api/v1/domains",
  headers={"Authorization": f"Bearer {os.environ['PIDGEON_API_KEY']}"},
  json={"domain": "acme.com"},
).json()["data"]

for record in domain["dns_records"]:
  print(record["record_type"], record["name"], record["value"])
```

Five kinds of record come back. `ownership` is a TXT record proving the domain
is yours; `mx` routes inbound mail to us; `spf` and `dkim` are what stop your
outgoing mail being treated as spam; `dmarc` asks other providers to report on
anything claiming to be you. Each carries a one-line `description` written for
the person who has to publish it, so you do not have to write those sentences
yourself.

**Check `dns_complete` before you show the list.** The DKIM records are derived
from what our mail provider issued, and until that has happened the list looks
complete and is missing three CNAMEs. Publishing it in that state gives you a
domain that verifies and cannot sign mail — which fails in a way nobody
debugs quickly.

## Wait for it

```bash
curl -X POST https://pidgeon.ai/api/v1/domains/$DOMAIN_ID/verify \
-H "Authorization: Bearer $PIDGEON_API_KEY"
```

```ts
const { data: state } = await domainsVerify({
path: { id: domain.data.id },
throwOnError: true,
});

if (state.data.verified && state.data.mail_configured) {
// Ready to send and receive.
}
```

```python
state = httpx.post(
  f"https://pidgeon.ai/api/v1/domains/{domain_id}/verify",
  headers={"Authorization": f"Bearer {os.environ['PIDGEON_API_KEY']}"},
).json()["data"]
```

Idempotent and safe to poll. Calling it on a domain that is already verified
re-checks and says so rather than erroring, because a poll loop that has to
special-case success is a poll loop with a bug in it.

Two states, not one: `verified` means we believe the domain is yours,
`mail_configured` means it can actually send and receive. A domain can sit at
the first for a while — somebody publishes the TXT record and stops reading
the instructions.

**Better than polling:** subscribe to `domain.verified` and do not poll at
all. DNS propagation is measured in a zone's TTL and neither of us controls
it, so a webhook costs you one request and a poll loop costs you hundreds.

## When it does not verify

`GET /v1/domains/{id}` returns every record with its own `status` —
`connected`, `waiting`, `incorrect`, or `not_required` — and `found`, which is
what we actually read at that name.

That last field is the one that matters. A record with the right name and the
wrong value looks exactly like a missing one until you can see what is there
instead, and "your domain is not verified" is not something your customer can
act on. "Your MX record points at `mail.oldhost.com`" is.

## Disconnecting

`DELETE /v1/domains/{id}` takes the domain's addresses, their mail, their
routing rules and their webhooks with it. There is no undo and no soft form of
it. It takes an id rather than a name on purpose: an id comes from a list you
just read, and a name comes from a variable that might be wrong.
