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.
Recommended: DMARC
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