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:
| Kind | Type | Name | Value |
|---|---|---|---|
DKIM ×3 | CNAME | <token>._domainkey.mail.example.com | <token>.dkim.amazonses.com |
MAIL_FROM_MX | MX (priority 10) | bounce.mail.example.com | feedback-smtp.us-east-1.amazonses.com |
MAIL_FROM_SPF | TXT | bounce.mail.example.com | v=spf1 include:amazonses.com ~all |
DMARC | TXT | _dmarc.mail.example.com | v=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_VALUEon 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 toquarantineorrejectyourself 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
failedwith "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
pendingonly if SES reports DKIM or MAIL FROM asFAILED/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 yeton 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 removedmeans someone deleted the identity in the AWS console. Delete the domain here and add it again.Cloudflare is not connectedon Apply — re-authorise from Settings → Sending and click again, or download the zone file and add the records by hand.