# Accounts

> API reference for Warmerly accounts (mailboxes): connect Gmail, Outlook, or custom SMTP/IMAP inboxes and manage warmup status, ramp schedule, and health score.

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

An **account** (also called a mailbox or sender account) is a connected email inbox used for warmup and/or campaign sending, for example a Gmail, Outlook, Zoho, Namecheap, or custom SMTP/IMAP mailbox. Every account belongs to a project and carries its own warmup status, ramp schedule, health score, and sending limits.

All endpoints below require `X-Api-Key`. Only connecting mailboxes (`POST /accounts` and `POST /accounts/bulk`) requires `X-Project-Id`; `GET /accounts` uses it when sent. Every `/accounts/{id}/...` endpoint finds the project from the account id in the URL, and answers `403 forbidden` for an account in a workspace you do not belong to and `400` for an id that is not a valid UUID. See [Authentication](https://warmerly.com/docs/authentication).

## The account object

Responses never include raw credentials: SMTP/IMAP passwords and OAuth tokens are encrypted at rest and stripped from the API response entirely. The object carries every stored setting of the mailbox, so it has more fields than the abridged example below (for example `lastCampaignSendAt`, `warmupEnabled`, `warmupStartedAt`, `warmupActiveDays`, `campaignSendingPausedAt`, `slowRampEnabled`, `managedBy`, `dkimSelector` and `trackingDomainVerified`). A typical account looks like this:

```json
{
  "id": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
  "projectId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
  "kind": "real",
  "email": "sales@yourdomain.com",
  "displayName": "Alex Carter",
  "provider": "custom",
  "authMethod": "password",
  "smtpHost": "smtp.yourdomain.com",
  "smtpPort": 587,
  "smtpUser": "sales@yourdomain.com",
  "imapHost": "imap.yourdomain.com",
  "imapPort": 993,
  "imapUser": "sales@yourdomain.com",
  "timezone": "UTC",
  "status": "aging",
  "createdAt": "2026-06-20T09:12:00.000Z",
  "activatedAt": null,
  "lastHealthScore": null,
  "lastSendAt": null,
  "consecutiveFailures": 0,
  "needsReconnect": false,
  "notes": null,
  "dnsLastCheckedAt": null,
  "dnsOverallStatus": null,
  "senderFirstName": null,
  "senderLastName": null,
  "fromName": null,
  "replyToAddress": null,
  "dailyCampaignLimit": 30,
  "minWaitMinutes": 10,
  "trackingDomain": null,
  "warmupEnabled": true,
  "warmupDailyLimit": null,
  "warmupStartedAt": "2026-06-20T09:12:00.000Z"
}
```

`status` is one of `aging`, `active`, `paused`, `flagged`, `dead`. Newly connected accounts start as `aging` while warmup ramps up sending volume.

## List accounts

```text
GET /accounts
```

Returns the accounts for a single project when `X-Project-Id` is set. If you omit `X-Project-Id`, this endpoint falls back to returning every account across all projects in your **active workspace** (the one named in `X-Workspace-Id` if you belong to it, otherwise your own workspace): useful for a workspace-wide overview. Mailboxes whose hosted subscription has been cancelled are left out.

Add `?view=summary` for a lighter list with only `id`, `email`, `provider`, `status`, `todayQuota`, `lastHealthScore`, `needsReconnect`, `campaignSendingPausedAt`, `managedBy`, `warmupStartedAt` and `createdAt`.

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

```json
{
  "accounts": [
    {
      "id": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
      "email": "sales@yourdomain.com",
      "provider": "custom",
      "status": "aging",
      "todayQuota": 4,
      "sentToday": 2,
      "campaignSendBlocked": false
    }
  ]
}
```

The default view returns the full account object plus `baseQuota` and `todayQuota` (today's warmup target), `sentToday`, `receivedToday` and `todayPlan` (today's warmup numbers) and, when `X-Project-Id` is set, `campaignSendBlocked` (`true` when the campaign engine will never send a campaign from that mailbox). The example above shows only some of those fields.

## Connect an account

```text
POST /accounts
```

Connects a mailbox using manual SMTP/IMAP credentials. Requires `X-Project-Id` and a plan that includes the email channel, see the [errors](https://warmerly.com/docs/accounts#errors) below. Fields you send that are not in the table are ignored.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Yes | The mailbox's email address. |
| `displayName` | string | No | Friendly name shown in the dashboard. |
| `provider` | string | Yes | One of `gmail`, `outlook`, `zoho`, `namecheap`, `custom`. |
| `smtp.host` | string | Yes | SMTP server hostname. |
| `smtp.port` | integer | Yes | SMTP server port. |
| `smtp.user` | string | Yes | SMTP username. |
| `smtp.pass` | string | Yes | SMTP password or app password. Encrypted at rest, never returned. |
| `imap.host` | string | Yes | IMAP server hostname. |
| `imap.port` | integer | Yes | IMAP server port. |
| `imap.user` | string | Yes | IMAP username. |
| `imap.pass` | string | Yes | IMAP password or app password. Encrypted at rest, never returned. |
| `timezone` | string | No | IANA timezone for scheduling sends. Defaults to `UTC`. |

```bash
curl -X POST https://app.warmerly.com/api/v1/accounts \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "sales@yourdomain.com",
    "displayName": "Alex Carter",
    "provider": "custom",
    "smtp": { "host": "smtp.yourdomain.com", "port": 587, "user": "sales@yourdomain.com", "pass": "app-password" },
    "imap": { "host": "imap.yourdomain.com", "port": 993, "user": "sales@yourdomain.com", "pass": "app-password" },
    "timezone": "Europe/London"
  }'
```

Returns `201` with the created account:

```json
{
  "account": {
    "id": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
    "projectId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
    "kind": "real",
    "email": "sales@yourdomain.com",
    "displayName": "Alex Carter",
    "provider": "custom",
    "authMethod": "password",
    "smtpHost": "smtp.yourdomain.com",
    "smtpPort": 587,
    "smtpUser": "sales@yourdomain.com",
    "imapHost": "imap.yourdomain.com",
    "imapPort": 993,
    "imapUser": "sales@yourdomain.com",
    "timezone": "Europe/London",
    "status": "aging",
    "warmupEnabled": true,
    "warmupStartedAt": "2026-06-20T09:12:00.000Z"
  }
}
```

The response is the full account object (abridged above). On creation, Warmerly starts warmup (`warmupStartedAt` is set) and kicks off a DNS pre-flight check (SPF/DKIM/DMARC/MX) in the background; check `dnsOverallStatus` on a subsequent `GET` to see the result. Connecting an address that is already in the same project returns `409 conflict`. SMTP or IMAP hosts that point at private or internal addresses are refused with `400 bad_request`.

## Get an account

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

```bash
curl https://app.warmerly.com/api/v1/accounts/3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10 \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{
  "account": {
    "id": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
    "email": "sales@yourdomain.com",
    "provider": "custom",
    "status": "aging",
    "lastHealthScore": 87,
    "todayQuota": 4,
    "spamPlacementScore": null
  }
}
```

Returns the full account object (abridged above) plus `trackingCnameTarget`, `sandboxSendingBlock` (a sentence, only for the free sandbox mailbox), `sendingFlag`, `todayQuota` (today's warmup target), `capacityExplain` (why the warmup quota is what it is), `spamPlacementScore` and `workspaceCapOverride`. Returns `404` if the account doesn't exist.

## Update an account

```text
PATCH /accounts/{id}
```

Updates settings on an existing account. Only send the fields you want to change. A few of the most commonly used fields:

| Field | Type | Description |
| --- | --- | --- |
| `displayName` | string | Friendly name shown in the dashboard. |
| `timezone` | string | IANA timezone for scheduling sends. |
| `notes` | string | Freeform notes shown in the dashboard. |
| `dailyCampaignLimit` | integer (0-1000) | Max campaign emails sent per day from this account. |
| `minWaitMinutes` | integer (0-1440) | Minimum wait between campaign emails from this account. |
| `slowRampEnabled` | boolean | Turns the slower campaign volume ramp on or off. |
| `dailyInboxPlacementTestLimit` | integer (0-100) | Daily limit on inbox placement tests for this account. |
| `warmupDailyLimit` | integer (1-500) or `null` | Per-mailbox ceiling on warmup emails per day. `null` removes it. |
| `senderFirstName` / `senderLastName` | string or `null` | Overrides the sender identity used in campaign merge fields. |
| `fromName` | string (max 80) or `null` | The name recipients see in the From header, for example your company. Empty or `null` falls back to first and last name. Does not change `{{senderFirstName}}`. |
| `signatureHtml` | string or `null` | Signature for outgoing campaign mail. |
| `replyToAddress` | valid email, `""` or `null` | Reply-to address for outgoing campaign mail. `""` clears it. |
| `trackingDomain` | string or `null` | A custom tracking hostname. It must be a bare hostname, and changing it resets its verification until the next DNS check. |
| `dkimSelector` | string or `null` | The DKIM selector to check first in DNS checks. |
| `smtpUser` / `imapUser` | valid email or `null` | Login identity override, for accounts connected with OAuth only (`400` otherwise). |
| `projectId` | string (UUID) | Moves the mailbox to another project, including one in another workspace. Needs `owner` or `admin` on both sides. |

These are the only fields `PATCH` accepts. Any other field in the body is ignored without an error, so check the returned account to confirm a change landed. On the free sandbox mailbox (`managedBy: "warmerly_sandbox"`) `dailyCampaignLimit`, `minWaitMinutes`, `slowRampEnabled` and `dailyInboxPlacementTestLimit` are refused with `400`.

```bash
curl -X PATCH https://app.warmerly.com/api/v1/accounts/3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10 \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "dailyCampaignLimit": 45, "notes": "Ramping faster for Q3 push" }'
```

```json
{
  "account": {
    "id": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
    "dailyCampaignLimit": 45,
    "notes": "Ramping faster for Q3 push"
  }
}
```

The response is the full account object (abridged above) with `trackingCnameTarget` added.

## Disconnect an account

```text
DELETE /accounts/{id}
```

Permanently disconnects and removes the account, including its warmup history. For a Warmerly-hosted mailbox it also suspends the mail account on Warmerly's mail server. An optional JSON body can carry `reason` and `note` (strings), which are recorded for the audit log.

```bash
curl -X DELETE https://app.warmerly.com/api/v1/accounts/3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10 \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

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

Returns `404` if the account doesn't exist.

## Connect several accounts at once

```text
POST /accounts/bulk
```

Requires `X-Project-Id` and a plan that includes the email channel (`403 channel_not_included` otherwise). Takes `{ "accounts": [...] }`, where each item has `email`, `provider` (`gmail` | `outlook` | `zoho` | `namecheap` | `custom`), `smtp` and `imap` objects, and optional `displayName` and `timezone` (default `UTC`), the same fields as `POST /accounts`. SMTP or IMAP hosts that point at private or internal addresses refuse the whole batch with `400 bad_request`.

Capacity is **all-or-nothing**: if the batch would breach the plan's mailbox limit the whole request is refused with `402 plan_capacity` rather than importing the first few. A billing-locked workspace is refused with `402 billing_locked`. Past that gate each account is inserted independently, so one bad row does not lose the others:

```json
{
  "results": [
    { "index": 0, "email": "a@yourdomain.com", "ok": true, "id": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10" },
    { "index": 1, "email": "b@yourdomain.com", "ok": false, "error": "duplicate key value violates unique constraint" }
  ]
}
```

A failed row carries the database's error text in `error`, so treat it as debugging context, not something to parse. Each connected mailbox starts with `status: "aging"` and warmup enabled (`warmupStartedAt` is set). Unlike `POST /accounts`, the bulk route does not start the DNS pre-flight check itself.

## Check settings before you connect

```text
POST /accounts/detect
POST /accounts/test-connection
```

`detect` takes `{ "email": "you@yourdomain.com" }` (3 to 320 characters) and returns `{ "detection": { … } }`: Warmerly's best guess at the provider, from the domain's live MX records, with the SMTP/IMAP hosts and ports the dashboard's connect wizard would prefill. A domain it cannot place comes back as an `unsupported` kind with a human-readable `note` rather than an error.

`test-connection` takes full `smtp` and `imap` objects (`host`, `port` from 1 to 65535, `user`, `pass`, all required) and reports whether each side authenticated, without saving anything. Use it to surface a credential problem before creating an account that would immediately pause. Hosts that point at private or internal addresses are refused with `400 bad_request`. On a failure, `message` is a plain-language reason; on success it is omitted.

```json
{ "smtp": { "ok": true }, "imap": { "ok": false, "message": "Invalid credentials" } }
```

## Domain health (records + blocklists in one call)

```text
GET  /accounts/{id}/domain-health
POST /accounts/{id}/domain-health
```

The single call behind the domain-health card in the dashboard, and the one to reach for first: it returns the record checks **and** the blocklist checks for the sending domain and its sending IP together, with per-record advisories rather than a bare pass/fail. `GET` reads the latest stored results; it does not run a new check. Any of `dns`, `domainBlocklist` and `ipBlocklist` is `null` when there is no stored result for it yet.

```json
{
  "domain": "yourdomain.com",
  "providerManaged": false,
  "dns": {
    "spfStatus": "pass",
    "dkimStatus": "none",
    "dmarcStatus": "pass",
    "mxStatus": "pass",
    "findings": [{ "record": "dkim", "severity": "warn", "title": "No DKIM record found" }],
    "checkedAt": "2026-09-12T08:14:00.000Z"
  },
  "domainBlocklist": { "subject": "yourdomain.com", "status": "clean", "listings": [], "zonesQueried": ["dbl.spamhaus.org"], "zonesErrored": [], "checkedAt": "2026-09-12T08:14:00.000Z" },
  "ipBlocklist": { "subject": "203.0.113.10", "via": "smtp.yourprovider.com", "status": "clean", "listings": [], "zonesQueried": [], "zonesErrored": [], "checkedAt": "2026-09-12T08:14:00.000Z" },
  "ipSkippedReason": null
}
```

| Field | Description |
| --- | --- |
| `providerManaged` | `true` for gmail.com, outlook.com and the like: the records belong to the provider, so a DKIM reading of them is unreliable and nothing here is your responsibility to fix. |
| `dns.findings` | Advisories beyond presence, SPF's `all` qualifier and its 10-lookup budget, DKIM key length and test mode, DMARC policy/`pct`/`rua`. |
| `domainBlocklist` / `ipBlocklist` | Listings on public blocklists, with the zones actually queried and any that errored, so a partial answer is visible as partial. |
| `ipSkippedReason` | Why there is no IP section (a provider-shared host, an unresolvable SMTP host). A hole with no reason reads as something silently broken. |

`POST` re-runs the DNS check and returns the same payload. It is **rate-limited internally**: a blocklist re-check inside the minimum interval (6 hours) reuses the stored result rather than re-querying third-party blocklist zones, so holding down a refresh button cannot multiply Warmerly's query volume across them.

The two endpoints below remain, and are the narrower views of the same data.

## DNS and domain checks

```text
GET  /accounts/{id}/dns
POST /accounts/{id}/dns
```

`GET` returns the last 10 DNS checks for the account, newest first, plus `latest` as a convenience (the first item of `history`, or `null`). `POST` re-runs the check now and returns `{ "result": { … } }`, or `400` with the error text if the check could not run.

```json
{
  "latest": {
    "id": "b1e7c2a4-0c3e-4c0a-9a6f-2b7d8e5f1a33",
    "accountId": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
    "domain": "yourdomain.com",
    "checkedAt": "2026-09-11T08:14:00.000Z",
    "spfStatus": "pass",
    "spfRecord": "v=spf1 include:_spf.google.com ~all",
    "dkimStatus": "none",
    "dkimSelector": null,
    "dkimRecord": null,
    "dmarcStatus": "pass",
    "dmarcRecord": "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com",
    "mxStatus": "pass",
    "mxRecords": ["aspmx.l.google.com"],
    "findings": [],
    "overallStatus": "warn"
  },
  "history": [ { "id": "b1e7c2a4-0c3e-4c0a-9a6f-2b7d8e5f1a33", "…": "same shape as latest" } ]
}
```

Each record status (SPF, DKIM, DMARC and MX) is `pass`, `none`, `fail` or `error`; `overallStatus` is `pass`, `warn` or `fail`. `mxStatus` is `null` on older checks. Two things to know before you treat a result as a verdict:

- `error` means DNS did not answer (timeout or resolver problem), **not** that the record is wrong. Re-check rather than re-publishing records.
- `dkimStatus: "none"` can mean "present but under a selector Warmerly could not guess", the check tries a list of common selector names and cannot discover an arbitrary one. Set `dkimSelector` on the account (see [Update an account](https://warmerly.com/docs/accounts#update-an-account)) and re-check.

SPF, DKIM, DMARC and MX are checked here, and if the account has a tracking domain its CNAME is re-verified on the same run. Everything each record does, and the order to publish them in, is in the [DNS records checklist](https://warmerly.com/docs/guides/dns-records).

## Placement tests

```text
GET  /accounts/{id}/placement
POST /accounts/{id}/placement
```

`GET` lists this account's 10 most recent placement tests, newest first, with a summary:

```json
{
  "tests": [
    {
      "id": "6d2f0b1a-9c4e-4f77-bb31-2a9e7c4d8e55",
      "accountId": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
      "startedAt": "2026-09-10T09:00:00.000Z",
      "completedAt": "2026-09-10T09:01:04.000Z",
      "status": "done",
      "score": 82,
      "details": { }
    }
  ],
  "summary": {
    "latestScore": 82,
    "latestStatus": "done",
    "latestStartedAt": "2026-09-10T09:00:00.000Z",
    "latestDegraded": false,
    "trendDelta": 6
  }
}
```

`status` is `pending`, `running`, `done` or `failed`. `trendDelta` is the score change against the previous comparable test, or `null` when there is nothing fair to compare with. `latestDegraded` is `true` when the latest test was measured against Warmerly's own warmup mailboxes rather than real seed inboxes. Its score is not a placement result and is not comparable with a clean one. Tests recorded before this flag existed also read as degraded.

**`POST` sends live email and spends quota.** It dispatches probe messages from this mailbox to seed addresses and debits one of your monthly placement tests (counted per user, as in [Usage & limits](https://warmerly.com/docs/usage)). Out of allowance returns `402` with `error.code` `quota_exhausted` and `details` carrying `used`, `limit`, `upgradeUrl` and, when credits can cover it, a `topUp` offer. The charge is taken before the run and handed back if the run fails, which returns `400 bad_request`. The response is `{ "test": { … } }`. The call waits at least 30 seconds for delivery before responding, so expect it to take that long or more.

## Blocklist status

```text
GET  /accounts/{id}/blocklist
POST /accounts/{id}/blocklist
```

Whether the account's sending **domain** appears on public DNS blocklists. `GET` returns the last 10 checks; `POST` runs one now, and returns a recent result with `cached: true` rather than re-querying the zones if one was taken moments ago.

```json
{ "domain": "yourdomain.com", "applicable": true, "latest": null, "history": [] }
```

`applicable: false` means the domain belongs to a free consumer provider (gmail.com and friends), which is never checked. A shared provider's reputation is not yours to read.

## Activity and warmup history

```text
GET /accounts/{id}/stats
GET /accounts/{id}/events
GET /accounts/{id}/log
GET /accounts/{id}/warmup-jobs
```

`stats` is today-and-7-day counters for one mailbox. "Today" is the UTC day:

```json
{ "sentToday": 24, "pendingToday": 6, "inboxToday": 22, "spamToday": 2, "openRate": 0.415, "replyRate": 0.123, "todayPlan": null, "firstWarmupSend": null }
```

`openRate` and `replyRate` here are **warmup-network** rates over 7 days, returned as fractions between 0 and 1 (not percentages), not campaign performance. The two are different metrics with similar names, explained in [How warmup works](https://warmerly.com/docs/how-warmup-works). Either can be `null` when there is nothing to divide by. `todayPlan` is the mailbox's warmup plan for its local day and `firstWarmupSend` describes the first warmup email during the first 7 days after warmup starts (`null` afterwards).

`events` returns up to 200 lifecycle events, newest first, as `{ "events": [...] }` with `{ id, accountId, type, payload, createdAt }` each. `type` is one of `smtp_fail`, `imap_fail`, `flagged`, `auto_paused`, `auto_resumed`, `manual_pause`, `aged_in`, `marked_dead`, `oauth_refresh_fail`, `oauth_connected` and `oauth_reconnected`.

`log` pages through the individual warmup messages this mailbox sent, newest first, as `{ "jobs": [...], "nextCursor": "..." }`: `?limit=` (1-100, default 50) and `?cursor=` (pass the previous response's `nextCursor`, a timestamp). `nextCursor` is `null` only when a page comes back empty, so keep paging until `jobs` is empty.

`warmup-jobs` returns the 100 most recent warmup messages this mailbox sent or received, newest first by scheduled time, as `{ "jobs": [...], "dailySummary": [...] }`, with no paging.

## Pause, resume and warmup

```text
POST /accounts/{id}/pause
POST /accounts/{id}/resume
POST /accounts/{id}/warmup/toggle
POST /accounts/{id}/send-test
POST /accounts/{id}/oauth/reconnect
```

`pause` sets the mailbox's `status` to `paused` and `resume` sets it back to `active`; both return `{ "ok": true }`. `resume` also clears a campaign-sending pause. If the mailbox is flagged as needing a reconnect, `resume` first tests the stored login and answers `409` with code `reconnect_required` when it fails. Resuming a `dead` or `flagged` mailbox can return `402 plan_capacity` when the workspace is at its mailbox limit.

`warmup/toggle` takes `{ "enabled": true | false }` (required) and is the supported way to start or stop warmup for one mailbox; it returns `{ "account": { … } }`. Enabling it for the first time also stamps the warmup start date the ramp is measured from.

The free mailbox every new account is given (`managedBy: "warmerly_sandbox"`) keeps warming for as long as you have it: `pause`, and `warmup/toggle` with `{ "enabled": false }`, answer `409` with code `sandbox_warmup_locked` for it. Delete it (`DELETE /accounts/{id}`) if you no longer want it.

`send-test` takes `{ "to": "you@example.com" }` (a valid email) and sends one real message through this mailbox, with a fixed subject and body, the fastest way to prove SMTP works end to end. It returns `{ "ok": true }`. A failed send returns `500` with `{ "error": { "message": ... } }` where `message` is `smtp_auth_failed`, `smtp_timeout`, `smtp_connection_failed` or `send_failed` (this response has no `error.code`).

`oauth/reconnect` is for mailboxes connected with Google or Microsoft sign-in. It returns `{ "auth_url": "..." }`, a consent URL to open in a browser; completing it reconnects the mailbox (for example a Microsoft 365 mailbox that authenticates but fails to send, see [Troubleshooting](https://warmerly.com/docs/troubleshooting)). It returns `400` for a mailbox that was not connected with OAuth.

## Channel account limits

```text
GET /accounts/limits
```

No `X-Project-Id`; the OneMail part follows your active workspace (`X-Workspace-Id`). Reports connected-vs-included counts for the per-account channels (LinkedIn and WhatsApp, counted for your user) with an overage price in USD, plus the OneMail hosted-mailbox add-on: its current quantity, per-mailbox price in minor units, the tier discount, and the full price ladder.

```json
{
  "limits": {
    "linkedin": { "used": 1, "included": 0, "overageUsd": 19 },
    "whatsapp": { "used": 0, "included": 2, "planIncluded": 2, "slots": 0, "overageUsd": 6 },
    "onemail": { "used": 4, "included": null, "unitAmountMinor": 199, "currency": "usd", "billableQuantity": 4, "recurringAmountMinor": 796, "tier": "growth", "discountPercent": 33 }
  }
}
```

`whatsapp.included` is the whole ceiling: `planIncluded` from the plan plus `slots` bought on top. `linkedin.overageUsd` and `whatsapp.overageUsd` are the USD price of one more slot, the same figure the slot checkout uses; they are always in USD, whatever currency your plan bills in. The `onemail` object has more fields than shown: `packSlots`, `packSlotsFree`, `minimumQuantity`, `tierPrices` and `upgrade`.

`included: null` on `onemail` is not a missing value: hosted mailboxes have no cap at all, they are billed per unit and never consume a plan mailbox slot, so never render that one as a progress bar. For monthly allowances and plan capacity see [Usage & limits](https://warmerly.com/docs/usage).

## OAuth-connected accounts

Outlook.com and Microsoft 365 accounts can also be connected via OAuth instead of raw SMTP/IMAP credentials. That is a browser sign-in flow built for the dashboard's **Add mailbox → Microsoft** wizard, and it is not part of this reference. Gmail and Google Workspace connect with SMTP/IMAP and a Google app password (`smtp.gmail.com:465`, `imap.gmail.com:993`); mailboxes connected with Google OAuth before 2026-09-15 keep working. Once connected (by either method), the resulting account is fully manageable through this API.

## Errors

Attempting to connect an account on a plan without the email channel returns `403`:

```json
{
  "error": {
    "code": "channel_not_included",
    "message": "Your plan does not include email features. Upgrade at /pricing.",
    "details": { "upgradeUrl": "/pricing" }
  }
}
```

An invalid request body (e.g. malformed `smtp`/`imap` object) returns `400`:

```json
{ "error": { "code": "bad_request", "message": "invalid_body", "details": { /* zod field errors */ } } }
```

Connecting beyond your plan's mailbox limit returns `402` with `error.code` `plan_capacity`, and a billing-locked workspace returns `402 billing_locked`. A missing account on `GET`, `PATCH`, or `DELETE` returns `404`:

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

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

- [The account object](https://warmerly.com/docs/accounts/the-account-object)
- [List accounts](https://warmerly.com/docs/accounts/list-accounts)
- [Connect an account](https://warmerly.com/docs/accounts/connect-an-account)
- [Get an account](https://warmerly.com/docs/accounts/get-an-account)
- [Update an account](https://warmerly.com/docs/accounts/update-an-account)
- [Disconnect an account](https://warmerly.com/docs/accounts/disconnect-an-account)
- [Connect several accounts at once](https://warmerly.com/docs/accounts/connect-several-accounts-at-once)
- [Check settings before you connect](https://warmerly.com/docs/accounts/check-settings-before-you-connect)
- [Domain health (records + blocklists in one call)](https://warmerly.com/docs/accounts/domain-health-records-blocklists-in-one-call)
- [DNS and domain checks](https://warmerly.com/docs/accounts/dns-and-domain-checks)
- [Placement tests](https://warmerly.com/docs/accounts/placement-tests)
- [Blocklist status](https://warmerly.com/docs/accounts/blocklist-status)
- [Activity and warmup history](https://warmerly.com/docs/accounts/activity-and-warmup-history)
- [Pause, resume and warmup](https://warmerly.com/docs/accounts/pause-resume-and-warmup)
- [OAuth-connected accounts](https://warmerly.com/docs/accounts/oauth-connected-accounts)

## 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,…
- [Warmup](https://warmerly.com/docs/warmup): API reference for Warmerly warmup: pull mailbox health scores and the inbox-placement emails sent between…
- [Verify](https://warmerly.com/docs/verify): API reference for Warmerly's email verification: check syntax, MX records, disposable/role addresses, and…
