Install it, then send a real email in six calls.
The open-core package (warmhawk-core-engine) is a complete API with no web UI attached — that’s the point of Tier 0. Run install.sh once, then everything below runs against your own instance with nothing but curl.
This page gets you from a fresh WarmHawk install to a real sent email: install the stack, authenticate, register a sending domain, connect a mailbox, create a campaign, import a lead, launch, and check the queue. It’s the fastest way to confirm your instance works end-to-end before building anything on top of the API.
0. Install the stack
On a fresh server with a domain already pointed at it, run the installer. It preflight-checks Docker/Compose, ports 80/443, and DNS before touching anything, and it’s safe to re-run at any point:
API details (Tier 0 / self-hosters) · Install WarmHawk — Tier 0 (free, API-only)▸
curl -fsSL https://warmhawk.com/install | bash -s -- --domain yourcompany.comPass your bare company domain, not a subdomain — the installer derives api.yourcompany.com for the engine (and dashboard.yourcompany.com for the operator dashboard, if you install it). Both need to resolve to this server before you run it.
Bought Tier 1 or Tier 2? Use the licensed form instead — same command, plus the token from your purchase email. It installs the engine and the operator dashboard in one pass:
API details (Tier 0 / self-hosters) · Install WarmHawk — Tier 1/2 (adds the operator dashboard)▸
curl -fsSL https://warmhawk.com/install | bash -s -- \
--license <token-from-your-purchase-email> \
--domain yourcompany.com \
--owner-email you@yourcompany.comRan into a failure? See install.sh troubleshooting for the three most common causes (missing Docker, a bound port, DNS not propagated yet) before continuing below. Every call from here on uses YOUR_API_KEY and api.yourcompany.com as placeholders for the token you get from step 1 and the API host the installer created.
1. Authenticate
Every endpoint below except this one requires a bearer token. Log in against the engine’s own user table (the account install.sh printed at the end of setup) to get one:
API details (Tier 0 / self-hosters) · POST /v1/auth/login▸
curl -X POST https://api.yourcompany.com/v1/auth/login \
-H "Content-Type: application/json" \
-d '{ "email": "you@yourcompany.com", "password": "YOUR_PASSWORD" }'API details (Tier 0 / self-hosters) · Response▸
{ "token": "YOUR_API_KEY" }2. Add a sending domain
A mailbox belongs to a domain, so create the domain first:
API details (Tier 0 / self-hosters) · POST /v1/domains▸
curl -X POST https://api.yourcompany.com/v1/domains \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "domainName": "yourcompany.com" }'Returns a Domain row with an id — you’ll pass that as domainId in the next step.
3. Connect a mailbox
A mailbox is one sending identity WarmHawk will send from. For a plain SMTP/IMAP inbox, register it directly:
API details (Tier 0 / self-hosters) · POST /v1/mailboxes▸
curl -X POST https://api.yourcompany.com/v1/mailboxes \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "you@yourcompany.com",
"domainId": "dom_a1b2c3",
"smtpHost": "smtp.yourdomain.com",
"smtpPort": 587,
"imapHost": "imap.yourdomain.com",
"imapPort": 993,
"authUsername": "you@yourcompany.com",
"authPassword": "YOUR_SMTP_PASSWORD",
"dailyCap": 25
}'Google Workspace or Microsoft 365 instead? Create the mailbox first (no authPassword needed), then send the browser to GET /v1/oauth/google/authorize?mailboxId=<id> (or microsoft) to run the OAuth consent flow — see Connecting mailboxes for the full walkthrough. Either way, the response returns a real Mailbox row (never the credential itself) with the id you’ll reference below.
4. Create a campaign
A campaign has two content fields that do different jobs: template is the plain-text body sent as-is (or used as a fallback if AI personalization is off or fails) — it supports native spintax {option one|option two} variation. aiPromptTemplate is the instruction handed to your BYOK Gemini/Claude key, which then writes the actual send — it supports flat {{fieldName}} placeholders resolved from the lead’s firstName/lastName/company and any customFields key (flat, so a custom field named recentNews is {{recentNews}}, never {{customFields.recentNews}}). Every email has to carry an unsubscribe link (CAN-SPAM). Leave unsubscribeUrlTemplate out and each email links to the unsubscribe page WarmHawk serves on your install’s domain; set it, as below, to send people to a page of your own:
API details (Tier 0 / self-hosters) · POST /v1/campaigns▸
curl -X POST https://api.yourcompany.com/v1/campaigns \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Quickstart test send",
"template": "Hi there — {quick question|one question} for you today, if you have a minute?",
"aiPromptTemplate": "Write a 2-sentence, specific opener to {{firstName}} at {{company}}, referencing that they {{recentNews}}. No generic filler.",
"unsubscribeUrlTemplate": "https://yourcompany.com/unsubscribe?email={{email}}"
}'The response echoes the created campaign plus a contentQuality block (spam-word score, spintax group count) computed on every save — a 422 here means the template’s spintax groups are unbalanced, not that the content was rejected for tone. See Campaigns, AI & content quality for the full field reference.
5. Import a lead
Leads carry whatever fields your personalization needs via an open customFields object:
API details (Tier 0 / self-hosters) · POST /v1/leads▸
curl -X POST https://api.yourcompany.com/v1/leads \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaignId": "camp_g7h8i9",
"email": "jane@prospect-co.com",
"firstName": "Jane",
"lastName": "Doe",
"company": "Prospect Co",
"customFields": {
"jobTitle": "VP Marketing",
"recentNews": "closed a $12M Series A"
}
}'Importing a real list instead of one test lead? See Leads & enrichment for CSV bulk import, webhook ingest, and the Clay/Apollo enrichment recipe.
6. Launch, then check the queue
API details (Tier 0 / self-hosters) · POST /v1/campaigns/:id/launch▸
curl -X POST https://api.yourcompany.com/v1/campaigns/camp_g7h8i9/launch \
-H "Authorization: Bearer YOUR_API_KEY"Launching enqueues the send — it doesn’t fire instantly. WarmHawk’s queue is jittered and capacity-aware on purpose (an 8-minute cadence floor plus jitter), even for a single test send, so the timing you see here matches production behavior. Poll the queue to confirm it moved:
API details (Tier 0 / self-hosters) · GET /v1/queue/status▸
curl https://api.yourcompany.com/v1/queue/status \
-H "Authorization: Bearer YOUR_API_KEY"API details (Tier 0 / self-hosters) · Example response▸
{
"counts": { "waiting": 0, "active": 0, "delayed": 0, "completed": 1, "failed": 0 },
"isPaused": false,
"jobs": [],
"throttling": { "cadenceFloorSeconds": 480, "jitterSeconds": 240 }
}A job that moves from waiting/delayed to completed means the whole path worked: mailbox credentials, queue, and outbound delivery through your own nginx. See Sending safely & domain health for what the throttling numbers mean and how the bounce circuit breaker protects a domain.
If something doesn’t come up clean
- install.sh troubleshooting — Docker/Compose missing, ports already bound, DNS not propagated yet.
- warmhawk update failures — what to do if updating an existing instance doesn’t go clean.
Every endpoint above matches the real, current /v1 API shape — see the API reference for the complete, field-by-field documentation of every route.
Engine running? A star on GitHub helps other self-hosters find it.