# Domains and DNS

> How to verify a domain on Couryo. The DKIM (couryo1 and couryo2), return path and DMARC records the API generates, why there is no SPF on your root domain, and how DMARC moves up to p=reject.

Source: https://couryo.com/en/docs/domains

To send with your domain in `from`, it has to be verified. This proves to mailbox providers (Gmail, Microsoft, Yahoo and others) that Couryo may send on your behalf, and it is the single biggest factor in reaching the inbox.

## Add a domain

Creating a domain needs a key with the `admin` scope (or the dashboard).

```bash title="cURL"
curl https://api.couryo.com/v1/domains \
  -H "Authorization: Bearer $COURYO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "example.com" }'
```

The response (`201`) lists the records you need to create, each with its own `status`:

```json title="Response 201"
{
  "id": "dom_7h2kq9w4x1abcdef",
  "name": "example.com",
  "status": "pending",
  "dmarc_policy": "missing",
  "cloudflare_connected": false,
  "records": [
    { "purpose": "dkim", "type": "CNAME", "name": "couryo1._domainkey.example.com", "value": "couryo1.7h2kq9w4x1abcdef.dkim.couryo.com", "status": "pending" },
    { "purpose": "dkim", "type": "CNAME", "name": "couryo2._domainkey.example.com", "value": "couryo2.7h2kq9w4x1abcdef.dkim.couryo.com", "status": "pending" },
    { "purpose": "return_path", "type": "CNAME", "name": "bounces.example.com", "value": "rp.couryo.com", "status": "pending" },
    { "purpose": "dmarc", "type": "TXT", "name": "_dmarc.example.com", "value": "v=DMARC1; p=none; rua=mailto:dmarc@couryo.com; adkim=r; aspf=r", "status": "pending" }
  ],
  "created_at": "2026-10-07T18:30:00Z"
}
```

Both DKIM targets always follow `<selector>.<domain ID without the dom_ prefix>.dkim.couryo.com`. The ID above is an example: copy the values shown in the dashboard or in the API response for your domain.

> **Domains per plan:** Free 1, Pro 10, Scale 50 and Enterprise unlimited, across all projects of the account. Above that, the API answers `403` with the [`domain_limit_reached`](https://couryo.com/en/docs/errors.md#domain_limit_reached) code; deleting a domain frees the slot. A domain can only be registered once on Couryo (`409 domain_exists`).

## What each record does

| Record | Type | Name | What it is for |
|---|---|---|---|
| **DKIM** | CNAME | `couryo1._domainkey` and `couryo2._domainkey` | Every email is signed with a 2048-bit RSA key of your own domain. Both selectors point to Couryo's zone, where the public keys live: we sign with one while the other one's key is replaced, so keys rotate without you touching DNS. |
| **Return path** | CNAME | `bounces` | The technical envelope address, on a subdomain of yours. It carries SPF (aligned with your domain) and routes bounces to Couryo, which turns them into events. |
| **DMARC** | TXT | `_dmarc` | Tells providers what to do with email pretending to be from your domain, and where to send reports. `rua=mailto:dmarc@couryo.com` brings those reports to Couryo. It starts at `p=none`. |
| **Inbound** | MX | | Optional, to receive email in Couryo (coming soon). |

### Why there is no SPF on your root domain

SPF is checked on the envelope domain (the return path), not on the `From` domain. Since the envelope uses `bounces.example.com`, which points to Couryo by CNAME, SPF passes and is aligned with your domain. **Leave the SPF of `example.com` alone**: the one you already have (Google Workspace, Microsoft 365...) stays as it is, and Couryo uses none of the 10 DNS lookups SPF allows.

> If the response has a `return_path` record of type `MX` plus an `spf` record (`TXT v=spf1 include:spf.couryo.com ~all`), that is the alternative return path mode. Create both on the `bounces.` subdomain, never on the root domain.

## Verify

With Cloudflare connected, Couryo creates the records and verifies them on its own. Anywhere else, create the records and request a check (`admin` scope):

```bash title="cURL"
curl -X POST https://api.couryo.com/v1/domains/dom_7h2kq9w4x1abcdef/verify \
  -H "Authorization: Bearer $COURYO_API_KEY"
```

The check queries public DNS. Each record comes back with a `status`:

- `ok`: found and correct.
- `pending`: not found yet. DNS propagation can take from minutes to a few hours.
- `wrong`: found with a different value. The `found` field shows what is published, so you can compare.

If you already have your own DMARC record, it is respected: the record shows `ok` and `found` holds your value. Two DMARC records on the same name invalidate each other and show as `wrong`.

The domain becomes `verified` once DKIM, return path and DMARC are `ok` and Couryo's engine confirms the DKIM signature. Pending domains are checked again automatically every 10 minutes for 72 hours; verified ones, once a day. The first verified domain moves the account from level 0 to level 1 (see [Limits and levels](https://couryo.com/en/docs/limits.md)).

## Connect Cloudflare

In the dashboard, under **Domains**, use **Connect Cloudflare** and paste a token from your account with the **Zone > DNS > Edit** permission for the domain. Couryo creates the records in your zone and verifies right away. An existing DMARC record is never overwritten.

Possible errors: `invalid_cloudflare_token` (token refused or missing the permission) and `cloudflare_zone_not_found` (the token cannot reach the domain's zone).

## Tips by DNS provider

- **Cloudflare (by hand):** keep the CNAMEs DNS-only (grey cloud).
- **Other providers:** some append the domain automatically. If a record shows up as `couryo1._domainkey.example.com.example.com`, remove the repeated domain from the name.

## DMARC from p=none to p=reject

DMARC starts at `p=none`: providers only observe and send reports. Once reports are clean for a few weeks, it is worth moving to `p=quarantine` (spoofed email goes to spam) and then `p=reject` (spoofed email is refused). Walking DMARC up with your permission is the deliverability agent's next step (coming soon).

The domain's `dmarc_policy` field shows the policy published today: `none`, `quarantine`, `reject` or `missing`.

## Other endpoints

| Method and path | Scope | What it does |
|---|---|---|
| `GET /v1/domains` | `read` | lists the project's domains |
| `GET /v1/domains/{id}` | `read` | one domain, with its records |
| `DELETE /v1/domains/{id}` | `admin` | removes the domain (`204`) and frees the plan slot |
