Mail guides
01 · Foundations

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 domain pending and 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 to active automatically 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 /templates upserts 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.
  • kindtemplate (sendable, the default), or layout / partial for 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 from address'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 with a PAK
curl https://api.mail.productcraft.co/v1/workspaces/<workspaceId>/domains \
  -H 'authorization: Bearer pcft_live_...'