Webhooks
Subscribe a URL you control to receive HTTP POST notifications for mailbox health
events — health score drops, auto-pauses, OAuth disconnects, DNS changes, and similar.
This page covers managing outbound webhook subscriptions; it's unrelated to
Warmerly's own inbound provider webhooks (Stripe, Unipile, Zernio), which aren't part of
the public API.
Webhook subscriptions are an Agency-plan feature. Listing, creating, and re-enabling
a subscription return 403 agency_plan_required on Starter and Growth workspaces.
Deleting a subscription you already hold is always allowed, even after a downgrade.
All endpoints below require X-Api-Key and are not project-scoped — a subscription
receives events for every account you own across every project.
Event types
Pass one or more of these in eventTypes when creating a subscription:
| Event type | Description |
|---|---|
score_drop | A mailbox's warmup health score dropped. |
score_floor | A mailbox's health score hit a critical floor. |
oauth_disconnected | An OAuth-connected mailbox (Gmail/Outlook) lost its connection. |
auth_fail | A mailbox failed to authenticate. |
dns_changed | A mailbox's DNS records (SPF/DKIM/DMARC) changed. |
smtp_fail | An SMTP send failed for a mailbox. |
imap_fail | An IMAP sync failed for a mailbox. |
flagged | A mailbox was flagged. |
auto_paused | A mailbox was automatically paused. |
auto_resumed | A mailbox was automatically resumed after a pause. |
manual_pause | A mailbox was manually paused from the dashboard. |
aged_in | A mailbox finished warmup ramp-up and is now fully aged in. |
marked_dead | A mailbox was marked dead. |
oauth_refresh_fail | An OAuth token refresh failed for a mailbox. |
oauth_connected | A mailbox was connected via OAuth. |
oauth_reconnected | A previously disconnected OAuth mailbox was reconnected. |
List subscriptions
GET /webhooks/subscriptions
Returns your active (non-revoked) subscriptions. The secret is masked to its last 4 characters — it's only shown in full at creation time.
curl https://app.warmerly.com/api/v1/webhooks/subscriptions \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{
"subscriptions": [
{
"id": "e5b3a7c2-8d4e-4f0a-9c2b-6a1d3e8f5c7b",
"name": "Mailbox alerts → Slack relay",
"targetUrl": "https://example.com/hooks/warmerly",
"secret": "••••7c9e",
"eventTypes": ["score_drop", "oauth_disconnected"],
"active": true,
"lastDispatchedAt": "2026-08-18T09:00:00.000Z",
"lastDeliveryStatus": "success",
"lastDeliveryError": null,
"consecutiveFailures": 0,
"createdAt": "2026-07-01T10:00:00.000Z"
}
]
}
Create a subscription
POST /webhooks/subscriptions
| Field | Type | Required | Description |
|---|---|---|---|
name | string (1–120 chars) | Yes | A label to identify the subscription. |
targetUrl | string (URL) | Yes | Must be a public https:// URL — localhost, private/link-local IP ranges, and non-https URLs are rejected. |
eventTypes | string[] | Yes | One or more values from Event types. |
Each user can hold at most 10 active subscriptions; creating an 11th returns 400 too_many_subscriptions.
curl -X POST https://app.warmerly.com/api/v1/webhooks/subscriptions \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Mailbox alerts",
"targetUrl": "https://example.com/hooks/warmerly",
"eventTypes": ["score_drop", "auto_paused", "oauth_disconnected"]
}'
201 Created:
{
"subscription": {
"id": "e5b3a7c2-8d4e-4f0a-9c2b-6a1d3e8f5c7b",
"name": "Mailbox alerts",
"targetUrl": "https://example.com/hooks/warmerly",
"eventTypes": ["score_drop", "auto_paused", "oauth_disconnected"]
},
"secret": "8f2a7c9e4b1d6f3a9c2e5b8d1a4f7c9e6b3d8a1f4c7e9b2d5a8f1c4e7b9d3a6f"
}
secret is only ever returned in full here. Store it — it's used to verify the
X-Warmerly-Signature header on every delivery (see Verifying deliveries
below). It cannot be retrieved again; delete the subscription and create a new one if
you lose it.
Errors: 400 bad_request (invalid_body for a malformed request, or unknown_event_types
with details: { unknown: [...] } if eventTypes contains a value not in the table
above), 403 agency_plan_required if your plan doesn't include webhooks.
Update a subscription
PATCH /webhooks/subscriptions/{id}
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | Yes | Enable or disable delivery. Re-enabling also resets consecutiveFailures to 0. |
Currently the only supported update is toggling active — there's no way to change
targetUrl or eventTypes in place; delete and recreate the subscription instead. This
endpoint is also how you manually re-enable a subscription the delivery sweep
auto-disabled (see Delivery & retries).
curl -X PATCH https://app.warmerly.com/api/v1/webhooks/subscriptions/e5b3a7c2-8d4e-4f0a-9c2b-6a1d3e8f5c7b \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "active": true }'
{
"subscription": {
"id": "e5b3a7c2-8d4e-4f0a-9c2b-6a1d3e8f5c7b",
"name": "Mailbox alerts",
"targetUrl": "https://example.com/hooks/warmerly",
"eventTypes": ["score_drop", "auto_paused", "oauth_disconnected"],
"active": true
}
}
404 not_found if the subscription doesn't exist or isn't yours.
Delete a subscription
DELETE /webhooks/subscriptions/{id}
Revokes (soft-deletes) the subscription; deliveries stop immediately. Not gated on plan tier — a downgraded workspace can still turn off deliveries it can no longer manage.
curl -X DELETE https://app.warmerly.com/api/v1/webhooks/subscriptions/e5b3a7c2-8d4e-4f0a-9c2b-6a1d3e8f5c7b \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{ "revoked": true }
404 not_found if the subscription doesn't exist, isn't yours, or was already revoked.
Delivery & retries
Deliveries are dispatched by a background sweep (not real-time) that polls for new
events roughly every few minutes. Each delivery is a POST to your targetUrl:
{
"id": "9d1e0b7a-5e7b-4f10-b2aa-1c8f0d3e6a4b",
"type": "score_drop",
"accountId": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
"occurredAt": "2026-08-18T09:00:00.000Z",
"data": { "severity": "warning", "title": "Health score dropped", "scoreBefore": 88, "scoreAfter": 61 }
}
data's shape varies by type — it mirrors the underlying alert/account event and
isn't separately typed per event today (documenting the exact data shape for every
event type is a known gap; see Known gaps).
- A delivery is retried up to 3 times with exponential backoff if your endpoint
doesn't respond
2xxwithin 5 seconds, or errors/times out. - A subscription is automatically disabled (
active: false) after 10 consecutive failed sweeps. Re-enable it withPATCH .../{id}. - Up to 50 events per subscription are delivered per sweep; if you have a burst larger than that, the remainder is delivered on the next sweep.
- Redirects are not followed automatically (
redirect: "manual") — respond2xxdirectly fromtargetUrl.
Verifying deliveries
Every delivery carries an X-Warmerly-Signature header: the hex-encoded HMAC-SHA256 of
the raw request body, signed with your subscription's secret.
X-Warmerly-Signature: 3f9a7c2e... (hex-encoded HMAC-SHA256 of the raw body)
Verify it by recomputing the HMAC over the exact bytes of the received body and comparing:
const crypto = require("crypto");
const expected = crypto.createHmac("sha256", subscriptionSecret).update(rawBody).digest("hex");
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSignature));
Known gaps in this page
- The exact
datapayload shape is not separately documented per event type — it passes through the underlying alert/account event record as-is, and cataloguing all 16 shapes precisely was out of scope for this pass. - There's no endpoint to list recent delivery attempts/history beyond the single
lastDispatchedAt/lastDeliveryStatus/lastDeliveryErrorsnapshot on the subscription itself.
