# Email Accounts
Source: https://developer.mailbeast.ai/accounts
Connect and manage the sending mailboxes your campaigns send from.
Email accounts are the sending mailboxes your campaigns use. Connect them two ways:
* **SMTP/IMAP.** Any provider, including Gmail and Outlook using an
[app password](https://support.google.com/mail/answer/185833) (host
`smtp.gmail.com` / `imap.gmail.com`, etc.). See [Connect a mailbox](#connect-a-mailbox).
* **Native OAuth.** Google Workspace and Microsoft, with tokens refreshed for you. See
[Connect Google / Microsoft over OAuth](#connect-google-microsoft-over-oauth).
## Scopes
| Action | Scope |
| --------------------------------------- | ---------------- |
| Read mailboxes and verification status | `accounts:read` |
| Connect, update, pause / resume, delete | `accounts:write` |
## Connect a mailbox
`POST /v1/accounts` connects one mailbox or up to 50. Each is handled
**independently**, so the response tells you which connected and which failed (for
example, an email already connected to another workspace).
A new mailbox starts out unverified and is checked automatically. Poll
`verification.overallStatus` (or `status`) to know when it is ready to send.
```bash theme={null}
curl -X POST https://api.mailbeast.ai/v1/accounts \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accounts": [
{
"emailAddress": "jane@acme.com",
"firstName": "Jane",
"lastName": "Doe",
"smtpHost": "smtp.acme.com",
"smtpPort": 587,
"smtpUsername": "jane@acme.com",
"smtpPassword": "•••",
"imapHost": "imap.acme.com",
"imapPort": 993,
"imapUsername": "jane@acme.com",
"imapPassword": "•••"
}
]
}'
```
```json Response theme={null}
{
"summary": { "submitted": 1, "created": 1, "failed": 0 },
"accounts": [
{
"id": "…",
"emailAddress": "jane@acme.com",
"status": "inactive",
"healthScore": 100,
"smtp": { "host": "smtp.acme.com", "port": 587, "username": "jane@acme.com" },
"verification": { "overallStatus": "pending", "smtp": "inactive", "imap": "not_checked",
"dns": { "mx": "pending", "spf": "pending", "dkim": "pending", "dmarc": "pending" } }
}
],
"errors": []
}
```
## Connect Google / Microsoft over OAuth
For a native Google Workspace or Microsoft mailbox (managed token refresh, no app
password), use the OAuth flow. It's a three-step handshake because a person has to
grant consent in a browser:
`POST /v1/accounts/oauth/{google|microsoft}/init` returns a consent URL and a `state` handle.
```bash theme={null}
curl -X POST https://api.mailbeast.ai/v1/accounts/oauth/google/init \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
# → { "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth?…",
# "state": "…", "expiresInSeconds": 600 }
```
Open `authorizationUrl` in a browser. After they grant access, the provider
redirects back to MailBeast, which exchanges the code and connects the mailbox.
`GET /v1/accounts/oauth/{provider}/status?state=…` returns `pending`, then
`connected` (with the new `accountId`) or `failed`.
```bash theme={null}
curl "https://api.mailbeast.ai/v1/accounts/oauth/google/status?state=…" \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
# → { "status": "connected", "accountId": "…", "error": null }
```
The `state` is valid for 10 minutes; once it resolves, the result stays pollable
for 30 minutes after.
Like any new mailbox it is verified automatically and flips to `active` once verification
passes (expect `inactive` / `checking` briefly right after connecting).
## List, get, update
`GET /v1/accounts` is **cursor-paginated**, newest-first, and filterable by
`status` (`active`, `inactive`, `checking`, `failed`, `revoked`) and by
`tags`. Keep requesting with `?cursor=meta.nextCursor` until
`meta.hasMore` is `false`.
`PATCH /v1/accounts/{id}` updates settings/limits **and** rotates credentials in
one call. `emailAddress` is immutable. Any `smtp*` / `imap*` field present rotates
the credentials and re-runs verification.
```bash theme={null}
# raise the daily cap and rotate the SMTP password (triggers re-verification)
curl -X PATCH https://api.mailbeast.ai/v1/accounts/{id} \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "maxEmailsPerDay": 60, "smtpPassword": "•••" }'
```
`POST /v1/accounts/bulk` applies **one** settings `patch` **or** one `status`
change (`active` / `inactive`) across many ids. The two are mutually exclusive.
## Check verification
Mailboxes are verified for you: when you connect one, when you rotate its
credentials, when you `resume` a failed one, and periodically after that. Read
the latest result at any time:
```bash theme={null}
curl https://api.mailbeast.ai/v1/accounts/{id}/verification \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
```json Verification result theme={null}
{
"overallStatus": "verified",
"smtp": { "status": "verified", "error": null },
"imap": { "status": "verified", "error": null },
"dns": {
"mx": { "status": "verified", "error": null },
"spf": { "status": "failed", "error": "No SPF record found" },
"dkim": { "status": "verified", "error": null },
"dmarc":{ "status": "verified", "error": null },
"a": { "status": "verified", "error": null }
},
"lastVerifiedAt": "2026-07-02T00:00:00.000Z"
}
```
## Pause and resume
Pause sending on a mailbox (for a suspected deliverability issue, say) and resume
it later. A paused mailbox reports `status: "inactive"` and stays that way until
you resume it - campaigns and warmup both skip it, and nothing re-activates it on
its own.
```bash theme={null}
# pause (only an active mailbox can be paused; 400 otherwise). No body.
curl -X POST https://api.mailbeast.ai/v1/accounts/{id}/pause \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
# resume
curl -X POST https://api.mailbeast.ai/v1/accounts/{id}/resume \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
## Delete a mailbox
Delete a mailbox with `DELETE /v1/accounts/{id}`. Sending from it stops
immediately.
# Analytics
Source: https://developer.mailbeast.ai/analytics
Engagement reporting – per campaign, over time, and across the workspace.
Three endpoints cover reporting: one campaign's totals, one campaign over time, and a
workspace-wide roll-up. They all draw from the same source as your dashboard, so the
numbers you get here match the ones you see there.
## Scopes
| Action | Scope |
| ------------------ | -------------- |
| Read any analytics | `metrics:read` |
## Campaign analytics
```bash theme={null}
curl "https://api.mailbeast.ai/v1/campaigns/{id}/analytics?startDate=2026-06-01T00:00:00Z" \
-H "Authorization: Bearer mb_live_..."
```
`opens` and `clicks` come in **two cardinalities**:
* `total` – counted **per email**, bots excluded. The same email opened five times counts
once; opening two different steps counts twice.
* `uniqueLeads` – distinct leads who did it at least once.
Rates are computed from `uniqueLeads`, which is why a single enthusiastic reader cannot
push your open rate above 100%.
`replies` is a flat number – distinct leads who replied. There is no raw reply-event
count, so it is deliberately not dressed up as two cardinalities.
```json theme={null}
{
"totals": {
"sent": 1200,
"failed": 8,
"delivered": 1186,
"leadsContacted": 640,
"opens": { "total": 240, "uniqueLeads": 180 },
"clicks": { "total": 31, "uniqueLeads": 24 },
"replies": 26,
"bounces": 14,
"unsubscribed": 5,
"positiveLeads": 21,
"conversions": 6,
"conversionValue": 18000,
"rates": { "open": 28.1, "click": 3.7, "reply": 4.0, "bounce": 1.1, "delivery": 98.8, "unsubscribe": 0.4, "conversion": 0.9 }
}
}
```
**A rate of `null` means "no denominator", not "zero percent".** A campaign that has not
sent anything yet returns `"open": null` – not `0`. If we returned `0`, a chart would
show your open rate collapsing to zero on days you simply weren't sending. Check for
`null` before plotting.
### Out-of-office replies
`replies` counts distinct leads who replied. Pass `excludeOOO=true` to exclude
out-of-office auto-replies and count only replies from humans:
```bash theme={null}
curl ".../analytics?excludeOOO=true" -H "Authorization: Bearer mb_live_..."
```
This is not a cosmetic switch. Out-of-office replies can inflate reply rate noticeably,
and they inflate it *unevenly* across campaigns – a campaign that happened to run over a
holiday looks better than it was. Pick one setting and use it consistently when you
compare campaigns.
## Breakdowns
Ask for extra detail with `breakdown`. Repeat the parameter or pass a comma-separated
list. Each breakdown is a separate query, so request only what you will actually read.
```bash theme={null}
curl ".../analytics?breakdown=sequence,bounce" -H "Authorization: Bearer mb_live_..."
```
| Value | Adds | Answers |
| ---------- | ----------------- | ----------------------------------------------------------------------------- |
| `smtp` | `bySmtp[]` | Which mailbox is carrying the campaign – and which one is dragging it down. |
| `sequence` | `bySequence[]` | Per step **and** per A/B variant. Which follow-up actually earns the replies. |
| `bounce` | `bounceBreakdown` | Hard vs soft, plus the reason. |
### Bounce breakdown
Most outreach APIs give you a single `bounced` number. That number cannot tell you what
to *do*. This one can:
```json theme={null}
{
"bounceBreakdown": {
"total": 14,
"hard": 9,
"soft": 5,
"senderBounces": 3,
"recipientBounces": 11,
"unknownBounces": 0,
"byCategory": [
{ "category": "invalid-mailbox", "label": "Invalid mailbox", "type": "recipient", "count": 9, "percentage": 64.3 }
]
}
}
```
`recipientBounces` are their problem – the address is dead, so clean your list.
`senderBounces` are **yours** – reputation, authentication, or content got you refused.
Scrubbing your list will not fix those, and treating them as one number is how a
deliverability problem hides behind a "normal" bounce rate.
## Over time
```bash theme={null}
curl ".../analytics/timeseries?startDate=2026-06-01T00:00:00Z&interval=day" \
-H "Authorization: Bearer mb_live_..."
```
Each point carries `sent`, `delivered`, `opens`, `clicks`, `replies`, **`bounces`** and
**`unsubscribed`** – so bounce rate and unsubscribe rate over time are chartable directly,
without a second call.
`interval` accepts `day`, `week` or `month`. Long windows are automatically coarsened to
keep the series readable; the response echoes the `interval` it actually used, which may
be coarser than the one you asked for. Read it back rather than assuming.
## Workspace roll-up
```bash theme={null}
curl "https://api.mailbeast.ai/v1/campaigns/analytics/overview" \
-H "Authorization: Bearer mb_live_..."
```
Totals across every campaign, plus `activeCampaigns`, `sendingAccounts`, and the
highest-volume campaigns. Use it for a top-level view, then drill into a campaign.
## Reporting windows and retention
`startDate` and `endDate` are ISO-8601. Passing only `startDate` means "from then until
now".
Your plan retains a fixed window of history. If you ask for a start date older than that,
the window is moved forward and the response **tells you**:
```json theme={null}
{
"period": {
"start": "2026-04-16T00:00:00.000Z",
"end": "2026-07-15T00:00:00.000Z",
"clamped": true,
"retentionDays": 90
}
}
```
When `clamped` is `true`, the data is real but the window is **shorter than you asked
for**. Do not label such a result "all time" – check `clamped` before you do.
## What this API does not report
These are deliberately absent rather than silently zero:
* **Spam-complaint rate.** We do not ingest feedback loops, so we cannot count complaints.
Returning `0` would read as "no complaints" instead of "not measured".
* **Hourly granularity.** The smallest bucket is a day.
* **Per-link click-through.** Clicks are counted, but not attributed to individual links.
# Campaign analytics
Source: https://developer.mailbeast.ai/api-reference/analytics/campaign-analytics
/openapi.json get /v1/campaigns/{id}/analytics
Aggregate engagement for one campaign. Ask for extra breakdowns with `breakdown=smtp,sequence,bounce` – each is an extra query, so request only what you will read.
Requires one of the following scopes: `metrics:read`, `all:read`, `all:all`.
# Campaign analytics over time
Source: https://developer.mailbeast.ai/api-reference/analytics/campaign-analytics-over-time
/openapi.json get /v1/campaigns/{id}/analytics/timeseries
One point per interval so you can chart a campaign – this series carries bounces AND unsubscribes, so those rates are chartable without extra calls. Long windows auto-coarsen (day → week → month); the response says which interval it used.
Requires one of the following scopes: `metrics:read`, `all:read`, `all:all`.
# Workspace analytics roll-up
Source: https://developer.mailbeast.ai/api-reference/analytics/workspace-analytics-roll-up
/openapi.json get /v1/campaigns/analytics/overview
Engagement rolled up across every campaign in the workspace, plus the highest-volume campaigns. Use it for a top-level dashboard; drill into a campaign for detail.
Requires one of the following scopes: `metrics:read`, `all:read`, `all:all`.
# Create an API key
Source: https://developer.mailbeast.ai/api-reference/api-keys/create-an-api-key
/openapi.json post /v1/api-keys
Mint a new key for your workspace. The full token is returned **once**, so store it immediately. Requested scopes must be a subset of the calling key’s own scopes, and cannot include `apikeys:manage`.
Requires the `apikeys:manage` scope, which no wildcard grants and which can only be created from the dashboard.
# List API keys
Source: https://developer.mailbeast.ai/api-reference/api-keys/list-api-keys
/openapi.json get /v1/api-keys
All keys in the caller’s workspace, with masked prefixes (no secrets).
Requires the `apikeys:manage` scope, which no wildcard grants and which can only be created from the dashboard.
# Revoke an API key
Source: https://developer.mailbeast.ai/api-reference/api-keys/revoke-an-api-key
/openapi.json delete /v1/api-keys/{id}
Revoke a key by id - effective immediately. Cannot be undone.
Requires the `apikeys:manage` scope, which no wildcard grants and which can only be created from the dashboard.
# Campaign send status
Source: https://developer.mailbeast.ai/api-reference/campaigns/campaign-send-status
/openapi.json get /v1/campaigns/{id}/status
Lightweight live send-state (running/paused/completed), schedule-window state, and headline metrics. Deep reporting lives under the analytics endpoints.
Requires one of the following scopes: `campaigns:read`, `campaigns:all`, `all:read`, `all:all`.
# Create a campaign
Source: https://developer.mailbeast.ai/api-reference/campaigns/create-a-campaign
/openapi.json post /v1/campaigns
Create a campaign with its schedule, settings, sequence, sending accounts, and optionally its leads, in one call. Set `launch: true` to start it right away, which also needs the `campaigns:lifecycle` scope. If it cannot launch, the campaign is still created and `launchError` says why.
Requires one of the following scopes: `campaigns:write`, `campaigns:all`, `all:all`.
# Delete a campaign
Source: https://developer.mailbeast.ai/api-reference/campaigns/delete-a-campaign
/openapi.json delete /v1/campaigns/{id}
Delete a campaign to clean up programmatically-created resources.
Requires one of the following scopes: `campaigns:write`, `campaigns:all`, `all:all`.
# Get a campaign
Source: https://developer.mailbeast.ai/api-reference/campaigns/get-a-campaign
/openapi.json get /v1/campaigns/{id}
Full config - schedule, settings, sequence steps, attached accounts, headline metrics.
Requires one of the following scopes: `campaigns:read`, `campaigns:all`, `all:read`, `all:all`.
# List campaigns
Source: https://developer.mailbeast.ai/api-reference/campaigns/list-campaigns
/openapi.json get /v1/campaigns
Cursor-paginate the workspace campaigns (id, name, status, lead count), newest-first and stable under concurrent changes.
Requires one of the following scopes: `campaigns:read`, `campaigns:all`, `all:read`, `all:all`.
# Pause campaign
Source: https://developer.mailbeast.ai/api-reference/campaigns/pause-campaign
/openapi.json post /v1/campaigns/{id}/pause
Pause an active campaign. In-flight sends finish; nothing new is scheduled.
Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`.
# Resume campaign
Source: https://developer.mailbeast.ai/api-reference/campaigns/resume-campaign
/openapi.json post /v1/campaigns/{id}/resume
Resume a paused campaign from where it left off.
Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`.
# Send a test email
Source: https://developer.mailbeast.ai/api-reference/campaigns/send-a-test-email
/openapi.json post /v1/campaigns/{id}/test-email
Send yourself a rendered preview of one step variant before launching. It goes from a real mailbox and counts toward that mailbox's daily limit, but not your monthly quota. Capped at 12 per day per workspace.
Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`.
# Start campaign
Source: https://developer.mailbeast.ai/api-reference/campaigns/start-campaign
/openapi.json post /v1/campaigns/{id}/start
Begin sending (or resume a paused campaign). Needs a lead list and a sequence - returns 400 if the campaign has nothing to send.
Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`.
# Update a campaign
Source: https://developer.mailbeast.ai/api-reference/campaigns/update-a-campaign
/openapi.json patch /v1/campaigns/{id}
Partial update - only the supplied sections are touched. `accounts` and `sequence`, when present, REPLACE the existing set.
Requires one of the following scopes: `campaigns:write`, `campaigns:all`, `all:all`.
# Bulk update mailboxes
Source: https://developer.mailbeast.ai/api-reference/email-accounts/bulk-update-mailboxes
/openapi.json post /v1/accounts/bulk
Apply ONE settings/limits `patch` OR one `status` change (`active`/`inactive`) across many mailboxes. `patch` and `status` are mutually exclusive.
Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`.
# Connect mailboxes
Source: https://developer.mailbeast.ai/api-reference/email-accounts/connect-mailboxes
/openapi.json post /v1/accounts
Connect one SMTP/IMAP mailbox, or up to 50 at once. Any provider works, including Gmail and Outlook with an app password. There is no limit on how many mailboxes a workspace may connect.
Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`.
# Delete a mailbox
Source: https://developer.mailbeast.ai/api-reference/email-accounts/delete-a-mailbox
/openapi.json delete /v1/accounts/{id}
Delete a mailbox from your workspace. Sending from it stops immediately.
Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`.
# Get a mailbox
Source: https://developer.mailbeast.ai/api-reference/email-accounts/get-a-mailbox
/openapi.json get /v1/accounts/{id}
Fetch one mailbox with its embedded status, health score, and verification snapshot.
Requires one of the following scopes: `accounts:read`, `accounts:all`, `all:read`, `all:all`.
# List mailboxes
Source: https://developer.mailbeast.ai/api-reference/email-accounts/list-mailboxes
/openapi.json get /v1/accounts
List your mailboxes, newest first, with stable cursor pagination. Filter by `status` or `tags` (any-of), or pass `email` to look one mailbox up by its address instead of by `id`.
Requires one of the following scopes: `accounts:read`, `accounts:all`, `all:read`, `all:all`.
# Pause a mailbox
Source: https://developer.mailbeast.ai/api-reference/email-accounts/pause-a-mailbox
/openapi.json post /v1/accounts/{id}/pause
Stop sending from an active mailbox. It stays paused until you `resume` it - campaigns and warmup both skip it, and nothing re-activates it on its own. Returns `400` if the mailbox is not currently active.
Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`.
# Poll OAuth connect
Source: https://developer.mailbeast.ai/api-reference/email-accounts/poll-oauth-connect
/openapi.json get /v1/accounts/oauth/{provider}/status
Poll a connect started with `init`. `pending` until the mailbox owner finishes consent, then `connected` (with `accountId`) or `failed`.
Requires one of the following scopes: `accounts:read`, `accounts:all`, `all:read`, `all:all`.
# Resume a mailbox
Source: https://developer.mailbeast.ai/api-reference/email-accounts/resume-a-mailbox
/openapi.json post /v1/accounts/{id}/resume
Start sending from a mailbox again. One you paused resumes immediately; one we stopped after repeated failures is re-verified first and only resumes if the check passes.
Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`.
# Start OAuth connect
Source: https://developer.mailbeast.ai/api-reference/email-accounts/start-oauth-connect
/openapi.json post /v1/accounts/oauth/{provider}/init
Start connecting a Google Workspace or Microsoft mailbox over OAuth. Returns a consent URL to open in a browser, plus a `state` handle. Once the owner grants access, poll `GET /v1/accounts/oauth/{provider}/status?state=…`.
Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`.
# Update a mailbox
Source: https://developer.mailbeast.ai/api-reference/email-accounts/update-a-mailbox
/openapi.json patch /v1/accounts/{id}
Update a mailbox’s settings and limits, and rotate its SMTP/IMAP credentials. The email address cannot be changed. Including any `smtp*` or `imap*` field rotates the credentials and re-runs verification.
Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`.
# Verification result
Source: https://developer.mailbeast.ai/api-reference/email-accounts/verification-result
/openapi.json get /v1/accounts/{id}/verification
The latest SMTP + IMAP + DNS verification result for a mailbox.
Requires one of the following scopes: `accounts:read`, `accounts:all`, `all:read`, `all:all`.
# Count unread emails
Source: https://developer.mailbeast.ai/api-reference/emails/count-unread-emails
/openapi.json get /v1/emails/unread/count
Number of unread emails, optionally narrowed to specific mailboxes or a campaign. Emails outside a conversation have no read state and are not counted.
Requires one of the following scopes: `emails:read`, `emails:all`, `all:read`, `all:all`.
# Delete an email
Source: https://developer.mailbeast.ai/api-reference/emails/delete-an-email
/openapi.json delete /v1/emails/{id}
Remove an email from your inbox. It stops appearing in listings. The copy on the mail server is not touched.
Requires one of the following scopes: `emails:write`, `emails:all`, `all:all`.
# Forward an email
Source: https://developer.mailbeast.ai/api-reference/emails/forward-an-email
/openapi.json post /v1/emails/{id}/forward
Forward an email to recipients you choose. It is sent from the mailbox that owns the thread, with the original quoted below your note; all recipients are blocklist-checked.
Requires one of the following scopes: `emails:send`, `emails:all`, `all:all`.
# Get an email
Source: https://developer.mailbeast.ai/api-reference/emails/get-an-email
/openapi.json get /v1/emails/{id}
Fetch one email with its body – the only endpoint that returns the body. A `null` body means none was stored; if the stored body can't be read right now the call returns `503` rather than a false empty.
Requires one of the following scopes: `emails:read`, `emails:all`, `all:read`, `all:all`.
# List emails
Source: https://developer.mailbeast.ai/api-reference/emails/list-emails
/openapi.json get /v1/emails
List emails across your mailboxes, newest first. Results carry a `snippet` preview but never the message body; fetch a single email to read it. Filter by mailbox, campaign, conversation, lead, read state, direction, folder, date, or full-text search.
Requires one of the following scopes: `emails:read`, `emails:all`, `all:read`, `all:all`.
# Mark a conversation read
Source: https://developer.mailbeast.ai/api-reference/emails/mark-a-conversation-read
/openapi.json post /v1/threads/{id}/read
Mark every email in a conversation as read in one call. Read state lives on the conversation, not the individual email - this is the one call that clears it.
Requires one of the following scopes: `emails:write`, `emails:all`, `all:all`.
# Reply to an email
Source: https://developer.mailbeast.ai/api-reference/emails/reply-to-an-email
/openapi.json post /v1/emails/{id}/reply
Reply within the email's conversation. It is sent from the mailbox that owns the thread to the other party – you supply only the body (plus optional cc/bcc, which are blocklist-checked).
Requires one of the following scopes: `emails:send`, `emails:all`, `all:all`.
# Get a lead search
Source: https://developer.mailbeast.ai/api-reference/lead-finder/get-a-lead-search
/openapi.json get /v1/lead-searches/{id}
One search’s status and live progress counters (companies found, companies processed, emails found) – the single target to poll while a run is in flight.
Requires one of the following scopes: `leadfinder:read`, `all:read`, `all:all`.
# List lead searches
Source: https://developer.mailbeast.ai/api-reference/lead-finder/list-lead-searches
/openapi.json get /v1/lead-searches
Cursor-paginate the workspace’s lead searches (newest-first, stable under concurrent changes) for history and to discover ids to poll. In-progress chat drafts are not listed.
Requires one of the following scopes: `leadfinder:read`, `all:read`, `all:all`.
# Add leads
Source: https://developer.mailbeast.ai/api-reference/leads/add-leads
/openapi.json post /v1/campaigns/{cid}/leads
Add 1-100 leads to a campaign. Each is deduplicated by email within the campaign, provider-detected, and counts toward the monthly-imports quota. The response summarizes the outcome and returns the created leads.
Requires one of the following scopes: `leads:write`, `leads:all`, `all:all`.
# Bulk mutate leads
Source: https://developer.mailbeast.ai/api-reference/leads/bulk-mutate-leads
/openapi.json post /v1/campaigns/{cid}/leads/mutate
Apply one operation (`set_status`, `add_tags`, `remove_tags`, or `delete`) across many leads. Target them by `ids`; `delete` also accepts a `filter`, or `all: true` to remove every lead. Reply-based statuses can only be set on leads that have already replied.
Requires one of the following scopes: `leads:write`, `leads:all`, `all:all`.
# Delete a lead
Source: https://developer.mailbeast.ai/api-reference/leads/delete-a-lead
/openapi.json delete /v1/campaigns/{cid}/leads/{leadId}
Remove a single lead. Contacted leads are soft-deleted with a retention window.
Requires one of the following scopes: `leads:write`, `leads:all`, `all:all`.
# Get a lead
Source: https://developer.mailbeast.ai/api-reference/leads/get-a-lead
/openapi.json get /v1/campaigns/{cid}/leads/{leadId}
One lead including live sequence progress (steps sent/planned, next send).
Requires one of the following scopes: `leads:read`, `leads:all`, `all:read`, `all:all`.
# List campaign leads
Source: https://developer.mailbeast.ai/api-reference/leads/list-campaign-leads
/openapi.json get /v1/campaigns/{cid}/leads
Page through a campaign’s leads with filters (status, tags, search).
Requires one of the following scopes: `leads:read`, `leads:all`, `all:read`, `all:all`.
# Search leads by email
Source: https://developer.mailbeast.ai/api-reference/leads/search-leads-by-email
/openapi.json get /v1/leads/search
Find leads across all of the workspace’s campaigns by email (or fragment), for dedupe and cross-campaign lookup. Returns up to 10 matches.
Requires one of the following scopes: `leads:read`, `leads:all`, `all:read`, `all:all`.
# Update a lead
Source: https://developer.mailbeast.ai/api-reference/leads/update-a-lead
/openapi.json patch /v1/campaigns/{cid}/leads/{leadId}
Update a lead’s fields, tags, custom fields, or status. The email address can only be changed while the lead has not been contacted yet. Reply-based statuses can only be set once the lead has replied.
Requires one of the following scopes: `leads:write`, `leads:all`, `all:all`.
# Current usage
Source: https://developer.mailbeast.ai/api-reference/workspace/current-usage
/openapi.json get /v1/usage
Everything you have used and everything you have left, grouped by product: your Email Outreach plan limits and your Lead Finder credits. Check it before a large send or import. For billing cycles and renewal dates, see `GET /v1/account/subscription`.
Requires one of the following scopes: `usage:read`, `all:read`, `all:all`.
# Current usage for one limit
Source: https://developer.mailbeast.ai/api-reference/workspace/current-usage-for-one-limit
/openapi.json get /v1/usage/{limitType}
Just one limit (used/limit/remaining/percentage/isUnlimited) - handy when you only care about the one you’re about to spend.
Requires one of the following scopes: `usage:read`, `all:read`, `all:all`.
# Subscription
Source: https://developer.mailbeast.ai/api-reference/workspace/subscription
/openapi.json get /v1/account/subscription
Everything you pay for, grouped by product: your Email Outreach subscription and your separate Lead Finder subscription. Each has its own plan, status, and billing cycle.
Requires one of the following scopes: `usage:read`, `all:read`, `all:all`.
# Workspace identity
Source: https://developer.mailbeast.ai/api-reference/workspace/workspace-identity
/openapi.json get /v1/workspace
Who this key belongs to - your workspace name and timezone. (Your plan lives in `GET /v1/usage` and `GET /v1/account/subscription`.)
Requires one of the following scopes: `usage:read`, `all:read`, `all:all`.
# Authentication
Source: https://developer.mailbeast.ai/authentication
API keys, scopes, and how the workspace is resolved.
Every request carries an API key as a Bearer token:
```bash theme={null}
curl https://api.mailbeast.ai/v1/api-keys \
-H "Authorization: Bearer mb_live_7Fq2Ka9Lm3Xb0ZpH_…"
```
The key identifies **both** the caller and the workspace - you never put an
organization id in the path. A missing or revoked key returns `401`.
## Scopes
Keys are least-privilege: each carries only the scopes it needs, and every route
declares the one scope it requires. A key missing a required scope returns `403`.
### Granular scopes
Every scope maps to a live capability - there are no scopes without an endpoint
behind them.
| Scope | Grants |
| ---------------------------------------------- | --------------------------------------------------------------------- |
| `campaigns:read` / `campaigns:write` | Read / create & update campaigns |
| `campaigns:lifecycle` | Start / pause / resume sending (spends quota) |
| `leads:read` / `leads:write` | Read / ingest campaign leads |
| `emails:read` / `emails:send` / `emails:write` | Read emails / send replies & forwards (billable) / mark-read & delete |
| `accounts:read` / `accounts:write` | Read / connect & update sending mailboxes |
| `metrics:read` | Read campaign & workspace analytics |
| `leadfinder:read` | Read Lead Finder searches |
| `usage:read` | Read plan usage, subscription & workspace identity |
| `apikeys:manage` | Create / list / revoke API keys |
### Wildcard scopes
A key can also hold a wildcard that covers many scopes at once. A route always
requires a **granular** scope; any wildcard that covers it is accepted just the same.
| Wildcard | Covers |
| ------------------------------------------------------------- | ----------------------------------------- |
| `campaigns:all` / `leads:all` / `emails:all` / `accounts:all` | Every action within that one domain |
| `all:read` | Every read-only scope, across all domains |
| `all:all` | Everything - **except** `apikeys:manage` |
For example, a route requiring `emails:read` accepts any of `emails:read`,
`emails:all`, `all:read`, or `all:all`. `all:read` never covers a write, send, or
lifecycle scope - those need the matching `*:write` / `*:all` / `all:all`.
`apikeys:manage` is opt-in and **never** covered by any wildcard (not even
`all:all`). It is grantable only from the dashboard - never delegable through the
API - and a key holding it can mint only keys whose scopes are a **subset** of its
own, so there is no privilege escalation.
## Rate limits
The API is rate-limited **per workspace** - the limit is shared across every API
key in the workspace, so minting extra keys does not raise your ceiling.
| Limit | Window | Scope |
| ---------------- | ---------- | ---------------------------- |
| **120 requests** | 60 seconds | Per workspace (organization) |
Exceed the limit and you get `429` with a `Retry-After` header (seconds) and the
standard error envelope:
```json theme={null}
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Rate limit exceeded: 120 requests per minute per workspace. Retry in 60s.",
"status": 429,
"requestId": "…",
"retryAfter": 60
}
}
```
Watch `X-RateLimit-Remaining` and slow down before you hit `0`. On a `429`, wait
`Retry-After` seconds before retrying; back off exponentially on repeated hits.
For bulk lead loads, batch and space your calls rather than firing them all at once.
## Managing keys
Create your first key in the dashboard ([**Settings → API Keys**](https://app.mailbeast.ai/settings/api)), where the full
secret is shown **once**. After that you can rotate keys programmatically with
the [API Keys endpoints](/api-reference/api-keys/list-api-keys) using a key that has `apikeys:manage`.
# Campaigns
Source: https://developer.mailbeast.ai/campaigns
Create, configure, launch and monitor email campaigns.
A campaign bundles a **schedule**, **settings**, an email **sequence**, the sending
**accounts**, and its **leads**. The API is task-shaped: you create a
fully-configured campaign in one call rather than stitching together six.
## Scopes
| Action | Scope |
| ------------------------------- | --------------------- |
| Read campaigns and status | `campaigns:read` |
| Create / update / delete config | `campaigns:write` |
| Start / pause / resume sending | `campaigns:lifecycle` |
Lifecycle is deliberately a **separate** scope from `campaigns:write`: starting a
campaign spends send quota and sender reputation. A key with only `campaigns:write`
can build and edit a campaign but cannot start sending. Creating with
`launch: true` therefore needs **both** scopes.
## Create a campaign
One call configures everything. The configuration - campaign, schedule, settings,
sending accounts, and sequence - is **all-or-nothing**: if any part fails
validation (for example an SMTP account that isn't yours), nothing is created.
`accounts` must be your workspace's own accounts. `leads` are best-effort and
summarized in the response. If `launch: true` is set and the campaign can't start,
the call still returns `201` - the campaign is created as a draft, with
`launched: false` and a `launchError` explaining why (start it later with
`POST /v1/campaigns/{id}/start`). Because the response is a success, a retry with
the same `Idempotency-Key` replays it instead of creating a second draft.
```bash theme={null}
curl -X POST https://api.mailbeast.ai/v1/campaigns \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q4 Product Launch Outreach",
"schedule": {
"timezone": "America/New_York",
"sendingDays": [1, 2, 3, 4, 5],
"sendingMinutesStart": 540,
"sendingMinutesEnd": 1020,
"dailyLimit": 50
},
"settings": { "trackOpens": true, "trackClicks": true, "stopOnReply": true },
"sequence": [
{ "subject": "Quick question about {{companyName}}", "body": "
Hi {{firstName}}, …
" },
{ "subject": "Following up", "body": "Just circling back.
", "delayDays": 3 }
],
"accounts": ["123e4567-e89b-12d3-a456-426614174000"],
"leads": ["jane@acme.com", "sam@globex.com"],
"launch": false
}'
```
`sendingMinutesStart` / `sendingMinutesEnd` are minutes from midnight in the
campaign timezone - `540` = 09:00, `1020` = 17:00. Step delays are counted from
the previous step; the first step sends immediately when a lead enters.
The response returns the created campaign plus a lead-ingest summary:
```json Response theme={null}
{
"campaign": {
"id": "333e9ef7-734d-40b2-9e3f-cbce63411b69",
"name": "Q4 Product Launch Outreach",
"status": "draft",
"schedule": { "timezone": "America/New_York", "dailyLimit": 50, "…": "…" },
"settings": { "trackOpens": true, "trackClicks": true, "stopOnReply": true, "sendAsTextOnly": false },
"metrics": { "leadsCount": 2, "sent": 0, "opens": 0, "clicks": 0, "replies": 0 },
"sequence": [ { "subject": "Quick question about {{companyName}}", "body": "Hi {{firstName}}, …
", "delayDays": 0, "delayHours": 0 } ],
"accounts": ["123e4567-e89b-12d3-a456-426614174000"]
},
"leads": { "submitted": 2, "created": 2, "duplicates": 0, "invalid": 0 },
"launched": false
}
```
## Launch and control sending
Drive the send lifecycle through explicit routes - `/start`, `/pause`, or `/resume`.
To end a campaign, pause it (or delete it); completion otherwise happens
automatically when the end date passes or every lead has been sent.
```bash theme={null}
curl -X POST https://api.mailbeast.ai/v1/campaigns/{id}/start \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
You can also launch at creation time by passing `"launch": true` in the create
body (requires `campaigns:lifecycle`).
A campaign must have **at least one lead and a sequence** to start. A launch
attempt without them is rejected `400` on `POST /start`; on create-with-`launch`
the campaign is still created and returned as a draft (`201`, `launched: false`,
with a `launchError`).
## Send a test email
Before you launch, send yourself a rendered preview of one step variant, from a
real mailbox, exactly as a lead would receive it.
```bash theme={null}
curl -X POST https://api.mailbeast.ai/v1/campaigns/{id}/test-email \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"variantId": "550e8400-e29b-41d4-a716-446655440000",
"accountId": "3f1c2b7e-9a4d-4e18-b2c1-6d8f5a0e7c93",
"recipient": "you@acme.com",
"personalization": "dummy"
}'
```
```json Response theme={null}
{ "sent": true, "messageId": "" }
```
`personalization` decides where merge variables come from. `dummy` fills them
with placeholders. `lead` uses a real lead, which you name with `leadId`. There
is no fallback: asking for `lead` without a `leadId` is rejected, so a test can
never quietly leak the wrong person's data into an email.
The content is the campaign's own: you cannot supply a body here, so this is not
a way to send arbitrary mail.
A test goes out from a real mailbox and counts toward **that mailbox's daily
limit**, but not toward your monthly email quota. It is capped at **12 tests per
day per workspace** (`429` beyond that).
The recipient must not be blocked in your workspace. A test carries your campaign
content, so the same do-not-email rules apply to it as to a real send: it is
refused for anyone on your blacklist, by address or by domain. That list is your
own instruction not to contact someone, and a test is not an exception to it.
A `400` means something about the request needs changing – the mailbox rejected
the recipient, for example – and the message says what. If the send fails on our
side instead, you get a **`503`**: the request was fine, it costs you no quota,
and retrying shortly is the right move. Distinguishing the two matters if you
retry automatically: a `400` is worth giving up on, a `503` is not.
## Tags
Every campaign in `GET /v1/campaigns` carries its `tags` – the ones you set in the
dashboard. The API reads them and lets you filter by them, so "the campaigns for
client X" is a single call rather than a full page-through.
```bash theme={null}
# every campaign, with its tags
curl https://api.mailbeast.ai/v1/campaigns \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
# → "tags": [ { "id": "8debf023-…", "name": "q4-launch", "color": "#10B981" } ]
# only campaigns carrying at least ONE of these tags (comma-separated ids)
curl "https://api.mailbeast.ai/v1/campaigns?tags=8debf023-…,1f2e3d4c-…" \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
Tags are **created, renamed, and deleted in the dashboard**, not through the API.
There is no `/v1/tags` resource.
Campaign tags and mailbox tags are **separate sets**. Two tags with
the same name on the two pages are different tags, and their ids never cross over: a
campaign tag id passed to `GET /v1/accounts?tags=` matches nothing.
## Check status
A lightweight poll of live send-state, the schedule window, and headline metrics:
```bash theme={null}
curl https://api.mailbeast.ai/v1/campaigns/{id}/status \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
```json Response theme={null}
{
"id": "333e9ef7-734d-40b2-9e3f-cbce63411b69",
"status": "active",
"isWithinSchedule": true,
"nextScheduleOpensAt": null,
"metrics": { "leadsCount": 2, "sent": 2, "opens": 1, "clicks": 0, "replies": 0 }
}
```
## Update
`PATCH` touches only the sections you supply. Supplying `accounts` or `sequence`
**replaces** that section entirely (the sequence is replaced in place - you always
end up with exactly the steps you sent). A supplied `sequence` must have **at least
one step** - an empty array is rejected (`400`); omit the field to leave the
sequence unchanged.
```bash theme={null}
curl -X PATCH https://api.mailbeast.ai/v1/campaigns/{id} \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "schedule": { "dailyLimit": 100 } }'
```
Unlike create, `PATCH` applies each supplied section independently - it is not a
single cross-section transaction, so a later section failing does not roll back an
earlier one. Send one section at a time if you need to isolate failures. The
`sequence` of a campaign that is **actively sending** cannot be changed; pause it
first. Sending `accounts` **can** be updated live - the change takes effect on the
next sends.
# Emails
Source: https://developer.mailbeast.ai/emails
Read the replies your campaigns get, and answer them.
Emails are flat: one object per message, across every mailbox you have connected.
Conversations are not fetched as a whole. Instead, `threadId` ties messages
together, and you can ask for just the newest message of each conversation.
## Scopes
| Action | Scope |
| ----------------------------------- | -------------- |
| List and read emails | `emails:read` |
| Delete an email, mark a thread read | `emails:write` |
| Reply and forward | `emails:send` |
**List results never include the message body.** They carry a `snippet` preview
instead, which keeps paging fast no matter how large the messages are. Fetch a
single email when you need to read it.
## List emails
`GET /v1/emails` is cursor-paginated, newest first. Keep requesting with
`?cursor=meta.nextCursor` until `meta.hasMore` is `false`.
```bash theme={null}
# unread replies from one mailbox
curl -G https://api.mailbeast.ai/v1/emails \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-d mailbox=jane@acme.com \
-d type=received \
-d isUnread=true
```
```json Response theme={null}
{
"data": [
{
"id": "8c5a1e07-2b93-4d6f-a0e8-7c4b9d1f2a36",
"threadId": "13a265fd-f0f2-b1d1-d6fc-154e483183e7",
"messageId": "",
"mailbox": { "id": "3f1c2b7e-9a4d-4c8e-b1f6-5d2a7e0c3b94", "email": "jane@acme.com" },
"from": { "email": "john@prospect.io", "name": "John Carter" },
"to": ["jane@acme.com"],
"cc": [],
"bcc": [],
"subject": "Re: Pricing question",
"snippet": "Thanks for the details, this works for us.",
"isUnread": true,
"type": "received",
"campaignId": "c1f3b0a2-6d4e-4f8b-9a1c-2e5d7f0a3b6c",
"leadId": "9d4e2f10-5b6a-4c73-8e19-0f2a7c4d8b31",
"hasAttachments": false,
"receivedAt": "2026-07-13T09:24:11.000Z",
"sentAt": null
}
],
"meta": { "hasMore": false, "nextCursor": null }
}
```
Filters combine freely: `mailbox` (comma-separated addresses), `campaignId`,
`threadId`, `leadId`, `isUnread`, `type` (`sent` or `received`),
`folder` (`inbox`, `untracked`, `important`, `snoozed`, or `archived`), `since`,
`until`, and `search`.
`search` is full text over subject, preview text and sender address. Quoted
phrases, `OR` and `-exclude` all work.
### Conversations without fetching them
Pass `latestOfThread=true` to get one email per conversation. That gives you a
conversation-style view at the cost of an ordinary list.
```bash theme={null}
curl -G https://api.mailbeast.ai/v1/emails \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-d latestOfThread=true -d limit=25
```
Your other filters are applied **first**, and the newest email that survives them
is the one you get back. So this returns the last message each contact sent you –
including in conversations you have already answered, where the newest message in
the thread is your own reply:
```bash theme={null}
curl -G https://api.mailbeast.ai/v1/emails \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-d latestOfThread=true -d type=received
```
An email that belongs to no conversation is a conversation of one, and is always
kept.
To open one conversation, list its messages with `?threadId=...`.
## Read one email
`GET /v1/emails/{id}` is the only endpoint that returns the body.
```bash theme={null}
curl https://api.mailbeast.ai/v1/emails/{id} \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
It returns the same object as the list, plus:
```json theme={null}
{
"body": {
"html": "Thanks for the details, this works for us.
",
"text": "Thanks for the details, this works for us."
}
}
```
## Reply and forward
`POST /v1/emails/{id}/reply` answers in the conversation the email belongs to.
You supply the body; the sender and the recipient are decided for you. The reply
goes out from the mailbox that owns the conversation and is addressed to the
other party, so a reply can never turn into a message to someone else.
```bash theme={null}
curl -X POST https://api.mailbeast.ai/v1/emails/{id}/reply \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"body": { "text": "Happy to set up a call. Does Thursday work?" },
"cc": ["cto@acme.com"]
}'
```
```json Response theme={null}
{
"id": "5e9a3c81-7d24-4b60-9f13-8a6c2e0d4b57",
"threadId": "13a265fd-f0f2-b1d1-d6fc-154e483183e7",
"status": "queued"
}
```
`POST /v1/emails/{id}/forward` sends the message on to recipients you choose,
with the original quoted below your note. It also goes out from the mailbox that
owns the conversation.
```bash theme={null}
curl -X POST https://api.mailbeast.ai/v1/emails/{id}/forward \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": ["colleague@acme.com"], "body": { "text": "FYI, worth a look." } }'
```
### Recipients you add are checked against your blocklist
Anyone you name yourself – `cc` and `bcc` on a reply, `to`, `cc` and `bcc` on a
forward – is checked against your workspace blocklist. If one of them is on it, the whole request is refused with
`400` and nothing is sent; the message names every blocked address, so you can
fix the list in one pass.
## Read state
`isUnread` on an email means "this message arrived after the last time its
conversation was read". A conversation can therefore hold both read and unread
messages at once, which is exactly what you want when polling: it tells you which
replies are new, not merely which conversations have something new in them.
You **read** that state per email, and you **clear** it per conversation.
`POST /v1/threads/{id}/read` marks every email in a conversation as read. It is
the only write there is for read state, and it is the same action a person takes
by opening the conversation in the dashboard, so what you do through the API and
what your team sees stay in step.
```bash theme={null}
curl -X POST https://api.mailbeast.ai/v1/threads/{id}/read \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
## Delete an email
`DELETE /v1/emails/{id}` removes an email from your inbox. It stops appearing in
listings. The copy on the mail server is left alone.
# Idempotency
Source: https://developer.mailbeast.ai/idempotency
Retry writes safely - 24h response replay, plus durable dedup for quota and leads.
Network calls fail in ambiguous ways - a request times out, but you can't tell
whether the server processed it. Sending an `Idempotency-Key` lets you retry
safely: within a 24-hour window the API replays the original response instead of
running the operation again.
Send the header on any mutating request (`POST`, `PATCH`, `DELETE`). If you retry
with the **same key**, you get the original response back:
```bash theme={null}
curl https://api.mailbeast.ai/v1/campaigns \
-H "Authorization: Bearer mb_live_…" \
-H "Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Content-Type: application/json" \
-d '{ "name": "Q3 outreach" }'
```
Retry that request within the window and the second call returns the same body
with an extra header instead of creating another campaign:
```
X-Idempotency-Replayed: true
```
Idempotency is **opt-in**. A request with no `Idempotency-Key` header behaves
normally - nothing is cached and every call runs. Reads (`GET`) are already
idempotent and ignore the header.
## How it works
* **Generate a unique key per operation** - a UUID is ideal. Use one key for one
logical write, and reuse it only when retrying that same write.
* **Keys are scoped to your workspace** - a key you use never collides with
another workspace's, and the scope is derived server-side from your API key.
* **Only successful (`2xx`) responses are cached.** If the first attempt failed,
retrying with the same key genuinely re-runs the operation.
**Exception - creating an API key is *not* idempotent.** Its response contains a
one-time token that is never stored in retrievable form, so it cannot be replayed.
An `Idempotency-Key` on `POST /v1/api-keys` is ignored, and a retry mints a **new**
key - revoke any extras from the [list endpoint](/api-reference/api-keys/list-api-keys).
## Edge cases
| Situation | Result |
| -------------------------------------------- | ------------------------------------------------------------ |
| Same key, **same** body | Original response replayed (`X-Idempotency-Replayed: true`) |
| Same key, **different** body | `422` - the key is already bound to a different request |
| Same key, two requests **in flight** at once | The second returns `409` while the first is still processing |
| Key longer than 255 characters | `400` |
A `422` from a body mismatch looks like this:
```json theme={null}
{
"error": {
"type": "invalid_request_error",
"code": "idempotency_key_reused",
"message": "Idempotency-Key already used with a different request body.",
"status": 422,
"requestId": "…"
}
}
```
# Overview
Source: https://developer.mailbeast.ai/introduction
One API for the full local lead-gen & cold-outreach loop.
The MailBeast API is a **task-shaped** REST API: each endpoint maps to a job you
actually do - connect a mailbox, launch a campaign, load leads, read replies, discover new leads - not to an internal database table.
`https://api.mailbeast.ai`
`Authorization: Bearer mb_live…`
## Design principles
These principles shape every endpoint as it ships:
* **Composites over ceremony.** Create a fully-configured, optionally-live
campaign in **one** call instead of six.
* **One-or-many bodies.** Ingest endpoints accept a single object *or* an array
of the same shape.
* **Cursor pagination** on every list. **Least-privilege scopes** on every route.
* **One error envelope** everywhere: `{ "error": { "type", "code", "message", "status", "requestId" } }`,
plus an optional `details[]` on validation errors and `retryAfter` on `429`s.
Create a key and make your first authenticated call in two steps.
# Lead Finder
Source: https://developer.mailbeast.ai/lead-finder
List past lead searches and poll a running search's progress.
Lead Finder discovers new companies and leads from a natural-language query. This first
release exposes the two **reads** you need to follow a search that was started in the
dashboard: list your past searches, and poll one search's status and progress while it
runs.
Starting, stopping, and pulling results from a search over the API is coming in a later
release. For now you create and drive a search in the dashboard, and read its state here.
## Scopes
| Action | Scope |
| ------------------------------ | ----------------- |
| List searches, read one search | `leadfinder:read` |
### Pagination
The list is cursor-paginated and stable under concurrent changes. When `meta.hasMore` is
`true`, pass `meta.nextCursor` back as `?cursor=` to fetch the next page; it is `null` on
the last page. Cursors are opaque – do not parse or build them by hand.
```bash theme={null}
curl "https://api.mailbeast.ai/v1/lead-searches?cursor=eyJ0cyI6..." \
-H "Authorization: Bearer mb_live_..."
```
`limit` defaults to 50 and caps at 100.
## Get one search
```bash theme={null}
curl "https://api.mailbeast.ai/v1/lead-searches/{id}" \
-H "Authorization: Bearer mb_live_..."
```
This is the **single poll target** while a search runs. It returns the search's status
plus live progress counters.
```json theme={null}
{
"id": "a7d4e2f1-08b3-4c65-9e7a-1b2c3d4e5f60",
"query": "dental clinics in Berlin",
"status": "processing",
"searchMode": "companies",
"maxResults": 500,
"discoveryFinished": false,
"progress": {
"companiesFound": 84,
"companiesProcessed": 51,
"emailsFound": 137
},
"errorMessage": null,
"createdAt": "2026-07-10T12:00:00.000Z",
"updatedAt": "2026-07-10T12:15:00.000Z",
"completedAt": null
}
```
# Leads
Source: https://developer.mailbeast.ai/leads
Add, read, update and bulk-manage a campaign's leads.
Leads live under a campaign. Add them, sync their state to your CRM, and manage
them in bulk. Adding leads counts toward your monthly-imports quota.
## Scopes
| Action | Scope |
| ----------------------------------- | ------------- |
| List / get / search leads | `leads:read` |
| Add / update / delete / bulk-mutate | `leads:write` |
## Add leads
Add 1-100 leads in one call. Each is **deduplicated by email** within the
campaign and provider-detected. The response summarizes the outcome and returns
the leads that were created.
```bash theme={null}
curl -X POST https://api.mailbeast.ai/v1/campaigns/{cid}/leads \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{ "email": "jane@acme.com", "firstName": "Jane", "companyName": "Acme",
"tags": ["vip"], "customFields": { "industry": "SaaS" } },
{ "email": "sam@globex.com", "firstName": "Sam" }
]
}'
```
```json Response theme={null}
{
"summary": { "submitted": 2, "created": 2, "duplicates": 0, "invalid": 0 },
"leads": [ { "id": "…", "email": "jane@acme.com", "status": "not_contacted", "…": "…" } ]
}
```
## List and filter
**Cursor-paginated**, newest-first, and stable under concurrent inserts/deletes -
the right shape for syncing leads to a CRM (offset pages would skip or duplicate
rows as leads change).
```bash theme={null}
# first page
curl "https://api.mailbeast.ai/v1/campaigns/{cid}/leads?limit=50&status=lead_replied_interested" \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
# next page - pass the previous response's meta.nextCursor
curl "https://api.mailbeast.ai/v1/campaigns/{cid}/leads?limit=50&cursor=eyJ0cyI6IjIwMjYtMDctMDJUMDA6MDA6MDAuMDAwWiIsImlkIjoiM2YxYzJiN2UtOWE0ZC00ZTE4LWIyYzEtNmQ4ZjVhMGU3YzkzIn0" \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
```json Response theme={null}
{
"data": [ { "id": "…", "email": "jane@acme.com", "…": "…" } ],
"meta": { "hasMore": true, "nextCursor": "eyJ0cyI6IjIwMjYtMDctMDJUMDA6MDA6MDAuMDAwWiIsImlkIjoiM2YxYzJiN2UtOWE0ZC00ZTE4LWIyYzEtNmQ4ZjVhMGU3YzkzIn0" }
}
```
Filter by `status`, `tags`, or `search` (email / name / company). Keep requesting
with `?cursor=meta.nextCursor` until `meta.hasMore` is `false` (then `nextCursor`
is `null`). `limit` defaults to 50 (max 250).
## Get one lead
Returns the lead plus **live sequence progress** (steps sent/planned, next send).
```bash theme={null}
curl https://api.mailbeast.ai/v1/campaigns/{cid}/leads/{leadId} \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
## Update a lead
```bash theme={null}
curl -X PATCH https://api.mailbeast.ai/v1/campaigns/{cid}/leads/{leadId} \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "firstName": "Janet", "customFields": { "tier": "gold" } }'
```
Changing a lead's **email** safely re-runs the add / dedup / provider-detect
pipeline, and is only allowed while the lead has not been contacted.
## Bulk mutate
One operation over an id list - `set_status`, `add_tags`, `remove_tags`, or
`delete`. `delete` also accepts a `filter` instead of ids.
```bash theme={null}
# add a tag to specific leads
curl -X POST https://api.mailbeast.ai/v1/campaigns/{cid}/leads/mutate \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "operation": "add_tags", "ids": ["…", "…"], "tags": ["hot"] }'
# delete every lead matching a filter
curl -X POST https://api.mailbeast.ai/v1/campaigns/{cid}/leads/mutate \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "operation": "delete", "filter": { "status": "lead_replied_unsubscribed" } }'
# delete EVERY lead - explicit opt-in (an empty filter will NOT do this)
curl -X POST https://api.mailbeast.ai/v1/campaigns/{cid}/leads/mutate \
-H "Authorization: Bearer $MAILBEAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "operation": "delete", "all": true }'
```
## Search across campaigns
Find a lead by email across every campaign in the workspace - one row per
campaign the email appears in.
```bash theme={null}
curl "https://api.mailbeast.ai/v1/leads/search?email=jane@acme.com" \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
# Quickstart
Source: https://developer.mailbeast.ai/quickstart
From zero to your first authenticated call in two steps.
In the dashboard, open [**Settings → API Keys**](https://app.mailbeast.ai/settings/api), click **Create API key**,
include the **`apikeys:manage`** scope (plus any scopes you want to delegate,
e.g. `campaigns:read`), and copy the secret - it is shown only once.
List the keys in your workspace. A `200` with your key in the response
confirms the token and scope are resolved correctly.
```bash theme={null}
curl https://api.mailbeast.ai/v1/api-keys \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
```json Response theme={null}
{
"data": [
{
"id": "b6fd2c26-…",
"name": "Bootstrap",
"keyPrefix": "mb_live_7Fq2Ka9Lm3Xb0ZpH_••••",
"scopes": ["apikeys:manage", "campaigns:read"],
"environment": "live",
"lastUsedAt": null,
"expiresAt": null,
"revokedAt": null,
"createdAt": "2026-07-11T09:12:04.000Z"
}
]
}
```
For security reasons, the API key will be displayed only once, and there is no way to recover it if you lose it.
# Workspace & Usage
Source: https://developer.mailbeast.ai/workspace
Read your limits, subscription, and plan - know what's left before you hit a limit.
These endpoints let you read your own limits, subscription, and plan. They're all
read-only. Use them to see how much you have left **before** a request would hit a
plan limit (`402`) or the rate limit (`429`) - so nothing catches you off guard.
## Scopes
| Action | Scope |
| --------------------------------------- | ------------ |
| Read usage, subscription, and workspace | `usage:read` |
## Check your usage before you spend
`GET /v1/usage` returns how much you've used and have left, grouped by product.
Email Outreach and Lead Finder are separate products, so each is reported on its
own - your Email Outreach plan limits and your Lead Finder credit balance.
```bash theme={null}
curl https://api.mailbeast.ai/v1/usage \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
```json theme={null}
{
"emailOutreach": {
"planName": "pro",
"limits": [
{ "limitType": "monthly_emails", "used": 1240, "limit": 100000, "remaining": 98760, "isUnlimited": false, "usagePercentage": 1.24 },
{ "limitType": "active_contacts", "used": 5000, "limit": 25000, "remaining": 20000, "isUnlimited": false, "usagePercentage": 20 }
]
},
"leadFinder": {
"credits": { "limitType": "lead_finder_credits", "used": 0, "limit": 500, "remaining": 500, "isUnlimited": false, "usagePercentage": 0 }
}
}
```
Before a large lead import or a send, read the relevant limit and only proceed
while `remaining` is comfortable. This turns a `402` from a surprise into a
decision you control.
## Subscription: everything you pay for, in one place
`GET /v1/account/subscription` groups your billing by product. **Email Outreach**
and **Lead Finder** are independent subscriptions, each with its own billing cycle
and renewal date.
```bash theme={null}
curl https://api.mailbeast.ai/v1/account/subscription \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
```json theme={null}
{
"emailOutreach": {
"planName": "pro",
"status": "active",
"billingInterval": "monthly",
"currentPeriodEnd": "2026-08-01T00:00:00.000Z",
"cancelAtPeriodEnd": false,
"trial": null,
"addons": {
"contacts": 20000,
"monthlyEmails": 10000,
"aiCredits": 5000,
"emailValidation": 0
}
},
"leadFinder": {
"tier": "pro",
"status": "active",
"billingInterval": "annual",
"currentPeriodEnd": "2027-02-01T00:00:00.000Z",
"cancelAtPeriodEnd": false
}
}
```
* **Your Email Outreach status** - `active`, `on_hold`, or `inactive` when you have
no active subscription (for example, on the free plan).
* **`currentPeriodEnd`** - when each product's current paid period ends. It renews
then, or ends if `cancelAtPeriodEnd` is `true`, and is `null` when there's no
active subscription. Each product has its own, so they can differ.
* **Your Email Outreach add-ons** - the extra capacity you've purchased, in units.
They're already folded into the caps in `GET /v1/usage`, so this is just the
breakdown. `null` when you have no add-ons, and while the plan is on hold, since
the extra capacity isn't active then either.
* **Your Lead Finder subscription** - billed separately from Email Outreach.
`null` if you don't have one. The Lead Finder credit balance itself lives in
`GET /v1/usage` under `leadFinder.credits`.
## Workspace identity
`GET /v1/workspace` returns the workspace resolved from your key: org name and
timezone. Your plan lives in [`GET /v1/usage`](#check-your-usage-before-you-spend)
and [`GET /v1/account/subscription`](#subscription-everything-you-pay-for-in-one-place).
```bash theme={null}
curl https://api.mailbeast.ai/v1/workspace \
-H "Authorization: Bearer $MAILBEAST_API_KEY"
```
```json theme={null}
{
"organizationId": "…",
"name": "Acme Inc",
"timezone": "America/New_York"
}
```