Leads & campaigns.
Every lead-ingest route and the full campaign lifecycle, field by field. All routes below require Authorization: Bearer $WARMHAWK_KEY except POST /v1/leads/webhook, which is deliberately public.
Three lead-ingest paths (single create, CSV import, unauthenticated webhook) share one validation function and an open customFields object; DELETE /v1/leads/erase handles GDPR erasure. Campaigns are created with a template (spintax-capable fallback body) and an aiPromptTemplate (mustache-placeholder AI instructions). Each campaign picks the mailboxes it sends from (mailboxIds) and can carry up to three follow-ups (steps). unsubscribeUrlTemplate is optional: without it every email links to the built-in unsubscribe page. Launch is gated on picked senders, a mailing address on every sending domain, a working unsubscribe link, non-empty copy and the bounce circuit breaker.
Leads
| Route | What it does |
|---|---|
| GET /v1/leads?campaignId= | List leads, optionally scoped to a campaign. Returns { leads, total }. |
| POST /v1/leads | Single-lead create. Body: campaignId, email, firstName?, lastName?, company?, customFields?. 201, or 409 on suppressed/duplicate. |
| POST /v1/leads/webhook | Unauthenticated ingest webhook — no bearer token. Rate-limited (30/min). Skips (200) rather than errors on suppressed/duplicate. |
| POST /v1/leads/import | Bulk CSV import — multipart/form-data, fields: campaignId, file. Caps: 50,000 rows / 10MB. |
| POST /v1/leads/:id/suppress | Manually suppress a lead (adds to the suppression list, flips status). |
| DELETE /v1/leads/erase | GDPR erasure by email. Body: { email }. Anonymizes PII across every campaign, preserves aggregate counts. |
| DELETE /v1/leads/:id | Hard delete one lead row by id. 204, or 404 if not found. |
API details (Tier 0 / self-hosters) · POST /v1/leads/import — multipart, not JSON▸
curl -X POST https://your-instance/v1/leads/import \
-H "Authorization: Bearer $WARMHAWK_KEY" \
-F "campaignId=camp_g7h8i9" \
-F "file=@leads.csv"Full walkthrough with request/response bodies for every lead route: Leads & enrichment.
Campaigns
| Route | What it does |
|---|---|
| GET /v1/campaigns | List campaigns, including mailboxIds, steps, senders (each ticked mailbox and whether its domain has an address), launch { canLaunch, problems }, progress and leadsCount/sentCount/repliesCount/domainsCount/lastActivityAt. |
| GET /v1/campaigns/:id | Fetch one campaign, with mailboxIds and steps. 404 if not found. |
| GET /v1/campaigns/:id/launch-check | The launch check without launching. Returns { canLaunch, problems, warnings, passed }. |
| POST /v1/campaigns | Create. Body: name, aiPromptTemplate, template?, subject?, aiProvider?, unsubscribeUrlTemplate?, mailboxIds?, steps?. Returns the row plus contentQuality. |
| PATCH /v1/campaigns/:id | Update content fields, bounceRateThreshold, pausedForBounceRate, mailboxIds or steps. mailboxIds and steps each replace the whole list. status is refused (422): use launch or pause. Recomputes contentQuality when template or subject changes. |
| POST /v1/campaigns/:id/launch | Runs the launch check and sets status: ACTIVE. Otherwise 422 with every problem at once: NO_SENDERS, DOMAIN_NO_ADDRESS (one per domain), NO_UNSUBSCRIBE, BOUNCE_PAUSED, EMAIL_EMPTY, STEP_EMPTY. Warnings never block. |
| POST /v1/campaigns/:id/pause | Sets status: PAUSED. |
| DELETE /v1/campaigns/:id | Archives (status: ARCHIVED) — does not hard-delete. |
API details (Tier 0 / self-hosters) · POST /v1/campaigns — request body▸
{
"name": "Q3 outbound — agencies",
"template": "Hi there — {quick question|one question} for you today?",
"aiPromptTemplate": "Write a 2-sentence opener to {{firstName}} at {{company}}.",
"aiProvider": "GEMINI",
"unsubscribeUrlTemplate": "https://yourcompany.com/unsubscribe?email={{email}}",
"mailboxIds": ["mbx_a1b2c3", "mbx_d4e5f6"],
"steps": [
{ "waitDays": 3, "body": "Just floating this back up — worth a look?", "aiRewrite": false },
{ "waitDays": 4, "body": "Last note from me on this one.", "aiRewrite": false }
]
}API details (Tier 0 / self-hosters) · POST /v1/campaigns/:id/launch — 422 when it can't launch yet▸
{
"error": "acme.com has no mailing address; Follow-up 2 needs backup text",
"problems": [
{ "code": "DOMAIN_NO_ADDRESS", "message": "acme.com has no mailing address",
"domainId": "dom_a1b2c3", "domainName": "acme.com", "mailboxCount": 2 },
{ "code": "STEP_EMPTY", "message": "Follow-up 2 needs backup text", "position": 2 }
],
"warnings": []
}Full field reference, spintax syntax, and the AI personalization mechanics: Campaigns, AI & content quality.
Back to Auth & mailboxes, or continue to Queue, domains & webhooks.