<!-- Warmup — https://docs.warmerly.com/warmup -->

# 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

```
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
current workspace, or pass it to scope the numbers to a single project.

```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": 96,
  "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 warmup paused. |
| `todaySent` | integer | Warmup emails sent or processed today (UTC). |
| `todayScheduled` | integer | Warmup emails still pending for today (UTC). |
| `avgHealthScore` | integer \| null | Average `last_health_score` across accounts that have one. |

## Get warmup trends

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

| 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` and covers the requested window only. `summary`
compares the requested window (`cur`) against the equal-length window immediately
before it (`prev`); 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

```
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` — filter by
`account_id` to scope results to a specific mailbox.

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

## 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

```
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` relative to the
previous day's `score`.

### Get an account's warmup job history

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

Returns the last 100 jobs (sent or received) for the account, plus a 30-day daily
summary. 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, "spam": 1 }
  ]
}
```

`dailySummary` covers the trailing 30 days: `sent` and `received` count jobs where the
account was sender/recipient respectively; `inbox`/`spam` count only processed jobs the
account sent that landed in each folder.

## 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.

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

Both take no request body and return `404` if the account doesn't exist.

```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`, and resets `consecutiveFailures` to `0`:

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

```
GET /alerts
```

The last 50 mailbox-health alerts raised for your account, newest first. These are the same
events that trigger a health email, so polling this is the pull equivalent of
[webhooks](https://docs.warmerly.com/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. |
| `delivery_unobservable` | Mail is going out but nothing can be classified, so health cannot be measured at all. 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.

## Is warmup actually running?

```
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://docs.warmerly.com/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://docs.warmerly.com/authentication).
