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 typeDescription
score_dropA mailbox's warmup health score dropped.
score_floorA mailbox's health score hit a critical floor.
oauth_disconnectedAn OAuth-connected mailbox (Gmail/Outlook) lost its connection.
auth_failA mailbox failed to authenticate.
dns_changedA mailbox's DNS records (SPF/DKIM/DMARC) changed.
smtp_failAn SMTP send failed for a mailbox.
imap_failAn IMAP sync failed for a mailbox.
flaggedA mailbox was flagged.
auto_pausedA mailbox was automatically paused.
auto_resumedA mailbox was automatically resumed after a pause.
manual_pauseA mailbox was manually paused from the dashboard.
aged_inA mailbox finished warmup ramp-up and is now fully aged in.
marked_deadA mailbox was marked dead.
oauth_refresh_failAn OAuth token refresh failed for a mailbox.
oauth_connectedA mailbox was connected via OAuth.
oauth_reconnectedA 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
FieldTypeRequiredDescription
namestring (1–120 chars)YesA label to identify the subscription.
targetUrlstring (URL)YesMust be a public https:// URL — localhost, private/link-local IP ranges, and non-https URLs are rejected.
eventTypesstring[]YesOne 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}
FieldTypeRequiredDescription
activebooleanYesEnable 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 2xx within 5 seconds, or errors/times out.
  • A subscription is automatically disabled (active: false) after 10 consecutive failed sweeps. Re-enable it with PATCH .../{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") — respond 2xx directly from targetUrl.

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 data payload 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/lastDeliveryError snapshot on the subscription itself.