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:
{
"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
/accountsReturns 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.
curl https://app.warmerly.com/api/v1/accounts \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a"{
"accounts": [
{
"id": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
"email": "sales@yourdomain.com",
"provider": "custom",
"status": "aging",
"dailyCap": 30
}
]
}Connect an account
/accountsConnects a mailbox using manual SMTP/IMAP credentials. Requires a plan that includes the email channel, see the error 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. |
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:
{
"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
/accounts/{id}curl https://app.warmerly.com/api/v1/accounts/3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10 \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"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
/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.
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" }'{
"account": {
"id": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
"dailyCap": 45,
"notes": "Ramping faster for Q3 push"
}
}Disconnect an account
/accounts/{id}Permanently disconnects and removes the account, including its warmup history.
curl -X DELETE https://app.warmerly.com/api/v1/accounts/3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10 \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{ "ok": true }Returns 404 if the account doesn't exist.
Connect several accounts at once
/accounts/bulkRequires 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:
{
"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-connectiondetect 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.
{ "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-healthThe 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.
{
"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}/dnsGET 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": { … } }.
{
"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:
errormeans 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. SetdkimSelectoron 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}/placementGET lists this account's placement tests with a summary:
{
"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}/blocklistWhether 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.
{ "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-jobsstats is today-and-7-day counters for one mailbox:
{ "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/reconnectwarmup/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
/accounts/limitsWorkspace-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.
{
"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:
{
"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:
{ "error": { "code": "bad_request", "message": "invalid_body", "details": { /* zod field errors */ } } }A missing account on GET, PATCH, or DELETE returns 404:
{ "error": { "code": "not_found", "message": "not_found", "details": null } }Summarize with AI

