Guardrails that protect a domain before it’s trashed, not after.
Three layers work together: a throttled, jittered send queue; a bounce circuit breaker that auto-pauses a bad list; and domain health checks — SPF/DKIM/DMARC, blocklist status, and seed-inbox placement sampling — you can pull on demand.
WarmHawk protects domain reputation structurally: the send queue enforces an 8-minute cadence floor plus jitter per mailbox, a bounce/complaint circuit breaker auto-pauses a campaign once its rolling bounce rate crosses 5% on at least 20 sends, and GET /v1/domains/:id/placement-sample reports real seed-inbox placement results — never a self-reported score.
Queue: status and pause
API details (Tier 0 / self-hosters) · GET /v1/queue/status▸
curl https://app.yourcompany.com/v1/queue/status -H "Authorization: Bearer YOUR_API_KEY"API details (Tier 0 / self-hosters) · Response▸
{
"counts": { "waiting": 3, "active": 1, "delayed": 12, "completed": 240, "failed": 2 },
"isPaused": false,
"jobs": [
{
"id": "1042",
"state": "delayed",
"mailboxEmail": "you@yourcompany.com",
"leadEmail": "jane@prospect-co.com",
"campaignName": "Q3 outbound — agencies",
"scheduledFor": "2026-08-23T18:04:00.000Z",
"attemptsMade": 0
}
],
"throttling": { "cadenceFloorSeconds": 480, "jitterSeconds": 240 }
}cadenceFloorSeconds: 480 is the real, structural 8-minute minimum between two sends from the same mailbox; jitterSeconds is the representative width of the randomized delay layered on top so sends don’t land on a suspiciously exact clock tick. jobs lists up to 50 waiting/active/delayed jobs; the queue is BullMQ-backed and real — pausing it is a genuine Redis-level pause, not a cosmetic flag:
API details (Tier 0 / self-hosters) · POST /v1/queue/pause▸
curl -X POST https://app.yourcompany.com/v1/queue/pause \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "paused": true }'The bounce circuit breaker
Evaluated from real ExecutionLog data, not a separate monitoring system: once a campaign/mailbox pair has at least 20 sends and its bounce rate exceeds 5% (both configurable via a campaign’s bounceRateThreshold), it flips pausedForBounceRate: true. From that point, POST /v1/campaigns/:id/launch refuses with 422 until a human clears the flag via PATCH /v1/campaigns/:id { "pausedForBounceRate": false } — deliberately not an automatic reset, since the underlying list-quality problem needs addressing first, not just waiting out.
Domain checks: SPF/DKIM/DMARC + blocklists
API details (Tier 0 / self-hosters) · POST /v1/domains/:domain/check — keyed by domain NAME▸
curl -X POST https://app.yourcompany.com/v1/domains/yourcompany.com/check \
-H "Authorization: Bearer YOUR_API_KEY"Runs live DNS-TXT resolution for SPF/DKIM/DMARC plus a Spamhaus DBL domain-blocklist lookup in one round trip and persists the result on the Domain row. The same full check also runs on its own every hour for every domain, so a DKIM key that disappears turns the badge red without anyone pressing Re-check. Blocklist lookups go straight to Spamhaus’s own nameservers, since Spamhaus refuses public resolvers like 1.1.1.1 and 8.8.8.8.
DKIM selectors. DNS has no way to list a domain’s DKIM keys, so WarmHawk tries the default selector of every common provider (Google Workspace, Microsoft 365, Cloudflare, SendGrid, Mailgun, Zoho, Fastmail, Proton, Brevo, Resend, HubSpot and more). If none is found the badge reads Pending, not Fail. Providers that mint a selector per customer — Amazon SES, Postmark, ZeptoMail — need it saved on the domain: set dkimSelector (the part before ._domainkey) in the dashboard or via PATCH /v1/domains/:id. Once saved, a missing or revoked key there is a Fail. A one-off ?selector= on the check route overrides it for that call.
Mailing address per domain
CAN-SPAM needs a postal address in every campaign email’s footer. WarmHawk keeps it on the sending domain, so an agency’s client brands each print their own and never share one; there is no install-wide fallback. Adding a domain never needs an address. A campaign can’t launch while any domain it sends from has none, and a mailbox whose domain loses its address stops sending until one is added back. Warm-up emails carry no footer and need no address.
API details (Tier 0 / self-hosters) · PATCH /v1/domains/:id — set the address▸
curl -X PATCH https://app.yourcompany.com/v1/domains/dom_a1b2c3 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Acme Logistics",
"mailingAddressParts": {
"businessName": "Acme Logistics LLC",
"street": "100 Main St", "suite": "Suite 200",
"city": "Denver", "region": "CO", "postalCode": "80202", "country": "USA"
}
}'mailingAddressParts needs at least street, city and country, and WarmHawk builds the printed mailingAddress from it. You can send mailingAddress as one block of text instead (up to 500 characters). Clearing an address that campaigns send with returns 409 ADDRESS_IN_USE with the campaigns listed; send confirm: true to clear it anyway. PUT /v1/instance-settings, the old install-wide address, now answers 410 Gone.
Seed-inbox placement sampling
A small, fixed set of email accounts you own are BCC’d on real campaign sends; WarmHawk reports which folder each one landed in — inbox, spam, or promotions. Manage the seed inboxes via GET/POST/PATCH/DELETE /v1/seed-accounts, then pull the rollup per domain:
API details (Tier 0 / self-hosters) · GET /v1/domains/:id/placement-sample — id-keyed▸
curl https://app.yourcompany.com/v1/domains/dom_a1b2c3/placement-sample \
-H "Authorization: Bearer YOUR_API_KEY"API details (Tier 0 / self-hosters) · Response▸
{
"domainId": "dom_a1b2c3",
"domainName": "yourcompany.com",
"label": "Placement sampling across 4 seed inboxes — not full inbox-placement testing.",
"sampledSeedAccountCount": 4,
"totalChecks": 18,
"byFolder": { "INBOX": 12, "SPAM": 3, "PROMOTIONS": 3, "UNCLASSIFIED": 0 },
"inboxPlacementRate": 0.667,
"mostRecentCheckAt": "2026-08-22T09:14:00.000Z",
"results": []
}The response is deliberately, explicitly labeled “placement sampling,” never “inbox-placement testing” — a small owned sample is directional evidence from real sends, not a statistically exhaustive placement panel, and WarmHawk doesn’t market it as one.
Every structural guardrail on this page — the cadence floor, the circuit breaker, CAN-SPAM enforcement — is covered end to end, alongside CSV-injection defense and GDPR erasure, in Guardrails & compliance.
Questions
Sending safely & domain health: questions worth answering up front
What is the 8-minute cadence floor?+
The minimum spacing the send queue enforces between two sends from the same mailbox, before jitter is layered on top — a structural floor, not a suggestion, so a burst of newly-imported leads can never blast a mailbox at machine speed.
What triggers the bounce circuit breaker, and what does it do?+
A rolling bounce rate above 5% on a campaign/mailbox pair, but only once at least 20 sends have happened (so one bounce out of two sends never trips it). Once tripped, the campaign is flagged pausedForBounceRate and further launches are refused with a 422 until it is explicitly resumed.
Is placement sampling the same thing as a warmup "heat score"?+
No, and WarmHawk is deliberate about the distinction. Placement sampling BCCs real campaign sends to a small, fixed set of seed inboxes you own and reports which folder each landed in — a real, first-party signal from an actual send, explicitly labeled as sampling across N seed inboxes, never marketed as exhaustive inbox-placement testing.
Is a domain health check keyed by domain id or domain name?+
POST /v1/domains/:domain/check is keyed by the domain NAME (e.g. yourcompany.com), unlike this API's other domain routes which use the internal id — it's the shape a customer actually thinks in when re-checking a domain.