The Mail mental model.
Workspace, domain, mailbox, template, suppression list — the five primitives. Five minutes of reading saves an hour of misconfigured DNS.
1
Workspace = boundary
Everything in Mail is scoped to your workspace. Your domains, your templates, your suppression list, your API keys, your bounce events. Two workspaces share no data. Activate Mail on the workspace (Console → Workspace → Services → Mail → Enable) before anything works — workspaces without Mail enabled return 403 SERVICE_NOT_ENABLED.
2
Domain
A domain is what your customer sees in their From: header — e.g. acme.com sends as noreply@acme.com. Each domain holds its own DKIM keypair, gets independently verified via DNS, and tracks its own deliverability stats. Customers running staging + prod usually verify both acme.com and staging.acme.com.
Domain status moves through:
pending— created, required DNS records not yet verified. Cannot send. A failed check keeps the domainpendingand stores a per-record diff (dns_check_results) so you can see exactly which record is missing or wrong.active— DNS verified, can send. Mail re-checks pending domains every ~5 minutes and flips them toactiveautomatically once the records land — you don't have to keep clicking Verify.
3
Mailbox
A mailbox is a specific localpart@domain that can receive mail. support@acme.com and billing@acme.com are two mailboxes on one domain. Inbound mail on your domain is caught, parsed (subject, body, attachments), and stored under the matching mailbox. You can list, fetch, mark-as-read, delete. Useful for support inboxes, reply-tracking, and email-to-API integrations.
Sending doesn't require a mailbox — once a domain is verified you can send from any address on it (noreply@acme.com, hello@acme.com, …). Create mailboxes only for the addresses you want to receive on.
4
Template
A template is your message body, authored in Handlebars. One template = one logical message (welcome email, order confirmation, password reset). You version templates by editing in place; previous versions aren't retained on this service — keep a copy in your repo if that matters.
Templates have:
- name — unique within the workspace.
welcome,password-reset,order-confirmation. Used in the send call.POST /templatesupserts on name — same name, same template, updated in place. - subject + body_html — both Handlebars-templated. A plain-text alternative (
body_text) is auto-derived unless you supply one. - kind —
template(sendable, the default), orlayout/partialfor reusable chrome referenced from other templates. - tags — optional, used by the console for filtering / organisation.
5
Suppression list
A workspace-scoped block list. Once a recipient is on it, Mail refuses to send to that address regardless of the template or sender. Entries land on the list automatically when:
- The recipient hard-bounces (permanent SMTP 5xx failure → suppression, source
bounce). - A spam complaint is reported for the recipient (source
complaint). - You add the address yourself via the API (e.g. from your unsubscribe form — source
manual).
Soft failures (mailbox full, temporary deferral) don't suppress.
The suppression list is a hard wall. Attempting to send to a suppressed address returns 422 immediately — no message row is created, nothing leaves the building. The address stays suppressed until you explicitly remove it (DELETE /suppression/:email). Only test-send bypasses the list.
6
Sender model
When you call send, two things have to align:
- The
fromaddress's domain must belong to your workspace (403 if not) and be verified (409 if still pending). - The recipient must not be on the suppression list (422).
Failing either returns the 4xx immediately. No message row is created; no DKIM signing happens. The send call is the right place to find configuration mistakes — surface 4xx body messages to your operator UI so people see “Domain … is not verified yet” without trawling logs.
7
API auth
All Mail endpoints are workspace-scoped — paths like/workspaces/:workspaceId/.... Auth is the same shared ProductCraft cookie that the console uses, OR a workspace PAK (pcft_live_*). For programmatic access from your backend, mint a PAK with the mail.* scopes you need (the API reference calls out each route's gate).
curl https://api.mail.productcraft.co/v1/workspaces/<workspaceId>/domains \
-H 'authorization: Bearer pcft_live_...'