Getting started

Quickstart

Dispatch is already running. To integrate it into one of your projects, you only need three things: a verified sending domain, a provider configured in the dashboard, and an API key.

  1. Log in at /login with your email — a magic link will be sent to you.
  2. Go to Providers and add at least one (SMTP, Resend, or Brevo).
  3. Go to Sending domains, add your domain, add the 3 DNS records it shows, then click Verify.
  4. Go to API keys, create a key, and copy it — it is shown only once.
  5. Call the send endpoint from your project:
curl https://dispatch.your-domain.com/api/v1/send \
  -H "x-api-key: dk_live_YOUR_KEY" \
  -H "content-type: application/json" \
  -d '{
    "from": "hello@acme.com",
    "to": "you@example.com",
    "subject": "Hello from Dispatch",
    "html": "<h1>It works!</h1>"
  }'
Getting started

Authentication

All API calls require an API key. Generate scoped keys in the dashboard. Keys look like dk_live_XXXXXXXXXXXXXXXXXXXXXX, are shown once, and stored as a SHA-256 hash on the server.

Send it as an x-api-key header or a bearer:

-H "x-api-key: dk_live_XXXX"
# or
-H "authorization: Bearer dk_live_XXXX"
Getting started

Calling the API

Dispatch is a plain HTTP API — use fetch, axios, or any HTTP client. There is no SDK package to install. Set two things in your project: the base URL of your Dispatch instance and your API key.

const DISPATCH_URL = process.env.DISPATCH_URL; // e.g. https://dispatch.acme.com
const DISPATCH_KEY = process.env.DISPATCH_API_KEY;

async function sendEmail(payload) {
  const res = await fetch(`${DISPATCH_URL}/api/v1/send`, {
    method: "POST",
    headers: {
      "x-api-key": DISPATCH_KEY,
      "content-type": "application/json",
    },
    body: JSON.stringify(payload),
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}
Setup

Providers

Dispatch does not send email itself — it delegates to a pluggable provider. Configure at least one from the Providers page in the dashboard:

  • Resend — HTTPS API, 3,000 sends/mo free forever.
  • Brevo — HTTPS API, 300 sends/day free forever (~9k/mo).
  • Amazon SES — SMTP mode, ~$0.10 per 1k sends (cheapest at scale).
  • Generic SMTP — Gmail App Password, Zoho, Postmark, Mailgun, SendGrid — anything.

Credentials are encrypted at rest with AES-256-GCM keyed by APP_ENCRYPTION_KEY.

Setup

Sending domains

To send from anything@acme.com, verify acme.com(or a subdomain like mail.acme.com) in the dashboard. You'll get 3 DNS records to add:

  • DKIM — Dispatch generates a 2048-bit RSA keypair; the public half becomes a TXT record.
  • SPF — a TXT record authorising the platform provider.
  • DMARC — a TXT record with policy p=none (tighten to quarantine/reject once you're confident).

Click Verify — Dispatch resolves DNS live and flips the domain to verified. Every verified domain enters a 7-day warmup that caps daily volume.

curl -X POST https://dispatch.your-domain.com/api/v1/domains \
  -H "x-api-key: dk_live_XXXX" \
  -H "content-type: application/json" \
  -d '{"domain": "mail.acme.com"}'
# then:
curl -X POST https://dispatch.your-domain.com/api/v1/domains/sd_XXXX/verify \
  -H "x-api-key: dk_live_XXXX"
Send

POST /api/v1/send

Send one message inline. This is the endpoint you call from a login-confirmation script or form handler.

Body

FieldTypeRequiredDescription
fromstringyesMust belong to a verified sending domain.
from_namestringnoDisplay name.
tostringyesRecipient email.
subjectstringyesHandlebars-templated.
htmlstringone of html/text/template_idBody HTML.
textstringone of html/text/template_idPlain text body.
template_idstringoptionalUse a saved template instead of html/text.
variablesobjectoptionalMerged into subject and body.
provider_idstringoptionalForce a specific provider.
trackingbooleandefault trueOpen/click tracking + pixel injection.
categorystringdefault transactionalFree-form label.
reply_tostringoptionalOverride default reply-to.
tagsstring[]optionalProvider-side tags for filtering.
headersobjectoptionalExtra headers on the outbound mail.

Response

{ "id": "em_XXXX", "status": "sent",
  "provider": "resend", "provider_message_id": "…" }
Send

Templates

Save reusable HTML templates (WYSIWYG editor in the dashboard) and passtemplate_id at send time. Uses Handlebars — {{variable}}, plus {{upper name}} and {{lower name}}. Every marketing send gets a {{unsubscribe_url}} variable auto-populated.

Send

Campaigns

Broadcasts to a list. Enqueue per-recipient jobs; the queue worker drains them.

curl -X POST https://dispatch.your-domain.com/api/v1/campaigns \
  -H "x-api-key: dk_live_XXXX" \
  -H "content-type: application/json" \
  -d '{
    "name": "July newsletter",
    "type": "newsletter",
    "subject": "What we shipped in July",
    "html": "<h1>Hi {{first_name}},</h1>",
    "sending_domain_id": "sd_XXXX",
    "list_id": "ls_XXXX"
  }'
curl -X POST https://dispatch.your-domain.com/api/v1/campaigns/cm_XXXX/send \
  -H "x-api-key: dk_live_XXXX"
Send

Sequences

Multi-step drips with per-step delays. Enroll contacts manually, from a list, or by API. The queue worker advances every active enrollment on each tick — unsubscribes / bounces stop the sequence automatically.

curl -X POST https://dispatch.your-domain.com/api/v1/sequences \
  -H "x-api-key: dk_live_XXXX" \
  -H "content-type: application/json" \
  -d '{
    "name": "Onboarding",
    "from_domain_id": "sd_XXXX",
    "steps": [
      { "position": 1, "delay_seconds": 0, "subject": "Welcome",
        "html": "<h1>Hi {{first_name}}</h1>" },
      { "position": 2, "delay_seconds": 86400, "subject": "Day 1", "html": "..." }
    ]
  }'
Audience

Lists

Named collections of contacts. Every list has a public_subscribe_token.

curl -X POST https://dispatch.your-domain.com/api/v1/lists \
  -H "x-api-key: dk_live_XXXX" \
  -H "content-type: application/json" \
  -d '{ "name": "newsletter" }'
Audience

Contacts

curl -X POST https://dispatch.your-domain.com/api/v1/contacts \
  -H "x-api-key: dk_live_XXXX" -H "content-type: application/json" \
  -d '{ "email": "person@example.com", "first_name": "Alex",
        "list_ids": ["ls_XXXX"] }'
Audience

Public subscribe

No API key required — just the list's public_subscribe_token. Safe to call from client-side JS.

<form action="https://dispatch.your-domain.com/api/v1/lists/ls_XXXX/subscribe?token=PUBLIC_TOKEN" method="POST">
  <input name="email" type="email" required />
  <button>Subscribe</button>
</form>
Audience

Segments

Dynamic filters over contact fields, metadata, and engagement counters.

{
  "match": "all",
  "rules": [
    { "field": "email", "op": "ends_with", "value": "@acme.com" },
    { "field": "engagement.opens_30d", "op": ">=", "value": 3 },
    { "field": "tags", "op": "contains", "value": "vip" }
  ]
}
Audience

Suppressions

Hard bounces and complaints auto-suppress. You can also add / remove addresses manually. Suppressed addresses cause 403 recipient_suppressed.

Feedback

Opens & clicks

When tracking: true (default), Dispatch injects a 1×1 open pixel and rewrites https:// links through a signed redirect. Events land in the events table as opened /clicked.

Feedback

Unsubscribe

Every campaign email carries a List-Unsubscribe header and a footer link to a signed unsubscribe page. Include{{unsubscribe_url}} in your HTML to place the link yourself.

Feedback

Outbound webhooks

Register an endpoint and Dispatch will POST HMAC-signed JSON to it as events happen. Header format: x-dispatch-signature: v1,ts=<unix>,sig=<hex>.

curl -X POST https://dispatch.your-domain.com/api/v1/webhooks \
  -H "x-api-key: dk_live_XXXX" -H "content-type: application/json" \
  -d '{ "url": "https://your-app.com/webhooks/dispatch",
        "events": ["sent","opened","clicked","bounced"] }'
Feedback

Inbound notifications

Providers (SES SNS, Resend, Brevo) can push bounce / complaint / reply notifications to POST /api/v1/inbound. Dispatch normalises them into the events table and updates suppressions.

Feedback

Deliverability

30-day rolling summary of sent / bounce / complaint / open / click / unsubscribe. Both a dashboard chart and GET /api/v1/deliverability?days=30.

Playbook

Deliverability playbook

Getting mail into inboxes is 20% code and 80% behaviour. Dispatch enforces the mechanics; the rest is on the sender. If you follow the rules below your bounce rate stays below 2% and your complaint rate below 0.1% — the two numbers Google and Microsoft actually look at.

Non-negotiables

  • Verify a real sending domain (never send from @gmail.com or @outlook.com through Dispatch — use a domain you own).
  • Publish SPF, DKIM and DMARC. Dispatch generates the DKIM keypair and shows the exact records — you add them in DNS.
  • Turn on the DMARC report email when you add a domain, and watch the aggregate reports for the first week.
  • Let the warmup complete (2–3 weeks) before pushing volume.
  • Never buy or scrape lists. Everyone on your list must have said yes.

What Dispatch enforces for you

  • DKIM signs every SMTP send with your domain's key so DKIM alignment survives Outlook, Gmail, or any generic SMTP relay.
  • Warmup caps daily volume on new domains (50 → 100 → 200 → 500 → 1000 → 2000 over 21 days).
  • Reputation guardrail auto-pauses a domain if 24-hour bounce rate crosses 5% or complaint rate crosses 0.1% (kicks in only after ≥ 20 sends so a single bounce won't lock you).
  • Hard bounces auto-suppress. Unsubscribes auto-suppress. Complaints auto-suppress.
  • Every marketing send carries a working List-Unsubscribe header and a footer link.

What you should also do

  • Send transactional and marketing from different subdomainsmail.acme.com for one, news.acme.com for the other — so a marketing misstep doesn't poison your login emails.
  • Keep the body <= 100 KB. Text-to-image ratio > 60% text.
  • Include a plain-text body alongside HTML (Dispatch does this automatically if you don't).
  • Send from a real reply-to. If nobody reads replies, use reply@ or set up a shared inbox — don't use noreply@ for marketing.
Playbook

Outlook M365 + Cloudflare setup

This is the exact flow if you're using paid Microsoft 365 Business and your DNS is on Cloudflare.

  1. Add your domain to M365. In the Microsoft 365 admin center → Settings → Domains → Add domain. Microsoft will ask you to add a TXT record proving ownership plus two CNAMEs for their DKIM selectors (selector1 and selector2). Add all three in Cloudflare (Cloudflare treats @ as the root; leave proxy off — DNS-only). Wait for M365 to say Verified.
  2. In Dispatch, add the Outlook provider. Providers → Microsoft 365. Host smtp.office365.com, port 587, username = your M365 email, password = your M365 password (or app password if 2FA is on).
  3. Add the sending domain in Dispatch. Domains → Add. Pick your Outlook provider as the preferred provider, optionally add a DMARC report email. Dispatch will generate 3 records.
  4. Add those 3 records in Cloudflare. For each row Dispatch shows:
    • Type = TXT
    • Name = the value under Host (Cloudflare will strip your domain suffix automatically)
    • Content = the value under Value (paste as-is; Cloudflare handles quoting)
    • Proxy = DNS only (never orange-cloud a TXT record)
    • TTL = Auto
    You now have M365's DKIM CNAMEs AND Dispatch's DKIM TXT coexisting — this is intentional. M365 signs its way, Dispatch signs ours, both align with your From domain.
  5. Verify. Back in Dispatch, click Verify on the domain card. Cloudflare propagation is usually seconds.
  6. Send a test. Curl POST /api/v1/send to yourself. Check the raw source in your inbox — you should seeAuthentication-Results: … spf=pass … dkim=pass … dmarc=pass. If any of those aren't pass, fix DNS before you send anything else.
Playbook

Safe volume by use case

Honest numbers based on what actually keeps you out of trouble.

Use caseRecommended providerSafe daily volumeWhy
Login confirmations, password resets, receiptsOutlook M365up to 500/dayMicrosoft expects this from a business mailbox. Reputation stays clean because recipients opted in.
Newsletter to opt-in subscribersOutlook M365 or Brevo Freeup to 500/day (Outlook) · 300/day (Brevo)Both work; Brevo has more headroom for growth without paying Microsoft for extra SMTP quota.
Cold outreach / sequencesBrevo Free (never Outlook)100–200/day per sending domainMicrosoft's abuse detection willflag cold outreach patterns and suspend the mailbox. Brevo permits outreach as long as it's truly opt-in (or falls under legitimate-interest B2B). Keep sends spaced out over the day, not blasted.
One-off broadcast to a large listBrevo Freeup to 300/day (spread the send)Dispatch will spread the queue over minutes automatically at the provider's per-minute limit.

The 3,000/mo Resend cap and 9,000/mo Brevo cap are the free-tier ceilings. If you outgrow them, Resend is $20/mo for 50k and Brevo is €19/mo for 20k. Or add both and let Dispatch round-robin.

Playbook

Cold outreach mode

Instantly / Apollo-style cold outreach in Dispatch is built on three things: a mailbox pool of many small mailboxes, peer warmup between them, and IMAP reply detection so sequences self-terminate when a prospect writes back.

The setup

  1. Get 5 Zoho Free mailboxes for your outreach domain. Zoho Mail Free gives you five mailboxes per custom domain forever — this is the cheapest legit way to build a pool. (You can add Gmail App Password or Outlook M365 mailboxes too; more provider diversity = better.)
  2. For each mailbox, add it as a provider in Dispatch (Providers → Zoho Mail (Free)). Fill BOTH the SMTP fields (for sending) AND the IMAP fields (for reply detection).
  3. Mailbox pools → New pool. Give it a name, pick the sending domain, enable peer warmup, pick a rotation strategy. After creation, open the pool and add each of your Zoho mailboxes as members.
  4. Wait 7 days for warmup. During this time Dispatch schedules small conversations between your own mailboxes to build reputation. Concurrently, the sending domain's own warmup schedule caps daily outreach volume.
  5. Sequences → New sequence. Set trigger, pick your outreach domain and the mailbox pool. Enable Auto-stop when the prospect replies. Send jitter ±20%. Write each step body using spintax where it helps — {Hi|Hey|Hello} {{first_name}}.
  6. Enroll contacts by list or by API call. Sends spread across the pool automatically.

Spintax

Anywhere in a subject or body you can write {one|two|three} and Dispatch will pick one option per send. Groups can be nested and options can be empty: just checking in{|,} — any thoughts? yields one of two comma placements.

Reply detection

When IMAP credentials are configured on a mailbox, the cron worker polls INBOX every tick, looks up any new message's In-Reply-To header, matches it to the RFC Message-ID Dispatch stamped on the outbound, and stops the sequence enrollment. The reply lands in inbound_replies and fires a webhook event.

Legal & ethical

Dispatch is not a legal shield. Cold outreach is regulated differently by jurisdiction (CAN-SPAM in the US, CASL in Canada, GDPR / PECR in the EU/UK). Have opt-in, have legitimate interest, or don't send. Every message carries a working List-Unsubscribe header and a footer link — always honor them, and Dispatch will auto-suppress anyone who unsubscribes or bounces hard.

Ops

Queue worker

POST /api/cron/tick drains the campaign job queue, advances sequence enrollments, retries failed webhook deliveries, runs peer warmup exchanges, and polls IMAP for replies — all in one call. It must be called on a schedule (every 1–5 minutes).

Protect the endpoint with the CRON_SECRET env var. Pass it as x-cron-secret: <secret> or Authorization: Bearer <secret>.

curl -X POST https://dispatch.acme.com/api/cron/tick \
  -H "x-cron-secret: YOUR_CRON_SECRET"
Ops

Team & roles

Owners can invite teammates from the dashboard. Roles: owner (full control including providers/team), admin, member, and readonly. Invites expire after 14 days.

Getting started

Errors

4xx and 5xx responses are shaped like:

{ "error": "validation_failed", "details": { ... } }
  • 400 — validation, missing verified domain, empty body, warmup cap hit, no provider available
  • 401 — missing / bad / revoked API key
  • 403 — recipient on the suppression list
  • 404 — resource not found
  • 429 — rate limit (see retry-after)
  • 502 — upstream provider rejected the send