WarmHawk
Docs / Get started / Quickstart & installation

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.com

Pass 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.com

Ran 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

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.