A domain is what you send from. Adding one creates its SES identity and gives you the DNS records to publish; from then on the platform checks those records and decides whether the domain may send.

Add a domain

curl
curl https://api.minket.com/v1/domains \
  -H "Authorization: Bearer $MINKET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "mail.acme.example", "stream": "transactional", "failover": true }'
Field Default Meaning
name required The domain or subdomain to send from. Use one subdomain per stream, for example mail. for transactional and news. for marketing mail.
stream transactional The stream every email from this domain belongs to.
receiving false Also receive mail for this domain; adds an MX record. See Inbound.
failover false Verify the identity in the secondary region (us-west-2) as well, so sending continues during an outage in us-east-1. Adds a second set of DKIM and MAIL FROM records.
auto_dns true Write the records through the organization's Cloudflare connection when the zone is on that account.
mail_from_subdomain bounce The MAIL FROM label. The secondary region uses the label followed by 2, so bounce2.
tracking { "subdomain": "links", "opens": false, "clicks": false } The tracking host and whether opens and clicks are recorded.

The response (201) is the domain object with its records. Each record has a purpose, type, name (fully qualified), value, priority (MX only), ttl (Auto), status, managed (true when the platform wrote it on Cloudflare), last_checked_at and last_error.

A domain name belongs to one project, so no other project, of this organization or another, can add it (409 conflict, which names no one); a subdomain is a name of its own. A project holds up to 50 domains.

One identity per region

Each region's identity has its own DKIM keys and MAIL FROM host, reported under primary (us-east-1) and secondary (us-west-2, with failover) on the domain object:

JSON
{
  "region": "us-east-1",
  "status": "verified",
  "dkim_status": "verified",
  "mail_from_status": "verified",
  "mail_from_domain": "bounce.mail.acme.example",
  "configuration_set": "minket-production-acme-transactional"
}

The configuration set is the project's set for the domain's stream and the identity's default, so mail sent over SMTP is tracked too. It suppresses addresses that bounced or complained and keeps SES's reputation metrics.

The records

For mail.acme.example with failover on:

Purpose Type Name Value
dkim, three per region CNAME <selector>._domainkey.mail.acme.example <selector>.dkim.amazonses.com
mail_from_mx MX bounce.mail.acme.example feedback-smtp.us-east-1.amazonses.com, priority 10
mail_from_spf TXT bounce.mail.acme.example v=spf1 include:amazonses.com ~all
mail_from_mx, secondary MX bounce2.mail.acme.example feedback-smtp.us-west-2.amazonses.com, priority 10
mail_from_spf, secondary TXT bounce2.mail.acme.example v=spf1 include:amazonses.com ~all
dmarc TXT _dmarc.mail.acme.example v=DMARC1; p=none; rua=mailto:dmarc@<receiving domain>
tracking CNAME links.mail.acme.example the platform's links. host
receiving_mx, with receiving on MX mail.acme.example inbound-smtp.us-east-1.amazonaws.com, priority 10
arc, with receiving on TXT <selector>._domainkey.mail.acme.example v=DKIM1; k=rsa; p=…, the key of the ARC seal on forwarded mail
bimi, on request TXT default._bimi.mail.acme.example v=BIMI1; l=<logo URL>

DKIM signs with 2048-bit keys. The custom MAIL FROM puts SES's bounce address under your domain, so SPF passes for a subdomain of the From domain and DMARC aligns on both SPF and DKIM.

The DMARC record starts at p=none, with reports sent to dmarc@ the project's receiving domain when it shares the organizational domain (inbound.acme.example for mail.acme.example, see DMARC reports). A DMARC record the domain already has is kept as it is. If the parent domain already applies quarantine or reject to its subdomains, the proposed record starts there instead: the platform never proposes a looser policy than the one in force.

Links in tracked mail use the platform's own links. host; the tracking CNAME is optional and never blocks sending.

Verify

POST /v1/domains/{id}/verify checks every record in public DNS and each SES identity now, and returns { "object": "domain", "id", "status", "records" }.

  • Domain status: not_started until the first check, then pending, verified, partially_verified (DKIM verified but MAIL FROM not yet, or the secondary region still pending) or failed.
  • Record status: pending while a record is missing in the first 72 hours, verified, failed (missing for longer, or holding another value; last_error says which) or temporary_failure (DNS did not answer).

Domains are checked again on their own: every hour until verified, every six hours after that. Every status change records a domain.updated event, and a verified domain that breaks (a record deleted or changed) also opens an alert in the console. A record that was verified and disappears fails at once, with no grace period.

The authentication gate

authentication on the domain object is passing, failing or unknown, computed from DNS checks only: the primary region's three DKIM CNAMEs and both MAIL FROM records must be in place. DKIM then signs with d= the domain itself and SPF passes for the MAIL FROM subdomain, so DMARC alignment holds whatever the alignment mode. DMARC aggregate reports never change it, so forged reports cannot pause your sending.

While it is failing, live sends from the domain are refused (the sending gate); test mode is exempt. When a verified domain's authentication turns failing, an alert opens in the console and resolves once the domain verifies and passes again. Sending resumes by itself when the next check passes. A DNS outage (temporary_failure) keeps the previous answer instead of pausing a domain that is fine.

sending: false on PATCH /v1/domains/{id} pauses a domain by hand.

DMARC, staged

Moving to quarantine and then reject is a decision for a person looking at the reports, so it happens in the console (the domain page, DMARC panel). It moves one step at a time, only while the domain is verified and its authentication passes, and nothing ever tightens it automatically. The API can always loosen it again with "dmarc_policy": "none":

Shell
curl -X PATCH https://api.minket.com/v1/domains/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b \
  -H "Authorization: Bearer $MINKET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "dmarc_policy": "none" }'

A new stage rewrites only the p= tag of the domain's DMARC record, keeping every other tag. The domain object reports what DNS holds now: dmarc.policy (missing, none, quarantine or reject, a policy inherited from the parent domain included), dmarc.record and dmarc.pass_rate_30d (pass rates). Tighten when the pass rate has stayed at 100% long enough to cover every system that sends as the domain, including the ones outside Minket.

BIMI

BIMI puts your logo beside your messages in Gmail, Apple Mail, Yahoo and other mailboxes. Set the logo, an SVG Tiny PS file over https, and optionally the mark certificate:

JSON
{ "bimi": { "logo_url": "https://acme.example/bimi/logo.svg", "certificate_url": "https://acme.example/bimi/vmc.pem" } }

The platform adds the default._bimi record and checks it like the others; "bimi": null removes it. Two conditions are outside the platform: DMARC at quarantine or reject (mailbox providers ignore BIMI under p=none), and for Gmail and Apple Mail a Verified Mark Certificate or Common Mark Certificate from a certificate authority, a paid purchase that needs a registered trademark (VMC) or a year of logo use (CMC). The console's BIMI panel says which of these is missing.

Automatic DNS on Cloudflare

Connect a Cloudflare API token with Zone, DNS, Edit in the console (Settings, Cloudflare). It is stored encrypted and never shown again. For a domain whose zone is on that account:

  • Through the API, auto_dns: true (the default) writes the records when the domain is created or changed.
  • In the console, adding a domain writes nothing: the domain page's Fix DNS shows the plan first (what would be created, updated, left alone, and why) and writes it when you confirm.

Records are always DNS-only, never proxied, and carry the comment minket:<domain id>. The platform updates or deletes only records it created itself; anything else at a record's name (an MX for Cloudflare Email Routing, an SPF record of another sender, your own DMARC record) is reported as a conflict and left alone. Removing the domain deletes the records the platform wrote and nothing else. For zones elsewhere, including partners' domains, copy the records from the console or from records.

Set up through your DNS provider

Without a token, the domain page can still add the records for you. Choose "set them up automatically" under DNS records: when your DNS provider supports it (Cloudflare first), "Sign in to Cloudflare" opens Cloudflare, which lists the records and adds them once you approve. You come back to the domain page, which checks DNS at once.

  • Each sign-in adds one set: DKIM, MAIL FROM and DMARC for the first region, then the second region's records if the domain fails over, then the receiving MX and ARC key.
  • A record already at a name is never replaced: an MX that routes the domain's mail elsewhere, or a DMARC record of your own, stays, and the page says which records to change yourself.
  • Later changes, such as a stricter DMARC policy, are yours to publish, or connect a token.

Update and remove

PATCH /v1/domains/{id} changes stream (the identity moves to the project's configuration set for that stream), receiving, failover (adds or removes the secondary identity and its records), auto_dns, sending, tracking.opens, tracking.clicks, bimi and dmarc_policy. DELETE /v1/domains/{id} removes the SES identities and the records the platform wrote; emails already sent keep their history. The response is { "object": "domain", "id", "status" }.

Updated