Domains: Add, DNS Setup & Verification

Set up sending domains with proper DNS authentication

A domain is your verified sending identity. Before you send live emails, you must add a domain and publish the DNS records Tratto returns for it.

Why Domains Matter

  • Sender Authentication: Prove you control the domain
  • Deliverability: SPF, DKIM, DMARC prevent spoofing and improve inbox placement
  • Trust: Recipients see your branded domain, not a third-party service

Add a Domain

Register your domain with Tratto. The number of domains is capped per plan and counted across all the workspaces of the account, including domains still pending and those that failed: past the cap the request fails with 429 QUOTA_EXCEEDED. See Plan Limits.

In the dashboard only the workspace owner adds, verifies and removes domains: a member can read them, and gets 403 FORBIDDEN (Only the workspace owner can do this.) on the rest. See Team.

cURL

curl -X POST https://api.tratto.email/v1/domains \
  -H "Authorization: Bearer tratto_live_..." \
  -H "Content-Type: application/json" \
  -d '{"domain":"hello.yourdomain.com"}'

Node.js

const response = await fetch('https://api.tratto.email/v1/domains', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer tratto_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ domain: 'hello.yourdomain.com' }),
});
const { data } = await response.json();
console.log('Domain ID:', data.id);

Python

import requests

response = requests.post(
  'https://api.tratto.email/v1/domains',
  headers={
    'Authorization': 'Bearer tratto_live_...',
    'Content-Type': 'application/json',
  },
  json={'domain': 'hello.yourdomain.com'},
)
data = response.json()['data']
print(f"Domain ID: {data['id']}")

PHP

$tratto = new \Tratto\Client(getenv('TRATTO_API_KEY'));

$domain = $tratto->domains->add('hello.yourdomain.com');
echo "Domain ID: {$domain['id']}\n";

Response (201; values are examples, always use the ones from your response):

{
  "data": {
    "id": "dom_abc123",
    "domain": "hello.yourdomain.com",
    "status": "pending",
    "records": [
      { "type": "CNAME", "host": "token1._domainkey.hello.yourdomain.com", "value": "token1.dkim.amazonses.com", "verified": false, "purpose": "dkim" },
      { "type": "CNAME", "host": "token2._domainkey.hello.yourdomain.com", "value": "token2.dkim.amazonses.com", "verified": false, "purpose": "dkim" },
      { "type": "CNAME", "host": "token3._domainkey.hello.yourdomain.com", "value": "token3.dkim.amazonses.com", "verified": false, "purpose": "dkim" },
      { "type": "TXT", "host": "hello.yourdomain.com", "value": "v=spf1 include:amazonses.com ~all", "verified": false, "purpose": "spf" },
      { "type": "TXT", "host": "_dmarc.hello.yourdomain.com", "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@hello.yourdomain.com", "verified": false, "purpose": "dmarc" },
      { "type": "TXT", "host": "_tratto.hello.yourdomain.com", "value": "tratto-verify=4f1c9a0e...", "verified": false, "purpose": "ownership" }
    ],
    "createdAt": "2026-06-30T12:00:00.000Z",
    "updatedAt": "2026-06-30T12:00:00.000Z",
    "verifiedAt": null
  }
}

Domain status is pending: Tratto is waiting for Amazon SES, which delivers your email, to find the DKIM records, and for the ownership record to be published.

DNS Records Setup

Add the records from records at your DNS provider (GoDaddy, Namecheap, Cloudflare, Route 53, etc.). Every domain gets six:

PurposeTypeHostValueNeeded to send
DKIMCNAME<token1>._domainkey.hello.yourdomain.com<token1>.dkim.amazonses.comYes
DKIMCNAME<token2>._domainkey.hello.yourdomain.com<token2>.dkim.amazonses.comYes
DKIMCNAME<token3>._domainkey.hello.yourdomain.com<token3>.dkim.amazonses.comYes
SPFTXThello.yourdomain.comv=spf1 include:amazonses.com ~allRecommended; if the host already has an SPF record, add include:amazonses.com to it
DMARCTXT_dmarc.hello.yourdomain.comv=DMARC1; p=quarantine; rua=mailto:dmarc@hello.yourdomain.comRecommended
OwnershipTXT_tratto.hello.yourdomain.comtratto-verify=<token>Yes

1. DKIM Records (required)

Type: CNAME, three records
Host: <token>._domainkey.hello.yourdomain.com
Value: <token>.dkim.amazonses.com

The three tokens are unique to your domain: copy each host and value from your response. Amazon SES generates and manages the signing key, and the CNAME records point to its public keys. All three must resolve before the domain becomes verified, together with the ownership record.

host is the full name. If your DNS provider appends your domain automatically, enter only the part before it (for example <token1>._domainkey.hello in the yourdomain.com zone), or the record ends up as <token1>._domainkey.hello.yourdomain.com.yourdomain.com.

Type: TXT
Host: hello.yourdomain.com
Value: v=spf1 include:amazonses.com ~all

This authorizes Amazon SES to send for the hostname.

A hostname must have exactly one v=spf1 record (RFC 7208, section 3.2). With two, the SPF check fails with a permanent error (permerror) for every sender on that hostname, not only Amazon SES. If the host already has an SPF record, for example from Cloudflare Email Routing, Google Workspace or Microsoft 365, add include:amazonses.com to it instead of creating a second one:

v=spf1 include:amazonses.com include:_spf.mx.cloudflare.net ~all

Type: TXT
Host: _dmarc.hello.yourdomain.com
Value: v=DMARC1; p=quarantine; rua=mailto:dmarc@hello.yourdomain.com

This asks receiving servers to quarantine mail that fails authentication. The policy covers all mail sent as this domain, not only mail sent through Tratto, so make sure your other senders authenticate before you publish it. Keep a single DMARC record per host. Tratto doesn't check this record, so you can adapt the policy and the rua address. If you are not sure that every other service sending as this domain authenticates, start with p=none, which only asks for reports, read them for a few weeks, then move to p=quarantine.

The suggested rua is a mailbox on the domain itself, because aggregate reports list the IPs and volumes of every source sending as your domain, Tratto or not, and that data belongs to you. A rua on a different domain only works if that domain publishes an authorization record for yours (RFC 7489, section 7.1); without it, receivers drop the reports. Tratto doesn't check that the mailbox exists, and doesn't receive or read your reports: create it, or point rua at an address on the same domain that you already read.

4. Ownership Record (required)

Type: TXT
Host: _tratto.hello.yourdomain.com
Value: tratto-verify=<token>

This record proves that your workspace controls the domain's DNS. The DKIM records alone do not: a domain removed from one workspace and added to another would otherwise be verified by the records the previous owner left in place. The token belongs to your workspace and to this exact domain, so a record published for another workspace never matches.

  • Copy the value from the record with purpose: "ownership" in records, returned by POST /v1/domains and GET /v1/domains/{id}.
  • The token stays the same if you remove the domain and add it again in the same workspace.
  • A subdomain and its parent domain are two domains: hello.yourdomain.com and yourdomain.com in the same workspace need two TXT records, _tratto.hello.yourdomain.com and _tratto.yourdomain.com, with different tokens.
  • A domain that is already verified does not need it: Tratto checks the record only when a domain moves to verified.

Verification

Tratto checks two things: it asks Amazon SES whether it has found the three DKIM CNAME records, then looks up the ownership TXT record itself. The domain becomes verified only when both are found. SPF and DMARC are never checked: their verified flag is always false, even on a verified domain. The three DKIM records share one flag, which turns true when Amazon SES has found them. The ownership record has its own flag, which turns true when Tratto finds the TXT record. Tratto looks up the TXT record only once the DKIM records are found, so while DKIM is still pending the ownership flag stays false, even if the TXT record is already published.

Checks happen in two ways:

  • Automatically: a background job re-checks every pending domain every 10 minutes
  • On demand: POST /v1/domains/{id}/verify checks right away

Amazon SES notes that DNS changes can take up to 72 hours to propagate, and it keeps looking for the DKIM records for up to 72 hours.

If the ownership record was looked up before you published it, a DNS resolver can remember that it was missing for as long as your zone's negative-caching TTL (RFC 2308), so the domain can stay pending for that long after you publish it. If the lookup itself fails (a DNS timeout or server error), the status stays as it was and the next check tries again.

Verify Now

cURL

curl -X POST https://api.tratto.email/v1/domains/dom_abc123/verify \
  -H "Authorization: Bearer tratto_live_..."

Node.js

const response = await fetch('https://api.tratto.email/v1/domains/dom_abc123/verify', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer tratto_live_...' },
});
const { data } = await response.json();
console.log('Status:', data.status);

Python

import requests

response = requests.post(
  'https://api.tratto.email/v1/domains/dom_abc123/verify',
  headers={'Authorization': 'Bearer tratto_live_...'},
)
data = response.json()['data']
print(f"Status: {data['status']}")

PHP

$tratto = new \Tratto\Client(getenv('TRATTO_API_KEY'));

$domain = $tratto->domains->verify('dom_abc123');
echo "Status: {$domain['status']}\n";

The response is the full domain, records included, with 200 whatever the outcome: read status, not the HTTP code.

StatusMeaning
pendingAmazon SES hasn't found all three DKIM records yet, the ownership record is not published yet, or a temporary SES or DNS issue prevented the check. Wait, or check again later.
verifiedThe DKIM records and the ownership record were found. Live emails can be sent from this domain.
failedAmazon SES stopped looking, typically because it couldn't find the DKIM records. The automatic check skips failed domains: see DNS Troubleshooting.

What verified Unlocks

  • Live sends: an email sent with a live key (tratto_live_...) is accepted only if its from domain is verified in your workspace. Otherwise the API returns 403 with FORBIDDEN.
  • Exact match: the check compares the full domain after @. Verifying yourdomain.com does not cover hello.yourdomain.com: add and verify each domain you send from.
  • Test sends: test keys (tratto_test_...) skip this check. See Test mode.
  • General default sender: when a domain first becomes verified and your workspace has no general default sender (defaultFromEmail), Tratto sets it to noreply@ on that domain. The per-type senders (marketing, automation, transactional) are not set automatically: see Senders.

After Verification

The automatic check looks only at pending domains. Once a domain is verified, nothing re-checks it on a schedule:

  • If you later remove or change a DKIM CNAME or the ownership TXT record, the domain stays verified in Tratto. Keep all the records in place for as long as you send from the domain.
  • POST /v1/domains/{id}/verify on a verified domain asks Amazon SES for the current DKIM status again. If SES no longer reports the DKIM records as found, the domain goes back to pending or failed, and live emails from it are refused until it is verified again. The ownership record is not looked up again while the domain stays verified.
  • SPF and DMARC are not checked, before or after verification.

Check Domain Status

Get a domain with its DNS records.

cURL

curl https://api.tratto.email/v1/domains/dom_abc123 \
  -H "Authorization: Bearer tratto_live_..."

Response:

{
  "data": {
    "id": "dom_abc123",
    "domain": "hello.yourdomain.com",
    "status": "verified",
    "records": [ ... ],
    "createdAt": "2026-06-30T12:00:00.000Z",
    "updatedAt": "2026-06-30T12:10:00.000Z",
    "verifiedAt": "2026-06-30T12:10:00.000Z"
  }
}

List Domains

Get all domains for your tenant. Each item has id, domain, status and timestamps, but no records: fetch a single domain to see them. Paginate with limit (1 to 100, default 50) and after.

cURL

curl https://api.tratto.email/v1/domains \
  -H "Authorization: Bearer tratto_live_..."

Remove a Domain

Delete a domain. Its identity in Amazon SES is deleted too.

cURL

curl -X DELETE https://api.tratto.email/v1/domains/dom_abc123 \
  -H "Authorization: Bearer tratto_live_..."

Removing a domain cannot be undone. Live emails from that domain are rejected until you add it again and verify it with the records from the new response.

DNS Troubleshooting

Still pending after adding records

Cause: DNS hasn't propagated yet, or a DKIM or ownership record doesn't match.

Fix:

  1. Compare each DKIM and ownership host and value with records from GET /v1/domains/{id}
  2. Check that your provider didn't append your domain to the host a second time
  3. Use MXToolbox or dig CNAME <token1>._domainkey.hello.yourdomain.com and dig TXT _tratto.hello.yourdomain.com to check that the records are live
  4. Wait for the next automatic check (every 10 minutes), or call POST /v1/domains/{id}/verify

SPF and DMARC don't affect the status: a missing SPF or DMARC record never keeps a domain pending.

Domain is failed

Cause: Amazon SES stopped looking, typically because it couldn't find the DKIM records.

Fix: correct the DKIM records, then call POST /v1/domains/{id}/verify, which reads the current status from Amazon SES. If the domain stays failed, remove it, add it again and publish the records from the new response.

Which Records Tratto Checks

RecordPurposeChecked by Tratto
DKIMSigns every email with a key Amazon SES manages for your domainYes, required to send
SPFAuthorizes Amazon SES to send for the hostnameNo, recommended
DMARCInstructs mail servers how to handle failed SPF/DKIMNo, recommended
OwnershipProves your workspace controls the domain's DNSYes, required to send

Best practice: Publish all six records for maximum deliverability.

Subdomain Sending

You can use subdomains for sending (e.g., hello.yourdomain.com) while your main domain stays clean. Tratto matches the from domain exactly, so add and verify the subdomain itself, with its own ownership record.

Advantages:

  • Isolate email reputation from main domain
  • Test new domains without affecting production
  • Separate sending and receiving infrastructure

Next Steps


Edit this page on GitHub

Last updated on