Overview
Outbound email goes through Resend. Every send is rendered server-side from a template in core.email_templates, addressed using your per-store sender, signed with the per-user signature of whoever triggered it, dispatched via the send-email edge function, and logged with delivery / open / bounce status from the resend-webhook handler.
Failures are visible. Hard bounces auto-suppress so you can’t accidentally hammer a dead inbox. Soft bounces retry with backoff. Suppressions are per-org and visible in Settings → Email log → Suppressions.
What working correctly looks like
- DNS for your sending domain shows green checks for SPF, DKIM, and Return-Path under Settings → Email → Domain.
- Sending a test from Settings → Email → Send test lands at the test address within ~30 seconds.
- Every send shows up in Settings → Email log with a status that progresses
queued → sent → delivered. - A
delivered_atand (when applicable)opened_attimestamp appears on each row within minutes. - Hard bounces append the recipient to Settings → Email log → Suppressions automatically.
- Magic-link emails from
auth-email-hookarrive in <30 seconds.
Setup — DNS, DKIM, SPF
Before you can send from billing@yourbrand.com, you’ll add four DNS records. Resend’s domain verification is the gate.
- Go to Settings → Email → Domain.
- Click Add domain and enter the apex of the address you want to send from (e.g.
yourbrand.com). -
Add the four records we show you to your DNS provider (Cloudflare, Route53, Namecheap, GoDaddy):
- 1 TXT for SPF (
v=spf1 include:resend.net ~all). - 2 CNAMEs for DKIM (
resend._domainkey,resend2._domainkey). - 1 MX for the Return-Path / bounce address.
- 1 TXT for SPF (
- Click Verify. Propagation is usually under 10 minutes; can take up to 48 hours.
If your domain already has a DMARC record, leave it. If not, start at v=DMARC1; p=none; rua=mailto:dmarc@yourbrand.com for monitoring before tightening to quarantine. We won’t fight an existing strict policy.
The full operator-side write-up with screenshots lives at Email setup walkthrough.
Per-store sender
Sender configuration is per-store. Settings → Email while the target store is active in the switcher.
- From address — the verified address recipients see (e.g.
orders@retail-brand.com). - From name — “Retail Brand — Orders”.
- Reply-to — defaults to the from address; override to route replies to a help-desk inbox.
- Footer text — small print appended to every send (legal address, unsubscribe note for marketing).
Each store can use a different sending domain. The pipeline picks up the right sender based on which store the trigger fired from.
Templates
Templates live in core.email_templates. Each template is a Mustache-style HTML body plus a subject line, keyed by a stable key. Default templates ship with the platform and can be overridden per-org.
Default templates
proposal_sent— the proposal you sent a client is ready for review.proposal_approved— client approved a proposal; sent to the operator.proposal_declined— client declined a proposal; sent to the operator.invoice_sent— a new invoice is ready to pay.invoice_reminder— invoice is approaching / past due.payment_received— receipt sent to the client.refund_issued— refund confirmation.order_shipped— carrier + tracking number.order_delivered— carrier delivery webhook fired.magic_link— sign-in / invite link (delivered viaauth-email-hook).password_reset— password-reset link.mfa_enrolled— new MFA factor was added to your account.billing_quota_warning— 50% / 80% / 100% quota notices.billing_payment_failed— Stripe payment failed; grace period started.team_invite— you’ve been invited to a workspace.
Per-org overrides
- Go to Settings → Email → Templates.
- Click the template you want to override.
- Edit the subject and body. The left side is your edit; the right side renders a live preview with sample tokens.
- Save. The override stores a row keyed by
(org_id, key); the platform falls back to the default if you ever delete the override.
Auth templates (magic_link, password_reset, mfa_enrolled) include tokens like {{ConfirmationURL}} and {{Token}}. Removing them silently breaks sign-in. The editor warns you, but it does not block save — ship a test send before relying on an edited auth template in production.
Per-user signatures
Email triggered by an operator (a proposal sent from Orders, a reply from the inbox) appends that operator’s signature.
- Each member: Settings → Profile → Signature.
- Edit the HTML signature (image, name, title, phone). The editor inlines images automatically.
- Save. The signature is rendered into
{{Signature}}on any template that uses it.
System emails (auth, billing, webhooks) don’t use a personal signature — they sign off with the org’s footer text from per-store sender.
Email log
Settings → Email log is the receipt for every send.
- Status —
queued,sent,delivered,opened,bounced,complained,suppressed. - Timestamps —
queued_at,sent_at,delivered_at,opened_at,bounced_at. - Recipient, subject, template key, store.
- Provider message id — clickable to open the entry in Resend’s console.
- Bounce reason — populated from the Resend webhook on bounce.
The log is retained for 365 days. Older rows are archived to cold storage with the audit archive cron.
Bounce handling & suppression
The resend-webhook edge function listens for delivery events and writes them back to your log.
- Hard bounce (mailbox doesn’t exist, domain unreachable) → recipient is appended to
core.email_suppressionsimmediately. Future sends to that address aresuppressedat queue time, never dispatched. - Soft bounce (full mailbox, transient 4xx) → we retry with exponential backoff up to 24 hours. After the 4th retry, we mark
bouncedbut do not suppress. - Spam complaint → recipient suppressed immediately and the audit log records a
email.complaint_receivedevent.
Suppression list
Settings → Email log → Suppressions shows every suppressed address with the reason and the timestamp. To unsuppress (only do this if you’ve confirmed the original cause is gone):
- Find the row.
- Click Remove from suppression list.
- Confirm. The next send to that address is dispatched normally.
If a domain hard-bounced, the address is dead. Sending again hurts your domain reputation. Un-suppress only when the recipient asks you to and confirms they fixed their mailbox.
Inbound replies
If you set the inbound webhook on your sending domain, replies sent to the from-address (or to a per-thread reply-to) flow back through resend-webhook and thread into the right place:
- Replies to a proposal email thread back into the proposal’s message timeline.
- Replies to an order email thread into the order’s message timeline.
- Replies to a generic
reply-to(e.g.support@) land in the workspace inbox under the matched client.
The reply-to address used on the outbound email determines threading. If you change it mid-thread, replies break threading and land in the inbox without a parent. See Clients → Conversations for the inbox itself.
Settings & permissions
- Settings → Email — per-store sender, domain, signature footer. Owner / admin.
- Settings → Email → Templates — per-org template overrides. Owner / admin.
- Settings → Email log — the send log + suppression list. Visible to all roles for sends in their granted stores; suppression management is admin-only.
- Settings → Profile → Signature — every member edits their own.
API + automation
The send-email edge function is internal. To send programmatically, fire one of the platform events that maps to a template (e.g. transition an order into shipped to fire order_shipped, post a payment to fire payment_received).
For one-off transactional sends from your own automation, hit the events endpoint and let our pipeline render and dispatch. There is no public “send arbitrary email” endpoint by design — that would let bad actors borrow our domain reputation. If you need it, write to {{CONTACT_EMAIL}}.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| “Domain not verified” | SPF / DKIM / Return-Path not propagated | Click Re-check on Settings → Email → Domain; if still red after 1 hour, re-paste the records. |
Email queued but never sent | Resend account paused or out of quota | Check Settings → Billing for our quota; check Resend status. |
Email sent but never delivered | Recipient’s server greylisting | Wait 5–15 min; if still nothing, check the bounce log on the row. |
Recipient says they got nothing, log says delivered | Spam folder | Have them whitelist the from-address; check for missing DMARC alignment. |
| All sends to one domain bouncing | That domain blocks our IP range | Suppression handles it. If you must reach them, ask them to whitelist resend.com. |
| Magic-link emails missing | auth-email-hook not configured or signature mismatch | Run a test sign-in; if blank, escalate to {{CONTACT_EMAIL}}. |
The full symptom-to-fix table lives at Troubleshooting → Email not delivered.
Related
- Email setup walkthrough — DNS records, the operator-side picture-by-picture.
- SMS pipeline — the other half of outbound communications.
- Auth & MFA — auth emails go through the same pipeline.
- Audit log — every send + every suppression event is recorded.
- Troubleshooting → Email not delivered