# 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": "
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": "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": "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.