# PostStack API Reference > Complete API documentation for PostStack — an email API platform for developers. Base URL: `https://api.poststack.dev` --- ## Authentication All API requests require a Bearer token in the Authorization header: ``` Authorization: Bearer sk_live_... ``` Three credential types are accepted on every endpoint: - `sk_live_…` — production API key (this is what most integrations use) - `sk_test_…` — test-mode key; full validation, never delivers, never bills - `pat_…` — OAuth 2.0 access token (see OAuth section below) ### API Key Permissions - `full_access`: Send emails, manage domains, contacts, templates, webhooks, and all other resources - `sending_access`: Send emails and view email status only ### Domain-Scoped Keys API keys can be scoped to specific domains: ```json { "name": "Marketing emails only", "permission": "sending_access", "domain_ids": ["dom_abc123", "dom_def456"] } ``` ### SMTP Authentication - Username: `poststack` - Password: Your API key (`sk_live_...`) ### OAuth 2.0 PostStack ships a full OAuth 2.0 / OpenID Connect Authorization Server for delegated agent access. Discover endpoints from: - `https://poststack.dev/.well-known/oauth-authorization-server` (RFC 8414) - `https://poststack.dev/.well-known/openid-configuration` (OIDC Discovery) - `https://poststack.dev/.well-known/oauth-protected-resource` (RFC 9728 — points `/api/mcp` at the AS) **Supported flows and extensions:** - `authorization_code` grant with PKCE (S256) — required - `refresh_token` grant - Public clients (RFC 8252) — no `client_secret`; PKCE only - Pushed Authorization Requests (PAR, RFC 9126) at `/oauth/par` - Rich Authorization Requests (RAR, RFC 9396) — `authorization_details` types: `email_send`, `domains_manage` - JWT-Secured Authorization Requests (JAR, RFC 9101) — `request` parameter - DPoP-bound access tokens (RFC 9449) — `RS256`, `PS256`, `ES256`, `ES384`, `EdDSA` - mTLS-bound access tokens (RFC 8705) **Scopes:** - `email:send` — send emails - `email:read` — read email history, analytics, suppressions - `domains:manage` — create, verify, delete sending domains - `domains:read` — view domains and verification status - `mailboxes:manage` — create and manage IMAP/POP3 mailboxes - `api-keys:manage` — create, rotate, revoke API keys - `mcp:read` — read-only MCP tools - `mcp:send` — MCP tools that send email - `mcp:admin` — MCP tools that mutate account state Issued access tokens start with `pat_`. They are revocable at `/oauth/revoke` (RFC 7009) and introspectable at `/oauth/introspect` (RFC 7662). OIDC ID tokens are signed with `RS256`; JWKS at `/oauth/jwks`. ### MCP (Model Context Protocol) For AI agents, prefer the hosted MCP server over raw REST: - Manifest: `https://poststack.dev/.well-known/mcp.json` (transport URL, auth methods, scopes) - Server card: `https://poststack.dev/.well-known/mcp/server-card.json` (SEP-1649) - Transport: `streamable-http` at `https://poststack.dev/api/mcp` - Auth: API key (Bearer `sk_live_…`) or OAuth (`mcp:*` scopes) Local install for Claude Code, Cursor, Windsurf, etc.: ```bash claude mcp add poststack -e POSTSTACK_API_KEY=sk_live_... -- npx -y @poststack.dev/mcp ``` 72+ tools covering the full REST API surface — emails, contacts, domains, templates, broadcasts, segments, workflows, webhooks, mailboxes. --- ## Emails ### Send Email ``` POST /emails ``` **Request Body:** ```json { "from": "you@yourdomain.com", "to": ["user@example.com"], "subject": "Welcome", "html": "

Welcome!

", "text": "Welcome!", "reply_to": "reply@yourdomain.com", "headers": { "X-Custom": "value" }, "tags": ["onboarding", "welcome"], "scheduled_at": "2026-03-25T10:00:00Z" } ``` Required: `from`, `to`, `subject`, and at least one of `html` or `text`. **Response (201):** ```json { "id": "em_abc123def456ghi789", "from": "you@yourdomain.com", "to": ["user@example.com"], "subject": "Welcome", "status": "queued", "created_at": "2026-03-25T09:00:00Z" } ``` **SDK:** ```typescript const email = await poststack.emails.send({ from: 'you@yourdomain.com', to: ['user@example.com'], subject: 'Welcome', html: '

Welcome!

', }); ``` ### Send Batch Emails ``` POST /emails/batch ``` Send up to 100 emails in one request. **Request Body:** ```json { "emails": [ { "from": "you@yourdomain.com", "to": ["user1@example.com"], "subject": "Hello User 1", "html": "

Hello!

" }, { "from": "you@yourdomain.com", "to": ["user2@example.com"], "subject": "Hello User 2", "html": "

Hello!

" } ] } ``` **Response (201):** ```json { "emails": [ { "id": "em_abc123", "status": "queued" }, { "id": "em_def456", "status": "queued" } ] } ``` ### List Emails ``` GET /emails ``` **Query Parameters:** | Parameter | Type | Description | |-----------|--------|--------------------------| | page | number | Page number (default: 1) | | per_page | number | Items per page (default: 20, max: 100) | | status | string | Filter: queued, sending, delivered, bounced, complained, failed | | domain | string | Filter by domain name | | tag | string | Filter by tag | **Response (200):** ```json { "data": [ { "id": "em_abc123", "from": "you@yourdomain.com", "to": ["user@example.com"], "subject": "Welcome", "status": "delivered", "created_at": "2026-03-25T09:00:00Z" } ], "meta": { "total": 150, "page": 1, "per_page": 20, "total_pages": 8 } } ``` ### Get Email ``` GET /emails/:id ``` **Response (200):** ```json { "id": "em_abc123", "from": "you@yourdomain.com", "to": ["user@example.com"], "subject": "Welcome", "status": "delivered", "html": "

Welcome!

", "text": "Welcome!", "tags": ["onboarding"], "created_at": "2026-03-25T09:00:00Z", "sent_at": "2026-03-25T09:00:01Z", "delivered_at": "2026-03-25T09:00:02Z", "events": [ { "type": "queued", "timestamp": "2026-03-25T09:00:00Z" }, { "type": "sent", "timestamp": "2026-03-25T09:00:01Z" }, { "type": "delivered", "timestamp": "2026-03-25T09:00:02Z" } ] } ``` ### Cancel Email ``` POST /emails/:id/cancel ``` Cancel a queued or scheduled email before it is sent. **Response (200):** ```json { "id": "em_abc123", "status": "cancelled" } ``` --- ## Domains ### Create Domain ``` POST /domains ``` **Request Body:** ```json { "name": "yourdomain.com" } ``` **Response (201):** ```json { "id": "dom_abc123", "name": "yourdomain.com", "status": "pending", "dns_records": [ { "type": "TXT", "name": "yourdomain.com", "value": "v=spf1 include:...", "purpose": "spf", "verified": false }, { "type": "CNAME", "name": "poststack._domainkey.yourdomain.com", "value": "...", "purpose": "dkim", "verified": false }, { "type": "TXT", "name": "_dmarc.yourdomain.com", "value": "v=DMARC1; p=quarantine;...", "purpose": "dmarc", "verified": false }, { "type": "CNAME", "name": "bounce.yourdomain.com", "value": "...", "purpose": "return_path", "verified": false } ], "created_at": "2026-03-25T09:00:00Z" } ``` ### List Domains ``` GET /domains ``` **Response (200):** ```json { "data": [ { "id": "dom_abc123", "name": "yourdomain.com", "status": "verified", "created_at": "2026-03-25T09:00:00Z" } ] } ``` ### Get Domain ``` GET /domains/:id ``` Returns domain with full `dns_records[]` including verification status for each record. ### Update Domain ``` PATCH /domains/:id ``` **Request Body (all fields optional):** ```json { "open_tracking": true, "click_tracking": true, "custom_return_path": "bounce.yourdomain.com", "tracking_domain": "track.yourdomain.com", "inbound_enabled": true, "bimi_logo_url": "https://yourdomain.com/logo.svg", "bimi_selector": "default" } ``` ### Delete Domain ``` DELETE /domains/:id ``` **Response (200):** ```json { "deleted": true } ``` ### Verify Domain ``` POST /domains/:id/verify ``` Trigger DNS verification check. Auto-verification also runs every 30 minutes. **Response (200):** Domain with updated `dns_records[]` verification status. ### Tracking Domain Setup To use a custom tracking domain, add a CNAME record: ``` track.yourdomain.com → track.poststack.dev ``` ### Inbound Email Setup To receive emails, add an MX record: ``` Type: MX Name: yourdomain.com Value: inbound.poststack.dev Priority: 10 ``` --- ## Contacts ### Create Contact ``` POST /contacts ``` **Request Body:** ```json { "email": "user@example.com", "first_name": "Alice", "last_name": "Smith", "properties": { "plan": "pro", "company": "Acme Inc" }, "unsubscribed": false } ``` **Response (201):** ```json { "id": "ct_abc123", "email": "user@example.com", "first_name": "Alice", "last_name": "Smith", "properties": { "plan": "pro", "company": "Acme Inc" }, "unsubscribed": false, "created_at": "2026-03-25T09:00:00Z" } ``` ### List Contacts ``` GET /contacts ``` **Query Parameters:** | Parameter | Type | Description | |------------|--------|--------------------------------| | search | string | Search by email or name | | segment_id | string | Filter by segment membership | | page | number | Page number (default: 1) | | per_page | number | Items per page (default: 20) | ### Get Contact ``` GET /contacts/:id ``` ### Update Contact ``` PATCH /contacts/:id ``` **Request Body (all fields optional):** ```json { "first_name": "Alice", "last_name": "Johnson", "properties": { "plan": "enterprise" } } ``` ### Delete Contact ``` DELETE /contacts/:id ``` ### Unsubscribe Contact ``` POST /contacts/:id/unsubscribe ``` **Response (200):** ```json { "id": "ct_abc123", "email": "user@example.com", "unsubscribed": true, "unsubscribed_at": "2026-03-25T09:00:00Z" } ``` ### Import Contacts (CSV) ``` POST /contacts/import ``` Multipart form-data with a CSV file. Required column: `email`. Optional columns: `first_name`, `last_name`, and any custom property fields. **Response (200):** ```json { "imported": 95, "updated": 5, "errors": 0 } ``` ### Export Contacts ``` GET /contacts/export ``` **Query Parameters:** | Parameter | Type | Description | |------------|--------|--------------------------------| | segment_id | string | Filter by segment (optional) | Returns a CSV file. --- ## Templates ### Create Template ``` POST /templates ``` **Request Body:** ```json { "name": "Welcome Email", "subject": "Welcome, {{first_name}}!", "html": "

Welcome, {{first_name}}!

Thanks for joining {{company_name}}.

", "text": "Welcome, {{first_name}}! Thanks for joining {{company_name}}.", "variables": ["first_name", "company_name"] } ``` Variables use handlebars-style `{{variable}}` placeholders in subject, html, and text. **Response (201):** ```json { "id": "tpl_abc123", "name": "Welcome Email", "subject": "Welcome, {{first_name}}!", "status": "draft", "variables": ["first_name", "company_name"], "created_at": "2026-03-25T09:00:00Z" } ``` ### List Templates ``` GET /templates ``` ### Get Template ``` GET /templates/:id ``` ### Update Template ``` PATCH /templates/:id ``` ### Delete Template ``` DELETE /templates/:id ``` ### Publish Template ``` POST /templates/:id/publish ``` ### Unpublish Template ``` POST /templates/:id/unpublish ``` ### Duplicate Template ``` POST /templates/:id/duplicate ``` Creates a copy with "(Copy)" appended to the name, status set to draft. ### Get Template Presets ``` GET /templates/presets ``` ### Using Templates When Sending ```typescript await poststack.emails.send({ from: 'you@yourdomain.com', to: ['user@example.com'], template_id: 'tpl_abc123', variables: { first_name: 'Alice', company_name: 'Acme Inc', }, }); ``` --- ## Broadcasts ### Create Broadcast ``` POST /broadcasts ``` **Request Body:** ```json { "name": "Product Launch", "from": "news@yourdomain.com", "subject": "Introducing our new product", "html": "

Big news!

", "segment_id": "seg_abc123", "topic_id": "top_abc123", "scheduled_at": "2026-03-26T10:00:00Z" } ``` `segment_id`: only send to contacts in this segment. `topic_id`: only send to contacts subscribed to this topic. ### List Broadcasts ``` GET /broadcasts ``` **Query Parameters:** | Parameter | Type | Description | |-----------|--------|--------------------------------------------| | status | string | Filter: draft, sending, sent, cancelled | | page | number | Page number | | per_page | number | Items per page | **Response includes:** id, name, status, recipients, delivered, opened, clicked, sent_at, created_at. ### Get Broadcast ``` GET /broadcasts/:id ``` Returns full broadcast with statistics: recipients, delivered, bounced, opened, clicked, unsubscribed. ### Send Broadcast ``` POST /broadcasts/:id/send ``` Send a draft broadcast immediately. **Response (200):** ```json { "id": "bc_abc123", "status": "sending", "recipients": 1500 } ``` ### Test Broadcast ``` POST /broadcasts/:id/test ``` **Request Body:** ```json { "to": "test@yourdomain.com" } ``` ### Cancel Broadcast ``` POST /broadcasts/:id/cancel ``` --- ## Segments ### Create Segment ``` POST /segments ``` **Request Body (dynamic segment):** ```json { "name": "Pro Users", "type": "dynamic", "rules": [ { "field": "properties.plan", "operator": "equals", "value": "pro" }, { "field": "created_at", "operator": "greater_than", "value": "2026-01-01" } ] } ``` **Request Body (manual segment):** ```json { "name": "VIP Customers", "type": "manual" } ``` ### Dynamic Segment Operators `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `greater_than`, `less_than`, `is_set`, `is_not_set` ### Available Fields Standard: `email`, `first_name`, `last_name`, `subscribed`, `created_at` Custom: `properties.plan`, `properties.company`, `properties.signup_source` (dot notation) ### List Segments ``` GET /segments ``` ### Get Segment ``` GET /segments/:id ``` ### Preview Segment ``` GET /segments/:id/preview ``` Returns matching contacts and total count without persisting. ### Add Contacts to Manual Segment ``` POST /segments/:id/contacts ``` **Request Body:** ```json { "contact_ids": ["ct_abc123", "ct_def456"] } ``` ### Remove Contacts from Segment ``` DELETE /segments/:id/contacts ``` **Request Body:** ```json { "contact_ids": ["ct_abc123"] } ``` ### Delete Segment ``` DELETE /segments/:id ``` --- ## Subscription Topics ### Create Topic ``` POST /subscription-topics ``` **Request Body:** ```json { "name": "Product Updates", "description": "News about new features and improvements" } ``` ### List Topics ``` GET /subscription-topics ``` ### Update Topic ``` PATCH /subscription-topics/:id ``` ### Delete Topic ``` DELETE /subscription-topics/:id ``` ### Subscribe Contact to Topic ``` POST /subscription-topics/:id/subscribers ``` **Request Body:** ```json { "contact_id": "ct_abc123" } ``` ### Unsubscribe Contact from Topic ``` DELETE /subscription-topics/:id/subscribers/:contactId ``` --- ## Workflows ### Create Workflow ``` POST /workflows ``` **Request Body:** ```json { "name": "Welcome Series", "trigger": "contact.created", "steps": [ { "type": "send_email", "template_id": "tpl_welcome", "delay": null }, { "type": "wait", "delay": "3d" }, { "type": "condition", "field": "properties.plan", "operator": "equals", "value": "pro", "on_true": [ { "type": "send_email", "template_id": "tpl_pro_tips" } ], "on_false": [ { "type": "send_email", "template_id": "tpl_upgrade" } ] } ] } ``` ### Trigger Types - `contact.created`: Enrolls new contacts automatically - `contact.updated`: Enrolls on property changes - `contact.subscribed`: Enrolls when subscribing to a topic - `manual`: Enroll via API only ### Step Types - `send_email`: Send a template with optional delay and from override - `wait`: Pause for a duration (`3d`, `12h`, `30m`) - `condition`: Branch based on contact field value ### List Workflows ``` GET /workflows ``` ### Get Workflow ``` GET /workflows/:id ``` ### Activate Workflow ``` POST /workflows/:id/activate ``` ### Pause Workflow ``` POST /workflows/:id/pause ``` ### Delete Workflow ``` DELETE /workflows/:id ``` --- ## Inbound Email Inbound email IDs are numeric (e.g. `7`); the base path is `/inbound`. ### List Inbound Emails ``` GET /inbound ``` **Query Parameters:** | Parameter | Type | Description | |-----------|--------|--------------------------------------| | page | number | Page number | | per_page | number | Items per page | | domain | string | Optional — filter by verified domain | ### Get Inbound Email ``` GET /inbound/:id ``` **Response (200):** (wrapped in an `inboundEmail` key) ```json { "inboundEmail": { "id": 7, "domainId": 12, "fromAddress": "sender@external.com", "fromName": "Jane Sender", "toAddress": "support@yourdomain.com", "messageId": "", "subject": "Need help with my account", "htmlBody": "

Hi, I need help...

", "textBody": "Hi, I need help...", "headers": {}, "createdAt": "2026-03-25T09:00:00Z", "mailboxHash": null, "spamScore": 1.8, "spamVerdict": "clean", "strippedReply": "Hi, I need help..." } } ``` Attachments are listed separately at `GET /inbound/:id/attachments` and downloaded at `GET /inbound/:id/attachments/:aid`. ### Setup 1. Enable inbound on domain: `PATCH /domains/:id` with `{ "inbound_enabled": true }` 2. Add MX record: `inbound.poststack.dev` priority 10 3. Receive via webhook event `email.inbound` or poll the list endpoint --- ## Webhooks ### Create Webhook ``` POST /webhooks ``` **Request Body:** ```json { "url": "https://yourapp.com/webhooks/poststack", "events": ["email.delivered", "email.bounced", "email.complained"], "description": "Production webhook" } ``` Use `"events": ["*"]` to subscribe to all events. **Response (201):** ```json { "id": "wh_abc123", "url": "https://yourapp.com/webhooks/poststack", "events": ["email.delivered", "email.bounced", "email.complained"], "signing_secret": "whsec_...", "active": true, "created_at": "2026-03-25T09:00:00Z" } ``` Store the `signing_secret` securely — it is only returned on creation. ### Webhook Events | Event | Description | |---------------------|--------------------------------------| | email.queued | Email accepted and queued | | email.sent | Sent to mail server | | email.delivered | Successfully delivered | | email.bounced | Hard or soft bounce | | email.complained | Marked as spam | | email.opened | Opened (tracking enabled) | | email.clicked | Link clicked (tracking enabled) | | email.unsubscribed | Unsubscribe link clicked | | email.inbound | Inbound email received | ### Webhook Payload ```json { "id": "evt_abc123def456", "type": "email.delivered", "timestamp": "2026-03-25T10:00:02Z", "data": { "email_id": "em_abc123def456ghi789", "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Welcome to PostStack" } } ``` ### Signature Verification Webhooks include an `X-PostStack-Signature` header: ``` X-PostStack-Signature: sha256={hex_digest} ``` Verify by computing HMAC-SHA256 of the raw JSON request body using your signing secret, then compare with a timing-safe equality check. ```typescript import { createHmac, timingSafeEqual } from 'crypto'; function verifyWebhook(body: string, signature: string, secret: string): boolean { const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex'); return timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } ``` ### List Webhooks ``` GET /webhooks ``` ### Update Webhook ``` PATCH /webhooks/:id ``` ### Delete Webhook ``` DELETE /webhooks/:id ``` ### Test Webhook ``` POST /webhooks/:id/test ``` Sends a test event to the webhook URL. --- ## SMTP Relay Send emails via standard SMTP instead of the REST API. ### Connection Details | Setting | Value | |----------|--------------------------| | Host | smtp.poststack.dev | | Port | 587 (STARTTLS) or 465 (SSL/TLS) | | Username | poststack | | Password | Your API key (sk_live_...) | ### Custom SMTP Headers | Header | Description | |------------------------------|------------------------------------| | X-PostStack-Tags | Comma-separated tags | | X-PostStack-Template-Id | Use a template by ID | | X-PostStack-Variables | JSON-encoded template variables | | X-PostStack-Idempotency-Key | Prevent duplicate sends | ### Node.js Example ```typescript import nodemailer from 'nodemailer'; const transporter = nodemailer.createTransport({ host: 'smtp.poststack.dev', port: 587, secure: false, auth: { user: 'poststack', pass: 'sk_live_...' }, }); await transporter.sendMail({ from: 'you@yourdomain.com', to: 'user@example.com', subject: 'Hello via SMTP', html: '

Sent via SMTP relay

', }); ``` ### Python Example ```python import smtplib from email.mime.text import MIMEText msg = MIMEText("

Hello via SMTP

", "html") msg["Subject"] = "Hello via SMTP" msg["From"] = "you@yourdomain.com" msg["To"] = "user@example.com" with smtplib.SMTP("smtp.poststack.dev", 587) as server: server.starttls() server.login("poststack", "sk_live_...") server.send_message(msg) ``` --- ## Tracking ### Enable Tracking ```typescript await poststack.domains.update('dom_abc123', { open_tracking: true, click_tracking: true, tracking_domain: 'track.yourdomain.com', }); ``` Custom tracking domain requires CNAME: `track.yourdomain.com → track.poststack.dev` ### Disable Per Email ```typescript await poststack.emails.send({ from: 'noreply@yourdomain.com', to: ['user@example.com'], subject: 'Password Reset', html: '

Click to reset

', tracking: { open: false, click: false }, }); ``` ### Tracking Events Open and click events are delivered via webhooks (`email.opened`, `email.clicked`) with client and location data: ```json { "type": "email.opened", "data": { "email_id": "em_abc123", "client": { "name": "Apple Mail", "type": "desktop", "os": "macOS" }, "location": { "country": "US", "region": "California", "city": "San Francisco" } } } ``` --- ## Test Mode Use API keys starting with `sk_test_` for development and testing. ### Behavior - Full validation (domain checks, schemas, permissions, rate limits) - No actual SMTP delivery - Simulated events: queued → sent → delivered - No billing impact - Webhooks still fire with simulated events ### Response ```json { "id": "em_test_abc123", "status": "delivered", "test_mode": true } ``` --- ## SDK Reference ### Installation ```bash npm install @poststack.dev/sdk ``` ### Configuration ```typescript import { PostStack } from '@poststack.dev/sdk'; const poststack = new PostStack('sk_live_...'); ``` ### Available Resources | Resource | Methods | |-------------------------------|------------------------------------------------------------------| | poststack.emails | send, list, get, cancel | | poststack.domains | create, list, get, verify, update, delete | | poststack.contacts | create, list, get, update, delete, import, export | | poststack.templates | create, list, get, update, delete, publish, unpublish, duplicate | | poststack.webhooks | create, list, update, delete, test | | poststack.broadcasts | create, list, get, send, test, cancel | | poststack.segments | create, list, get, preview, addContacts, removeContacts, delete | | poststack.subscriptionTopics | create, list, update, delete, subscribe, unsubscribe | | poststack.inboundEmails | list, get | | poststack.workflows | create, list, get, activate, pause, delete | ### Error Handling ```typescript import { PostStack, PostStackError } from '@poststack.dev/sdk'; try { await poststack.emails.send({ ... }); } catch (error) { if (error instanceof PostStackError) { console.error(error.status, error.message); } } ``` ### Exported Types `SendEmailRequest`, `SendEmailResponse`, `Email`, `Contact`, `Domain`, `Template`, `Webhook`, `Broadcast`, `Segment`, `Workflow`, `PostStackError` --- ## Errors ### Error Response Format All errors return: ```json { "error": "Human-readable error message" } ``` ### HTTP Status Codes | Code | Meaning | Description | |------|------------------------|------------------------------------| | 200 | OK | Success | | 201 | Created | Resource created | | 400 | Bad Request | Malformed request | | 401 | Unauthorized | Missing or invalid API key | | 403 | Forbidden | Insufficient permissions | | 404 | Not Found | Resource doesn't exist | | 409 | Conflict | Resource already exists | | 422 | Unprocessable Entity | Validation failed | | 429 | Too Many Requests | Rate limited | | 500 | Internal Server Error | Server error | ### Common Errors | Error Message | Code | Resolution | |----------------------------------|------|--------------------------------------| | Invalid API key | 401 | Check key format (sk_live_/sk_test_) | | API key does not have permission | 403 | Upgrade to full_access | | Domain not verified | 422 | Verify DNS records first | | Domain not found | 404 | Add domain via POST /domains | | Contact already exists | 409 | Use PATCH to update instead | | Rate limit exceeded | 429 | Wait per Retry-After header | | Batch size exceeds limit | 422 | Maximum 100 emails per batch | | Template not published | 422 | Publish template before using | | Missing required field: to | 400 | Include array of recipient addresses | | Invalid email address | 422 | Check email format | ### Rate Limiting When rate limited, the response includes a `Retry-After` header specifying seconds to wait. The SDK handles exponential backoff automatically.