# Warmup

> API reference for Warmerly warmup: pull mailbox health scores and the inbox-placement emails sent between accounts, to build dashboards or alert on score drops.

Source: https://warmerly.com/docs/warmup

Warmup endpoints report on the health of your mailboxes and the automated inbox-placement emails Warmerly sends and receives between accounts. Use them to build dashboards, alert on score drops, or list the individual warmup emails sent for an account.

## Get warmup stats

```text
GET /warmup/stats
```

Returns a point-in-time summary across your accounts: how many are active, aging, or paused, how many warmup emails have gone out today, and the average health score.

`X-Project-Id` is optional here, omit it to get stats across every project in your active workspace (the one named in `X-Workspace-Id` if you belong to it, otherwise your own workspace), or pass it to scope the numbers to a single project in that workspace.

```bash
curl https://app.warmerly.com/api/v1/warmup/stats \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a"
```

```json
{
  "activeAccounts": 12,
  "agingAccounts": 3,
  "pausedAccounts": 1,
  "todaySent": 84,
  "todayScheduled": 12,
  "todayPlanned": 96,
  "todayFailed": 0,
  "todayTarget": 110,
  "avgHealthScore": 87
}
```

| Field | Type | Description |
| --- | --- | --- |
| `activeAccounts` | integer | Accounts with `status = active` (past the ramp period). |
| `agingAccounts` | integer | Accounts still ramping up (`status = aging`). |
| `pausedAccounts` | integer | Accounts with `status = paused`. |
| `todaySent` | integer | Warmup emails sent today, summed from each mailbox's own day (its local timezone). |
| `todayScheduled` | integer | Warmup emails still pending for today. |
| `todayPlanned` | integer | Everything on today's schedule, in any status. |
| `todayFailed` | integer | Warmup emails that failed today. |
| `todayTarget` | integer | The sum of each mailbox's target for today. |
| `avgHealthScore` | integer \\| null | Average `last_health_score` across accounts that have one. |

## Get warmup trends

```text
GET /warmup/trends
```

Returns a daily time series plus a current-vs-previous-window summary, suitable for charting score and inbox-placement rate over time. Backed by daily `health_snapshots` rows, aggregated across the accounts you have access to.

`X-Project-Id` is optional, same as `/warmup/stats`, pass it to scope to one project. Without it the numbers cover your active workspace.

| Query param | Type | Default | Description |
| --- | --- | --- | --- |
| `days` | `7` \\| `30` \\| `90` | `30` | Length of the window. Any other value falls back to `30`. |

```bash
curl "https://app.warmerly.com/api/v1/warmup/trends?days=30" \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a"
```

```json
{
  "range": { "days": 30, "from": "2026-06-03", "to": "2026-07-02" },
  "series": [
    { "date": "2026-06-03", "sent": 41, "inbox": 37, "spam": 3, "bounce": 1, "score": 82 },
    { "date": "2026-06-04", "sent": 45, "inbox": 42, "spam": 2, "bounce": 1, "score": 84 }
  ],
  "summary": {
    "avgScore": 87,
    "avgScorePrev": 81,
    "avgScoreDelta": 6,
    "inboxRate": 92,
    "inboxRatePrev": 88,
    "inboxRateDelta": 4,
    "sentTotal": 1240,
    "sentTotalPrev": 1105
  }
}
```

`series` is ordered ascending by `date` (UTC days) and covers the requested window only, ending today. `summary` compares the requested window against the equal-length window immediately before it (the `...Prev` fields); any rate is `null` when there's no data to divide by. `inboxRate` is `inbox / (inbox + spam + bounce)`, expressed as a whole-number percentage.

## List warmup jobs

```text
GET /warmup/jobs
```

Returns individual warmup emails (a "job" is one email sent from one warmed mailbox to another), most recent first. This endpoint doesn't take `X-Project-Id`. It only returns jobs where one of your own mailboxes in your active workspace (`X-Workspace-Id`) is the sender or the recipient; filter by `account_id` to scope results to a specific mailbox. An `account_id` that is not one of yours returns `404 not_found`, and an invalid query returns `400 bad_request` (`invalid_query`).

| Query param | Type | Description |
| --- | --- | --- |
| `status` | `pending` \\| `sending` \\| `sent` \\| `processed` \\| `failed` | Filter by job status. |
| `account_id` | uuid | Only jobs where this account is the sender or recipient. |
| `limit` | integer, 1-100 | Default `50`. |

```bash
curl "https://app.warmerly.com/api/v1/warmup/jobs?account_id=1c2d3e4f-...&status=processed&limit=20" \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{
  "jobs": [
    {
      "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "senderAccountId": "1c2d3e4f-1111-4a2b-8c3d-4e5f6a7b8c9d",
      "recipientAccountId": "2d3e4f5a-2222-4a2b-8c3d-4e5f6a7b8c9d",
      "scheduledAt": "2026-07-02T08:00:00.000Z",
      "sentAt": "2026-07-02T08:00:42.000Z",
      "status": "processed",
      "subject": "Re: quick question about the roadmap",
      "deliveredFolder": "inbox",
      "wasOpened": true,
      "wasReplied": false,
      "emailType": "reply"
    }
  ]
}
```

| Field | Type | Description |
| --- | --- | --- |
| `id` | uuid | Job ID. |
| `senderAccountId` / `recipientAccountId` | uuid | The two mailboxes involved in the exchange. |
| `scheduledAt` | timestamp | When the job was scheduled to send. |
| `sentAt` | timestamp \\| null | When it actually sent. |
| `status` | string | `pending`, `sending`, `sent`, `processed`, or `failed`. |
| `subject` | string | Subject line used. |
| `deliveredFolder` | `inbox` \\| `spam` \\| `promotions` \\| `unknown` \\| null | Where the recipient mailbox filed it, once processed. |
| `wasOpened` / `wasReplied` | boolean | Whether the recipient side opened or replied to it. |
| `emailType` | string \\| null | Internal template category (e.g. `intro`, `reply`). |

This endpoint returns the stored job row, so each job also carries fields such as `bodyText`, `bodyHtml`, `messageId`, `error`, `errorKind`, `attempts` and `threadDepth`. The example above shows only some of them.

## Per-account health and job history

Two account-scoped endpoints give you the same data filtered to a single mailbox. Both take the account ID from the URL, not `X-Project-Id`.

### Get an account's health snapshots

```text
GET /accounts/{id}/health
```

| Query param | Type | Default | Description |
| --- | --- | --- | --- |
| `days` | integer, 1-365 | `90` | How far back to return daily snapshots. |

```bash
curl "https://app.warmerly.com/api/v1/accounts/1c2d3e4f-.../health?days=30" \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{
  "health": [
    {
      "id": "b3e1c9a0-....",
      "accountId": "1c2d3e4f-....",
      "date": "2026-07-02",
      "sentCount": 22,
      "inboxCount": 20,
      "spamCount": 1,
      "bounceCount": 1,
      "replyCount": 3,
      "openCount": 17,
      "score": 89,
      "trend": "up",
      "spamScore": 0,
      "computedAt": "2026-07-02T00:15:00.000Z"
    }
  ]
}
```

`health` is ordered most recent first. `trend` is `up`, `flat`, or `down`. Each entry also carries `inboxRate`, `bounceRate` and `replyRate`, fractions between 0 and 1 of `sentCount`.

### Get an account's warmup job history

```text
GET /accounts/{id}/warmup-jobs
```

Returns the last 100 jobs (sent or received) for the account, plus a daily summary of the last 90 days. Returns `404` if the account doesn't exist.

```bash
curl "https://app.warmerly.com/api/v1/accounts/1c2d3e4f-.../warmup-jobs" \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{
  "jobs": [
    {
      "id": "9b1deb4d-....",
      "scheduledAt": "2026-07-02T08:00:00.000Z",
      "sentAt": "2026-07-02T08:00:42.000Z",
      "status": "processed",
      "senderAccountId": "1c2d3e4f-....",
      "recipientAccountId": "2d3e4f5a-....",
      "subject": "Re: quick question about the roadmap",
      "deliveredFolder": "inbox",
      "wasOpened": true,
      "wasReplied": false,
      "emailType": "reply"
    }
  ],
  "dailySummary": [
    { "day": "2026-07-02", "sent": 22, "received": 19, "inbox": 20, "promotions": 0, "spam": 1, "bounce": 0 }
  ]
}
```

`dailySummary` covers the trailing 90 days, newest day first: `sent` counts jobs the account sent, `received` counts jobs delivered to it, `inbox`, `promotions` and `spam` count only processed jobs the account sent that landed in each folder, and `bounce` counts jobs it sent that failed with a permanent error.

## Pause and resume warmup on an account

Warmup only runs against accounts with `status = active` or `aging`. Pausing an account stops it from being scheduled as a sender or recipient in future warmup jobs.

```text
POST /accounts/{id}/pause
POST /accounts/{id}/resume
```

Both take no request body and return `404` if the account doesn't exist. `pause` is refused with `409` (`sandbox_warmup_locked`) for the free sandbox mailbox.

```bash
curl -X POST https://app.warmerly.com/api/v1/accounts/1c2d3e4f-.../pause \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{ "ok": true }
```

`resume` sets the account back to `active`, clears `needsReconnect` and `disconnectReason`, resets `consecutiveFailures` to `0` and clears any campaign-sending pause. If the account is flagged as needing a reconnect, it first tests the stored login and answers `409` with code `reconnect_required` when that fails. Resuming a `dead` or `flagged` account can return `402 plan_capacity` when the workspace is at its mailbox limit:

```bash
curl -X POST https://app.warmerly.com/api/v1/accounts/1c2d3e4f-.../resume \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{ "ok": true }
```

## Mailbox health alerts

```text
GET /alerts
```

The last 50 mailbox-health alerts that are still open for your user, newest first. Alerts whose fault has since been seen to clear are left out. These are the same events that trigger a health email, so polling this is the pull equivalent of [webhooks](https://warmerly.com/docs/webhooks) for anyone who would rather not run an endpoint.

```json
{
  "alerts": [
    {
      "id": "c3f1a2b4-5d6e-4f70-8a91-b2c3d4e5f607",
      "accountId": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
      "kind": "score_drop",
      "severity": "warning",
      "title": "Warmup health dropped for jane@yourdomain.com",
      "body": "Health score fell from 82 to 61 over the last 24 hours.",
      "scoreBefore": 82,
      "scoreAfter": 61,
      "sentAt": "2026-09-11T06:02:00.000Z",
      "createdAt": "2026-09-11T06:01:58.000Z"
    }
  ]
}
```

| `kind` | Meaning |
| --- | --- |
| `score_drop` | Health score fell sharply. |
| `score_floor` | Health score is below the acceptable floor. |
| `oauth_disconnected` | A Google/Microsoft grant was revoked or expired, reconnect needed. |
| `auth_fail` | SMTP/IMAP authentication is failing. |
| `dns_changed` | The domain's SPF/DKIM/DMARC result changed since the last check. |
| `domain_blocklisted` | The sending domain appeared on a public blocklist. |
| `ip_blocklisted` | The mail server's IP appeared on a public blocklist. |
| `spam_flag` | The mailbox or workspace was flagged for spam-like outcomes. |
| `delivery_unobservable` | Mail is going out but nothing can be classified, so health cannot be measured at all. This is distinct from a bad score: there is no score. |

`severity` is `info`, `warning` or `critical`. `sentAt` is when the notification went out (`null` if it has not been sent), as opposed to `createdAt`, when the condition was detected. Each alert is the stored row, so it also carries `userId`, `fingerprint` and `resolvedAt` (always `null` here).

## Is warmup actually running?

```text
GET /warmup/health
```

Whether Warmerly's warmup workers are alive. A plain authenticated caller gets the derived flag:

```json
{ "ok": true, "healthy": true }
```

`healthy` is `true` when the freshest worker heartbeat is under 10 minutes old. It is a platform-level answer, not a statement about your mailboxes: `healthy: true` with nothing sending means the problem is your campaign or mailbox configuration, and [`GET /campaigns/{id}/readiness`](https://warmerly.com/docs/campaigns#readiness-check) will say which. Staff callers additionally get `lastTick` per worker.

`POST /warmup/run` exists but is ops-only: it triggers a planner/dispatcher pass across every workspace on the platform, so it returns `404` for a customer key. Warmup runs continuously on its own; there is nothing to trigger.

## Errors

An account ID that doesn't exist returns `404` on any of the endpoints above:

```json
{ "error": { "code": "not_found", "message": "not_found", "details": null } }
```

Missing or invalid `X-Api-Key` returns `401`, following the same shape described in [Authentication](https://warmerly.com/docs/authentication).

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

- [Get warmup stats](https://warmerly.com/docs/warmup/get-warmup-stats)
- [Get warmup trends](https://warmerly.com/docs/warmup/get-warmup-trends)
- [List warmup jobs](https://warmerly.com/docs/warmup/list-warmup-jobs)
- [Per-account health and job history](https://warmerly.com/docs/warmup/per-account-health-and-job-history)
- [Pause and resume warmup on an account](https://warmerly.com/docs/warmup/pause-and-resume-warmup-on-an-account)
- [Mailbox health alerts](https://warmerly.com/docs/warmup/mailbox-health-alerts)
- [Is warmup actually running?](https://warmerly.com/docs/warmup/is-warmup-actually-running)

## More in API reference

- [Campaigns](https://warmerly.com/docs/campaigns): API reference for Warmerly campaigns: create and manage multi-step email and LinkedIn outbound sequences,…
- [Verify](https://warmerly.com/docs/verify): API reference for Warmerly's email verification: check syntax, MX records, disposable/role addresses, and…
- [Email Finder](https://warmerly.com/docs/email-finder): API reference for the Warmerly Email Finder: find and verify a person's or company's email from a domain,…
