Mail guides
03 · First send

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

POST /workspaces/:ws/mailboxes
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

POST /workspaces/:ws/templates
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.

POST /workspaces/:ws/templates/:name/render
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.

POST /workspaces/:ws/templates/:name/test-send
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

POST /workspaces/:ws/templates/:name/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.

POST /workspaces/:ws/templates/:name/send-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.