Verify your sender domain
Five-minute setup that takes your sends from "lands in spam" to "lands in inbox." Every step, every DNS provider, every gotcha — all in one page.
Why verify a domain#
Email is a trust system. Every receiver — Gmail, Outlook, Apple Mail, your corporate firewall — has to decide whether to deliver, route to spam, or reject. The signal they use is whether the sender owns the domain in the From header. Domain verification proves that.
Sendoka uses AWS SES DKIM for verification: when you add a domain, SES gives you three DNS records to add. Once they propagate, SES signs every outbound message with your domain's cryptographic key — receivers see the signature and trust the message.
Without verification: 403 DOMAIN_NOT_ALLOWED_FOR_KEY on send. Or, even if the domain exists in your account but the row is pending: SES rejects with MessageRejected: Email address is not verified.
Before you start#
You need three things to verify a domain:
- Owner-level access to your DNS provider (Cloudflare, Route 53, Namecheap, etc). You'll add 3 CNAME records.
- An owner role on the Sendoka org (or a platform-root API key with
write:domainsscope). - ~5 min of patience. DNS propagation is usually instant on modern providers (Cloudflare, Route 53), but some legacy providers can take up to 24h.
sandbox.sendoka.com auto-provisioned for testing. Sends from that domain hit a sandbox inbox — fine for prototyping, not for production. Real customer email needs a real verified domain.Quickstart (5 min)#
The fastest path. Add → copy 3 records → paste at DNS → verify.
1. Add the domain
In the dashboard or via API. We recommend a subdomain like mail.yourdomain.com over the root — see subdomain strategy.
Dashboard → Domains → Add domain
Enter: mail.yourdomain.com
Click "Add"curl -X POST https://api.sendoka.com/api/v1/domains \
-H "Authorization: Bearer $SENDOKA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "domain": "mail.yourdomain.com" }'const res = await fetch("https://api.sendoka.com/api/v1/domains", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDOKA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ domain: "mail.yourdomain.com" }),
});
const { id, dkim_tokens } = await res.json();2. Copy the three CNAME records
The response (or dashboard) shows three DKIM tokens. Each becomes a CNAME pointing at amazonses.com:
Type Host Target
───── ──────────────────────────────────────────────── ──────────────────────────────────
CNAME <token1>._domainkey.mail.yourdomain.com <token1>.dkim.amazonses.com
CNAME <token2>._domainkey.mail.yourdomain.com <token2>.dkim.amazonses.com
CNAME <token3>._domainkey.mail.yourdomain.com <token3>.dkim.amazonses.comThe exact tokens are unique to your domain — copy them from the dashboard or API response.
3. Add them at your DNS provider
Jump to your provider's specific recipe: Cloudflare · Route 53 · Vercel · Namecheap · GoDaddy · Google Domains.
4. Trigger verification
Most providers propagate in seconds. Click Re-check in the dashboard, or:
curl -X POST https://api.sendoka.com/api/v1/domains/dom_xxx/verify \
-H "Authorization: Bearer $SENDOKA_API_KEY"Status flips to verified. You can now send from any address at that domain.
DKIM (required)#
DomainKeys Identified Mail. A cryptographic signature added to every outbound message header. Receivers fetch your public key from DNS, verify the signature, and now know the message wasn't forged or modified in transit.
Sendoka creates an SES "Email Identity" for your domain when you add it. SES generates three DKIM key pairs and gives you the public-key CNAME records to publish. The three records are not optional — SES rotates between them, so all three must resolve.
Records live at <token>._domainkey.<your-domain>. They are CNAMEs, not TXT — a common mistake. The target ends in .dkim.amazonses.com.
SPF (recommended)#
Sender Policy Framework. Lists the servers allowed to send mail "from" your domain. DKIM proves authenticity; SPF proves the path. Most receivers want both aligned for inbox placement.
Add a single TXT record at the root of your sending domain (or update an existing v=spf1 record by appending to it):
Type Host Value
───── ───────────────────────── ───────────────────────────────────
TXT mail.yourdomain.com "v=spf1 include:amazonses.com ~all"~all is "soft fail" — receivers may treat unaligned senders as suspicious. Use -all ("hard fail") only after you're confident every legitimate sender is in the include list.
If you already have an SPF record at the same host, append include:amazonses.com to the existing one — you can have only one SPF TXT per host.
DMARC (recommended)#
Domain-based Message Authentication. Tells receivers what to do when a message fails DKIM and SPF — quarantine, reject, or just report. DMARC also lets you receive aggregate reports about who's sending as you (legit and otherwise).
A starter "monitoring" policy:
Type Host Value
───── ────────────────────────────────── ─────────────────────────────────────────────
TXT _dmarc.mail.yourdomain.com "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com"Start with p=none for 2–4 weeks while you read the aggregate reports. Then move to p=quarantine, then p=reject once nothing legitimate is failing.
MX (inbound only)#
MX records route incoming mail. You only need one if you want to receive email at this domain (e.g. catch replies for an auto-responder, run a help-desk inbox, etc).
For send-only domains, skip MX entirely. If you publish an MX record pointing at SES inbound, see the inbound messages guide.
Gmail / Google Workspace#
Most common confusion: “I'm using Gmail” can mean two different things, and only one of them works with Sendoka.
you@gmail.com via Sendoka (or any third-party provider). Google does not allow third parties to DKIM-sign mail from gmail.com— only Google's own servers can. Use a custom domain, even a free one.you@yourcompany.comhosted by Google): this works fine. You add Sendoka's DKIM CNAMEs at your DNS provider — wherever you actually registered the domain. Google Workspace handles inbound mail (MX records); DNS for everything else lives at your registrar.Step 1 — find where your DNS is hosted
Run this from any terminal:
dig NS yourdomain.com +short
# Examples:
# ns-cloud-a1.googledomains.com. ← Google Domains (legacy, migrating to Squarespace)
# adam.ns.cloudflare.com. ← Cloudflare DNS
# ns-1234.awsdns-... ← Route 53
# ns1.namecheap.com. ← NamecheapWhatever shows up is where you publish the DKIM CNAMEs. The provider guides below cover each common case. Workspace doesn't change this — having Workspace as your mail host doesn't mean your DNS is at Google.
Step 2 — publish the DKIM CNAMEs
Jump to the matching guide: Cloudflare · Google Domains / Squarespace · Route 53 · Namecheap · GoDaddy.
Step 3 — about Google Workspace's own DKIM
Workspace generates its own DKIM record (one TXT at google._domainkey.yourdomain.com) for mail sent from Workspace accounts. Sendoka's three CNAMEs are separate — they sit alongside Google's record at different host names. Both can coexist; they sign different mail. Don't replace one with the other.
Step 4 — required for sending to Gmail (Feb 2024+)
Google enforces rules for any sender hitting Gmail inboxes (still in effect 2026):
- SPF AND DKIM required for every send. Sendoka handles DKIM; you publish the SPF TXT (SPF section).
- DMARC required if you send more than ~5 000 messages/day to Gmail. Even
p=nonecounts — see DMARC. - One-click unsubscribe (RFC 8058) for marketing / bulk mail. Sendoka injects this header automatically when
track_opensortrack_clicksis on. - Spam complaint rate < 0.3%, ideally < 0.1%. Watch the Deliverability card on the dashboard.
Failing any of these = your mail bounces or lands in spam at Gmail. The bar is real — Yahoo / Apple followed in 2024 with similar policies.
Step 5 — verify with Gmail's message viewer
Best confirmation: send a test from Sendoka to a real Gmail inbox you control (not example.com). In Gmail, click the message → Show original. Top of the page:
SPF: PASS with IP 12.34.56.78
DKIM: 'PASS' with domain mail.yourdomain.com
DMARC: 'PASS'All three PASS = green light to scale. If DKIM is NEUTRAL or missing, the CNAMEs aren't resolving — re-run dig from troubleshooting.
Cloudflare#
Most-popular path; propagation is sub-second.
- Cloudflare dashboard → your domain → DNS → Records.
- Click Add record. Type:
CNAME. - Name: paste the host part shown in Sendoka (e.g.
<token1>._domainkey.mail) — Cloudflare auto-appends your root domain. - Target: the SES target (
<token1>.dkim.amazonses.com). - Proxy status: set to
DNS only(gray cloud). Proxied (orange cloud) breaks DKIM lookup. - Repeat for the other two tokens. Save.
AWS Route 53#
- AWS Console → Route 53 → Hosted zones → your domain.
- Create record. Routing policy: simple.
- Record name: the host part (
<token1>._domainkey.mail); the root is implicit. - Record type:
CNAME. Value: the SES target. - TTL: leave default (300s). Save.
- Repeat for the other two tokens.
Tip: if you're in CloudFormation / Terraform / CDK, templating the three records is one loop. We publish a CDK construct on the roadmap.
Vercel DNS#
- Vercel dashboard → your domain → DNS Records.
- Add. Type:
CNAME. - Name: the full host
<token1>._domainkey.mail.yourdomain.com(Vercel asks for the full record). - Value: the SES target.
- Save. Repeat for the other two tokens.
Namecheap#
- Namecheap dashboard → your domain → Advanced DNS.
- Add new record. Type:
CNAME Record. - Host: paste the host part (
<token1>._domainkey.mail); Namecheap appends the root. - Value: the SES target.
- TTL: Automatic. Save with the green checkmark.
- Repeat for the other two tokens.
Namecheap propagation can take up to an hour even though the dashboard shows the record immediately.
GoDaddy#
- My Products → your domain → DNS.
- Add → CNAME.
- Name: the host part. Value: the SES target.
- TTL: 1 hour (default is fine). Save.
- Repeat for the other two tokens.
GoDaddy is slowest of the major providers — 1–4h is typical, sometimes 24h.
Google Domains / Squarespace Domains#
Google Domains migrated to Squarespace in 2023 — UI varies depending on when your domain was migrated.
- Domain settings → DNS → Custom records.
- Add. Type:
CNAME. - Host: the host part. Data: the SES target.
- Repeat for all three tokens.
Trigger verification#
Once your DNS records are live, ask Sendoka to re-check. The dashboardRe-check button calls the same endpoint:
Internally we call SES GetEmailIdentityCommand and compare the DKIM verification status. If VerifiedForSendingStatus = true, your domain row flips to verified and verified_at is stamped.
How long does it take?
- Cloudflare, Route 53:< 30 seconds, often instant.
- Vercel, Namecheap: 1–10 minutes typically.
- GoDaddy, Google: 10 minutes to a few hours.
- Worst case: 24h. If you're past 24h, see troubleshooting.
Domain warmup#
New sender domains have no reputation. ISPs (Gmail, Outlook) treat a sudden spike from a brand-new domain as suspicious — even when DKIM, SPF, and DMARC all pass.
Sendoka enforces a daily ramp: 50 messages day 1, doubling each day, capped at the plan limit. Past the cap returns 429 WARMUP_LIMIT_EXCEEDED.
You can opt in / configure the window per-domain via warmup_started_at + warmup_days on the row. Most orgs leave it on the default 14-day ramp.
Practical advice
- Send to your most engaged users first. High open rates early build reputation fast.
- Avoid bulk newsletters during the first week.
- Watch the dashboard's Deliverability card — it shows bounce rate by domain, with warn/critical thresholds.
First test send#
Verified? Run a test from your own domain to a real inbox you control. Don't send to example.com — those bounce.
curl -X POST https://api.sendoka.com/api/v1/emails \
-H "Authorization: Bearer $SENDOKA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@mail.yourdomain.com",
"to": ["you@your-personal-inbox.com"],
"subject": "Verification test",
"html": "<p>If you can read this, DKIM signed correctly.</p>"
}'Open the email in Gmail and click "Show original". You want:
DKIM: PASSwith your domainSPF: PASS(if you added the SPF record)DMARC: PASS(if you added DMARC)
All three passing = green light to scale.
Subdomain strategy#
Recommendation: send from a subdomain, not the root.
Use mail.yourdomain.com or send.yourdomain.com instead of yourdomain.com. Why:
- Reputation isolation. If a marketing campaign tanks deliverability, your transactional sends from a different subdomain stay clean.
- Easier DMARC. Strict
p=rejecton transactional mail without affecting your team's regular email. - No conflicts. Existing SPF / DMARC at the root don't interfere.
Common conventions: mail. (transactional), news. (marketing), notifications. (product alerts). Pick one and stick with it.
Multiple domains#
Add as many as you need. Sendoka tracks reputation per-domain, so a transactional domain with p=reject DMARC and a marketing domain with looser policy can coexist.
The send picks DKIM signing key based on the from address — whichever domain matches gets used. Sending from a domain that isn't verified returns 403 DOMAIN_NOT_ALLOWED_FOR_KEY.
You can scope an API key to specific domains via allowed_domain_ids on the key — handy when you want a transactional service to be physically unable to send from a marketing domain even by mistake.
Per-tenant domains (platform mode)#
On the platform tier, each tenant can own its own sender domain — your customer Acme verifies mail.acme.com, your customer Globex verifies mail.globex.com, and the suppression / reputation books are kept separate.
Pass tenant_id when adding the domain (platform-root key) or use X-Sendoka-Tenant-Ref to auto-resolve the tenant slot:
curl -X POST https://api.sendoka.com/api/v1/domains \
-H "Authorization: Bearer $SENDOKA_PLATFORM_KEY" \
-H "X-Sendoka-Tenant-Ref: cust_12345" \
-H "Content-Type: application/json" \
-d '{ "domain": "mail.acme.com" }'The domain row gets stamped with tenant_id. Tenant-bound API keys can only send from their own tenant's domains — even if the platform owns other verified domains under the same org.
Remove or rotate#
Remove
Calls SES DeleteEmailIdentityCommand (best-effort) and removes the domain row. Existing messages stay queryable; the domain just can't be used for new sends.
Rotate
SES doesn't expose a "rotate DKIM keys" call directly — to rotate, delete the domain and re-add it. You'll get three new tokens, publish those CNAMEs, and the old ones can be removed once propagation completes (~1h).
In practice, almost no one rotates DKIM keys. The keys never leave AWS, so there's no leak vector.
Still pending after 24h#
Don't trust your DNS dashboard — check from outside.
# Replace <token>._domainkey.mail.yourdomain.com with one of yours
dig CNAME <token>._domainkey.mail.yourdomain.com +short
# Should return:
# <token>.dkim.amazonses.com.If dig returns nothing or a wrong target:
- Cloudflare: proxy is on (orange cloud). Switch to DNS only (gray cloud).
- Trailing dot: some providers want
<token>.dkim.amazonses.com.(with trailing dot), others want it without. Try both if the first doesn't resolve. - Wrong record type: these are CNAMEs, not TXT.
- Subdomain prefix collision: if you have a wildcard CNAME at
*.mail.yourdomain.com, more-specific tokens still resolve fine — but check your provider doesn't reject the more-specific record as "duplicate." - Hosting provider's DNS overrides yours: if your domain points NS records at Cloudflare but the actual records are at GoDaddy because you forgot to update them, none of this works. Run
dig NS yourdomain.comto see the authoritative resolver.
Partial DKIM verification#
You verified one or two of the three tokens but not all three. SES will still report the identity as VerifiedForSendingStatus = true once one passes — but until all three resolve, signing is brittle. Random percentage of sends could fail to sign, depending on which key SES rotates to.
Fix: re-check the missing record(s) with dig, fix at the DNS provider, wait, and trigger verify again.
Sends land in spam#
DKIM passes, SPF passes, but Gmail still routes to spam. Common causes, ranked by likelihood:
- You skipped warmup. If you blast 5 000 emails on day 1 from a fresh domain, Gmail tags it as suspicious. Send to engaged users first; ramp slowly.
- No DMARC record. Gmail strongly prefers DMARC-aligned senders. Add
v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com. - Subject + body trigger spam filters.
FREE!!!!, all-caps, lots of exclamation, link shorteners, no plain-text alternative — all signal spam. Test againstmail-tester.comfor a fresh score. - Recipient never engages. If your contact list hasn't opened anything in 6 months, they're a "sleeper" and lower your sender score. Suppress them.
- Reverse DNS / PTR mismatch. Less common with SES but worth checking — SES handles PTR for you on dedicated IPs.
The dashboard's Deliverability card surfaces bounce rate by domain. If it's above 5% you have a list quality problem, not a configuration problem.
Common error codes#
| Parameter | Type | Description | |
|---|---|---|---|
| DOMAIN_NOT_ALLOWED_FOR_KEY | 403 | optional | Key has allowed_domain_ids set; from-domain isn't on the list |
| DOMAIN_NOT_FOUND | 422 | optional | From-domain isn't in your org. Add it via /v1/domains. |
| DOMAIN_PENDING_VERIFICATION | 422 | optional | Domain row exists but still pending DKIM. Trigger /verify after DNS propagates. |
| DOMAIN_TENANT_MISMATCH | 403 | optional | Tenant-bound key trying to send from a domain owned by a different tenant |
| WARMUP_LIMIT_EXCEEDED | 429 | optional | Today's warmup cap reached. Resets at midnight UTC. |
| DOMAIN_ALREADY_ADDED | 409 | optional | POST /v1/domains where the domain already exists in the org |
API reference#
Endpoints
Scopes
read:domains— list, retrievewrite:domains— add, verify, remove
Domain object
{
"id": "dom_01HN…",
"domain": "mail.yourdomain.com",
"status": "verified", // pending | verified
"tenant_id": null, // or "tnt_…" in platform mode
"dkim_tokens": ["a1b2c3", "d4e5f6", "g7h8i9"],
"verified_at": "2026-04-30T10:14:22Z",
"warmup_started_at": "2026-04-30T10:14:22Z",
"warmup_days": 14,
"region": "us-east-1",
"fallback_region": null,
"created_at": "2026-04-30T09:30:00Z"
}