Ship, observe, debug.
The message log is your audit trail and your debugging surface. Plus CSV export and the right-to-be-forgotten deletion endpoint.
1
The message log
Every message Mail sends gets a row in the workspace-scoped message log. Outbound only (inbound messages live under the destination mailbox). Listable, filterable, exportable.
curl 'https://api.mail.productcraft.co/v1/workspaces/<ws>/messages?template=welcome&status=delivered&limit=20' \
-H 'authorization: Bearer pcft_live_...'{
"data": [
{
"id": "e89a0605-c5ce-4e13-9755-4d4d8dc33ae1",
"workspace_id": "<ws>",
"domain_id": "77aa9383-...",
"from_address": "noreply@acme.com",
"to_address": "ada@example.com",
"subject": "Welcome to Acme, Ada!",
"template_name": "welcome",
"status": "delivered",
"smtp_transcript": "250 2.0.0 Ok: queued as B701E3F169",
"spf_result": null,
"dkim_result": null,
"dmarc_result": null,
"bounce_code": null,
"bounce_detail": null,
"created_at": "2026-05-11T11:38:42.660Z",
"delivered_at": "2026-05-11T11:38:42.872Z"
}
],
"pagination": { "has_more": true, "next_cursor": "eyJjcmVh..." }
}Query params: recipient, template, status (queued / sent / delivered / bounced / complained / failed / suppressed), subject_contains, plus limit and cursor for pagination.
2
Per-message detail
curl https://api.mail.productcraft.co/v1/workspaces/<ws>/messages/<message-id> \
-H 'authorization: Bearer pcft_live_...'Returns the same shape as the list row: current status plus the delivery evidence — smtp_transcript, SPF/DKIM/DMARC verdicts, and bounce_code + bounce_detail when the receiver rejected. The rendered HTML/text body is gated separately:
curl https://api.mail.productcraft.co/v1/workspaces/<ws>/messages/<message-id>/body \
-H 'authorization: Bearer pcft_live_...'Why a separate endpoint? Message bodies often contain PII — reset links with one-time codes, billing amounts, account details. The mail.message.body.read permission is distinct from mail.message.read so you can grant message-list access to support without exposing message contents. It adds body_html + body_text to the detail shape — both come back null if the workspace has redact_bodies enabled (toggle via PATCH /workspaces/:ws/messages/settings).
3
Volume timeline
Day-bucketed send volume for the workspace — the quick 'did our send rate fall off a cliff / did bounces spike' chart.
curl 'https://api.mail.productcraft.co/v1/workspaces/<ws>/messages/timeline?granularity=day&since=2026-05-01T00:00:00Z&until=2026-05-04T00:00:00Z&metric=sent' \
-H 'authorization: Bearer pcft_live_...'{
"granularity": "day",
"metric": "sent",
"points": [
{ "date": "2026-05-01", "count": 412 },
{ "date": "2026-05-02", "count": 398 },
{ "date": "2026-05-03", "count": 0 }
]
}granularity (only day today), since, and until are required; metric is sent (default) or bounced. Days with zero messages still appear with count: 0.
4
Export the message log
For compliance + analytics, the CSV export endpoint streams the whole filtered log in one response. Same filters as the list endpoint (recipient, template, status, subject_contains):
curl -o messages.csv \
'https://api.mail.productcraft.co/v1/workspaces/<ws>/messages/export?status=bounced' \
-H 'authorization: Bearer pcft_live_...'Rate-limited: one concurrent export and five per hour per workspace (excess returns 429). Message bodies are intentionally omitted from the CSV — they stay gated behind mail.message.body.read.
5
Attachments
Outbound sends don't support attachments — the send body takes to, from, data, and an optional subject, nothing else. Host the file (invoice PDF, report, export) on your own storage/CDN and link to it in the email body. That's better for deliverability and UX anyway — attachment-heavy mail trips spam filters, and a link opens instantly on a phone.
6
Right-to-be-forgotten deletion
GDPR / CCPA workflow — a recipient asks you to delete every record of them. Mail exposes a workspace-scoped bulk delete by email address.
curl -X DELETE \
'https://api.mail.productcraft.co/v1/workspaces/<ws>/messages?recipient=ada@example.com' \
-H 'authorization: Bearer pcft_live_...'
# → { "deleted": 42 }Irreversible. Removes every outbound message log row for that recipient address (matched case-insensitively) from this workspace and returns the deleted-row count. Requires mail.message.delete.
Doesn't touch the suppression list — the recipient stays suppressed (you don't want a bounced address to start bouncing again). Remove from suppression separately if needed.
7
Debugging guide
When a customer reports “I didn't get the email,” walk through these in order:
- Is the address suppressed?
GET /suppressionlists the workspace's suppressed recipients with reason + timestamp. Sends to a suppressed address are rejected with422at send time — no log row is written. Correct the address, or remove it from suppression with the recipient's consent. - Did we send?
GET /messages?recipient=ada@example.com&template=welcome. If no row, your application never called send (or the send call errored — check your own logs). - Did delivery succeed? Fetch the message detail.
deliveredmeans the receiver accepted; the email is in their world.bouncedmeans the receiver rejected —bounce_code+bounce_detailcarry the reason to share with the customer. - If delivered but recipient says missing — it's in their spam folder or their corporate filter quarantined it. Tell them to check spam; have them whitelist your domain.
- If stuck in queued or sent —
queuedmeans the message is accepted but not yet relayed;sentmeans the relay accepted it and the receiver's verdict is pending. Both normally resolve todeliveredorbouncedquickly; a message stuck there for hours is worth a support ticket.
What's next
You've finished the series
From here:
- API reference for every endpoint with full request/response schemas.
- Auth guide 07 for the Mail-integrated verification + reset shortcut.
- Platform docs if you want to automate Mail setup (mint PAK, create mailboxes, deploy templates from CI).