---
title: Sender Domains
path: concepts/sender-domains
status: published
---

# Sender Domains

Every live `From:` address has to use a verified sender domain. Verification proves you control the domain and lets ScaiSend sign your mail with DKIM. Without it, modern mail providers (Gmail, Outlook, Yahoo) will at best rate-limit you and at worst drop your mail silently.

This page walks through what verification is, how to add a domain, and the records you need to publish.

## What a "verified sender domain" means

A domain is verified when all three checks pass:

| Check | What's verified | Record |
|-------|-----------------|--------|
| DKIM | The public key in DNS matches the key ScaiSend holds | `TXT scaisend._domainkey.<domain>` |
| SPF | ScaiSend is authorized to send for the domain | `TXT <domain>` |
| DMARC | A DMARC policy exists at `_dmarc.<domain>` | `TXT _dmarc.<domain>` |

SPF and DMARC are technically optional — a message can deliver with DKIM alone — but missing them drops you into Gmail's "unverified sender" path. Publish all three.

## Adding a domain

Create the record in ScaiSend first; that generates the DKIM keypair you'll publish in DNS.

```bash
curl -X POST https://scaisend.scailabs.ai/api/admin/domains \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "mail.example.com",
    "dmarc_policy": "quarantine",
    "dmarc_rua_email": "dmarc-reports@example.com"
  }'
```

```python
import os
import httpx

resp = httpx.post(
    "https://scaisend.scailabs.ai/api/admin/domains",
    headers={"Authorization": f"Bearer {os.environ['SCAISEND_JWT']}"},
    json={
        "domain": "mail.example.com",
        "dmarc_policy": "quarantine",
        "dmarc_rua_email": "dmarc-reports@example.com",
    },
)
domain = resp.json()
print(domain["dns_records"])
```

```typescript
const resp = await fetch("https://scaisend.scailabs.ai/api/admin/domains", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAISEND_JWT}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    domain: "mail.example.com",
    dmarc_policy: "quarantine",
    dmarc_rua_email: "dmarc-reports@example.com",
  }),
});
const domain = await resp.json();
console.log(domain.dns_records);
```

Response:

```json
{
  "id": "dom_01HXYZ",
  "domain": "mail.example.com",
  "verified": false,
  "dkim_selector": "scaisend",
  "dmarc_policy": "quarantine",
  "dmarc_rua_email": "dmarc-reports@example.com",
  "is_shared": false,
  "is_active": true,
  "dns_records": [
    {
      "type": "TXT",
      "host": "scaisend._domainkey.mail.example.com",
      "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
    },
    {
      "type": "TXT",
      "host": "mail.example.com",
      "value": "v=spf1 include:scaisend.scailabs.ai ~all"
    },
    {
      "type": "TXT",
      "host": "_dmarc.mail.example.com",
      "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com"
    }
  ]
}
```

Copy `dns_records` and publish every row at your DNS provider.

## Getting the records any time

If you lost the response, refetch:

```bash
curl https://scaisend.scailabs.ai/api/admin/domains/dom_01HXYZ/dns-records \
  -H "Authorization: Bearer $SCAISEND_JWT"
```

## Verifying

After DNS has propagated (cheap providers: minutes; slow providers: hours), run the verification:

```bash
curl -X POST https://scaisend.scailabs.ai/api/admin/domains/dom_01HXYZ/verify \
  -H "Authorization: Bearer $SCAISEND_JWT"
```

Response:

```json
{
  "domain": "mail.example.com",
  "verified": true,
  "dkim_verified": true,
  "spf_verified": true,
  "txt_verified": true,
  "errors": []
}
```

If any check fails, `errors[]` tells you which record is missing or mismatched. Fix the DNS, wait for TTL, rerun. Verification is idempotent.

Once verified, the domain becomes usable as a `From:` address for any live send from this tenant.

## Rotating DKIM keys

Rotate annually or whenever a key might be compromised. Rotation generates a new keypair, publishes the new public key as `scaisend2._domainkey.<domain>` (different selector), and switches signing over once DNS propagates.

```bash
curl -X POST https://scaisend.scailabs.ai/api/admin/domains/dom_01HXYZ/rotate-dkim \
  -H "Authorization: Bearer $SCAISEND_JWT"
```

```python
import os, httpx

resp = httpx.post(
    f"https://scaisend.scailabs.ai/api/admin/domains/{domain_id}/rotate-dkim",
    headers={"Authorization": f"Bearer {os.environ['SCAISEND_JWT']}"},
)
print(resp.json()["new_selector"])
```

Response includes the new selector and the new public key TXT record. Publish it, wait for propagation, then re-verify. The old selector keeps working until you explicitly remove it — messages in flight signed with the old key remain verifiable.

## Shared sender domains

Partner operators can mark a domain as `is_shared: true`. Shared domains can be used as a `From:` address by any tenant under the partner.

```bash
curl -X POST https://scaisend.scailabs.ai/api/admin/domains \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{"domain": "noreply.platform.example", "is_shared": true}'
```

Use cases:

- A platform-wide "noreply" for password resets, login codes, receipts.
- A branded sender for customers who don't have their own verified domain yet.

Shared domains are owned by the partner; individual tenants can't modify them.

## DMARC policies

`dmarc_policy` controls what receivers should do with mail that fails both SPF and DKIM alignment. Three values:

| Policy | Meaning |
|--------|---------|
| `none` | Monitor only. Failures are reported but not blocked. Safe default when you're just starting. |
| `quarantine` | Failures go to spam. Reasonable once you're confident your setup is correct. |
| `reject` | Failures are bounced hard. Appropriate for established sender reputations. |

Start with `none`, check the DMARC aggregate reports sent to your `dmarc_rua_email` for a week or two, then graduate to `quarantine` once you've confirmed there's no legitimate mail being misattributed to you.

See [DKIM, SPF, DMARC](dkim-spf-dmarc) for the deeper treatment.

## What happens if the domain isn't verified

You'll hit one of these:

| Situation | Behavior |
|-----------|----------|
| `sg_live_*` send, unverified `From:` domain | Request is accepted (`202`) but the message ends in status `failed` with a "domain not verified" error in the event timeline. |
| `sg_test_*` send, unverified `From:` domain | Accepted, status `sandbox`. Sandbox mode skips the domain check because no delivery happens. |
| `sg_live_*` send, verified domain but DKIM DNS removed | Message sends but DKIM signature is invalid. Receivers will probably drop or quarantine. Re-verify. |

## Deactivating a domain

Deactivate to prevent further sends without deleting the record:

```bash
curl -X PATCH https://scaisend.scailabs.ai/api/admin/domains/dom_01HXYZ \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{"is_active": false}'
```

Sends with an inactive domain are rejected at queue time. Reactivate by setting `is_active: true`. Use this when you're rotating sender infrastructure; delete the record outright only when you're retiring the domain.

## What's next

- [Sending Mail](../tutorials/sending-mail) — using a verified domain in a send.
- [DKIM, SPF, DMARC](dkim-spf-dmarc) — the underlying standards in detail.
- [Admin Reference](../reference/admin) — every admin endpoint including domains.
