# Unitpost > Unitpost is an email platform for developers: send transactional and marketing email, manage contacts, segments, topics, templates, sending domains, inbound email, webhooks, and suppressions — over a REST API, official SDKs (Node.js, Python, Ruby), an SMTP relay, or the Unitpost MCP server for AI agents. It also ships an email design suite: the @unitpost/email component library on npm (works standalone, no account needed), a hybrid visual + code template builder, a free template gallery, and a browser playground. ## AI & agents - [UnitAI](https://www.unitpost.com/guides#ai-unit): Unit, the AI assistant built into the Unitpost dashboard — design templates, launch campaigns, build segments, and explain deliverability, with approval before every send. - [MCP server](https://www.unitpost.com/guides#ai-mcp): Connect any MCP client (Cursor, Claude Code, Claude Desktop, Codex, Copilot, Gemini, Windsurf) to Unitpost. Streamable HTTP at https://mcp.unitpost.com/mcp, authenticated with a **production** workspace API key from www.unitpost.com (not unitpost.dev). One tool per API operation. Claude Desktop needs the mcp-remote stdio bridge (custom connector URL+OAuth alone cannot send a Bearer API key). - [MCP setup — Cursor](https://www.unitpost.com/guides/mcp/cursor): Add Unitpost as an MCP server in Cursor so the agent can send + manage email. - [MCP setup — Claude Code](https://www.unitpost.com/guides/mcp/claude-code): Add Unitpost to Claude Code with a single `claude mcp add` command. - [MCP setup — Claude Desktop](https://www.unitpost.com/guides/mcp/claude-desktop): Connect via the mcp-remote bridge in claude_desktop_config.json (API-key auth). - [MCP setup — Codex](https://www.unitpost.com/guides/mcp/codex): Add the Unitpost MCP server to OpenAI Codex CLI. - [MCP setup — GitHub Copilot](https://www.unitpost.com/guides/mcp/copilot): Use the Unitpost MCP server with VS Code agent mode / Copilot MCP. - [MCP setup — Gemini CLI](https://www.unitpost.com/guides/mcp/gemini): Register Unitpost as an MCP server for the Gemini CLI. - [MCP setup — Windsurf](https://www.unitpost.com/guides/mcp/windsurf): Register Unitpost as an MCP server in Windsurf (Cascade). - [MCP setup — Generic / custom client](https://www.unitpost.com/guides/mcp/custom): Connect any MCP-compatible client or your own agent to Unitpost. - [Skill](https://www.unitpost.com/guides#ai-skills): Install the Unitpost skill into your coding agent with `npx skills add unitpostcom/skills --skill unitpost`. - [llms-full.txt](https://www.unitpost.com/llms-full.txt): The full text of every guide below, inlined for one-shot ingestion. ## SMTP relay - [SMTP relay](https://www.unitpost.com/guides#smtp): Send through smtp.unitpost.com from any SMTP client — username `unitpost`, password = an API key with emails:send. STARTTLS on 587/2587, implicit TLS on 465/2465. - [SMTP setup — Nodemailer](https://www.unitpost.com/guides/smtp/nodemailer): Send from any Node.js app with the most popular SMTP client. - [SMTP setup — Supabase Auth](https://www.unitpost.com/guides/smtp/supabase): Deliver Supabase auth emails (magic links, confirmations) via Unitpost. - [SMTP setup — WordPress](https://www.unitpost.com/guides/smtp/wordpress): Route all WordPress email through Unitpost with an SMTP plugin. - [SMTP setup — Auth0](https://www.unitpost.com/guides/smtp/auth0): Send Auth0 verification, welcome, and reset emails via Unitpost. - [SMTP setup — Ruby on Rails](https://www.unitpost.com/guides/smtp/rails): Configure Action Mailer to deliver through Unitpost SMTP. - [SMTP setup — Laravel](https://www.unitpost.com/guides/smtp/laravel): Point Laravel's mail config at Unitpost with a few env vars. - [SMTP setup — Django](https://www.unitpost.com/guides/smtp/django): Use Django's SMTP email backend with Unitpost. - [SMTP setup — PHPMailer](https://www.unitpost.com/guides/smtp/php): Send from any PHP app using the PHPMailer library. ## Getting started - [Install the SDK](https://www.unitpost.com/docs#guide-install): Add the official Node, Python, or Ruby SDK — or call the API directly. - [Quickstart](https://www.unitpost.com/docs#guide-quickstart): Create a key, verify a domain, and send your first email. - [How it all fits together](https://www.unitpost.com/guides#pg-overview): The contacts → segments → campaigns flow and the rules that keep it safe. ## API reference guides - [Templates & variables](https://www.unitpost.com/docs#guide-templates): Reference a saved template and pass per-recipient data. - [Subscription topics](https://www.unitpost.com/docs#guide-topics): Let contacts opt out of one kind of mail without leaving your list. - [Sending domains](https://www.unitpost.com/docs#guide-domains): Add SPF, DKIM, and DMARC so mail sends from your domain. - [Open & click tracking](https://www.unitpost.com/docs#guide-tracking): Control engagement tracking per send, template, or category. - [Batch sending](https://www.unitpost.com/docs#guide-batch): Send up to 100 distinct messages — schedule and cancel as a unit. - [Scheduling & canceling sends](https://www.unitpost.com/docs#guide-canceling): Schedule for later and recall a send before it leaves. - [Idempotency & retries](https://www.unitpost.com/docs#guide-idempotency): Safely retry sends without duplicating mail. - [Bounces, complaints & suppression](https://www.unitpost.com/docs#guide-deliverability): How bad addresses are handled and where to review them. - [Webhooks](https://www.unitpost.com/docs#guide-webhooks): Receive signed event notifications at your own endpoint. - [Inbound routing](https://www.unitpost.com/docs#guide-inbound): Receive emails at your domain and get clean JSON POSTed to your webhook. ## Product guides - [Dashboard](https://www.unitpost.com/guides#pg-dashboard): Your account at a glance — sending health, recent activity, and what needs attention. - [Domains](https://www.unitpost.com/guides#pg-domains): Verify sending domains so your mail authenticates and lands in the inbox. - [Templates](https://www.unitpost.com/guides#pg-templates): Author reusable email content for campaigns and the API. - [Library](https://www.unitpost.com/guides#pg-library): Store and manage images and assets your templates reference. - [Contacts](https://www.unitpost.com/guides#pg-contacts): The people you can email — subscription state, custom fields, and imports. - [Suppressions](https://www.unitpost.com/guides#pg-suppressions): Addresses blocked from receiving mail — protecting your sending reputation. - [Segments](https://www.unitpost.com/guides#pg-segments): Named groups of contacts that campaigns send to. - [Subscription topics](https://www.unitpost.com/guides#pg-topics): Let contacts opt out of one kind of mail without leaving your list. - [Campaigns](https://www.unitpost.com/guides#pg-campaigns): Send a template to a segment — now or on a schedule — with server-enforced safety gates. - [Emails](https://www.unitpost.com/guides#pg-emails): Every individual message sent — delivery status, opens, clicks, and bounces. - [Activity](https://www.unitpost.com/guides#pg-activity): A chronological audit log of account events across every resource. - [API Keys](https://www.unitpost.com/guides#pg-api-keys): Authenticate the API with scoped, least-privilege keys. - [Webhooks](https://www.unitpost.com/guides#pg-webhooks): Receive signed, real-time event notifications at your own endpoint. - [Migration Assistant](https://www.unitpost.com/guides#pg-migration): Move from Resend, SendGrid, Mailgun, Postmark, Brevo, or Loops with one read-only API key. ## Email design suite - [Component library — Introduction](https://www.unitpost.com/components#introduction): A small set of cross-client-tested components for building email. - [Component library — Installation](https://www.unitpost.com/components#installation): Add the package and render your first email in a few minutes. - [Component reference](https://www.unitpost.com/components#components): Every @unitpost/email building block (Section, Row, Column, Heading, Text, Button, Link, Image, Divider, Spacer, Markdown, Code, Html) with live previews and props tables. - [Pre-built layouts](https://www.unitpost.com/components#layouts): Ready-made section bands — headers, heroes, content cards, split columns, CTAs, footers — with copyable TSX. - [Email builder](https://www.unitpost.com/templates): Hybrid visual + code template builder — drag blocks or write constrained TSX; both edit the same document. - [Template gallery](https://www.unitpost.com/templates/gallery): Free, ready-to-send transactional and marketing templates — preview and copy the code without an account, or clone into a workspace. - [Playground](https://www.unitpost.com/playground): Write email code in the browser and see the exact HTML the Unitpost engine produces — no account or install needed. - npm: `npm install @unitpost/email zod` — the MIT-licensed document model + cross-client HTML renderer. Works fully offline/standalone; no Unitpost account required to render email HTML. ## API endpoints - `GET /api/v1/emails`: List sent emails Retrieve a list of emails your workspace has sent or scheduled. Results are newest-first and cursor-paginated, and each row is a reference to a single email (status and lifecycle timestamps, not the rendered body). Narrow the list with the `status`, `campaign_id`, `batch_id`, and `created_after`/`created_before` filters. Use an email's `id` with [Retrieve an email](https://www.unitpost.com/docs#get-emails-id) to read its full record, or [Cancel or reschedule an email](https://www.unitpost.com/docs#patch-emails-id) to change a scheduled send. For aggregate deliverability numbers over a window, see [Email deliverability stats](https://www.unitpost.com/docs#get-emails-stats). Requires the `emails:read` capability. - `POST /api/v1/emails`: Send or schedule an email Send a single transactional email — now, or at a future time by passing `scheduled_at`. Provide the content inline as `html`/`text`, or reference a saved template with `template` (create one with [Create a template](https://www.unitpost.com/docs#post-templates)). Pass an `Idempotency-Key` header to make retries safe: the same key replays the original result instead of sending twice. The response returns the new email's `id` — read its delivery status back with [Retrieve an email](https://www.unitpost.com/docs#get-emails-id), change a scheduled send with [Cancel or reschedule an email](https://www.unitpost.com/docs#patch-emails-id), or send many at once with [Send a batch of emails](https://www.unitpost.com/docs#post-emails-batch). Requires the `emails:send` capability. - `POST /api/v1/emails/batch`: Send a batch of emails Send up to 100 transactional emails in a single request. Validation is all-or-nothing: if any item is invalid the whole batch is rejected and nothing is queued, so you never end up with a partial send. Pass an `Idempotency-Key` header to make retries safe — the same key replays the original result instead of re-sending. The response returns a `batch_id`; track the batch's per-status rollup with [Retrieve a batch](https://www.unitpost.com/docs#get-emails-batches-id) and stop the rest of it with [Cancel a batch](https://www.unitpost.com/docs#post-emails-batches-id-cancel). To send one email, use [Send or schedule an email](https://www.unitpost.com/docs#post-emails). Requires the `emails:send` capability. - `GET /api/v1/emails/batches/{id}`: Retrieve a batch Retrieve a batch and its live, per-status rollup (how many of its messages are queued, sent, delivered, bounced, and so on). The counts update as the batch progresses. Create a batch with [Send a batch of emails](https://www.unitpost.com/docs#post-emails-batch), or stop its remaining messages with [Cancel a batch](https://www.unitpost.com/docs#post-emails-batches-id-cancel). Requires the `emails:read` capability. - `POST /api/v1/emails/batches/{id}/cancel`: Cancel a batch Cancel every still-cancelable message in a batch in one call. Messages that have already been sent are left untouched — only queued or scheduled ones are stopped. Check what remains cancelable first with [Retrieve a batch](https://www.unitpost.com/docs#get-emails-batches-id). Requires the `emails:send` capability. - `GET /api/v1/emails/{id}`: Retrieve an email Retrieve a single email by `id`, including its current send status and lifecycle timestamps (created, scheduled, sent, delivered, and so on). Find an `id` by browsing [List sent emails](https://www.unitpost.com/docs#get-emails). To change a scheduled or queued email, use [Cancel or reschedule an email](https://www.unitpost.com/docs#patch-emails-id). Requires the `emails:read` capability. - `PATCH /api/v1/emails/{id}`: Cancel or reschedule an email Cancel a scheduled or queued email, or move it to a new send time. Only emails that haven't been handed off for delivery yet can be changed — once an email is sent this returns `409`. Look up the email's current state first with [Retrieve an email](https://www.unitpost.com/docs#get-emails-id). Requires the `emails:send` capability. - `GET /api/v1/emails/stats`: Email deliverability stats Retrieve aggregate deliverability and engagement metrics for your workspace over a rolling window. You get totals (sent, delivered, opened, clicked, bounced, complained, failed) alongside the rates that matter — delivery, open, click, bounce, and complaint. Open and click rates count only emails where that tracking was enabled, so turning tracking off for transactional mail won't skew them. Control the window with the `days` parameter (1–365, default 30). For the individual emails behind these numbers, see [List sent emails](https://www.unitpost.com/docs#get-emails). Requires the `emails:read` capability. - `GET /api/v1/emails/received`: List received emails Retrieve a list of inbound emails received on your receiving-enabled domains, newest-first and cursor-paginated. Rows are summaries (sender, subject, timestamps) without bodies — use a message's `id` with [Retrieve a received email](https://www.unitpost.com/docs#get-emails-received-id) to read the full content and attachment metadata. Requires the `emails:read` capability. - `GET /api/v1/emails/received/{id}`: Retrieve a received email Retrieve a full inbound message by `id`, including its HTML and text bodies plus metadata for every attachment. Attachment bytes aren't inlined — download each one via [Get an attachment download URL](https://www.unitpost.com/docs#get-emails-received-id-attachments-attachmentid). Browse your inbound mail with [List received emails](https://www.unitpost.com/docs#get-emails-received). Requires the `emails:read` capability. - `GET /api/v1/emails/received/{id}/attachments/{attachmentId}`: Get an attachment download URL Get a short-lived, signed URL for downloading one inbound attachment's bytes. Fetch the URL and download promptly — it expires quickly (the expiry is included in the response). Find attachment ids on the message via [Retrieve a received email](https://www.unitpost.com/docs#get-emails-received-id). Requires the `emails:read` capability. - `GET /api/v1/contacts`: List contacts Retrieve a list of the contacts in your workspace, newest-first and cursor-paginated. Each row includes the contact's core fields and any custom field values. Use a contact's `id` (or email) with [Retrieve a contact](https://www.unitpost.com/docs#get-contacts-id) for the full record, add contacts with [Create a contact](https://www.unitpost.com/docs#post-contacts), or bring a list in bulk with [Start an async contact import](https://www.unitpost.com/docs#post-contacts-imports). Requires the `contacts:read` capability. - `POST /api/v1/contacts`: Create a contact Add a single contact to your workspace. Set an `email` plus any standard or custom field values; custom fields must already exist (define them with [Create a custom contact field](https://www.unitpost.com/docs#post-contact-fields)). Emails are unique per workspace, so creating one that already exists returns `409` — use [Update a contact](https://www.unitpost.com/docs#patch-contacts-id) to change an existing contact, or [Start an async contact import](https://www.unitpost.com/docs#post-contacts-imports) to add many at once. Requires the `contacts:write` capability. - `GET /api/v1/contacts/{id}`: Retrieve a contact Retrieve a single contact and all of its field values. The `{id}` path segment accepts either the contact's id or its email address, so you can look a contact up without storing our id. Change it with [Update a contact](https://www.unitpost.com/docs#patch-contacts-id), or see which subscription topics it's opted into with [List a contact's topic subscriptions](https://www.unitpost.com/docs#get-contacts-id-topics). Requires the `contacts:read` capability. - `PATCH /api/v1/contacts/{id}`: Update a contact Update a contact's standard or custom field values. Only the fields you send are changed; omitted fields are left as-is. The `{id}` accepts a contact id or email. To manage a contact's subscription preferences instead, use [Set a contact's topic preference](https://www.unitpost.com/docs#post-contacts-id-topics). Requires the `contacts:write` capability. - `DELETE /api/v1/contacts/{id}`: Delete a contact Permanently delete a contact and its field values. This also removes the contact from every segment it belonged to. To stop mailing someone without deleting them, unsubscribe them from a topic with [Set a contact's topic preference](https://www.unitpost.com/docs#post-contacts-id-topics) or add them to your [suppression list](https://www.unitpost.com/docs#post-suppressions) instead. Requires the `contacts:write` capability. - `GET /api/v1/contacts/imports`: List contact imports Retrieve a list of your bulk contact-import jobs, newest-first and cursor-paginated. Each row is a reference to one import and its current progress. Start a new one with [Start an async contact import](https://www.unitpost.com/docs#post-contacts-imports), or drill into a single job with [Retrieve a contact import](https://www.unitpost.com/docs#get-contacts-imports-id). Requires the `contacts:read` capability. - `POST /api/v1/contacts/imports`: Start an async contact import Import a batch of contacts (up to 5,000 per request) in the background. The call returns immediately with an import job you poll for progress and per-row results — new contacts are created and existing ones (matched by email) are updated. Track it with [Retrieve a contact import](https://www.unitpost.com/docs#get-contacts-imports-id), or see all your imports via [List contact imports](https://www.unitpost.com/docs#get-contacts-imports). To add a single contact synchronously, use [Create a contact](https://www.unitpost.com/docs#post-contacts). Requires the `contacts:write` capability. - `GET /api/v1/contacts/imports/{id}`: Retrieve a contact import Retrieve a bulk import job by `id` to check its status and per-row results — how many contacts were created, updated, or skipped, and why any rows failed. Poll this while an import runs. Start one with [Start an async contact import](https://www.unitpost.com/docs#post-contacts-imports). Requires the `contacts:read` capability. - `GET /api/v1/contact-fields`: List custom contact fields Retrieve a list of the custom contact fields defined in your workspace, cursor-paginated. These are the typed attributes (text, number, date, boolean, and so on) you can set on any contact and reference as template variables. Define a new one with [Create a custom contact field](https://www.unitpost.com/docs#post-contact-fields). Requires the `contacts:read` capability. - `POST /api/v1/contact-fields`: Create a custom contact field Define a new typed custom field that can then be set on any contact and used as a template variable (e.g. `plan`, `signup_date`). The `key` and `type` are fixed once created — rename the key later with [Rename a custom field's key](https://www.unitpost.com/docs#post-contact-fields-id-rename), or change the label with [Update a custom contact field](https://www.unitpost.com/docs#patch-contact-fields-id). See existing fields via [List custom contact fields](https://www.unitpost.com/docs#get-contact-fields). Requires the `contacts:write` capability. - `GET /api/v1/contact-fields/{id}`: Retrieve a custom contact field Retrieve a single custom contact field by `id`, including its key, type, and label. Update its label with [Update a custom contact field](https://www.unitpost.com/docs#patch-contact-fields-id), or rename the key with [Rename a custom field's key](https://www.unitpost.com/docs#post-contact-fields-id-rename). Requires the `contacts:read` capability. - `PATCH /api/v1/contact-fields/{id}`: Update a custom contact field Update a custom field's editable metadata, such as its display label. The `key` and `type` are immutable here — to change the key (and migrate every contact's stored value) use [Rename a custom field's key](https://www.unitpost.com/docs#post-contact-fields-id-rename) instead. Requires the `contacts:write` capability. - `DELETE /api/v1/contact-fields/{id}`: Delete a custom contact field Permanently delete a custom field definition and strip its stored value from every contact. Any template variable that referenced it will resolve to empty. This can't be undone — recreate the field with [Create a custom contact field](https://www.unitpost.com/docs#post-contact-fields) if you need it back. Requires the `contacts:write` capability. - `POST /api/v1/contact-fields/{id}/rename`: Rename a custom field's key Change a custom field's `key`, migrating the stored value on every contact so no data is lost. Because the key is how templates and the API reference the field, update any template variables that used the old key. To change only the display label, use [Update a custom contact field](https://www.unitpost.com/docs#patch-contact-fields-id). Requires the `contacts:write` capability. - `GET /api/v1/segments`: List segments Retrieve a list of the segments in your workspace, cursor-paginated. A segment is a saved group of contacts you can target when sending a campaign. Create one with [Create a segment](https://www.unitpost.com/docs#post-segments), or list a segment's members with [List segment members](https://www.unitpost.com/docs#get-segments-id-contacts). Requires the `segments:read` capability. - `POST /api/v1/segments`: Create a segment Create a segment — a named group of contacts you can target when sending a campaign. Once created, add contacts with [Add a member](https://www.unitpost.com/docs#post-segments-id-contacts) and target it from [Create a campaign](https://www.unitpost.com/docs#post-campaigns). To let recipients opt out of a category of mail instead, see [Create a topic](https://www.unitpost.com/docs#post-topics). Requires the `segments:write` capability. - `GET /api/v1/segments/{id}`: Retrieve a segment Retrieve a single segment by `id`, including its name and member count. To page through the contacts inside it, use [List segment members](https://www.unitpost.com/docs#get-segments-id-contacts). Requires the `segments:read` capability. - `PATCH /api/v1/segments/{id}`: Update a segment Update a segment's editable metadata, such as its name. To change who's in the segment, add or remove members with [Add a member](https://www.unitpost.com/docs#post-segments-id-contacts) and [Remove a member](https://www.unitpost.com/docs#delete-segments-id-contacts-contact). Requires the `segments:write` capability. - `DELETE /api/v1/segments/{id}`: Delete a segment Delete a segment. The contacts themselves are untouched — only the grouping is removed. This is blocked with `409` while an active campaign still targets the segment; cancel or finish that campaign first (see [List campaigns](https://www.unitpost.com/docs#get-campaigns)). Requires the `segments:write` capability. - `GET /api/v1/segments/{id}/contacts`: List segment members Retrieve the contacts in a segment, cursor-paginated. Add a contact with [Add a member](https://www.unitpost.com/docs#post-segments-id-contacts) or take one out with [Remove a member](https://www.unitpost.com/docs#delete-segments-id-contacts-contact). Requires the `segments:read` capability. - `POST /api/v1/segments/{id}/contacts`: Add a member Add a contact to a segment, referenced by contact id or email. The operation is idempotent — adding a contact that's already a member is a no-op and still succeeds. See the current members with [List segment members](https://www.unitpost.com/docs#get-segments-id-contacts). Requires the `segments:write` capability. - `DELETE /api/v1/segments/{id}/contacts/{contact}`: Remove a member Remove a contact from a segment, referenced by contact id or email. The contact itself is not deleted — only its membership. Add it back any time with [Add a member](https://www.unitpost.com/docs#post-segments-id-contacts). Requires the `segments:write` capability. - `GET /api/v1/topics`: List topics Retrieve a list of your subscription topics, cursor-paginated. A topic is a category of email (e.g. Product updates, Promotions) that recipients can opt out of without leaving your list entirely. Pass `?archived=true` to list archived topics instead. Create one with [Create a topic](https://www.unitpost.com/docs#post-topics), or check a contact's preferences with [List a contact's topic subscriptions](https://www.unitpost.com/docs#get-contacts-id-topics). Requires the `topics:read` capability. - `POST /api/v1/topics`: Create a topic Create a subscription topic — a category recipients can opt out of on their own (e.g. Promotions) while staying on your list. `default_opt_in` decides whether contacts with no explicit preference are treated as subscribed. Scope a campaign to a topic so opted-out contacts are skipped automatically via [Create a campaign](https://www.unitpost.com/docs#post-campaigns), and set an individual contact's preference with [Set a contact's topic preference](https://www.unitpost.com/docs#post-contacts-id-topics). Topics differ from segments: a [segment](https://www.unitpost.com/docs#post-segments) decides who a campaign targets, a topic lets recipients opt out. Requires the `topics:write` capability. - `GET /api/v1/topics/{id}`: Retrieve a topic Retrieve a single topic by `id`, including its name and `default_opt_in` setting. Change it with [Update a topic](https://www.unitpost.com/docs#patch-topics-id), or list every topic with [List topics](https://www.unitpost.com/docs#get-topics). Requires the `topics:read` capability. - `PATCH /api/v1/topics/{id}`: Update a topic Update a topic's name or `default_opt_in` behavior, or pass `{ "archived": true | false }` to archive or restore it. Archiving a topic hides it and stops it from being targeted, but is blocked with `409` while an active campaign still targets it. To manage a single contact's preference, use [Set a contact's topic preference](https://www.unitpost.com/docs#post-contacts-id-topics). Requires the `topics:write` capability. - `DELETE /api/v1/topics/{id}`: Delete a topic Archive a topic (a soft delete — existing subscription records are preserved so you can restore it later). It's blocked with `409` while an active campaign still targets it. Restore an archived topic by sending `{ "archived": false }` to [Update a topic](https://www.unitpost.com/docs#patch-topics-id). Requires the `topics:write` capability. - `GET /api/v1/contacts/{id}/topics`: List a contact's topic subscriptions Retrieve a contact's effective subscription state for every active topic, cursor-paginated. Each row resolves the contact's explicit preference against the topic's `default_opt_in`, so `subscribed` is always the answer to "would this contact receive a campaign scoped to this topic?". Change a preference with [Set a contact's topic preference](https://www.unitpost.com/docs#post-contacts-id-topics). Requires the `topics:read` capability. - `POST /api/v1/contacts/{id}/topics`: Set a contact's topic preference Subscribe or unsubscribe a contact for one topic. This records an explicit preference that overrides the topic's `default_opt_in`, and it's idempotent — setting the same preference twice succeeds. `PUT` behaves identically. Read the contact's current state with [List a contact's topic subscriptions](https://www.unitpost.com/docs#get-contacts-id-topics). Requires the `topics:write` capability. - `PUT /api/v1/contacts/{id}/topics`: Set a contact's topic preference (alias of POST) Identical to [Set a contact's topic preference](https://www.unitpost.com/docs#post-contacts-id-topics) — provided so clients that prefer `PUT` semantics for an upsert can use it interchangeably. Requires the `topics:write` capability. - `GET /api/v1/campaigns`: List campaigns Retrieve a list of your campaigns, cursor-paginated. Each row is a reference to one campaign and its current status (`draft`, `scheduled`, `sending`, `sent`, …). Use a campaign's `id` with [Retrieve a campaign](https://www.unitpost.com/docs#get-campaigns-id) for the full record, or with [List sent emails](https://www.unitpost.com/docs#get-emails) (`?campaign_id=`) to see the individual emails it produced. Requires the `campaigns:read` capability. - `POST /api/v1/campaigns`: Create a campaign Create a campaign in `draft`. Target either a saved segment (`segment_id`, see [Create a segment](https://www.unitpost.com/docs#post-segments)) or an inline `recipients` list, and optionally scope it to a subscription topic (`topic_id`) so contacts who opted out of that topic are skipped automatically. Content comes from a `template_id` or inline `html`/`text`. Nothing is sent yet — edit the draft with [Update a campaign](https://www.unitpost.com/docs#patch-campaigns-id), check it with [Validate a campaign](https://www.unitpost.com/docs#get-campaigns-id-validate), then launch it with [Send a campaign](https://www.unitpost.com/docs#post-campaigns-id-send). Requires the `campaigns:write` capability. - `GET /api/v1/campaigns/{id}`: Retrieve a campaign Retrieve a single campaign by `id`, including its status, targeting, content references, and schedule. To see the individual emails a sent campaign produced, filter [List sent emails](https://www.unitpost.com/docs#get-emails) by `campaign_id`; for a pre-send readiness report, use [Validate a campaign](https://www.unitpost.com/docs#get-campaigns-id-validate). Requires the `campaigns:read` capability. - `PATCH /api/v1/campaigns/{id}`: Update a campaign Edit a campaign while it's still in `draft` (content edits after that return `409`). Beyond content and targeting, you can: send `archived` to archive/restore it; set `variable_fallbacks` (fill-only per-variable defaults) and/or `excluded_contact_ids` to resolve a send blocked by missing variables — the API equivalent of the dashboard's pre-send fix; set `topic_id` to scope it to a subscription topic (or `null` to clear); and set `open_tracking`/`click_tracking` (tri-state; `null` inherits the template/domain default). See what's blocking a send with [Validate a campaign](https://www.unitpost.com/docs#get-campaigns-id-validate). Requires the `campaigns:write` capability. - `DELETE /api/v1/campaigns/{id}`: Delete a campaign Permanently delete a campaign that's in `draft` or a terminal state (sent, canceled, failed). A campaign that's mid-flight can't be deleted — stop it first with [Cancel a campaign](https://www.unitpost.com/docs#post-campaigns-id-cancel). Requires the `campaigns:write` capability. - `POST /api/v1/campaigns/{id}/send`: Send a campaign Validate a campaign and then send it — immediately, or at its `scheduled_at` if one is set. If anything blocks the send (unverified domain, missing template variables, empty audience, …) you get a `409` with each blocking reason itemized in `error.details[]`. To see the full pre-send report without attempting a send, use [Validate a campaign](https://www.unitpost.com/docs#get-campaigns-id-validate); fix variable issues via [Update a campaign](https://www.unitpost.com/docs#patch-campaigns-id). A launched campaign can be [paused](https://www.unitpost.com/docs#post-campaigns-id-pause) or [canceled](https://www.unitpost.com/docs#post-campaigns-id-cancel). Requires the `campaigns:send` capability. - `POST /api/v1/campaigns/{id}/cancel`: Cancel a campaign Cancel a `draft`, `scheduled`, or `sending` campaign. Cancelation is terminal — the campaign can't be restarted afterwards. Messages already sent to recipients are not recalled. If you only want to stop temporarily, use [Pause a campaign](https://www.unitpost.com/docs#post-campaigns-id-pause) instead. Requires the `campaigns:send` capability. - `POST /api/v1/campaigns/{id}/pause`: Pause a campaign Pause a `scheduled` or `sending` campaign. A paused send stops cleanly between recipients — already-sent messages are untouched, and no partial email is ever produced. Continue where it left off with [Resume a campaign](https://www.unitpost.com/docs#post-campaigns-id-resume), or stop it for good with [Cancel a campaign](https://www.unitpost.com/docs#post-campaigns-id-cancel). Requires the `campaigns:send` capability. - `POST /api/v1/campaigns/{id}/resume`: Resume a campaign Resume a `paused` campaign. Sending continues with the recipients that hadn't been reached yet; anyone already delivered to is skipped idempotently, so nobody receives the campaign twice. Pause a send with [Pause a campaign](https://www.unitpost.com/docs#post-campaigns-id-pause). Requires the `campaigns:send` capability. - `POST /api/v1/campaigns/{id}/reschedule`: Reschedule a campaign Move a `scheduled` campaign to a new send time. Only campaigns that haven't started sending can be rescheduled — once a send is in flight, [pause](https://www.unitpost.com/docs#post-campaigns-id-pause) or [cancel](https://www.unitpost.com/docs#post-campaigns-id-cancel) it instead. Requires the `campaigns:send` capability. - `GET /api/v1/campaigns/{id}/validate`: Validate a campaign Run a non-mutating pre-send check and get the full readiness report: how many contacts will receive the campaign, how many are suppressed or excluded, which contacts are missing template variables, and a final `can_send` verdict. Nothing is sent or changed. Fix reported variable gaps with [Update a campaign](https://www.unitpost.com/docs#patch-campaigns-id) (`variable_fallbacks` / `excluded_contact_ids`), then launch with [Send a campaign](https://www.unitpost.com/docs#post-campaigns-id-send). Requires the `campaigns:read` capability. - `GET /api/v1/templates`: List templates Retrieve a list of your email templates, cursor-paginated. List rows omit the full `design` document to keep responses small — fetch a single template with [Retrieve a template](https://www.unitpost.com/docs#get-templates-id) to get it. Reference a template's `id` when sending with [Send or schedule an email](https://www.unitpost.com/docs#post-emails) or when creating a campaign. Requires the `templates:read` capability. - `POST /api/v1/templates`: Create a template Create a reusable email template from a canonical `design` document — the same block-based format the dashboard editor produces. Templates support `{{variables}}` that are filled per-recipient at send time. Once created, preview it in the dashboard, send with it via [Send or schedule an email](https://www.unitpost.com/docs#post-emails), and iterate with [Update a template](https://www.unitpost.com/docs#patch-templates-id). Requires the `templates:write` capability. - `GET /api/v1/templates/{id}`: Retrieve a template Retrieve a single template by `id`, including the full canonical `design` document (which [List templates](https://www.unitpost.com/docs#get-templates) omits). Use this to read a design for editing, then save changes with [Update a template](https://www.unitpost.com/docs#patch-templates-id). Requires the `templates:read` capability. - `PATCH /api/v1/templates/{id}`: Update a template Update a template's metadata and/or its `design` document. Changes take effect for future sends only — emails already sent or queued keep the design they were rendered with. Preview the result in the dashboard before sending; read the current design first with [Retrieve a template](https://www.unitpost.com/docs#get-templates-id). Requires the `templates:write` capability. - `DELETE /api/v1/templates/{id}`: Delete a template Permanently delete a template. Emails already sent with it are unaffected, but future sends that reference its id will fail — update any code or campaigns that still point at it first (see [List campaigns](https://www.unitpost.com/docs#get-campaigns)). Requires the `templates:write` capability. - `GET /api/v1/brand-kits`: List brand kits Retrieve every Brand Kit profile in the workspace (up to five named profiles). Each profile holds tone, about, voice notes, labeled colors, pinned logo/background library images, and core pages — the primary brand context for on-brand template authoring. Pass `?name=` to filter to one profile by display name (case-insensitive). The response includes `default_id` for the Default profile. Fetch one by id with [Retrieve a brand kit](https://www.unitpost.com/docs#get-brand-kits-id). Writes and URL import stay in the dashboard (Settings → Brand). Requires the `templates:read` capability. - `GET /api/v1/brand-kits/{id}`: Retrieve a brand kit Retrieve one Brand Kit profile by `id` (`bk_…`), including voice, labeled colors, pinned graphics (`library_image_id` for `/img/{id}` in design TSX), and core pages. List every profile with [List brand kits](https://www.unitpost.com/docs#get-brand-kits). Requires the `templates:read` capability. - `GET /api/v1/domains`: List domains Retrieve a list of your sending domains, cursor-paginated, with each domain's verification status. Add a new one with [Add a domain](https://www.unitpost.com/docs#post-domains), or fetch a single domain (including its DNS records) with [Retrieve a domain](https://www.unitpost.com/docs#get-domains-id). Requires the `domains:read` capability. - `POST /api/v1/domains`: Add a domain Add a domain you'll send email from. The response includes the DNS `records` (SPF, DKIM, and so on) to publish with your DNS provider. Once they're published, trigger a check with [Verify a domain](https://www.unitpost.com/docs#post-domains-id-verify) — sending from the domain is blocked until it verifies. Requires the `domains:write` capability. - `GET /api/v1/domains/{id}`: Retrieve a domain Retrieve a single sending domain by `id`, including its current verification status and the DNS `records` you need to publish. Re-run the DNS check with [Verify a domain](https://www.unitpost.com/docs#post-domains-id-verify), or change its tracking/TLS settings with [Update a domain](https://www.unitpost.com/docs#patch-domains-id). Requires the `domains:read` capability. - `DELETE /api/v1/domains/{id}`: Delete a domain Remove a sending domain from your workspace. Sends that reference an address on this domain will be rejected afterwards, so update your `from` addresses first. Re-add it any time with [Add a domain](https://www.unitpost.com/docs#post-domains) (you'll need to verify it again). Requires the `domains:write` capability. - `PATCH /api/v1/domains/{id}`: Update a domain Update a domain's sending options: open/click tracking defaults, TLS enforcement, and the tracking subdomain. These act as the domain-level defaults that individual emails and campaigns can override (see [Update a campaign](https://www.unitpost.com/docs#patch-campaigns-id)). Requires the `domains:write` capability. - `POST /api/v1/domains/{id}/verify`: Verify a domain Run an on-demand DNS check and advance the domain's verification status. The response includes per-record `checks` so you can see exactly which DNS records are still missing or wrong. Get the records to publish from [Retrieve a domain](https://www.unitpost.com/docs#get-domains-id). Verification also re-runs periodically in the background. Requires the `domains:write` capability. - `GET /api/v1/webhooks`: List webhook endpoints Retrieve a list of your webhook endpoints, cursor-paginated, with each endpoint's URL, subscribed events, and status. Signing secrets are never returned here — they're shown exactly once by [Create a webhook endpoint](https://www.unitpost.com/docs#post-webhooks). Check an endpoint's wiring end-to-end with [Send a test event](https://www.unitpost.com/docs#post-webhooks-id-test). Requires the `webhooks:read` capability. - `POST /api/v1/webhooks`: Create a webhook endpoint Register an `https` URL to receive event notifications (deliveries, bounces, opens, clicks, and more) and choose which events it subscribes to. The response includes the `signing_secret` exactly once — store it now, as it can never be retrieved again; use it to verify each delivery's signature. Confirm the endpoint works with [Send a test event](https://www.unitpost.com/docs#post-webhooks-id-test), and adjust its URL or events later with [Update a webhook endpoint](https://www.unitpost.com/docs#patch-webhooks-id). Requires the `webhooks:manage` capability. - `GET /api/v1/webhooks/{id}`: Retrieve a webhook endpoint Retrieve a single webhook endpoint by `id` — its URL, subscribed events, and status. The signing secret is never included; it's returned exactly once when the endpoint is [created](https://www.unitpost.com/docs#post-webhooks). Requires the `webhooks:read` capability. - `PATCH /api/v1/webhooks/{id}`: Update a webhook endpoint Update a webhook endpoint's `url`, subscribed `events`, `name`, and/or `status` (use `status` to pause and re-enable deliveries without deleting the endpoint). The signing secret is never changed by an update. Verify your changes with [Send a test event](https://www.unitpost.com/docs#post-webhooks-id-test). Requires the `webhooks:manage` capability. - `DELETE /api/v1/webhooks/{id}`: Delete a webhook endpoint Permanently delete a webhook endpoint and its delivery log. Deliveries stop immediately. The operation is idempotent — deleting an already-deleted endpoint still returns `204`. To stop deliveries temporarily instead, set `status` via [Update a webhook endpoint](https://www.unitpost.com/docs#patch-webhooks-id). Requires the `webhooks:manage` capability. - `POST /api/v1/webhooks/{id}/test`: Send a test event Fire a sample event at the endpoint synchronously and return the receiver's response so you can debug your handler end-to-end. The probe is signed exactly like a real delivery (verify it the same way) but is not persisted to the delivery log. Configure the endpoint itself with [Update a webhook endpoint](https://www.unitpost.com/docs#patch-webhooks-id). Requires the `webhooks:manage` capability. - `GET /api/v1/api-keys`: List API keys Retrieve a list of your workspace's API keys — the key prefix and metadata only, never the secret. Create a new key with [Create an API key](https://www.unitpost.com/docs#post-api-keys) or revoke one with [Revoke an API key](https://www.unitpost.com/docs#delete-api-keys-id). This endpoint is session-gated: it requires a dashboard session and cannot be called with a Bearer API key. - `POST /api/v1/api-keys`: Create an API key Create an API key scoped to the capabilities you choose. The response includes the plaintext `key` exactly once — store it securely now, because it can never be retrieved again (only [revoked](https://www.unitpost.com/docs#delete-api-keys-id)). This endpoint is session-gated: it requires a dashboard session and cannot be called with a Bearer API key. - `DELETE /api/v1/api-keys/{id}`: Revoke an API key Permanently revoke an API key. Requests made with it start failing with `401` immediately, and revocation cannot be undone — create a replacement first with [Create an API key](https://www.unitpost.com/docs#post-api-keys) if you're rotating. This endpoint is session-gated: it requires a dashboard session and cannot be called with a Bearer API key. - `GET /api/v1/suppressions`: List suppressed addresses Retrieve a list of email addresses that are blocked from receiving your mail, newest-first and cursor-paginated. The list includes your workspace's own suppressions AND the read-only Unitpost-wide (`platform`) entries that would block a send — so it's the complete answer to "why isn't this address getting mail?". Pass `?scope=workspace` to hide platform rows. Add an address with [Suppress an address](https://www.unitpost.com/docs#post-suppressions) or lift a block with [Un-suppress an address](https://www.unitpost.com/docs#delete-suppressions-id). Requires the `suppressions:read` capability. - `POST /api/v1/suppressions`: Suppress an address Block one address (`{ email }`) or up to 1,000 at once (`{ emails: [...] }`) from receiving any future sends from your workspace. The operation is idempotent — re-suppressing an existing address returns `200` rather than a conflict. The bulk form returns an `{ object: "list", added, skipped, total }` summary. Suppression is the hard stop; to let recipients opt out of just one kind of email, use topics (see [Create a topic](https://www.unitpost.com/docs#post-topics)). Undo with [Un-suppress an address](https://www.unitpost.com/docs#delete-suppressions-id). Requires the `suppressions:write` capability. - `GET /api/v1/suppressions/{id}`: Retrieve a suppressed address Retrieve one suppression by id (`supp_…`) or by the email address itself. Returns your own workspace row, or a read-only `platform` row when the address is blocked Unitpost-wide. Lift a workspace block with [Un-suppress an address](https://www.unitpost.com/docs#delete-suppressions-id). Requires the `suppressions:read` capability. - `DELETE /api/v1/suppressions/{id}`: Un-suppress an address Remove a workspace suppression by id or email so the address can receive your mail again. The email form is idempotent — removing an address that isn't suppressed still returns `204`. Unitpost-wide (`platform`) entries can't be removed here (`403`) — contact support. Check what's blocking an address first with [Retrieve a suppressed address](https://www.unitpost.com/docs#get-suppressions-id). Requires the `suppressions:write` capability. - `GET /api/v1/usage`: Retrieve usage Retrieve your workspace's current billing-period usage snapshot: the plan, the period window, and the email counter. On paid plans the response also carries the dollar sending `wallet` — the monthly allowance included with the plan, spend so far, any upgrade carryover, the non-expiring purchased-credit balance, and billable overage — all as integer USD cents. On the free plan `wallet` is null and the `emails` object carries the monthly quota and daily cap instead. Amounts are read from the same meters that gate sending, so this matches the dashboard exactly. Requires the `emails:read` capability. ## OpenAPI - [OpenAPI 3.1 spec](https://www.unitpost.com/api/v1/openapi.json) ## Email engineering guides - [How to fix an SPF record that isn't found](https://www.unitpost.com/blog/spf-record-not-found): Why an SPF lookup returns nothing even after you've added the record, and how to diagnose it: propagation, wrong host, quoting, and the one-SPF-record rule. - [Why DKIM verification fails (and how to fix each cause)](https://www.unitpost.com/blog/dkim-verification-failing): The concrete reasons a DKIM check fails after you've published the key: selector mismatch, a truncated public key, DNS flattening, and message modification in transit. - [Why your transactional email goes to spam](https://www.unitpost.com/blog/email-going-to-spam): A diagnostic order of operations for transactional mail landing in spam: authentication, reputation, content, and the engagement signals you can't see. - [Transactional vs marketing email: what's the difference?](https://www.unitpost.com/blog/transactional-vs-marketing-email): The practical distinction between transactional and marketing email — consent, unsubscribe obligations, reputation risk — and when one tool for both is the right call. - [What is an idempotency key, and when do you need one?](https://www.unitpost.com/blog/what-is-an-idempotency-key): How idempotency keys prevent duplicate sends when a request is retried, how to choose one, and the failure modes they don't cover. - [Why domain verification should never be a one-time check](https://www.unitpost.com/blog/continuous-domain-verification): DNS changes after you verify a domain. How continuous re-verification with a grace period catches regressions without turning a transient blip into an outage. - [How to let an AI agent send email safely](https://www.unitpost.com/blog/send-email-from-ai-agent): The concrete controls that make agent-initiated email safe: scoped credentials, a verified-domain gate, idempotency, approval before send, and an audit trail. - [What is an email MCP server?](https://www.unitpost.com/blog/email-mcp-server): How Model Context Protocol exposes email sending as agent tools, what a well-designed email MCP surface looks like, and how authentication and scopes work. ## Comparisons - [Unitpost vs Resend](https://www.unitpost.com/alternatives/resend): A developer-first transactional email API, known for the cleanest DX in the category and for React Email. Includes where Resend is the better choice, and a one-key migration path. - [Unitpost vs SendGrid](https://www.unitpost.com/alternatives/sendgrid): The Twilio-owned incumbent, processing billions of emails a month across transactional and marketing with deep enterprise tooling. Includes where SendGrid is the better choice, and a one-key migration path. - [Unitpost vs Postmark](https://www.unitpost.com/alternatives/postmark): A transactional-only specialist with the strongest deliverability and time-to-inbox reputation in the category. Includes where Postmark is the better choice, and a one-key migration path. - [Unitpost vs Mailgun](https://www.unitpost.com/alternatives/mailgun): Sinch-owned email infrastructure aimed at developers, with strong validation and deliverability tooling. Includes where Mailgun is the better choice, and a one-key migration path. - [Unitpost vs Brevo](https://www.unitpost.com/alternatives/brevo): An all-in-one marketing platform (formerly Sendinblue) covering email, SMS, chat, and CRM, with a transactional API alongside. Includes where Brevo is the better choice, and a one-key migration path. - [Unitpost vs Loops](https://www.unitpost.com/alternatives/loops): A modern, design-led email platform aimed at SaaS companies, focused on marketing sequences and lifecycle email. Includes where Loops is the better choice, and a one-key migration path.