# Domains & DNS

> Add a sending domain and publish its DKIM, return-path (MX + SPF) and DMARC records. How verification works and how to fix the usual DNS problems.

Source: https://docs.pidgely.com/guides/domains/

Pidgely sends only from domains you have verified. Verification proves you control the domain and, more importantly, sets up the authentication that mailbox providers check before they trust your mail: **DKIM**, an **SPF**-aligned return path and **DMARC**.

## Add a domain

```js tab="Node.js"
import { Pidgely } from 'pidgely';

const pidgely = new Pidgely(process.env.PIDGELY_API_KEY);

const { data, error } = await pidgely.domains.create({
  name: 'acme.com',
});
console.log(data ?? error);
```

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

pidgely.api_key = os.environ["PIDGELY_API_KEY"]

result = pidgely.Domains.create({
    "name": "acme.com",
})
print(result)
```

```php tab="PHP"
<?php

require __DIR__ . '/vendor/autoload.php';

$pidgely = Pidgely::client(getenv('PIDGELY_API_KEY'));

$result = $pidgely->domains->create([
    'name' => 'acme.com',
]);
var_dump($result);
```

```go tab="Go"
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/pidgely/pidgely-go"
)

func main() {
	ctx := context.Background()
	client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY"))

	result, err := client.Domains.Create(ctx, &pidgely.CreateDomainRequest{
		Name: "acme.com",
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(result)
}
```

```ruby tab="Ruby"
require "pidgely"

Pidgely.api_key = ENV["PIDGELY_API_KEY"]

result = Pidgely::Domains.create({
  name: "acme.com"
})
puts result
```

```bash tab="cURL"
curl -X POST 'https://api.pidgely.com/domains' \
  -H "Authorization: Bearer $PIDGELY_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "acme.com"
}'
```

The response ([Create a domain](https://docs.pidgely.com/api-reference/domains/create/)) includes the records to publish. Names are **relative to your domain**:

| Purpose | Type | Name | Value |
|---|---|---|---|
| DKIM | `TXT` | `pdg202610._domainkey` | `v=DKIM1; k=rsa; p=MIIBIjANBg…` (RSA-2048 public key) |
| Return path | `MX` | `bounces` | `mta.pidgely.com`, priority `10` |
| SPF | `TXT` | `bounces` | `v=spf1 include:_spf.mta.pidgely.com ~all` |
| DMARC (recommended) | `TXT` | `_dmarc` | `v=DMARC1; p=none;` |

Always copy the exact values from the API response or the dashboard: the DKIM selector (`pdg` plus the year and month the domain was added) and the key are unique to your domain.

> [!TIP]
> SPF lives on the **return-path subdomain** (`bounces.acme.com`), not on `acme.com`. You don't need to touch the SPF record of your root domain, and you can't exceed its 10-lookup limit because of Pidgely.

### Why these records

- **DKIM** signs every message with a key only Pidgely holds; receivers fetch the public key from `pdg…._domainkey.acme.com`. Pidgely signs with relaxed/relaxed canonicalization and oversigns From, Subject and To.
- **Return path.** Bounces go to `bounces.acme.com` (the envelope sender, with a per-message VERP address). The `MX` routes bounces back to Pidgely so they are [processed automatically](https://docs.pidgely.com/guides/suppressions/), and the SPF record authorizes Pidgely's servers for that subdomain. Because it is a subdomain of yours, SPF **aligns** with your From domain for DMARC.
- **DMARC** tells receivers what to do when authentication fails and where to send reports. Start with `p=none`, watch the reports, then move to `quarantine` or `reject`. Pidgely does not require DMARC to send, but Gmail and Yahoo require it for bulk senders.

Read more in [SPF, DKIM and DMARC explained](https://pidgely.com/blog/spf-dkim-dmarc-explained/).

## Options

| Field | Description |
|---|---|
| `custom_return_path` | Label of the return-path subdomain, default `bounces`. One DNS label: lowercase letters, digits and hyphens (`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`). |
| `tls` | `opportunistic` (default): use TLS whenever the receiving server offers it. `enforced`: deliver only over TLS, otherwise retry and eventually fail. Change it later with [Update a domain](https://docs.pidgely.com/api-reference/domains/update/). |

`region` is informative, and open/click tracking and receiving are [not supported yet](https://docs.pidgely.com/guides/migrate-from-resend/#not-supported-yet): `capabilities.receiving: "enabled"` is rejected.

## Verification

After publishing the records, call [Verify a domain](https://docs.pidgely.com/api-reference/domains/verify/) or wait: new domains are re-checked **every 5 minutes during their first 3 days**, and every domain is re-checked **daily** afterwards.

| Status | Meaning |
|---|---|
| `not_started` | The domain was added but never checked. |
| `pending` | Checked, but some required records are missing or wrong. DNS can take minutes (rarely hours) to propagate. |
| `verified` | DKIM, the return-path `MX` and SPF all pass. You can send. |
| `failed` | Records that used to pass no longer do (DNS changed), or the domain stayed unverified for more than 72 hours when verification was requested. Fix the DNS and verify again. |

Each record in `records[]` has its own `status`, so you can see exactly which one is missing. Status changes emit a [`domain.updated` webhook](https://docs.pidgely.com/guides/webhooks/#events).

## Rules

- **Exact match.** Mail from `news.acme.com` needs `news.acme.com` added as its own domain; `acme.com` doesn't cover subdomains.
- **One account per domain.** A domain can be verified by only one Pidgely account. Verifying a domain already verified elsewhere fails with `… is verified by another account`.
- **Plan limits.** Free allows 1 domain, Starter 3, Growth 10, Scale unlimited ([plans](https://docs.pidgely.com/rate-limits/#plans)). Adding more returns `403 validation_error`.
- **Deleting a domain** stops sending from it immediately; messages still queued for it are retried and eventually fail.

## Troubleshooting

**The record exists but Pidgely says it's missing.** Many DNS providers append your domain automatically. If you entered `pdg202610._domainkey.acme.com` as the name, the provider may have created `pdg202610._domainkey.acme.com.acme.com`. Enter only the relative part (`pdg202610._domainkey`).

**Check what the world sees** with `dig` (or `nslookup -type=TXT`):

```bash
dig +short TXT pdg202610._domainkey.acme.com
dig +short MX bounces.acme.com
dig +short TXT bounces.acme.com
dig +short TXT _dmarc.acme.com
```

**DKIM value split or truncated.** A 2048-bit key is longer than 255 characters. Most providers split long TXT values automatically; if yours asks for it, paste the value as several quoted strings in one record (`"v=DKIM1; k=rsa; p=MIIB…" "…rest"`). Make sure no quotes or spaces were added inside the key.

**MX on `bounces` conflicts with a CNAME.** A name can't have both. Remove the CNAME or choose another label with `custom_return_path`.

**Two SPF records on the same name.** If `bounces.acme.com` already had an SPF record, merge them into one: a name can have only one `v=spf1` record.

**Cloudflare and other proxies.** `MX` and `TXT` records are never proxied, so no special setting is needed.

**Still stuck?** [Contact support](https://docs.pidgely.com/support/) with the domain name; we can see which check fails.
