Contents

Domains

You can only send from a domain this instance has verified with SES. Adding one creates the SES identity, computes the DNS it needs, and then polls until SES agrees. When Cloudflare is connected, one click writes the records into the zone for you; nothing is written until you click. Otherwise the records come as one BIND zone file to upload at your provider.

Add a domain

Dashboard → Domains → Add, or with the SDK:

import { Sendsprite } from "sendsprite";

const sendsprite = new Sendsprite();

const domain = await sendsprite.domains.create({ name: "mail.example.com" });
// domain.status === "pending"; domain.records is empty until SES has
// issued the DKIM tokens, then lists every record with an `ok` flag.

Over REST:

curl https://sendsprite.com/api/v1/domains \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "mail.example.com" }'

The name is lowercased, a trailing dot is stripped, and it must be a valid hostname with at least two labels. A domain can only exist once per instance (conflict on a duplicate), and AWS has to be connected first (not_configured otherwise). A subdomain such as mail.example.com is the usual choice: it keeps your transactional reputation away from whatever else example.com sends.

dnsMode is not something you pass. Sendsprite picks it: auto when a connected Cloudflare zone covers the name, manual otherwise.

sendsprite.domains.get(id) and sendsprite.domains.list() return the same object, so a script can poll status and records[].ok the way the dashboard does.

The records

Provisioning creates the SES identity, sets the custom MAIL FROM domain to bounce.<your domain>, and stores the exact records it expects. For mail.example.com in us-east-1 that is six records:

KindTypeNameValue
DKIM ×3CNAME<token>._domainkey.mail.example.com<token>.dkim.amazonses.com
MAIL_FROM_MXMX (priority 10)bounce.mail.example.comfeedback-smtp.us-east-1.amazonses.com
MAIL_FROM_SPFTXTbounce.mail.example.comv=spf1 include:amazonses.com ~all
DMARCTXT_dmarc.mail.example.comv=DMARC1; p=none

What each one does:

  • DKIM — three CNAMEs pointing at SES-hosted keys, so SES can sign outgoing mail with your domain. Without them nothing sends.
  • MAIL FROM MX — routes bounce and complaint reports for the custom MAIL FROM subdomain back to SES. The identity is configured with USE_DEFAULT_VALUE on MX failure, so SES falls back to its own MAIL FROM rather than refusing to send.
  • MAIL FROM SPF — authorises SES for that subdomain, which is what makes SPF align with the visible From:.
  • DMARC — published at p=none, i.e. monitor only. Tighten it to quarantine or reject yourself once you are confident nothing else sends as this domain.

DKIM and MAIL FROM are what SES gates sending on. SPF and DMARC are checked too, but they are advisory: they show up per record in the dashboard and do not block verification.

The domain page

The page refreshes itself: every 5 seconds until SES has issued the DKIM tokens, every 15 seconds while the domain is pending, and only while the tab is visible. Nobody needs to reload to see a record turn green.

Cloudflare (auto mode)

auto means the domain sits in a Cloudflare zone you have authorised (see self-hosting). Once SES has issued the records, the domain page shows them with an Apply to Cloudflare button; nothing is written to the zone until someone clicks it. Each record is then upserted by type and name and its Cloudflare id stored alongside it — so Re-apply patches instead of duplicating (and repairs a record edited or deleted by hand), and deleting the domain removes what it created. If the grant has been revoked or has expired, Apply says so and the domain stays in auto: reconnect Cloudflare and the button works again, or add the records yourself in the meantime.

Apply is a dashboard action, not an API call: there is no /api/v1 route for it, and the SDK does not have one.

Anywhere else (manual mode)

In manual mode you add the records yourself. As soon as SES has issued them the page offers Download BIND zone file — every record above in one sendsprite-<domain>.zone file, which Cloudflare (DNS → Records → Import), Route 53 and BIND all accept. It carries absolute names, a $ORIGIN and a $TTL of 3600, and deliberately no SOA or NS: it adds records to your zone rather than defining one, so a provider will not read it as a replacement. It is deterministic, so downloading it twice gives the same bytes.

The file is served from the dashboard (/app/domains/:id/zone, session authenticated, any member) rather than the REST API, and answers 404 until provisioning has issued the records — which is exactly when the button appears.

Sendsprite also checks the domain's nameservers once during provisioning. When they are Cloudflare's, the page links straight to that zone's DNS records — no credentials involved, but you still enter the values yourself.

Verification timing

  • The first check runs 30 seconds after provisioning finishes.
  • A pending domain is re-checked by a sweep every 2 minutes (rows checked within the last 100 seconds are skipped so two ticks never duplicate work).
  • A domain that has not verified within 72 hours is marked failed with "Verification timed out after 72 hours. Check the records and click Re-verify."
  • A verified domain is re-checked once a day. It stays verified while SES still reports success, and is demoted to pending only if SES reports DKIM or MAIL FROM as FAILED/NOT_STARTED, or the identity is gone. A transient SES state (PENDING, TEMPORARY_FAILURE) keeps it verified — sending carries on — with a note on the domain. A demotion opens a fresh 72-hour window.

Re-verify resets the window and runs one check inline, so the call answers immediately:

const domain = await sendsprite.domains.verify(id); // safe to retry

Over REST that is POST /api/v1/domains/:id/verify.

Status transitions fan out as webhooks: domain.verified when a domain becomes verified, domain.failed when a verified domain is demoted or a check fails it for good.

Deleting a domain

const result = await sendsprite.domains.delete(id);
// undefined when everything was cleaned up (204), or
// { leftoverDnsRecords: n } (200) when n Cloudflare records need removing by hand.

Over REST, DELETE /api/v1/domains/:id. Deleting removes the SES identity and the Cloudflare records Sendsprite created; anything it could not remove is reported back as leftoverDnsRecords for you to clean up.

Troubleshooting

  • Stuck pending. Compare each record in the dashboard against what your resolver returns; the per-record ✓/✗ says which one SES or DNS is still unhappy about. TXT records split across strings and CNAMEs flattened by a proxy are the usual culprits — DKIM CNAMEs must not be proxied.
  • Provisioning hasn't finished yet on Re-verify means the SES identity does not exist yet, so there are no DKIM tokens to check. Wait for provisioning, or use Retry provisioning if it never ran.
  • SES identity was removed means someone deleted the identity in the AWS console. Delete the domain here and add it again.
  • Cloudflare is not connected on Apply — re-authorise from Settings → Sending and click again, or download the zone file and add the records by hand.