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.
- Log in at
/loginwith your email — a magic link will be sent to you. - Go to Providers and add at least one (SMTP, Resend, or Brevo).
- Go to Sending domains, add your domain, add the 3 DNS records it shows, then click Verify.
- Go to API keys, create a key, and copy it — it is shown only once.
- 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>"
}'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"
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();
}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.
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 toquarantine/rejectonce 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"POST /api/v1/send
Send one message inline. This is the endpoint you call from a login-confirmation script or form handler.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| from | string | yes | Must belong to a verified sending domain. |
| from_name | string | no | Display name. |
| to | string | yes | Recipient email. |
| subject | string | yes | Handlebars-templated. |
| html | string | one of html/text/template_id | Body HTML. |
| text | string | one of html/text/template_id | Plain text body. |
| template_id | string | optional | Use a saved template instead of html/text. |
| variables | object | optional | Merged into subject and body. |
| provider_id | string | optional | Force a specific provider. |
| tracking | boolean | default true | Open/click tracking + pixel injection. |
| category | string | default transactional | Free-form label. |
| reply_to | string | optional | Override default reply-to. |
| tags | string[] | optional | Provider-side tags for filtering. |
| headers | object | optional | Extra headers on the outbound mail. |
Response
{ "id": "em_XXXX", "status": "sent",
"provider": "resend", "provider_message_id": "…" }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.
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"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": "..." }
]
}'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" }'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"] }'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>
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" }
]
}Suppressions
Hard bounces and complaints auto-suppress. You can also add / remove addresses manually. Suppressed addresses cause 403 recipient_suppressed.
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.
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.
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"] }'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.
Deliverability
30-day rolling summary of sent / bounce / complaint / open / click / unsubscribe. Both a dashboard chart and GET /api/v1/deliverability?days=30.
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.comor@outlook.comthrough 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-Unsubscribeheader and a footer link.
What you should also do
- Send transactional and marketing from different subdomains —
mail.acme.comfor one,news.acme.comfor 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 usenoreply@for marketing.
Outlook M365 + Cloudflare setup
This is the exact flow if you're using paid Microsoft 365 Business and your DNS is on Cloudflare.
- 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 (
selector1andselector2). Add all three in Cloudflare (Cloudflare treats@as the root; leave proxy off — DNS-only). Wait for M365 to say Verified. - 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). - 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.
- 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
- Type =
- Verify. Back in Dispatch, click Verify on the domain card. Cloudflare propagation is usually seconds.
- Send a test. Curl
POST /api/v1/sendto 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.
Safe volume by use case
Honest numbers based on what actually keeps you out of trouble.
| Use case | Recommended provider | Safe daily volume | Why |
|---|---|---|---|
| Login confirmations, password resets, receipts | Outlook M365 | up to 500/day | Microsoft expects this from a business mailbox. Reputation stays clean because recipients opted in. |
| Newsletter to opt-in subscribers | Outlook M365 or Brevo Free | up to 500/day (Outlook) · 300/day (Brevo) | Both work; Brevo has more headroom for growth without paying Microsoft for extra SMTP quota. |
| Cold outreach / sequences | Brevo Free (never Outlook) | 100–200/day per sending domain | Microsoft'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 list | Brevo Free | up 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.
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
- 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.)
- 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).
- 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.
- 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.
- 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}}. - 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.
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"
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.
Errors
4xx and 5xx responses are shaped like:
{ "error": "validation_failed", "details": { ... } }400— validation, missing verified domain, empty body, warmup cap hit, no provider available401— missing / bad / revoked API key403— recipient on the suppression list404— resource not found429— rate limit (seeretry-after)502— upstream provider rejected the send