Send your first transactional email.
With a verified domain (Chapter 2), you're three API calls away from a delivered message. Create a mailbox, author a template, send.
1
Create a mailbox
curl https://api.mail.productcraft.co/v1/workspaces/<ws>/mailboxes \
-H 'authorization: Bearer pcft_live_...' \
-H 'content-type: application/json' \
-d '{
"domain_id": "<verified-domain-id>",
"localpart": "noreply",
"display_name": "Acme",
"description": "Operational notifications"
}'The mailbox is now noreply@acme.com. The display_name appears in the receiver's client as Acme <noreply@acme.com>.
2
Author a template
curl https://api.mail.productcraft.co/v1/workspaces/<ws>/templates \
-H 'authorization: Bearer pcft_live_...' \
-H 'content-type: application/json' \
-d '{
"name": "welcome",
"subject": "Welcome to {{product}}, {{name}}!",
"body_html": "<h1>Hi {{name}},</h1><p>Thanks for signing up for {{product}}. Get started at {{startUrl}}.</p>",
"body_text": "Hi {{name}}, thanks for signing up for {{product}}. Get started: {{startUrl}}",
"tags": ["onboarding"]
}'name is the lookup key — unique within the workspace, no spaces, kebab-case by convention. body_text is optional but recommended (some clients render plain text; some receivers penalise HTML-only messages).
3
Preview the rendered output
Before sending to a real recipient, render the template with sample data and inspect the output. Pure validation; doesn't dispatch anything.
curl https://api.mail.productcraft.co/v1/workspaces/<ws>/templates/welcome/render \
-H 'authorization: Bearer pcft_live_...' \
-H 'content-type: application/json' \
-d '{
"data": {
"name": "Ada",
"product": "Acme",
"startUrl": "https://app.acme.com/onboarding"
}
}'{
"subject": "Welcome to Acme, Ada!",
"body_html": "<h1>Hi Ada,</h1><p>Thanks for signing up for Acme. Get started at https://app.acme.com/onboarding.</p>",
"body_text": "Hi Ada, thanks for signing up for Acme. Get started: https://app.acme.com/onboarding"
}4
Send to a test recipient
Before sending to a real customer, the test-send endpoint lets you target one address with the template fully rendered. Useful for “does this actually look right in Gmail” checks.
curl https://api.mail.productcraft.co/v1/workspaces/<ws>/templates/welcome/test-send \
-H 'authorization: Bearer pcft_live_...' \
-H 'content-type: application/json' \
-d '{
"to": "yourself@acme.com",
"from_mailbox_id": "<noreply-mailbox-id>",
"data": { "name": "Ada", "product": "Acme", "startUrl": "https://..." }
}'5
Production send
curl https://api.mail.productcraft.co/v1/workspaces/<ws>/templates/welcome/send \
-H 'authorization: Bearer pcft_live_...' \
-H 'content-type: application/json' \
-H 'idempotency-key: welcome-2026-05-22-ada' \
-d '{
"to": "ada@example.com",
"from_mailbox_id": "<noreply-mailbox-id>",
"data": {
"name": "Ada",
"product": "Acme",
"startUrl": "https://app.acme.com/onboarding"
},
"metadata": {
"user_id": "648616c8-...",
"trigger": "signup"
}
}'Response includes the message_id — store it in your DB if you need to correlate later (bounce events, user complaints, audit trail). The send is asynchronous — the response confirms queued for delivery, notdelivered to inbox. The webhook (Chapter 5) is how you learn what actually happened.
Pre-flight failures (4xx, immediate):
- 412 — domain not verified, mailbox suspended, sender unconfigured
- 422 — recipient on suppression list
- 400 — template validation error, missing required variable
These return BEFORE any message row is created. Surface the body message in your operator UI so config errors don't silently swallow sends.
6
Batch send
For sending one template to many recipients with per-recipient data, use the batch endpoint. Each row is processed independently; partial failures don't abort the batch.
curl https://api.mail.productcraft.co/v1/workspaces/<ws>/templates/order-receipt/send-batch \
-H 'authorization: Bearer pcft_live_...' \
-H 'content-type: application/json' \
-d '{
"from_mailbox_id": "<billing-mailbox-id>",
"recipients": [
{ "to": "ada@example.com", "data": { "name": "Ada", "total": "$29" } },
{ "to": "alan@example.com", "data": { "name": "Alan", "total": "$49" } }
]
}'Response includes per-recipient status — successes get amessage_id, failures get an error with the reason (suppressed, template error, etc.). Soft limit: 100 recipients per call. For larger blasts, chunk client-side; we don't do that for you.