Warmerlydocs
API reference

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.

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.

Shell
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 below if it's not.

FieldTypeRequiredDescription
emailstringYesThe mailbox's email address.
displayNamestringNoFriendly name shown in the dashboard.
providerstringYesOne of gmail, outlook, zoho, namecheap, custom.
smtp.hoststringYesSMTP server hostname.
smtp.portintegerYesSMTP server port.
smtp.userstringYesSMTP username.
smtp.passstringYesSMTP password or app password. Encrypted at rest, never returned.
imap.hoststringYesIMAP server hostname.
imap.portintegerYesIMAP server port.
imap.userstringYesIMAP username.
imap.passstringYesIMAP password or app password. Encrypted at rest, never returned.
timezonestringNoIANA timezone for scheduling sends. Defaults to UTC.
rampScheduleIdstring (UUID)NoRamp schedule to control warmup volume growth.
Shell
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}
Shell
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:

FieldTypeDescription
displayNamestringFriendly name shown in the dashboard.
timezonestringIANA timezone for scheduling sends.
dailyCapinteger (1-100)Max warmup emails sent per day.
rampScheduleIdstring (UUID) or nullRamp schedule controlling warmup volume growth.
notesstringFreeform notes shown in the dashboard.
dailyCampaignLimitinteger (0-1000)Max campaign emails sent per day from this account.
senderFirstName / senderLastNamestring or nullOverrides the sender identity used in campaign merge fields.
replyToAddressstring or nullReply-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.

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

Shell
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
}
FieldDescription
providerManagedtrue 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.findingsAdvisories beyond presence, SPF's all qualifier and its 10-lookup budget, DKIM key length and test mode, DMARC policy/pct/rua.
domainBlocklist / ipBlocklistListings on public blocklists, with the zones actually queried and any that errored, so a partial answer is visible as partial.
ipSkippedReasonWhy 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) 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.

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

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.

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

Summarize with AI

Markdown version for LLMsllms.txt

Something unclear or out of date? Ask Warmi in the chat, or email support@warmerly.com.