# Webhooks

> API reference for Warmerly webhook subscriptions: get HTTP-signed notifications for mailbox health events like score drops, auto-pauses, and OAuth disconnects.

Source: https://warmerly.com/docs/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

```text
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.

```bash
curl https://app.warmerly.com/api/v1/webhooks/subscriptions \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{
  "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

```text
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](https://warmerly.com/docs/webhooks#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`.

```bash
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`:

```json
{
  "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](https://warmerly.com/docs/webhooks#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

```text
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](https://warmerly.com/docs/webhooks#delivery--retries)).

```bash
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 }'
```

```json
{
  "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

```text
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.

```bash
curl -X DELETE https://app.warmerly.com/api/v1/webhooks/subscriptions/e5b3a7c2-8d4e-4f0a-9c2b-6a1d3e8f5c7b \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{ "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:

```json
{
  "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](https://warmerly.com/docs/webhooks#known-gaps-in-this-page)).

- 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 `2xx` within **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 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 never followed; a `3xx` response counts as a failure, so respond `2xx` directly from `targetUrl`.
- `lastDeliveryStatus` is `success` or `failed`. On failure `lastDeliveryError` is one of `http_<status>`, `timeout`, `connection_failed`, `unsafe_target_url` or `dns_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`.

```text
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:

```js
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 documented for the 11 account event types (`smtp_fail` through `oauth_reconnected`); it is the stored event payload as-is.
- There's no endpoint to list recent delivery attempts/history beyond the single `lastDispatchedAt`/`lastDeliveryStatus`/`lastDeliveryError` snapshot on the subscription itself.

## Each part of this reference has its own page (7)

- [Event types](https://warmerly.com/docs/webhooks/event-types)
- [List subscriptions](https://warmerly.com/docs/webhooks/list-subscriptions)
- [Create a subscription](https://warmerly.com/docs/webhooks/create-a-subscription)
- [Update a subscription](https://warmerly.com/docs/webhooks/update-a-subscription)
- [Delete a subscription](https://warmerly.com/docs/webhooks/delete-a-subscription)
- [Delivery & retries](https://warmerly.com/docs/webhooks/delivery-and-retries)
- [Known gaps in this page](https://warmerly.com/docs/webhooks/known-gaps-in-this-page)

## More in API reference

- [Suppression](https://warmerly.com/docs/suppression): API reference for the Warmerly suppression list: add addresses that must never be emailed in any campaign,…
- [Workspaces & Projects](https://warmerly.com/docs/workspaces): API reference for Warmerly workspaces and projects: the tenant/billing hierarchy that mailboxes, campaigns,…
- [API Keys](https://warmerly.com/docs/keys): API reference for managing Warmerly API keys programmatically: create, list, and revoke keys used to…
