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), which aren't part of the public API.
Webhook subscriptions are an Agency-plan feature. Listing, creating, and updating a subscription return 403 agency_plan_required on any workspace that is not on the Agency plan. The delete route itself has no plan check, but an API key is only accepted while the workspace the request acts on is on the Agency plan, so a key stops working for every endpoint after a downgrade. Deliveries also pause while the owner is not on the Agency plan, without counting as failures.
All endpoints below require X-Api-Key and are not project-scoped, a subscription receives events for every mailbox in the workspaces you own, across every project (workspaces that have been deleted are skipped).
#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 | A sign-in connected mailbox (Microsoft 365, Outlook, or Gmail connected before 2026-09-15) 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/subscriptionsReturns 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. Non-https URLs, URLs with a username or password, localhost, .local and .internal names, and private, link-local or reserved IP addresses are rejected. The hostname is checked again at delivery time. |
eventTypes | string[] | Yes | One or more values from Event types, at most 16 entries. |
Each user can hold at most 10 subscriptions that have not been deleted (disabled ones count); 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, as 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. Any update also resets consecutiveFailures to 0 and clears lastDeliveryError. |
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
}
}400 bad_request (invalid_body) if active is missing or not a boolean, 403 agency_plan_required on a non-Agency workspace, and 404 not_found if the subscription doesn't exist, isn't yours, or was deleted.
#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 every 5 minutes. Each delivery is a POST to your targetUrl, with content-type: application/json and the X-Warmerly-Signature header:
{
"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", "body": "...", "scoreBefore": 88, "scoreAfter": 61 }
}id is the ID of the underlying event and occurredAt is when it was recorded. data's shape varies by type. For score_drop, score_floor, oauth_disconnected, auth_fail and dns_changed it is { severity, title, body, scoreBefore, scoreAfter } (severity is info, warning or critical; the score fields can be null). For the other event types it is the stored event payload as-is, which is not separately documented per type (see Known gaps).
- Each delivery is tried up to 3 times (0.5 s, then 1 s, between attempts). An attempt fails if your endpoint doesn't respond
2xxwithin 5 seconds, or errors. - Events are delivered in order. An event that fails all 3 attempts is not redelivered in a later sweep: the subscription moves on past it.
- A subscription is automatically disabled (
active: false) after 10 consecutive sweeps in which a delivery failed. 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 never followed; a
3xxresponse counts as a failure, so respond2xxdirectly fromtargetUrl. lastDeliveryStatusissuccessorfailed. On failurelastDeliveryErroris one ofhttp_<status>,timeout,connection_failed,unsafe_target_urlordns_resolution_failed.
#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 documented for the 11 account event types (smtp_failthroughoauth_reconnected); it is the stored event payload as-is. - There's no endpoint to list recent delivery attempts/history beyond the single
lastDispatchedAt/lastDeliveryStatus/lastDeliveryErrorsnapshot on the subscription itself.