Skip to content

Sending from your own domain

For ctx.email.send itself, see Sending email. Until you add a domain, your app’s emails come from <project>@mail.flatstack.app with your reply-to address. To send from your own domain, add it and create one DNS record.

Add the domain

curl -X POST https://api.flatstack.dev/v1/projects/$PROJECT/email-domains \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"domain":"example.com"}'

The response includes record, the one record to create at your DNS host:

Name Type Value
flatstack._domainkey.example.com CNAME <id>.dkim.mail.flatstack.app

That record proves you control the domain and lets us sign your emails, so they pass DMARC checks. You don’t need to change SPF.

A subdomain works too, such as mail.example.com. The name is then flatstack._domainkey.mail.example.com.

How checking works

We check DNS every minute for the first hour, then every hour for up to 72 hours. To check right away, call POST …/email-domains/example.com/check.

Status Meaning
pending Waiting for the record, or for the mail provider to confirm it. Emails use the default address.
verified Emails can come from this domain. We recheck the record once a day.
failed No correct record within 72 hours. Checks stop; fix the record, then check again.
lost The record was removed, or another project’s record now owns the domain. Emails use the default address with your from as the reply-to, and owners get an email; restore the record to resume.

When something’s wrong, diagnostics says what to fix. These are the common ones:

Code What to do
not_found Add the record. New records can take a few minutes to appear.
doubled_name Your DNS host adds the domain for you. Enter only flatstack._domainkey (plus any subdomain) as the name.
txt_instead_of_cname Delete the TXT record and add a CNAME instead.
proxied Set the record to DNS only. In Cloudflare, that’s the grey cloud.
points_elsewhere, wrong_target Change the record’s value to the one in record.value.
awaiting_provider Nothing to do. The record is right, and the mail provider confirms it within minutes.
in_use Another project sends from this domain. It moves here once your record points at this project’s value.

dnsProvider names your DNS host when we recognise it, so your agent can point you at its instructions.

Once the domain is verified, dmarc shows your existing DMARC policy. If you have none, it suggests one:

Name Type Value
_dmarc.example.com TXT v=DMARC1; p=none

This record is optional. We never change an existing one.

Moving a domain between projects

A domain sends for one project at a time. Adding it to a second project changes nothing until you point the CNAME at the second project’s value. Once it verifies there, the first project goes back to the default address and its owners get an email.

Rotating the key

POST …/email-domains/example.com/rotate-key replaces the signing key without any DNS change on your side. For about five minutes, while DNS caches pick up the new key, emails from the domain go from the default address with your from as the reply-to. Then they send from the domain again.

Removing a domain

DELETE …/email-domains/example.com with { "confirm": true } stops sending from the domain at once, and the action is recorded in your audit log. Then delete the CNAME at your DNS host. Until you do, it points at a name that no longer exists, and nobody can reuse that name.

API

Request Who What it does
GET /v1/projects/{id}/email-domains Everyone The project’s sender domains.
POST /v1/projects/{id}/email-domains Owners, admins, tokens with projects:write { "domain" }. Returns 201 with the record to add, or 200 with the existing domain.
GET /v1/projects/{id}/email-domains/{domain} Everyone Status, the record, the latest diagnostics and DMARC findings.
POST /v1/projects/{id}/email-domains/{domain}/check Owners, admins, tokens with projects:write Checks now.
POST /v1/projects/{id}/email-domains/{domain}/rotate-key Owners, admins, tokens with projects:write New signing key under the same record.
DELETE /v1/projects/{id}/email-domains/{domain} Owners, admins, tokens with projects:write Needs confirm: true.

A domain that can’t send, such as an IP address or a flatstack.app subdomain, returns 400 email_domain_invalid.

As Markdown: apps/email-domains.md