Mailboxes
Connect, configure, warm up, verify, and send from sender mailboxes (email accounts).
Mailboxes are the sender accounts Warmbly sends campaign and warmup mail from. These endpoints live under /emails and let you list and inspect connected mailboxes, update their sending and warmup settings, point a custom tracking domain at a mailbox, drive the warmup lifecycle, check authentication and ban status, verify addresses before sending, and send a one-off message from a specific mailbox.
Most read routes require the Read emails scope and write routes require the Write emails scope. The mailbox connection (onboarding) routes are session only because they write user-encrypted refresh tokens through the SPA popup flow, and the send route requires the Send campaigns scope because it transmits real mail. When an API key is scoped to specific mailboxes, every /:id route is additionally gated to keys allowed to act on that mailbox.
List mailboxes
GET /emails
Returns the organization's connected mailboxes, newest first, with cursor pagination.
Auth: Scope READ_EMAILS · Org permission view_campaigns
| Parameter | In | Type | Description |
|---|---|---|---|
q | query | string | Optional free-text search over mailbox address and name. |
tag | query | string (UUID) | Optional tag id to filter by. Must be a valid UUID. |
cursor | query | string (UUID) | Opaque cursor from a previous pagination.next_cursor. |
limit | query | integer | Page size. Defaults to 50. Invalid limits return 400. |
Response
A data array of mailbox objects plus a pagination envelope.
{
"data": [
{
"id": "0c0f1a2b-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
"user_id": "a1b2c3d4-...",
"organization_id": "f9e8d7c6-...",
"worker_id": "7b6a5c4d-...",
"email": "[email protected]",
"name": "Acme Sales",
"signature_plain": "",
"signature_html": "",
"signature_sync": false,
"signature_code": false,
"provider": "gmail",
"status": "active",
"last_synced_at": "2026-06-11T09:14:00Z",
"last_id": 184213,
"campaign_limit": 50,
"min_wait_time": 600,
"reply_to": "",
"tracking_domain": "t.acme.com",
"tracking_domain_verified": true,
"tracking_domain_verified_at": "2026-06-01T12:00:00Z",
"warmup": "2026-05-20T00:00:00Z",
"warmup_paused_at": null,
"warmup_base": 10,
"warmup_max": 40,
"warmup_increase": 1,
"warmup_reply_rate": 30,
"warmup_tag": "",
"warmup_pool_type": "premium",
"warmup_start_time": "09:00",
"warmup_end_time": "17:00",
"warmup_days": 5,
"timezone": "America/New_York",
"tags": ["outbound"],
"created_at": "2026-05-19T18:00:00Z",
"updated_at": "2026-06-11T09:14:00Z"
}
],
"pagination": {
"total": 12,
"next_cursor": "c1_b3BhcXVlLWN1cnNvcg",
"has_more": true
}
}provider is one of gmail, outlook, or smtp_imap. status is one of active, inactive, or revoked. warmup is the warmup anchor timestamp (null when warmup has never been enabled); a non-null warmup_paused_at means warmup is enabled but paused. total and next_cursor may be null when not applicable.
Get a mailbox
GET /emails/:id
Returns a single mailbox by id.
Auth: Scope READ_EMAILS · Org permission view_campaigns
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox (email account) id. |
Response
A bare mailbox object, same shape as one element of the list data array.
{
"id": "0c0f1a2b-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
"email": "[email protected]",
"name": "Acme Sales",
"provider": "gmail",
"status": "active",
"campaign_limit": 50,
"min_wait_time": 600,
"warmup": "2026-05-20T00:00:00Z",
"warmup_paused_at": null,
"warmup_pool_type": "premium",
"tracking_domain": "t.acme.com",
"tracking_domain_verified": true,
"tags": ["outbound"],
"created_at": "2026-05-19T18:00:00Z",
"updated_at": "2026-06-11T09:14:00Z"
}Update a mailbox
PATCH /emails/:id
Updates mailbox settings: display name, signature, status, sending caps, reply-to, warmup configuration, and tags. All fields are optional; only present fields are applied.
Auth: Scope WRITE_EMAILS · Org permission manage_emails
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | Display name on outgoing mail. |
signature_plain | string | no | Plain-text signature. |
signature_html | string | no | HTML signature. |
signature_sync | boolean | no | Keep the signature synced from the provider. |
signature_code | boolean | no | Treat the HTML signature as raw code. |
status | string | no | active, inactive, or revoked. |
campaign_limit | integer | no | Daily cold-campaign cap for this mailbox (validated up to 100). |
min_wait_time | integer | no | Minimum seconds between sends. |
reply_to | string | no | Reply-to address. |
warmup | boolean | no | Enable or disable warmup. |
warmup_base | integer | no | Warmup starting volume per day. |
warmup_max | integer | no | Warmup daily ceiling. |
warmup_increase | integer | no | Per-day warmup ramp increment. |
warmup_reply_rate | integer | no | Percentage of warmup threads to reply to. |
warmup_tag | string | no | Tag applied to warmup threads. |
warmup_start_time | string | no | Daily warmup window start, HH:MM. |
warmup_end_time | string | no | Daily warmup window end, HH:MM. |
warmup_days | integer | no | Number of active warmup days per week. |
tags | string[] | no | Tag ids assigned to the mailbox. |
{
"name": "Acme Sales (US)",
"status": "active",
"campaign_limit": 40,
"min_wait_time": 720,
"reply_to": "[email protected]",
"warmup_max": 35,
"tags": ["outbound", "us"]
}Response
The updated mailbox object (same shape as Get a mailbox).
Update the tracking domain
PATCH /emails/:id/track
Sets or clears the custom open/click tracking domain for a mailbox. The backend resolves the CNAME on save and marks it verified once the customer subdomain points at the shared tracking host (t.warmbly.com). DNS can lag a freshly added record, so a miss is reported as unverified (pending), not an error. Send an empty domain to clear the custom domain and fall back to the shared default.
Auth: Scope WRITE_EMAILS · Org permission manage_emails
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. |
domain | query | string | The custom tracking subdomain (for example t.acme.com). Empty clears it. |
Response
{
"tracking_domain": "t.acme.com",
"tracking_domain_verified": true,
"tracking_domain_verified_at": "2026-06-11T09:20:00Z"
}tracking_domain_verified_at is null until the CNAME resolves to the tracking host.
Start warmup
POST /emails/:id/warmup/start
Enables warmup for a mailbox. When resuming from a paused state it preserves ramp progress and seeds the warmup task chain immediately rather than waiting for the next reconciler pass.
Auth: Scope WRITE_EMAILS · Org permission manage_emails
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. |
Response
The updated mailbox object, reflecting the new warmup state.
{
"id": "0c0f1a2b-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
"email": "[email protected]",
"warmup": "2026-06-11T09:25:00Z",
"warmup_paused_at": null,
"warmup_pool_type": "premium"
}Pause warmup
POST /emails/:id/warmup/pause
Pauses warmup without losing ramp progress. A later start continues from the same daily volume.
Auth: Scope WRITE_EMAILS · Org permission manage_emails
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. |
Response
The updated mailbox object. A paused mailbox has a non-null warmup_paused_at.
{
"id": "0c0f1a2b-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
"warmup": "2026-06-11T09:25:00Z",
"warmup_paused_at": "2026-06-11T11:00:00Z"
}Resume warmup
POST /emails/:id/warmup/resume
Resumes a paused warmup, shifting the ramp anchor forward so progress continues where it left off, and re-seeds the warmup task chain immediately.
Auth: Scope WRITE_EMAILS · Org permission manage_emails
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. |
Response
The updated mailbox object, with warmup_paused_at cleared.
Stop warmup
POST /emails/:id/warmup/stop
Disables warmup entirely and clears ramp progress. Distinct from pause: a later start begins a fresh ramp.
Auth: Scope WRITE_EMAILS · Org permission manage_emails
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. |
Response
The updated mailbox object, with warmup disabled.
{
"id": "0c0f1a2b-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
"warmup": null,
"warmup_paused_at": null
}Check domain authentication
GET /emails/:id/auth-check
Validates SPF, DKIM, and DMARC for the mailbox's sending domain on demand. Authentication alignment is a hard bulk-sender requirement and a common silent deliverability failure, so this confirms the domain is configured correctly without leaving the dashboard.
Auth: Scope READ_EMAILS · Org permission view_campaigns
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. The domain is derived from the mailbox address. |
Response
{
"domain": "acme.com",
"spf_found": true,
"spf_record": "v=spf1 include:_spf.google.com ~all",
"dkim_found": true,
"dkim_selectors": ["google"],
"dmarc_found": true,
"dmarc_policy": "quarantine",
"all_aligned": true,
"summary": "SPF, DKIM, and DMARC are all present and aligned."
}spf_record, dkim_selectors, and dmarc_policy are omitted when the corresponding record is not found.
Verify an email address
POST /emails/verify
Verifies a single email address on demand (syntax, then MX, then an SMTP RCPT probe, then catch-all detection). This is pre-send verification: confirm an address is deliverable before a worker ever sends to it, instead of learning from a hard bounce. The probe runs from the backend (a non-sending IP), never from worker IPs.
Auth: Scope READ_EMAILS · Org permission view_campaigns
The address may be supplied in the JSON body or as the email query param; the body takes precedence.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | no | The address to verify. Required if the email query param is not set. |
{
"email": "[email protected]"
}Response
{
"email": "[email protected]",
"status": "valid",
"reason": "accepted by recipient mail server",
"is_catch_all": false,
"has_mx": true,
"checked_at": "2026-06-11T09:30:00Z"
}status is one of valid, risky, invalid, or unknown. A missing or empty address returns a 400 error envelope.
Get warmup ban status
GET /emails/:id/warmup/ban-status
Returns whether a mailbox is blocked from the shared warmup pool, why, and whether the owner can appeal. Powers the dashboard ban banner.
Auth: Scope READ_EMAILS · Org permission view_campaigns
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. |
Response
{
"email_account_id": "0c0f1a2b-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
"blocked": true,
"health_state": "quarantined",
"reason": "spam-folder placement above threshold",
"blocked_at": "2026-06-09T14:00:00Z",
"blocked_until": "2026-06-16T14:00:00Z",
"can_appeal": true,
"pending_appeal": false
}reason, blocked_at, and blocked_until are omitted when the mailbox is not blocked. health_state reflects the mailbox's rolling warmup health (for example healthy, watch, throttled, quarantined, or blocked).
Submit a warmup appeal
POST /emails/:id/warmup/appeal
Lets the mailbox owner appeal a warmup ban with a reason.
Auth: Scope WRITE_EMAILS · Org permission manage_emails
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | no | The owner's explanation for the appeal. |
{
"reason": "Authentication is fixed and the high-bounce list has been removed."
}Response
{
"appeal_id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b"
}Delete a mailbox
DELETE /emails/:id
Disconnects and deletes a mailbox. It is removed from all warmup pools and an account-disconnected event fans out.
Auth: Scope WRITE_EMAILS · Org permission manage_emails
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The mailbox id. |
Response
204 No Content with an empty body.
Send from a mailbox
POST /emails/:id/send
Sends a one-off email from a specific mailbox. The send is scheduled and dispatched through the mailbox's assigned worker. Choose how it is scheduled with send_mode.
Auth: Scope SEND_CAMPAIGNS · Org permission send_campaigns. Requires an active organization.
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string (UUID) | The sending mailbox id. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
to | string[] | yes | Recipient addresses. |
cc | string[] | no | CC addresses. |
bcc | string[] | no | BCC addresses. |
subject | string | yes | Email subject. |
body_html | string | no | HTML body. |
body_plain | string | no | Plain-text body. |
in_reply_to | string[] | no | Message ids this email replies to. |
thread_id | string | no | Thread id to attach the message to. |
send_mode | string | no | instant (default), smart (next per-mailbox scheduler gap), or scheduled (use scheduled_at). |
scheduled_at | string (RFC 3339) | no | Required when send_mode is scheduled. Must be in the future. |
{
"to": ["[email protected]"],
"subject": "Quick question about your rollout",
"body_html": "<p>Hi Jane, ...</p>",
"body_plain": "Hi Jane, ...",
"send_mode": "smart"
}Response
{
"task_id": "9a0b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d",
"scheduled_at": "2026-06-11T09:45:00Z",
"send_mode": "smart"
}task_id identifies the queued send task. scheduled_at is the resolved dispatch time (immediate for instant, the next gap for smart, or the requested time for scheduled).
Connect a mailbox (onboarding)
The three onboarding routes connect a new mailbox. They are session only (not available to API keys) because they write user-encrypted provider refresh tokens through the SPA popup flow.
Start OAuth
POST /emails/onboarding/oauth/start
Begins an OAuth round trip for a Gmail or Outlook mailbox and returns the provider authorization URL plus an opaque state to round-trip back.
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | yes | gmail or outlook. |
{
"provider": "gmail"
}Response:
{
"url": "https://accounts.google.com/o/oauth2/auth?...",
"state": "n0nc3-opaque-state"
}Finish OAuth
POST /emails/onboarding/oauth/finish
Completes the OAuth round trip with the authorization code and state from the provider, then creates the mailbox.
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | Authorization code from the provider. |
state | string | yes | The state returned from start. |
{
"code": "4/0Ax...",
"state": "n0nc3-opaque-state"
}Response: 201 Created with the new mailbox object (same shape as Get a mailbox).
Connect SMTP/IMAP
POST /emails/onboarding/smtp-imap
Connects an SMTP/IMAP mailbox in a single call.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | The mailbox address. |
name | string | no | Display name. |
smtp | object | yes | SMTP credentials: username, password, host, port. |
imap | object | yes | IMAP credentials: username, password, host, port. |
{
"email": "[email protected]",
"name": "Acme Sales",
"smtp": {
"username": "[email protected]",
"password": "app-specific-password",
"host": "smtp.acme.com",
"port": 587
},
"imap": {
"username": "[email protected]",
"password": "app-specific-password",
"host": "imap.acme.com",
"port": 993
}
}Response: 201 Created with the new mailbox object.