Guides

Learn Unitpost

A page-by-page tour of the dashboard, plus integration walkthroughs for SMTP and AI agents.

Getting started

What every part of the dashboard does, and how contacts, segments, and campaigns connect.

How it all fits together

The contacts → segments → campaigns flow and the rules that keep it safe.

Open this guide on its own page

Unitpost has two ways to send: the API (transactional mail — receipts, password resets, one-to-one messages triggered by your app) and Campaigns (one-to-many marketing sends to an audience). Most of the dashboard exists to make the campaign path safe and repeatable, and to give you visibility into both.

The core flow

Three resources form a pipeline, each building on the one before it: Contacts → Segments → Campaigns.

  1. Contacts are the people you can email — an address plus optional name and custom fields. They carry a marketing subscription state; an unsubscribed or suppressed contact is never mailed.
  2. Segments are named groups of contacts. A campaign sends to exactly one segment, so a segment is the unit of "who receives this send." Only subscribed members are mailable.
  3. Campaigns combine a template (the content, authored on the Templates page) with a segment (the audience) and a verified from-domain, then send now or on a schedule.

Read the chain bottom-up when something won't send

A campaign can only send if its template is publishable, its segment has subscribed members, and its from-domain is verified. When a send is blocked, walk back down the chain — domain → segment members → template — and the offending link is almost always one of those three.

Supporting pieces

  • Domains establish sending identity (DKIM/SPF/DMARC). Nothing sends from an address on an unverified domain.
  • Templates hold reusable content; the Library stores the images and assets templates reference.
  • Emails and Activity are the read side — every individual message and every account event, for debugging and auditing.
  • API Keys and Webhooks are the developer surface: keys authenticate the API, webhooks push event notifications back to you.

Archive, don't delete

Deleting a segment or campaign archives it rather than removing it. Archiving keeps history intact and avoids orphaning references. Archived items are hidden from pickers, can be restored, and are swept ~30 days later once nothing active references them.

Active references block destructive actions

You can't archive a segment that a non-terminal campaign (draft, scheduled, sending) needs — cancel or finish that campaign first. This is a clean, explained stop in both the UI and the API.

Each page has its own guide below. New to the product? Follow the sidebar order: verify a Domain, build a Template, add Contacts, group them into a Segment, then create a Campaign.

Dashboard

Your account at a glance — sending health, recent activity, and what needs attention.

Open this guide on its own page

The Dashboard is the home screen: a summary of sending volume, deliverability signals (bounces, complaints), and recent campaigns and messages. It's a read-only overview — every figure links through to the page where you can act on it.

Use it as a daily health check. A rising bounce or complaint rate, a domain that's slipped out of verification, or a campaign stuck in draft all surface here before they become a deliverability problem.

Start here when onboarding

If your account is new, the Dashboard guides you through the first run: verify a domain, create a template, and send a test. Those are the prerequisites every campaign depends on.

Domains

Verify sending domains so your mail authenticates and lands in the inbox.

Open this guide on its own page

A domain is your sending identity. Before you can send from an address like hello@yourcompany.com, the domain yourcompany.com must be added here and verified by publishing the DNS records we generate (a DKIM TXT record, a custom MAIL FROM subdomain, and a DMARC policy).

Verification runs automatically once DNS propagates — you don't click anything. A domain moves through Pending → Verified; if records change or are removed it can fall back, at which point sends from it are blocked.

Re-checked at send time

A domain's status is verified again the moment a campaign sends, not just when you created it. If the domain isn't in a sending-allowed state at send time, the send is blocked with a clear message — so a domain that lapses after a campaign is scheduled won't silently send unauthenticated mail.

Domains feed directly into Templates and Campaigns: the from-address you pick when sending must belong to a verified domain. This is the first link in the contacts → segments → campaigns chain.

Inbound routing

You can also receive mail at your domain. Enable inbound routing on the domain's Receiving tab and publish the MX records, and we'll parse incoming mail and POST it to your webhook as clean JSON. See the Inbound email guide for details.

Templates

Author reusable email content for campaigns and the API.

Open this guide on its own page

Templates are reusable email designs — a subject and a body built from the component library (or raw HTML). A template can be used by a campaign, or referenced by API sends via its id, so you author content once and send it many ways.

Templates carry a category. Marketing templates are the ones campaigns use. Transactional templates are for API-triggered, one-to-one mail (receipts, resets) and are intentionally NOT selectable for campaigns.

Transactional templates can't be campaigned

Creating a campaign from a transactional template is blocked by the server (not just the form), because campaign mail to a marketing audience must honor unsubscribe and consent rules that transactional mail doesn't. Use — or clone into — a marketing template instead.

Images and other assets a template references live in the Library; a template that's still a draft, or that references missing assets, can't back a campaign send.

Library

Store and manage images and assets your templates reference.

Open this guide on its own page

The Library is shared asset storage for your workspace — upload images and files once, then reference them from any template. Hosting assets here (rather than hot-linking from elsewhere) means they stay available and load fast for every recipient.

Assets are referenced by Templates, so deleting an asset that a live template uses will leave a broken image in that mail. Check usage before removing anything you're unsure about.

Keep originals reasonable

Large images slow rendering and can trip spam filters. Upload appropriately sized assets — the Library is for delivery, not as a full-resolution archive.

Contacts

The people you can email — subscription state, custom fields, and imports.

Open this guide on its own page

Contacts are individual recipients: an email address plus optional name and custom fields you define. Each contact has a marketing subscription state — subscribed contacts are mailable; unsubscribed or suppressed contacts are skipped automatically and never receive campaign mail.

Importing

Import contacts in bulk from CSV. The import is per-row resilient: valid rows are created even if others fail. You get a summary of imported, skipped, and rejected rows, so one bad line never sinks the file.

Custom-field limits

Custom fields are capped per contact (and on key/value length). Rows that exceed the caps are rejected in the import rather than truncated silently.

Deleting a contact

Deleting a contact removes them from every segment. If segments are targeted by an active campaign, the confirmation shows the impact.

Contacts are the first link in the core flow: group them into Segments, then send to a segment from a Campaign.

Suppressions

Addresses blocked from receiving mail — protecting your sending reputation.

Open this guide on its own page

A suppression is an address-level block that excludes a single email address from every send — transactional, batch, and campaign — including CC and BCC. The match is case-insensitive. Suppressions keep you from mailing addresses that have already bounced or complained.

Automatic suppression

When a message hard-bounces or the recipient complains, the address is added to your suppression list automatically and silently skipped on future sends. This is the single most important thing you can do for deliverability.

Adding your own

Add addresses by hand from the Suppressions tab on the Contacts page — paste one or many and they're suppressed in a single step. The same operations are available via API at /api/v1/suppressions.

Excluded, not failed

A send to a suppressed address isn't an error — the send succeeds, the suppressed recipients are simply dropped, and the result tells you how many were excluded and why. One bad address never fails the whole send.

Platform-wide blocks

Some blocks are read-only

Addresses on the Unitpost-wide list (reputation/abuse protection) appear with a “Platform” badge and can't be removed from your workspace. They're shown so you understand why a send was skipped — if you believe one is in error, contact support.

Removing an address

Any member with the manage-suppressions permission can remove an address you added. Removing an address that previously bounced or complained shows a warning, as re-sending to it can damage deliverability.

Segments

Named groups of contacts that campaigns send to.

Open this guide on its own page

A segment is a named audience — a group of contacts a campaign can target. Each campaign sends to exactly one segment, so segments are how you decide who receives a given send.

The list shows each segment's subscribed-member count alongside its total. Only subscribed members are mailable. A segment with zero subscribed contacts is flagged, because a campaign to it can't send.

Empty segments block sends

A campaign whose segment has no subscribed members is stopped before it sends — there's no one to mail. Watch for the "0 subscribed" / "Empty" badge in the list and fix the audience before scheduling.

Archiving & deletion

Deleting a segment archives it. Archived segments disappear from pickers, but the historical record of campaigns stays intact. You can restore an archived segment, and it's permanently swept ~30 days later once unused.

Can't archive while actively referenced

If a non-terminal campaign (draft, scheduled, sending) still targets a segment, archiving is blocked with a 409 — cancel or finish that campaign first.

Subscription topics

Let contacts opt out of one kind of mail without leaving your list.

Open this guide on its own page

A topic is a subscription category — "Product updates," "Promotions" — that a contact can opt out of. It's orthogonal to segments and global unsubscribe: a contact can stay on your list but silence one kind of mail.

Manage topics from the Topics page in the dashboard. Recipients control membership via the unsubscribe preference page, and campaigns simply point at a topic to scope a send.

Opt-in vs. opt-out

Each topic has one decision baked in — its default — which sets what "no explicit preference" means for a contact who has never touched it:

  • Opt-out (the default) — contacts are auto-enrolled and receive the topic unless they opt out. Use it for newsletters and product updates.
  • Opt-in — contacts are silent until they explicitly subscribe. Use it for beta invites or sensitive announcements you don't want going out by default.

An explicit choice always wins

Once a contact subscribes or unsubscribes from a topic, that choice overrides the default in both directions. The default only governs contacts who've never recorded a preference.

Scoping a campaign

Scope a campaign to a topic and anyone opted out is skipped at send (counted as suppressed in the validation report). You can't point a campaign at an archived or missing topic.

One-click unsubscribe respects the topic

Mail for a topic-scoped campaign carries a topic-aware unsubscribe link and an RFC 8058 List-Unsubscribe-Post header, so a recipient's one-click opt-out silences just that topic — not all of your marketing. They keep getting everything else they didn't object to.

Managing topics & preferences

Manage topics on the Topics page (or via /api/v1/email/topics) — create, list, edit, and archive. Archiving is blocked (409) while a non-terminal campaign still targets it. A contact's per-topic state is read and set at /api/v1/contacts/{id}/topics.

Campaigns

Send a template to a segment — now or on a schedule — with server-enforced safety gates.

Open this guide on its own page

A campaign is a one-to-many send: a template (content) + a segment (audience) + a from-address on a verified domain, sent immediately or scheduled. Campaigns are the end of the contacts → segments → campaigns pipeline.

Send-readiness gates

Before a campaign sends, every one of these must hold (enforced on the server):

  • The from-domain is verified and sending-allowed (re-checked at send, not just at create).
  • There's a non-empty subject (from the campaign or the template default).
  • The template is a publishable marketing template — not transactional, not an archived/draft template.
  • The segment is not archived and has at least one subscribed member.
  • Any schedule is within bounds — not in the past, and no further than 90 days out.

When a send is blocked, the validation dialog lists exactly which gate failed and how to fix it. The API returns the same reasons as a structured 409/422.

Lifecycle & archiving

A campaign moves through draft → scheduled → sending → sent (or canceled/failed). Terminal campaigns can be archived directly; a non-terminal campaign must be canceled first. Archived campaigns are restorable and auto-pruned after ~30 days.

Scheduling semantics

A schedule in the past is treated as "send now"; a schedule beyond 90 days is rejected at both create and send time. These bounds are consistent across the dashboard and the v1 API.

Emails

Every individual message sent — delivery status, opens, clicks, and bounces.

Open this guide on its own page

The Emails page is the message-level log: one row per email sent (campaign or API). Each row shows the recipient, subject, delivery state, and engagement (opens, clicks).

Use it to answer "what happened to this specific message?" — trace a delivery, confirm an open, or see why something bounced or was suppressed.

Suppression is automatic

A hard bounce or complaint suppresses that address, so future sends skip it. You'll see the suppression reflected here and on the contact — it protects your domain's reputation without manual cleanup.

Activity

A chronological audit log of account events across every resource.

Open this guide on its own page

Activity is the cross-resource event feed — domain verifications, template changes, campaign sends, key creation, webhook deliveries, and more, in one timeline. Where the Emails page is per-message, Activity is per-account.

It's the place to reconstruct "who changed what, when" — useful for debugging an unexpected state, confirming an action took effect, or auditing access. Every member can reach it.

API Keys

Authenticate the API with scoped, least-privilege keys.

Open this guide on its own page

API keys authenticate requests to the public v1 API. Create a key, choose a scope preset — Full, Sending, or Read only — and optionally narrow it to specific capabilities. The secret is shown once at creation; store it somewhere safe immediately.

Scope tightly

Give each integration the least it needs — a server that only sends mail gets a Sending key, not Full. Keys can never hold privileged capabilities (managing other keys, the team, the workspace, or billing); those stay in the dashboard behind a human login.

Revoke a key the moment it's no longer needed or may have leaked; revocation is immediate. Pair keys with Webhooks to build a full integration: keys for outbound API calls, webhooks for inbound event notifications.

Webhooks

Receive signed, real-time event notifications at your own endpoint.

Open this guide on its own page

Webhooks push events to your application as they happen — a delivery, a bounce, a contact change — so you can react without polling. Register a public HTTPS endpoint, subscribe to the events (or categories) you care about, and we POST a signed payload whenever one fires.

Full reference in the Webhooks guide

This page manages your endpoints; the API-reference Webhooks guide covers the payload shapes, signature verification (including timestamp tolerance), the retry schedule, the HTTPS/SSRF requirement, and rotating the signing secret. Read it before wiring up your verifier.

Each endpoint shows a live health state — Enabled, Failing, or auto-Disabled after sustained failures — so a dead URL surfaces early. Use Send test to fire a sample signed event and confirm your handler works end to end.

Migration Assistant

Move from Resend, SendGrid, Mailgun, Postmark, Brevo, or Loops with one read-only API key.

Open this guide on its own page

The Migration Assistant (Settings → Migration) moves your data from another email platform into Unitpost in about five minutes. It supports Resend, SendGrid, Mailgun, Postmark, Brevo, and Loops, and carries over seven kinds of data: contacts (with custom fields and subscription state), segments, topics, templates, domains, suppressions, and webhook endpoints.

How it works

  1. Connect — pick your provider and paste an API key (a read-only key is all it needs; it only ever reads from your old provider). The key is validated, KMS-encrypted, used once to fetch a snapshot, then deleted.
  2. Preview — the assistant fetches everything in the background and shows a full preview: per-entity counts, samples, conflicts with data already in your workspace, and explicit warnings for anything the provider can't export. You can leave the page; the job persists and updates live.
  3. Accept — nothing is written until you accept. Choose which entities to include and how to resolve conflicts (skip or update), then import. A full report shows every row that was imported, skipped, or failed.

What key do I need?

For every provider, one API key is all it takes — the assistant does the rest. A few providers have one extra detail worth knowing before you paste the key:

  • Resend — any API key (resend.com/api-keys); read access is enough.
  • SendGrid — a key with Full Access, or Restricted Access with read permission on Contacts, Marketing, Templates, Suppressions, Sender Authentication, and Webhooks (Settings → API Keys).
  • Mailgun — your account-level Private API key (Settings → API Security), not a domain sending key.
  • Postmark — an Account API token (Account → API Tokens) migrates everything in one run: domains plus every server's templates, suppressions, and webhooks. A single Server token also works, but can't read domains.
  • Brevo — an API key from Settings → SMTP & API. One thing to check first: if your account blocks unauthorized IPs for API keys (Security → Authorized IPs), turn it off before migrating — under “Blocking unauthorized IP addresses”, choose Deactivate for API keys. Brevo rejects API calls from unlisted IPs even when the key is valid. Keys created with the MCP toggle work too.
  • Loops — any API key (Settings → API).

Safety model

  • Read-only on the source — the assistant never sends, changes, or deletes anything on your old provider.
  • Key custody — the API key is encrypted with KMS the moment you submit it, stored separately from the job, used once for the snapshot fetch, and deleted immediately after (also on failure, cancel, or expiry). It is never shown or re-readable.
  • Opt-in writes — your Unitpost workspace is untouched until you explicitly accept the preview. Cancel any time before that.
  • Zero downtime — your current provider keeps sending throughout. Migrate, verify DNS on the recreated domains, then switch your API key over whenever you're ready.

What to expect

  • Domains are recreated with fresh DKIM keys — you'll publish new DNS records here; your old provider's records stay valid until you remove them.
  • Webhook endpoints get new signing secrets, so update your verifier after import.
  • Templates carry over their HTML and variables; provider-specific template syntax is flagged in the preview when it can't be converted.
  • Suppressions (bounces, complaints, unsubscribes) carry over so you never re-email someone who opted out.
  • Anything a provider's API can't export is called out in the preview — never silently dropped. The preview expires if unaccepted, and one migration runs per workspace at a time.

Ask Unit to help

The Unit assistant in the dashboard can check your migration's status, walk you through the preview, and explain any warning — just ask it about your migration.

SMS Messages

Send transactional and marketing SMS, and follow each message's delivery live.

Open this guide on its own page

The SMS log shows every message your workspace has sent or received, live — queued, sent, delivered, or failed, with the carrier's failure reason when there is one. Billing is per segment: 160 GSM-7 characters fit one segment (153 each when a message spans several); Unicode messages fit 70/67. The composer shows the segment count as you type.

Marketing SMS is held to a stricter bar than email: the recipient must have prior express consent on record, and sends respect recipient-local quiet hours (a marketing message queued at midnight is deferred to the morning window, never dropped and never sent at 2am). Transactional messages — receipts, alerts, verification codes — are not window-gated.

STOP always wins

When a recipient replies STOP, the carrier and Unitpost both record the opt-out immediately. Further sends to that number are suppressed on the SMS channel — it never touches their email subscription, and vice versa.

SMS Senders

The phone numbers and identities your SMS messages originate from.

Open this guide on its own page

SMS needs an origination identity the way email needs a verified domain. Start with the sandbox sender: it works immediately but only delivers to verified test numbers (a carrier rule for unverified senders). A dedicated toll-free number unlocks production sending to any US number after a one-time verification.

A sender moves through registration and verification states before it's active, and its status is re-checked at send time — a suspended or released number can't silently keep sending. Number rental is billed to your card as a recurring line item, never drawn from your usage wallet.

Keep marketing and OTP apart

Carriers degrade deliverability for numbers that mix bulk marketing with time-sensitive transactional traffic. When you start sending campaigns, add a dedicated marketing sender so a blast can never slow your verification codes.

SMS Dashboard

Your SMS sending at a glance — volume and delivery over the last 30 days.

Open this guide on its own page

The SMS dashboard summarizes the channel's last 30 days: how many messages went out, how many the carriers confirmed delivered, and how many failed or were suppressed. It's the channel-scoped counterpart of the email dashboard — same idea, SMS-honest numbers (no opens or clicks; carriers don't report them).

From here, jump into the message log for per-message delivery detail, or into Senders to provision an origination number.

SMS Activity

The workspace activity stream, scoped to the SMS channel.

Open this guide on its own page

SMS Activity is the same workspace activity stream you know from email, locked to channel messages: sends, delivery receipts, retries, and opt-outs (STOP). Use it to answer "what happened on SMS recently" without scanning the full cross-resource feed.

SMS Templates

Reusable plain-text messages with {{variables}}, published before use.

Open this guide on its own page

SMS templates are plain text with the same {{variable}} merge fields as email templates — {{first_name}} and your custom contact fields resolve per recipient at send time, and a send fails closed rather than shipping a literal token. The editor shows the live segment count as you type (160 GSM-7 characters per segment; 70 for Unicode).

Templates start as drafts; publish one to use it in sends and campaigns. For marketing messages, end with an opt-out hint like "Reply STOP to opt out" — STOP is honored automatically either way.

SMS Topics

Per-category subscription preferences, tracked separately from email.

Open this guide on its own page

SMS topics work like email topics — contacts opt in and out per category — but the preferences are tracked per channel: "Product updates" can be on for email and off for SMS for the same contact. A campaign tied to a topic skips recipients who opted out of it.

STOP always wins on top: a recipient who replies STOP is suppressed on the SMS channel entirely, regardless of topic preferences — and it never touches their email subscription.

SMS Campaigns

Bulk SMS to a segment — consent-gated, quiet-hours aware, paced per sender.

Open this guide on its own page

An SMS campaign sends a published SMS template to every reachable contact in a segment. Marketing SMS is held to the strict bar: only recipients with prior express consent on record receive it, sends respect the recipient-local quiet-hours window (a blast fired at midnight waits for the morning window — deferred, never dropped), and delivery is paced to your sender's carrier throughput.

Budget pauses, never surprises

If your messaging balance runs out mid-campaign, the campaign pauses — remaining recipients are untouched and sending resumes once you top up. Billing is per segment actually sent.

AI in Unitpost

First-class ways AI meets email in Unitpost: UnitAI, the assistant built into your dashboard, plus a Model Context Protocol server and drop-in Agent Skills for your own agents. Everything authenticates with scoped, revocable keys and flows through the same send gate as the REST API — every action is visible in your dashboard and webhooks.

UnitAIBeta

Unit is the AI teammate inside your dashboard — open the Ask Unit widget from any page and describe the outcome you want. It drives the real product through the same gated tools you use, and always shows you the plan (and asks) before anything sends.

  • Design templates by describing them — “a green-themed 20%-off template for our plant app”; Unit builds it from real components, then you tweak it in the editor.
  • Launch and schedule campaigns — it sets up the send, shows the plan, and waits for your approval.
  • Build segments and manage contacts — plain English in, a real segment out; list cleanup and suppressions included.
  • Explain your numbers — bounces, opens, reputation, DNS: Unit reads your activity and answers with the next step.
  • A human is one tap away — “Talk to a human” hands the thread to our team whenever you want.
Learn more about UnitAI

Agent Skills

The Unitpost skill packages everything a coding agent needs to integrate Unitpost correctly the first time — the API shape, the SDKs, the ID scheme, scopes, pagination, error handling, and send-safety rules — as a skill it loads on demand. Where the MCP server gives an agent the tools to act, the skill teaches it how Unitpost works.

Install it with the skills CLI — it auto-detects your agents (Claude Code, Cursor, Codex, Copilot, Gemini, Windsurf, and 60+ more) and writes the skill into each one's skills directory:

Shell
# Install the Unitpost skill into the current project
npx skills add unitpostcom/skills --skill unitpost

# …or globally, for every project
npx skills add unitpostcom/skills --skill unitpost -g

# List what's available first
npx skills add unitpostcom/skills --list
  • Pairs with the MCP server: the skill teaches the agent how to think about Unitpost; the MCP server below gives it the tools to act. Install both for the best experience.
  • Scoped by design: a skill never widens what a key can do — capability limits, suppression, tracking, and webhooks all still apply.
  • Open source: the skill lives at unitpostcom/skills — documentation only, no secrets; you supply the API key at runtime.

MCP server

A live Model Context Protocol server that exposes Unitpost as tools any MCP client can call — one tool per API operation (email_send, email_campaigns_send, email_domains_verify, and the rest). It authenticates with the same API key as the REST API and runs every call through the same gate.

Connection settings

Server URLhttps://mcp.unitpost.com/mcp
TransportStreamable HTTP
Auth (recommended)OAuth 2.1 — add the URL, approve in the browserscopes match API capabilities; tokens refresh automatically
Auth (legacy)Authorization: Bearer pk_live_YOUR_KEYworkspace API key from Settings → API keys; for clients that can only paste a key
Toolsone per API operation (email_send, email_campaigns_send, …)

Create the key on www.unitpost.com Settings → API keys. In every snippet, replace pk_live_YOUR_KEY and keep the Bearer prefix. Endpoint: https://mcp.unitpost.com/mcp. Each tool needs its matching capability (e.g. email_send emails:send).

Pick your client — each guide shows exactly where to open and what to paste. There is also an MCP guides index.

Cursor

Add the server URL and approve in the browser (Cursor handles OAuth itself, including reconnect). Static key config still works as a fallback.

Open the Cursor guide

  1. Connect with OAuth (recommended)

    Add a remote server with the URL below — Cursor opens Unitpost in the browser to approve. No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically. If Cursor ever signs you out, the Authenticate button re-runs the same approval (a 401 from us triggers its reconnect).

    JSON
    {
      "mcpServers": {
        "unitpost": {
          "url": "https://mcp.unitpost.com/mcp"
        }
      }
    }

    Prefer this over pasting secrets. To pin credentials instead, add an auth object with CLIENT_ID: cursor-oauth (public client, PKCE — no secret needed).

  2. Reload and verify

    Reload MCP servers in Cursor. unitpost should show connected with tools like email_send, email_campaigns_send, email_domains_verify.

  3. Send a test

    Ask the agent to send a test email from a verified domain, then confirm it in Unitpost Activity.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

Claude Code

Add the server URL, then authenticate from the in-session MCP panel. The browser opens at the Authenticate step, not at add time. API keys still work as a fallback.

Open the Claude Code guide

  1. Add the server

    Run once. This only saves the URL — no browser opens yet.

    Shell
    claude mcp add --transport http unitpost https://mcp.unitpost.com/mcp

    Use -s user to install for every project. claude mcp list now shows unitpost as ! Needs authentication — that is expected.

  2. Authenticate with OAuth (recommended)

    Start a Claude Code session, run /mcp, select unitpost, and choose Authenticate. Your browser opens to Unitpost — pick a workspace, approve the scopes, and you're connected. No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically.

    If the browser doesn't open automatically, copy the URL shown in the terminal and open it manually. Tokens refresh automatically; if auth ever lapses, repeat this step.

  3. Verify

    Run claude mcp list (or /mcp in a session). unitpost should show connected with its tools listed — no ! Needs authentication flag.

    Don't see it? Fully quit and reopen the client — closing the window is often not enough — then check again.

  4. Send a test

    Ask Claude to send an email from a verified domain and confirm it in Unitpost Activity.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

Claude Desktop

Claude Desktop connects with OAuth: add the server URL, approve in the browser, done. No browser or older plan? The headless guide has the mcp-remote bridge config.

Open the Claude Desktop guide

  1. Add a custom connector (recommended)

    In Claude Desktop: Settings → Connectors → Add custom connector. Paste the server URL below as the Remote MCP server URL and complete the browser approval. No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically.

    Remote MCP server URLhttps://mcp.unitpost.com/mcp
  2. Verify

    The unitpost connector should show as connected with its tools available in chat.

    Don't see it? Fully quit and reopen the client — closing the window is often not enough — then check again.

  3. Send a test

    Ask Claude to send an email from a verified domain and confirm it in Unitpost Activity.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

Codex

In ChatGPT, add the server URL as a connector and approve in the browser. The Codex CLI reads static config only (no browser flow) — its key setup lives on the headless page.

Open the Codex guide

  1. Connect with OAuth (recommended)

    In ChatGPT, open the MCPs tab and connect a custom MCP. Select Streamable HTTP (not STDIO), paste the server URL below, then complete the browser approval. No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically.

    MCP server URLhttps://mcp.unitpost.com/mcp
  2. Verify

    The unitpost connector should show as connected with its tools available. Codex CLI users: the CLI can't do the browser flow — follow the headless guide instead.

    Adding a connector requires a restart before chats see it: restart the Codex app, then check /mcp. Don't see it? Fully quit and reopen the client — closing the window is often not enough — then check again.

  3. Send a test

    Ask Codex to send an email from a verified domain and confirm it in Unitpost Activity.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

GitHub Copilot

Add the server URL and approve in the browser. If your setup needs a static key, VS Code prompts for it at runtime (never stored in the file).

Open the GitHub Copilot guide

  1. Connect with OAuth (recommended)

    Create or edit `.vscode/mcp.json` in your workspace (or MCP: Add Server from the Command Palette for a user-level entry) with the server URL below, then start it. No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically.

    JSON
    {
      "servers": {
        "unitpost": {
          "type": "http",
          "url": "https://mcp.unitpost.com/mcp"
        }
      }
    }

    VS Code tries HTTP streamable first, then OAuth on 401 (DCR). Official redirects we allow: https://vscode.dev/redirect and http://127.0.0.1:33418.

  2. Start the server

    Open the MCP view and start unitpost. Complete the browser approval, then confirm tools are listed. If it disconnects later, restart it here.

  3. Send a test

    In Copilot agent mode, ask it to send a test email from a verified domain and confirm in Unitpost Activity.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

Gemini CLI

Prefer the OAuth browser approval. If your setup needs static config, paste the key block instead.

Open the Gemini CLI guide

  1. Connect with OAuth (recommended)

    Add this block to `~/.gemini/settings.json` (no headers — Gemini discovers OAuth from the 401). No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically.

    JSON
    {
      "mcpServers": {
        "unitpost": {
          "httpUrl": "https://mcp.unitpost.com/mcp"
        }
      }
    }
  2. Verify

    Restart the Gemini CLI and run /mcp. unitpost should list its tools. If it shows as needing authentication, run /mcp auth unitpost — the browser opens for approval, and tokens refresh automatically after that.

  3. Send a test

    Ask Gemini to send an email from a verified domain and confirm it in Unitpost Activity.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

Windsurf

Prefer the OAuth browser approval. If your setup needs static config, paste the key block instead.

Open the Windsurf guide

  1. Connect with OAuth (recommended)

    In Windsurf: Settings → Cascade → MCP Servers → Add Server, or paste this into `~/.codeium/windsurf/mcp_config.json`. Windsurf uses serverUrl (not url) for remote HTTP. No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically.

    JSON
    {
      "mcpServers": {
        "unitpost": {
          "serverUrl": "https://mcp.unitpost.com/mcp"
        }
      }
    }
  2. Refresh and verify

    Refresh the MCP server list. Unitpost's tools should appear under unitpost.

  3. Send a test

    Ask Cascade to send a test email from a verified domain, then confirm in Unitpost Activity.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

OpenCode

OpenCode auto-detects OAuth on 401 (DCR). Add a remote server and run `opencode mcp auth unitpost`.

Open the OpenCode guide

  1. Add the remote server

    Put this in ~/.config/opencode/opencode.jsonc (or project opencode.json). No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically.

    JSON
    {
      "$schema": "https://opencode.ai/config.json",
      "mcp": {
        "unitpost": {
          "type": "remote",
          "url": "https://mcp.unitpost.com/mcp",
          "oauth": {}
        }
      }
    }
  2. Authenticate

    Restart OpenCode, then run opencode mcp auth unitpost (or wait for the first tool call). Complete the browser approval.

  3. Verify

    Run opencode mcp list. unitpost should show connected.

  4. Send a test

    Ask the agent to send a test email from a verified domain and confirm it in Unitpost Activity.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

Grok

In Grok: add a custom connector with the name and URL below, then approve in the browser. The xAI marketplace plugin lives at github.com/unitpostcom/unitpost-grok-plugin (catalog PR on hold until Connect is verified).

Open the Grok guide

  1. Connect with OAuth (recommended)

    Add a custom connector. Name it Unitpost, paste the server URL below (the /mcp URL — streamable HTTP — not an /sse URL). No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically.

    NameUnitpost
    Server URLhttps://mcp.unitpost.com/mcp
    Text
    https://mcp.unitpost.com/mcp

    Don't see it? Fully quit and reopen the client — closing the window is often not enough — then check again.

  2. Send a test

    Ask Grok to send a test email from a verified domain and confirm it in Unitpost Activity.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

Generic / custom client

Any streamable-HTTP MCP client that speaks MCP OAuth connects with just the URL. Anything else uses a Bearer API key.

Open the Generic / custom client guide

  1. Connect with OAuth (recommended)

    Point the client at the server URL below. A spec-compliant client discovers OAuth itself: it reads the WWW-Authenticate: Bearer resource_metadata="…" challenge on the first 401, fetches /.well-known/oauth-protected-resource, runs the PKCE code flow against the advertised authorization server, and retries with the token. No key to copy. Your client opens Unitpost in the browser — pick a workspace, approve the scopes, and you're connected. Tokens refresh automatically.

    Server URLhttps://mcp.unitpost.com/mcp
    TransportStreamable HTTP
    Auth (recommended)OAuth 2.1 — add the URL, approve in the browser
    Auth (legacy)Authorization: Bearer pk_live_YOUR_KEY
    Toolsone per API operation (email_send, email_campaigns_send, …)
  2. Discover tools

    The server advertises one tool per API operation (email_send, email_campaigns_send, email_domains_verify, …). A write's body maps to the API request body. Every tool declares securitySchemes: [{ type: "oauth2", scopes: […] }] so linking-aware clients (ChatGPT) show the Connect UI.

  3. Verify

    Call a read tool (e.g. list domains). An unauthenticated call answers 401 with a WWW-Authenticate: Bearer resource_metadata="…" challenge (start OAuth there). With a key, a 401 Invalid API key usually means a missing Bearer prefix or a mistyped key — recreate the key and retry.

    Don't see it? Fully quit and reopen the client — closing the window is often not enough — then check again.

No browser on this machine? For headless setups, CI, SSH, or clients that can only paste a key, use the API-key setup instead.

Every MCP call uses the same pipeline as the REST API (scopes, rate limits, suppression, tracking, webhooks). Full tool list: /api/v1/openapi.json.

Headless / API key

OAuth needs a browser. Where there isn't one — CI jobs, SSH sessions, containers, Codex CLI, or any client that can only paste a static header — send a workspace API key as the Bearer token instead. Same scopes, same gates, same tools.

Open the Headless / API key guide

  1. Create an API key

    Open https://www.unitpost.comSettings → API keys → create a scoped key. Copy the full pk_live_… value (shown once). In every snippet below, replace pk_live_YOUR_KEY with that value.

    Shown once. Keep the Bearer prefix in every snippet. Grant only the scopes the agent needs (e.g. emails:send for email_send). Prefer a revocable key dedicated to the agent.

  2. Cursor (static key)

    Edit `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (this project only) — or Cursor Settings → MCP → Add new MCP server. Put the unitpost entry under top-level mcpServers.

    JSON
    {
      "mcpServers": {
        "unitpost": {
          "url": "https://mcp.unitpost.com/mcp",
          "headers": {
            "Authorization": "Bearer pk_live_YOUR_KEY"
          }
        }
      }
    }
  3. Claude Code (static header)

    Flags stay before the name/URL.

    Shell
    claude mcp add --transport http --header "Authorization: Bearer pk_live_YOUR_KEY" unitpost https://mcp.unitpost.com/mcp
  4. Claude Desktop (mcp-remote bridge)

    For plans without custom connectors. In Claude Desktop: Settings → Desktop app → Developer → Edit Config, then merge the block below — mcpServers must be a top-level key (sibling of preferences), not nested inside it.

    JSON
    {
      "mcpServers": {
        "unitpost": {
          "command": "npx",
          "args": [
            "-y",
            "mcp-remote",
            "https://mcp.unitpost.com/mcp",
            "--header",
            "Authorization:${AUTH_HEADER}"
          ],
          "env": {
            "AUTH_HEADER": "Bearer pk_live_YOUR_KEY"
          }
        }
      }
    }

    Deliberately no space after Authorization: in args — Claude Desktop mangles spaces there; the value lives in env instead. Requires Node.js 18+. If npx is not found, set "command" to your absolute npx path (e.g. /opt/homebrew/opt/node@22/bin/npx). Save, then ⌘Q (Quit) Claude Desktop — closing the window is not enough.

  5. Codex CLI (TOML)

    The CLI reads static config only. Edit `~/.codex/config.toml` (create it if needed).

    TOML
    [mcp_servers.unitpost]
    url = "https://mcp.unitpost.com/mcp"
    
    [mcp_servers.unitpost.http_headers]
    Authorization = "Bearer pk_live_YOUR_KEY"
  6. VS Code (runtime-prompted key)

    Create or edit `.vscode/mcp.json`. When VS Code prompts later, paste only the pk_live_… value — not Bearer (the config already adds it).

    JSON
    {
      "inputs": [
        {
          "id": "unitpost-key",
          "type": "promptString",
          "description": "Unitpost API key (pk_live_…)",
          "password": true
        }
      ],
      "servers": {
        "unitpost": {
          "type": "http",
          "url": "https://mcp.unitpost.com/mcp",
          "headers": {
            "Authorization": "Bearer ${input:unitpost-key}"
          }
        }
      }
    }
  7. Gemini CLI (static header)

    Edit `~/.gemini/settings.json` (user-level) or `.gemini/settings.json` (project).

    JSON
    {
      "mcpServers": {
        "unitpost": {
          "httpUrl": "https://mcp.unitpost.com/mcp",
          "headers": {
            "Authorization": "Bearer pk_live_YOUR_KEY"
          }
        }
      }
    }
  8. Windsurf (static header)

    Edit `~/.codeium/windsurf/mcp_config.json`. Windsurf uses serverUrl (not url) for remote HTTP.

    JSON
    {
      "mcpServers": {
        "unitpost": {
          "serverUrl": "https://mcp.unitpost.com/mcp",
          "headers": {
            "Authorization": "Bearer pk_live_YOUR_KEY"
          }
        }
      }
    }
  9. OpenCode (static header)

    Set oauth: false and pass the Bearer key.

    JSON
    {
      "mcp": {
        "unitpost": {
          "type": "remote",
          "url": "https://mcp.unitpost.com/mcp",
          "oauth": false,
          "headers": {
            "Authorization": "Bearer pk_live_YOUR_KEY"
          }
        }
      }
    }
  10. Any other client (generic)

    Configure a streamable-HTTP MCP server with the URL below, and send the header on every MCP request.

    JSON
    {
      "url": "https://mcp.unitpost.com/mcp",
      "headers": {
        "Authorization": "Bearer pk_live_YOUR_KEY"
      }
    }
  11. Verify with the key

    Call a read tool (e.g. list domains). A 401 Invalid API key usually means a missing Bearer prefix or a mistyped key — recreate the key and retry. Then send a test email from a verified domain and confirm it in Unitpost Activity.

    Don't see it? Fully quit and reopen the client — closing the window is often not enough — then check again.

Send over SMTP

Point any SMTP client at Unitpost and send from your verified domain. The connection details are the same everywhere — pick your platform below for a step-by-step setup, or open the SMTP guides index.

Connection settings

Hostsmtp.unitpost.com
Usernameunitpostalways this literal value
PasswordYour Unitpost API keymust carry emails:send
Port (STARTTLS)587or 2587 if blocked
Port (implicit TLS)465or 2465 if blocked
From addressAny address on a verified domain

The username is always unitpost; the password is a Unitpost API key that carries emails:send. TLS is mandatory.

Send email with Nodemailer over SMTP

Nodemailer is not deprecated. Create a transport with createTransport, point it at smtp.unitpost.com on port 587 (STARTTLS) or 465 (implicit TLS), and send from a verified domain. Port 25 is blocked on most clouds — don't use it. The same transport works in Express, Next.js, and NestJS.

Open the Send email with Nodemailer over SMTP guide

  1. Install Nodemailer

    Add it to your Node.js project (Node 18+). The package is actively maintained.

    Shell
    npm install nodemailer
  2. Create a transport

    Point createTransport at smtp.unitpost.com. Use port 587 with secure: false for STARTTLS, or port 465 with secure: true for implicit TLS. The username is always unitpost; the password is your API key with emails:send.

    JavaScript
    import nodemailer from "nodemailer";
    
    const transporter = nodemailer.createTransport({
      host: "smtp.unitpost.com",
      port: 587,
      secure: false,
      requireTLS: true,
      auth: {
        user: "unitpost",
        pass: process.env.UNITPOST_API_KEY,
      },
    });

    Port 25 vs 587: 25 is the legacy submission port and is blocked by AWS, GCP, and most VPS providers. 587 with STARTTLS is what you want. 465 is implicit TLS if 587 is filtered.

  3. Send an email

    The from address must be on a domain you've verified in Unitpost. This is the same sendMail call in a Next.js Route Handler or a NestJS provider.

    JavaScript
    await transporter.sendMail({
      from: "you@yourdomain.com",
      to: "customer@example.com",
      subject: "Hello from Unitpost",
      html: "<h1>Welcome!</h1><p>Sent via SMTP.</p>",
    });
  4. Verify it in the dashboard

    Open Activity in your Unitpost dashboard — the message appears alongside API sends, with delivery, open, and click status. Framework-specific walkthroughs: Next.js, NestJS, Express.

Send Supabase auth email over SMTP

Point Supabase's custom SMTP at Unitpost so auth emails send from your own verified domain — and skip Supabase's built-in auth-email rate limits.

Open the Send Supabase auth email over SMTP guide

  1. Open Supabase SMTP settings

    In the Supabase dashboard go to Project Settings → Authentication → SMTP Settings and enable Custom SMTP.

  2. Enter your Unitpost credentials

    Hostsmtp.unitpost.com
    Port587
    Usernameunitpost
    PasswordYour Unitpost API key
    Sender emailauth@yourdomain.com (verified)
    Sender nameYour app name

    The sender email must be on a domain you've verified in Unitpost, or sends are rejected with a 550.

  3. Save and send a test

    Save the settings, then trigger a magic link or signup confirmation. Supabase renders its own auth templates; Unitpost just delivers them.

  4. Confirm delivery

    The auth email shows up in your Unitpost Activity view with full tracking.

Because delivery now runs through Unitpost, your plan's sending limits apply instead of Supabase's default auth-email throttle.

Send WordPress email over SMTP

WordPress sends mail via PHP's `mail()` by default, which most hosts deliver poorly. Route it through Unitpost SMTP with a plugin like WP Mail SMTP.

Open the Send WordPress email over SMTP guide

  1. Install an SMTP plugin

    From Plugins → Add New, install and activate WP Mail SMTP (or a comparable SMTP plugin).

  2. Choose the "Other SMTP" mailer

    In the plugin settings, pick the generic Other SMTP option (not a named provider).

  3. Enter your Unitpost SMTP settings

    SMTP Hostsmtp.unitpost.com
    EncryptionTLS (STARTTLS)
    SMTP Port587
    AuthenticationOn
    SMTP Usernameunitpost
    SMTP PasswordYour Unitpost API key
    From Emailyou@yourdomain.com (verified)

    Store the API key in the plugin's password field. Set the From Email to an address on your verified domain.

  4. Send a test email

    Use the plugin's Email Test tab to send yourself a message, then confirm it in your Unitpost Activity view.

Send Auth0 email over SMTP

Configure a custom SMTP email provider in Auth0 so your tenant's emails deliver from your verified domain through Unitpost.

Open the Send Auth0 email over SMTP guide

  1. Open the email provider settings

    In the Auth0 dashboard go to Branding → Email Provider (or Emails → Provider on older tenants) and choose SMTP.

  2. Enter your Unitpost credentials

    Fromyou@yourdomain.com (verified)
    Hostsmtp.unitpost.com
    Port587
    Usernameunitpost
    PasswordYour Unitpost API key
  3. Save and send a test

    Save the provider, then use Auth0's Send Test Email button to verify the connection.

  4. Enable the emails you want

    Under Branding → Email Templates, toggle on verification, welcome, password-reset, and other templates. Auth0 renders them; Unitpost delivers.

Send email from Rails over SMTP

Set Action Mailer's SMTP settings to route your Rails app's mail through Unitpost. Same job as Nodemailer — different stack.

Open the Send email from Rails over SMTP guide

  1. Configure Action Mailer

    In your environment config (e.g. config/environments/production.rb), set the delivery method and SMTP settings.

    Ruby
    config.action_mailer.delivery_method = :smtp
    config.action_mailer.smtp_settings = {
      address:              "smtp.unitpost.com",
      port:                 587,
      user_name:            "unitpost",
      password:             ENV["UNITPOST_API_KEY"],
      authentication:       :plain,
      enable_starttls_auto: true,
    }
  2. Set your sender

    Use a from address on a verified domain in your mailer.

    Ruby
    class UserMailer < ApplicationMailer
      default from: "you@yourdomain.com"
    
      def welcome_email
        mail(to: "customer@example.com", subject: "Welcome!")
      end
    end
  3. Deliver and verify

    Trigger the mailer (UserMailer.welcome_email.deliver_now) and confirm the send in your Unitpost Activity view.

Send email from Laravel over SMTP

Laravel reads SMTP settings from your `.env`. Point the mailer at Unitpost and send from a verified domain.

Open the Send email from Laravel over SMTP guide

  1. Set your mail environment variables

    Add the following to your .env file.

    Shell
    MAIL_MAILER=smtp
    MAIL_HOST=smtp.unitpost.com
    MAIL_PORT=587
    MAIL_USERNAME=unitpost
    MAIL_PASSWORD=${UNITPOST_API_KEY}
    MAIL_ENCRYPTION=tls
    MAIL_FROM_ADDRESS=you@yourdomain.com
    MAIL_FROM_NAME="${APP_NAME}"

    MAIL_FROM_ADDRESS must be on a domain you've verified in Unitpost.

  2. Clear the config cache

    If you cache config in production, refresh it so the new values load.

    Shell
    php artisan config:clear
  3. Send and verify

    Send a mailable (e.g. Mail::to($user)->send(new WelcomeMail())) and confirm it in your Unitpost Activity view.

Send email from Django over SMTP

Configure Django's built-in SMTP email backend to send through Unitpost. This is the same job as Nodemailer — Python instead of Node.

Open the Send email from Django over SMTP guide

  1. Configure your settings

    Add the SMTP settings to settings.py. Keep the API key in an environment variable.

    Python
    EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
    EMAIL_HOST = "smtp.unitpost.com"
    EMAIL_PORT = 587
    EMAIL_USE_TLS = True
    EMAIL_HOST_USER = "unitpost"
    EMAIL_HOST_PASSWORD = os.environ["UNITPOST_API_KEY"]
    DEFAULT_FROM_EMAIL = "you@yourdomain.com"
  2. Send an email

    Use send_mail or an EmailMessage. The from address must be on a verified domain.

    Python
    from django.core.mail import send_mail
    
    send_mail(
        subject="Hello from Unitpost",
        message="Sent via SMTP.",
        from_email="you@yourdomain.com",
        recipient_list=["customer@example.com"],
        html_message="<h1>Welcome!</h1>",
    )
  3. Verify delivery

    Check your Unitpost Activity view for the message and its status.

Send email with PHPMailer over SMTP

Use PHPMailer's SMTP mode to deliver through Unitpost from plain PHP. Port 587 with STARTTLS; don't use port 25.

Open the Send email with PHPMailer over SMTP guide

  1. Install PHPMailer

    Add it via Composer.

    Shell
    composer require phpmailer/phpmailer
  2. Configure and send

    Set PHPMailer to SMTP mode with your Unitpost credentials.

    PHP
    <?php
    use PHPMailer\PHPMailer\PHPMailer;
    
    $mail = new PHPMailer(true);
    $mail->isSMTP();
    $mail->Host       = "smtp.unitpost.com";
    $mail->SMTPAuth   = true;
    $mail->Username   = "unitpost";
    $mail->Password   = getenv("UNITPOST_API_KEY");
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->Port       = 587;
    
    $mail->setFrom("you@yourdomain.com", "Your App");
    $mail->addAddress("customer@example.com");
    $mail->isHTML(true);
    $mail->Subject = "Hello from Unitpost";
    $mail->Body    = "<h1>Welcome!</h1><p>Sent via SMTP.</p>";
    $mail->send();
  3. Verify delivery

    The message appears in your Unitpost Activity view with full tracking.

Send email from Next.js over SMTP

Send from a Next.js Route Handler with Nodemailer pointed at Unitpost SMTP. Keep the API key on the server — never in a Client Component.

Open the Send email from Next.js over SMTP guide

  1. Install Nodemailer

    Shell
    npm install nodemailer
    npm install -D @types/nodemailer
  2. Create a Route Handler

    Server-only. The from address must be on a verified domain.

    TypeScript
    import nodemailer from "nodemailer";
    import { NextResponse } from "next/server";
    
    const transporter = nodemailer.createTransport({
      host: "smtp.unitpost.com",
      port: 587,
      secure: false,
      requireTLS: true,
      auth: {
        user: "unitpost",
        pass: process.env.UNITPOST_API_KEY,
      },
    });
    
    export async function POST() {
      await transporter.sendMail({
        from: "you@yourdomain.com",
        to: "customer@example.com",
        subject: "Hello from Next.js",
        html: "<p>Sent via SMTP.</p>",
      });
      return NextResponse.json({ ok: true });
    }
  3. Verify delivery

    Check Activity. Same transport as the Nodemailer guide — this page is the Next.js wiring.

Send email from NestJS over SMTP

Inject a Nodemailer transport in NestJS and send over Unitpost SMTP. Same credentials as any other stack.

Open the Send email from NestJS over SMTP guide

  1. Install Nodemailer

    Shell
    npm install nodemailer
    npm install -D @types/nodemailer
  2. Provide a transporter

    TypeScript
    import { Injectable } from "@nestjs/common";
    import nodemailer from "nodemailer";
    
    @Injectable()
    export class MailService {
      private transporter = nodemailer.createTransport({
        host: "smtp.unitpost.com",
        port: 587,
        secure: false,
        auth: {
          user: "unitpost",
          pass: process.env.UNITPOST_API_KEY,
        },
      });
    
      sendWelcome(to: string) {
        return this.transporter.sendMail({
          from: "you@yourdomain.com",
          to,
          subject: "Welcome",
          html: "<p>Sent via SMTP.</p>",
        });
      }
    }
  3. Verify delivery

    The send shows up in Activity. See also Nodemailer.

Send email from Express over SMTP

Send from an Express route with Nodemailer pointed at Unitpost. Keep the key in env, not in the repo.

Open the Send email from Express over SMTP guide

  1. Install Nodemailer

    Shell
    npm install nodemailer
  2. Send from a route

    JavaScript
    import express from "express";
    import nodemailer from "nodemailer";
    
    const transporter = nodemailer.createTransport({
      host: "smtp.unitpost.com",
      port: 587,
      secure: false,
      requireTLS: true,
      auth: {
        user: "unitpost",
        pass: process.env.UNITPOST_API_KEY,
      },
    });
    
    const app = express();
    app.post("/send", async (_req, res) => {
      await transporter.sendMail({
        from: "you@yourdomain.com",
        to: "customer@example.com",
        subject: "Hello from Express",
        html: "<p>Sent via SMTP.</p>",
      });
      res.json({ ok: true });
    });
  3. Verify delivery

    Confirm in Activity.

Send email from Python over SMTP

Python's smtplib talks SMTP directly. Point it at Unitpost on port 587 with STARTTLS. Django and Flask wrap this; this page is the bare client.

Open the Send email from Python over SMTP guide

  1. Send with smtplib

    Python
    import os
    import smtplib
    from email.message import EmailMessage
    
    msg = EmailMessage()
    msg["From"] = "you@yourdomain.com"
    msg["To"] = "customer@example.com"
    msg["Subject"] = "Hello from Python"
    msg.set_content("Sent via SMTP.")
    msg.add_alternative("<p>Sent via SMTP.</p>", subtype="html")
    
    with smtplib.SMTP("smtp.unitpost.com", 587) as smtp:
        smtp.starttls()
        smtp.login("unitpost", os.environ["UNITPOST_API_KEY"])
        smtp.send_message(msg)
  2. Framework wrappers

    Using a framework? Django, Flask, FastAPI.

Send email from Flask over SMTP

Configure Flask-Mail's SMTP settings for Unitpost and send from a verified domain.

Open the Send email from Flask over SMTP guide

  1. Install Flask-Mail

    Shell
    pip install Flask-Mail
  2. Configure and send

    Python
    import os
    from flask import Flask
    from flask_mail import Mail, Message
    
    app = Flask(__name__)
    app.config.update(
        MAIL_SERVER="smtp.unitpost.com",
        MAIL_PORT=587,
        MAIL_USE_TLS=True,
        MAIL_USERNAME="unitpost",
        MAIL_PASSWORD=os.environ["UNITPOST_API_KEY"],
        MAIL_DEFAULT_SENDER="you@yourdomain.com",
    )
    mail = Mail(app)
    
    def send_welcome(to: str) -> None:
        msg = Message("Welcome", recipients=[to], html="<p>Sent via SMTP.</p>")
        mail.send(msg)
  3. Verify delivery

    Check Activity. Bare Python: smtplib.

Send email from FastAPI over SMTP

Call Python's SMTP client from a FastAPI route. Same host, username, and API-key password as every other stack.

Open the Send email from FastAPI over SMTP guide

  1. Send from a route

    Python
    import os
    import smtplib
    from email.message import EmailMessage
    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.post("/send")
    def send() -> dict[str, bool]:
        msg = EmailMessage()
        msg["From"] = "you@yourdomain.com"
        msg["To"] = "customer@example.com"
        msg["Subject"] = "Hello from FastAPI"
        msg.set_content("Sent via SMTP.")
        with smtplib.SMTP("smtp.unitpost.com", 587) as smtp:
            smtp.starttls()
            smtp.login("unitpost", os.environ["UNITPOST_API_KEY"])
            smtp.send_message(msg)
        return {"ok": True}
  2. Verify delivery

    Confirm in Activity.

Send email from Go over SMTP

Go's net/smtp client can STARTTLS to Unitpost on port 587. Username is the literal `unitpost`; password is an API key.

Open the Send email from Go over SMTP guide

  1. Send with net/smtp

    Go
    package main
    
    import (
        "crypto/tls"
        "net/smtp"
        "os"
    )
    
    func main() {
        host := "smtp.unitpost.com"
        auth := smtp.PlainAuth("", "unitpost", os.Getenv("UNITPOST_API_KEY"), host)
        to := []string{"customer@example.com"}
        msg := []byte("From: you@yourdomain.com\r\n" +
            "To: customer@example.com\r\n" +
            "Subject: Hello from Go\r\n" +
            "\r\n" +
            "Sent via SMTP.\r\n")
    
        c, err := smtp.Dial(host + ":587")
        if err != nil {
            panic(err)
        }
        defer c.Close()
        if err = c.StartTLS(&tls.Config{ServerName: host}); err != nil {
            panic(err)
        }
        if err = c.Auth(auth); err != nil {
            panic(err)
        }
        if err = c.Mail("you@yourdomain.com"); err != nil {
            panic(err)
        }
        if err = c.Rcpt(to[0]); err != nil {
            panic(err)
        }
        w, err := c.Data()
        if err != nil {
            panic(err)
        }
        if _, err = w.Write(msg); err != nil {
            panic(err)
        }
        if err = w.Close(); err != nil {
            panic(err)
        }
        _ = c.Quit()
    }

    Port 587 needs an explicit STARTTLS before AUTH (PLAIN and LOGIN are both accepted). From must be on a verified domain.

  2. Verify delivery

    The message appears in Activity.

Send email from Spring Boot over SMTP

Set Spring Mail properties to Unitpost SMTP and send with JavaMailSender. Port 587, STARTTLS, username `unitpost`.

Open the Send email from Spring Boot over SMTP guide

  1. Set mail properties

    Properties
    spring.mail.host=smtp.unitpost.com
    spring.mail.port=587
    spring.mail.username=unitpost
    spring.mail.password=${UNITPOST_API_KEY}
    spring.mail.properties.mail.smtp.auth=true
    spring.mail.properties.mail.smtp.starttls.enable=true
  2. Send with JavaMailSender

    Java
    MimeMessage message = mailSender.createMimeMessage();
    MimeMessageHelper helper = new MimeMessageHelper(message, true);
    helper.setFrom("you@yourdomain.com");
    helper.setTo("customer@example.com");
    helper.setSubject("Hello from Spring");
    helper.setText("<p>Sent via SMTP.</p>", true);
    mailSender.send(message);
  3. Verify delivery

    Check Activity.

Send email from .NET over SMTP

System.Net.Mail.SmtpClient is obsolete. Use MailKit, STARTTLS on 587, username `unitpost`, API key as password.

Open the Send email from .NET over SMTP guide

  1. Add MailKit

    Shell
    dotnet add package MailKit
  2. Send

    C#
    using MailKit.Net.Smtp;
    using MailKit.Security;
    using MimeKit;
    
    var message = new MimeMessage();
    message.From.Add(MailboxAddress.Parse("you@yourdomain.com"));
    message.To.Add(MailboxAddress.Parse("customer@example.com"));
    message.Subject = "Hello from .NET";
    message.Body = new TextPart("html") { Text = "<p>Sent via SMTP.</p>" };
    
    using var client = new SmtpClient();
    await client.ConnectAsync("smtp.unitpost.com", 587, SecureSocketOptions.StartTls);
    await client.AuthenticateAsync("unitpost", Environment.GetEnvironmentVariable("UNITPOST_API_KEY"));
    await client.SendAsync(message);
    await client.DisconnectAsync(true);
  3. Verify delivery

    Confirm in Activity.