Three ways in, one open field for everything else.
Every lead-ingest path — single create, CSV import, webhook — accepts the same email/firstName/lastName/company shape plus an open customFields object for anything else your personalization needs.
WarmHawk has three lead-ingest paths sharing one validation function: POST /v1/leads for a single authenticated create, POST /v1/leads/import for a CSV bulk upload, and the unauthenticated POST /v1/leads/webhook for live pipelines. All three carry an open customFields object, and DELETE /v1/leads/erase handles GDPR right-to-erasure by anonymizing PII while preserving aggregate counts.
Single create
The direct, authenticated entry point — reports suppression/duplicate conflicts as real errors the caller can act on:
API details (Tier 0 / self-hosters) · POST /v1/leads▸
curl -X POST https://app.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" }
}'201 with the created lead on success; 409 if the email is on the suppression list or already a lead on that campaign. List leads with GET /v1/leads?campaignId=camp_g7h8i9, which returns { leads, total }.
CSV bulk import
A real file upload — multipart/form-data, not a JSON body. Send a campaignId field alongside the CSV file field. Any column that isn’t email/firstName/lastName/company (case-insensitive) folds automatically into customFields:
API details (Tier 0 / self-hosters) · POST /v1/leads/import — multipart, not JSON▸
curl -X POST https://app.yourcompany.com/v1/leads/import \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "campaignId=camp_g7h8i9" \
-F "file=@leads.csv"API details (Tier 0 / self-hosters) · leads.csv▸
email,firstName,lastName,company,jobTitle,recentNews
jane@prospect-co.com,Jane,Doe,Prospect Co,VP Marketing,closed a $12M Series AAPI details (Tier 0 / self-hosters) · Response▸
{ "imported": 1, "skippedDuplicate": 0, "skippedSuppressed": 0, "rejected": [] }Capped at 50,000 rows and 10MB per file (413 past either). Every cell is CSV-injection-defended on ingest — a value starting with =, +, -, or @ is neutralized before it’s stored, so a malicious lead field can’t turn into a spreadsheet formula when you later export and open the data.
Webhook ingest — unauthenticated by design
Built for a live pipeline pushing rows continuously (Clay, Apollo, a form backend). No bearer token — rate-limited (30/min) instead, and it skips rather than errors on a suppressed or duplicate address, since an automated caller has no way to act on a 409:
API details (Tier 0 / self-hosters) · POST /v1/leads/webhook — no Authorization header▸
curl -X POST https://app.yourcompany.com/v1/leads/webhook \
-H "Content-Type: application/json" \
-d '{
"campaignId": "camp_g7h8i9",
"email": "jane@prospect-co.com",
"firstName": "Jane",
"company": "Prospect Co",
"customFields": { "jobTitle": "VP Marketing" }
}'Returns 201 { status: "queued", leadId } on success, 200 { status: "skipped", reason: "duplicate" | "suppressed" } on a skip, or 422 { status: "rejected", reason } if required fields are missing.
GDPR erasure
API details (Tier 0 / self-hosters) · DELETE /v1/leads/erase — anonymizes, doesn't hard-delete▸
curl -X DELETE https://app.yourcompany.com/v1/leads/erase \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "jane@prospect-co.com" }'Every matching row (across every campaign) has its email/name/company/customFields overwritten with a non-reversible placeholder and piiErasedAt stamped — the row itself is kept so send/reply/campaign aggregate counts referencing it stay accurate. Returns { email, leadsErased }, vacuously succeeding with leadsErased: 0 if nothing matches. This is distinct from DELETE /v1/leads/:id, a real hard delete of one row by id, and from a manual suppress (POST /v1/leads/:id/suppress) — erasure removes data, suppression stops future sends; a lead who wants both needs both calls.
Recipe: Clay / Apollo enrichment
Enrich a list in Clay or Apollo first — job title, company size, recent funding news, tech stack, whatever signal makes an email feel specific rather than templated. There’s nothing WarmHawk-specific about that step. Then map each enriched column straight onto a customFields key using whichever ingest path fits: CSV import for a one-time export, or the webhook above for Clay’s own live enrichment table pushing rows as they finish.
Reference those fields directly in the campaign’s aiPromptTemplate (not the plain template field, which has no per-lead substitution) with a flat {{fieldName}} placeholder — a customField named recentNews is {{recentNews}}, never {{customFields.recentNews}}:
API details (Tier 0 / self-hosters) · aiPromptTemplate referencing enriched customFields▸
Write a 2-sentence opener to {{firstName}} at {{company}}, referencing that they
{{recentNews}}. Mention their stack ({{techStack}}) only if it's naturally relevant.
No generic filler, no invented facts.WarmHawk fills whichever placeholders it has data for, then hands the whole lead context to your connected Gemini or Claude key (BYOK — you hold the key, never routed through a shared gateway) to write the finished body. See Campaigns, AI & content quality for the full personalization mechanics, including what happens when AI is unavailable.
Questions
Leads & enrichment: questions worth answering up front
Is CSV import a JSON body or a file upload?+
A real multipart/form-data file upload — POST /v1/leads/import takes a campaignId field and a file field (the CSV itself), not a JSON array of lead objects. It is capped at 50,000 rows and 10MB per file.
Does the webhook ingest endpoint require an API key?+
No — POST /v1/leads/webhook is deliberately unauthenticated, built for a third-party tool's outbound webhook that can't hold a bearer token. It rate-limits instead (30 requests/minute) and skips duplicates/suppressed addresses silently rather than erroring, since an automated caller can't act on a 409 anyway.
What actually happens when I erase a lead’s data under GDPR?+
DELETE /v1/leads/erase anonymizes every matching row’s PII (email, name, company, customFields) across every campaign and stamps piiErasedAt — the row itself stays, so campaign/send/reply aggregate counts stay accurate. It deliberately does not add the address to the suppression list; erasure and suppression are two distinct rights.
Does WarmHawk have its own built-in enrichment?+
No, and that's intentional. Enrichment is a deep, fast-moving space Clay and Apollo already do well — WarmHawk's job is to accept whatever enrichment output you already have via an open customFields object, not rebuild a worse version of Clay.