# Domains

Verify a sending domain with two DNS records.

Your account sends only from domains it has verified, and every server of the account can send from them. Verification is two DNS records, added once, and Sendora checks them for you. You never touch your own SPF record.

## Before you start

Domains belong to the account. Add and check them in the dashboard under [Domains](https://app.sendora.se/domains), or through the API with an account key (`ak_…`). An administrator creates that key under [Account › API keys](https://app.sendora.se/account/tokens). A server key can only send from the domains, as [Keys](https://sendora.se/docs/keys) explains.

## Add the domain

The response has the two records to add. Sendora generates a DKIM key pair for the domain when you add it, and the private key never leaves Sendora.

cURL:

```bash
curl -X POST https://api.sendora.se/v1/domains \
  -H "Authorization: Bearer $SENDORA_ACCOUNT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "domain": "mail.example.se"
}'
```

TypeScript:

```ts
import { SendoraAccount } from '@sendora/sdk';

const account = new SendoraAccount({
  token: process.env.SENDORA_ACCOUNT_TOKEN,
});

const domain = await account.domains.create({
  domain: 'mail.example.se',
});

console.log(
  `${domain.returnPath.type} ${domain.returnPath.host} -> ${domain.returnPath.value}`,
);
console.log(
  `${domain.dkim.type} ${domain.dkim.host} -> ${domain.dkim.value}`,
);
```

Python:

```python
import os

from sendora import SendoraAccount

account = SendoraAccount(
    os.environ["SENDORA_ACCOUNT_TOKEN"]
)

domain = account.domains.create(domain="mail.example.se")

for record in (domain.return_path, domain.dkim):
    print(record.type, record.host, "->", record.value)
```

Response:

```json
{
  "domainId": "0d7f6a1e-4c0b-4b7e-9d3c-2a1f5e8b9c01",
  "domain": "mail.example.se",
  "verified": false,
  "createdAt": "2026-09-15T12:00:00.000Z",
  "lastCheckedAt": null,
  "failingSince": null,
  "returnPath": {
    "type": "CNAME",
    "host": "sendora-bounces.mail.example.se",
    "value": "bounces.sendora.se",
    "verified": false,
    "verifiedAt": null
  },
  "dkim": {
    "type": "TXT",
    "host": "sendora-a1b2c3d4._domainkey.mail.example.se",
    "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…",
    "verified": false,
    "verifiedAt": null
  }
}
```

Add both records at your DNS provider:

- `returnPath`: a CNAME from `sendora-bounces.<your domain>` to `bounces.sendora.se`. Bounce reports and feedback-loop reports for your mail arrive there and become events on your messages.
- `dkim`: a TXT record under `<selector>._domainkey.<your domain>` holding the public key. The response gives the selector and the value. Every message is signed with the key, so receivers see your domain vouching for the mail.

The domain may also be a subdomain, such as `mail.example.se`, which keeps the records away from the rest of your zone. The From address must use a verified domain exactly: verifying `mail.example.se` does not cover `example.se` or `news.mail.example.se`.

Adding a domain the account already has fails with `domain_exists`, which gives the id of the existing one.

## Check the records

Sendora looks the records up on its own: an unverified domain every two minutes, a verified one every day, and one in its warning every hour. To see the result now, ask for a check:

cURL:

```bash
curl -X POST https://api.sendora.se/v1/domains/{domainId}/verify \
  -H "Authorization: Bearer $SENDORA_ACCOUNT_TOKEN"
```

TypeScript:

```ts
import { SendoraAccount } from '@sendora/sdk';

const account = new SendoraAccount({
  token: process.env.SENDORA_ACCOUNT_TOKEN,
});

const domain = await account.domains.verify(
  '0d7f6a1e-4c0b-4b7e-9d3c-2a1f5e8b9c01',
);

console.log(
  domain.verified,
  domain.check.returnPath,
  domain.check.dkim,
);
```

Python:

```python
import os

from sendora import SendoraAccount

account = SendoraAccount(
    os.environ["SENDORA_ACCOUNT_TOKEN"]
)

domain = account.domains.verify(
    "0d7f6a1e-4c0b-4b7e-9d3c-2a1f5e8b9c01"
)

print(
    domain.verified,
    domain.check.return_path,
    domain.check.dkim,
)
```

Response:

```json
{
  "domainId": "0d7f6a1e-4c0b-4b7e-9d3c-2a1f5e8b9c01",
  "domain": "mail.example.se",
  "verified": true,
  "createdAt": "2026-09-15T12:00:00.000Z",
  "lastCheckedAt": "2026-09-15T12:10:00.000Z",
  "failingSince": null,
  "returnPath": {
    "type": "CNAME",
    "host": "sendora-bounces.mail.example.se",
    "value": "bounces.sendora.se",
    "verified": true,
    "verifiedAt": "2026-09-15T12:10:00.000Z"
  },
  "dkim": {
    "type": "TXT",
    "host": "sendora-a1b2c3d4._domainkey.mail.example.se",
    "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…",
    "verified": true,
    "verifiedAt": "2026-09-15T12:10:00.000Z"
  },
  "check": {
    "returnPath": "ok",
    "dkim": "ok"
  }
}
```

`check` says what each lookup found:

| `check`     | Meaning                                                                               |
| ----------- | ------------------------------------------------------------------------------------- |
| `ok`        | The record is there with the right value.                                             |
| `missing`   | There is no record.                                                                   |
| `mismatch`  | A record is there with another value.                                                 |
| `dns_error` | The resolver got no trustworthy answer, for example because DNSSEC validation failed. |

The domain is `verified` once both records have been seen.

## When a record goes missing

A verified domain rides out DNS trouble rather than stopping at the first failed lookup:

- A `dns_error` changes nothing. The resolver could not answer, which says nothing about your records, so the domain stays as it was.
- A record found `missing` or `mismatch` starts a 24-hour warning. The domain stays `verified` and keeps sending, `failingSince` says when the warning began, Sendora looks the records up every hour, and your account's administrators are mailed.
- Any check that finds both records ends the warning.
- A check 24 hours or more after `failingSince` that still misses a record ends the verification. Your administrators are mailed again, and mail from the domain is refused with `from_domain_not_verified` until both records are back. Once they are, the next check finds them within minutes.

A DNS move is the usual cause. Publish both records at the new provider before you switch, and the domain never enters its warning.

## SPF, DKIM and DMARC

- SPF is satisfied by the return-path domain, `sendora-bounces.<your domain>`, whose records Sendora keeps. It aligns with your domain under DMARC's relaxed mode.
- DKIM signs with your domain, through the key above, so it aligns strictly.
- DMARC is your own record, `_dmarc.<your domain>`. Both signals align, so a policy of `p=quarantine` or `p=reject` is safe for mail sent through Sendora. Sendora does not create or change your DMARC record.

## List and remove domains

cURL:

```bash
curl https://api.sendora.se/v1/domains \
  -H "Authorization: Bearer $SENDORA_ACCOUNT_TOKEN"
```

TypeScript:

```ts
import { SendoraAccount } from '@sendora/sdk';

const account = new SendoraAccount({
  token: process.env.SENDORA_ACCOUNT_TOKEN,
});

const { domains } = await account.domains.list();

for (const domain of domains) {
  console.log(
    domain.domain,
    domain.verified ? 'verified' : 'waiting for DNS',
  );
}
```

Python:

```python
import os

from sendora import SendoraAccount

account = SendoraAccount(
    os.environ["SENDORA_ACCOUNT_TOKEN"]
)

listed = account.domains.list()

for domain in listed.domains:
    if domain.verified:
        print(domain.domain, "verified")
    else:
        print(domain.domain, "waiting for DNS")
```

Response:

```json
{
  "domains": [
    {
      "domainId": "0d7f6a1e-4c0b-4b7e-9d3c-2a1f5e8b9c01",
      "domain": "mail.example.se",
      "verified": true,
      "createdAt": "2026-09-15T12:00:00.000Z",
      "lastCheckedAt": "2026-09-15T12:10:00.000Z",
      "failingSince": null,
      "returnPath": {
        "type": "CNAME",
        "host": "sendora-bounces.mail.example.se",
        "value": "bounces.sendora.se",
        "verified": true,
        "verifiedAt": "2026-09-15T12:10:00.000Z"
      },
      "dkim": {
        "type": "TXT",
        "host": "sendora-a1b2c3d4._domainkey.mail.example.se",
        "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…",
        "verified": true,
        "verifiedAt": "2026-09-15T12:10:00.000Z"
      }
    }
  ]
}
```

[Removing a domain](https://sendora.se/docs/api/delete-v1-domains-id) refuses mail from it at once and discards its key. Adding it again creates a new key and new records.
