<!-- Accounts — https://docs.warmerly.com/accounts -->

# 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`. Most also require `X-Project-Id`, see
[Authentication](https://docs.warmerly.com/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. 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,
  "dailyCap": 30,
  "currentRampDay": 0,
  "rampScheduleId": null,
  "lastHealthScore": null,
  "lastSendAt": null,
  "consecutiveFailures": 0,
  "needsReconnect": false,
  "notes": null,
  "dnsLastCheckedAt": null,
  "dnsOverallStatus": null,
  "senderFirstName": null,
  "senderLastName": null,
  "replyToAddress": null,
  "dailyCampaignLimit": 30,
  "minWaitMinutes": 10,
  "trackingDomain": null,
  "warmupFilterTag": null,
  "warmupDailyLimit": 30
}
```

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

## List accounts

```
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 **current active workspace**: useful for a workspace-wide overview.

```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",
      "dailyCap": 30
    }
  ]
}
```

## Connect an account

```
POST /accounts
```

Connects a mailbox using manual SMTP/IMAP credentials. Requires a plan that includes the
email channel, see the [error](#errors) below if it's not.

| 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`. |
| `rampScheduleId` | string (UUID) | No | Ramp schedule to control warmup volume growth. |

```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",
    "dailyCap": 30,
    "currentRampDay": 0,
    "rampScheduleId": null
  }
}
```

On creation, Warmerly kicks off a DNS pre-flight check (SPF/DKIM/DMARC) in the
background; check `dnsOverallStatus` on a subsequent `GET` to see the result.

## Get an account

```
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",
    "dailyCap": 30,
    "currentRampDay": 4,
    "lastHealthScore": 87
  }
}
```

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

## Update an account

```
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. |
| `dailyCap` | integer (1-100) | Max warmup emails sent per day. |
| `rampScheduleId` | string (UUID) or `null` | Ramp schedule controlling warmup volume growth. |
| `notes` | string | Freeform notes shown in the dashboard. |
| `dailyCampaignLimit` | integer (0-1000) | Max campaign emails sent per day from this account. |
| `senderFirstName` / `senderLastName` | string or `null` | Overrides the sender identity used in campaign merge fields. |
| `replyToAddress` | string or `null` | Reply-to address for outgoing campaign mail. |

Beyond these, there are 15+ additional fields for fine-tuning warmup behavior (e.g.
`warmupDailyLimit`, `warmupIncreasePerDay`, `warmupReplyRatePct`,
`warmupReadEmulation`, `warmupOpenRatePct`) and tracking-domain configuration
(`trackingDomain`, `trackingDomainVerified`). These map directly to the warmup/tracking
settings available in the dashboard's account settings panel.

```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 '{ "dailyCap": 45, "notes": "Ramping faster for Q3 push" }'
```

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

## Disconnect an account

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

Permanently disconnects and removes the account, including its warmup history.

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

```
POST /accounts/bulk
```

Requires `X-Project-Id`. Takes `{ "accounts": [...] }`, where each item has `email`,
`provider` (`gmail` | `outlook` | `zoho` | `namecheap` | `custom`), `smtp` and `imap`
objects, and optional `displayName` and `timezone` (default `UTC`).

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" }
  ]
}
```

Each connected mailbox starts with `status: "aging"` and warmup already started.

## Check settings before you connect

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

`detect` takes `{ "email": "you@yourdomain.com" }` 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`, `user`, `pass`) and
reports whether each side authenticated, without saving anything. Use it to surface a
credential problem before creating an account that would immediately pause.

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

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

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

```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 everything and returns the same payload. It is **rate-limited internally**:
a re-check inside the minimum interval reuses the stored result rather than re-querying a
dozen 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

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

`GET` returns the last 10 DNS checks for the account's domain, newest first, plus `latest`
as a convenience. `POST` re-runs the check now and returns `{ "result": { … } }`.

```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",
    "overallStatus": "warn"
  },
  "history": []
}
```

Each record status is `pass`, `none`, `fail` or `error`; `overallStatus` is `pass`, `warn`
or `fail`. 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](#update-an-account)) and re-check.

Only SPF, DKIM and DMARC are checked here. Everything each record does, and the order to
publish them in, is in the [DNS records checklist](https://docs.warmerly.com/guides/dns-records).

## Placement tests

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

`GET` lists this account's placement tests 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` flags a test that ran against an incomplete seed set. Its score is not
comparable with a clean one.

**`POST` sends live email and spends quota.** It dispatches probe messages from this
mailbox to seed addresses across providers and debits one of the workspace's monthly
placement tests. Out of allowance returns `402 payment_required` with the used/limit figures
and an `upgradeUrl`. The call waits for the probes to dispatch before responding, so expect
it to take tens of seconds.

## Blocklist status

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

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

```json
{ "sentToday": 24, "pendingToday": 6, "inboxToday": 22, "spamToday": 2, "openRate": 41.5, "replyRate": 12.3 }
```

`openRate` and `replyRate` here are **warmup-network** rates over 7 days, not campaign
performance, the two are different metrics with similar names, explained in
[How warmup works](https://docs.warmerly.com/how-warmup-works). Either can be `null` when there is nothing to
divide by.

`events` returns up to 200 lifecycle events, newest first (`{ id, accountId, type, payload,
createdAt }`), for example `type: "aged_in"` with `payload: { "rampDay": 45 }`.

`log` pages through the individual warmup messages this mailbox sent, newest first:
`?limit=` (1-100, default 50) and `?cursor=` (pass the previous response's `nextCursor`).

## Pause, resume and warmup

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

`warmup/toggle` takes `{ "enabled": true | false }` and is the supported way to start or
stop warmup for one mailbox; 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" }` and sends one real message through this
mailbox, the fastest way to prove SMTP works end to end.

`oauth/reconnect` re-reads a Google/Microsoft mailbox's identity and refreshes its tokens.
It is the fix for a Microsoft 365 mailbox that authenticates but fails to send, because a
reconnect re-reads the real primary SMTP address, see
[Troubleshooting](https://docs.warmerly.com/troubleshooting).

## Channel account limits

```
GET /accounts/limits
```

Workspace-level, no `X-Project-Id`. Reports connected-vs-included counts for the per-account
channels (LinkedIn and WhatsApp) with their 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": 50 },
    "whatsapp": { "used": 0, "included": 2, "overageUsd": 6 },
    "onemail": { "used": 4, "included": null, "unitAmountMinor": 199, "currency": "usd", "billableQuantity": 4, "recurringAmountMinor": 796, "tier": "growth", "discountPercent": 33 }
  }
}
```

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

## OAuth-connected accounts

Outlook.com and Microsoft 365 accounts can also be connected via OAuth instead of raw
SMTP/IMAP credentials, but that flow is only available through the dashboard's **Add
mailbox → Microsoft** wizard, there is no public API endpoint to initiate an OAuth
connection. 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 */ } } }
```

A missing account on `GET`, `PATCH`, or `DELETE` returns `404`:

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