# 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). > [!NOTE] > > 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: **1. Start the connection** `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 } ``` **2. Send the mailbox owner to the consent URL** Open `authorizationUrl` in a browser. After they grant access, the provider redirects back to MailBeast, which exchanges the code and connects the mailbox. **3. Poll for the result** `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 } ``` > [!NOTE] > > 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 } } } ``` > [!NOTE] > > **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_..." ``` > [!WARNING] > > 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. ### Subsequences A campaign with [subsequences](/subsequences) also gets `subsequences` - one entry per subsequence, with all-time counters (not limited to the period): ```json theme={null} { "subsequences": [ { "id": "5b2f3c4d-…", "name": "Interested - book a call", "status": "active", "entered": 42, "inProgress": 12, "replied": 9, "positive": 5, "positiveRate": 55.6 } ] } ``` `positiveRate` is positive leads among those who replied (`null` when nobody did). `excludeOOO=true` applies here too: leads whose status is Out of office are not counted in `replied`. By default the campaign's `totals` and breakdowns cover the campaign **together with its subsequences** (deleted subsequences included – their emails were really sent). Nothing is counted twice: * every email event (sent, open, click, reply, bounce, unsubscribe) belongs to the sequence that sent the email; * a positive lead, a conversion and its value belong to the sequence whose email the lead replied to. If that subsequence was deleted before the lead's status changed, they count for the main sequence; what was recorded before the deletion stays with the deleted subsequence; * a person counts once in the campaign's lead counts, even after going through the main sequence and a subsequence. So over the campaign's whole life, the main sequence plus every subsequence - deleted ones included - adds up to the campaign's `sent`, `delivered`, `bounces`, `unsubscribed`, `opens.total`, `clicks.total`, `positiveLeads`, `conversions` and `conversionValue`. Deleted subsequences are in the campaign's totals only: they are not listed in `subsequences`. For a campaign that had a deleted subsequence, the main sequence plus the listed subsequences can therefore be lower than the campaign's totals - the difference is the deleted subsequences' share. Lead counts do not add up: `leadsContacted`, `opens.uniqueLeads`, `clicks.uniqueLeads` and `replies` (leads who replied, not reply emails) count a person once in each sequence and once in the campaign. A lead who replied in the main sequence and in a subsequence is 1 + 1 across the sequences and 1 in the campaign's `replies`, so the campaign can be lower than the sum of its parts. With `startDate` / `endDate`, opens, clicks and replies count the leads emailed in the period and their engagement: for the campaign, anyone it emailed in the period; for one sequence, only the leads that sequence emailed. In a period these numbers of the campaign can therefore also be higher than the sum of its parts. | Parameter | Covers | | - | - | | *(none)* or `scope=family` | The campaign with its subsequences. | | `scope=main` | The main sequence only. | | `subsequenceId={id}` | One subsequence (id from `GET /v1/campaigns/{id}/subsequences`). | The same parameters work on `/analytics/timeseries`. With `breakdown=sequence`, the steps are those of the main sequence, or of the subsequence when `subsequenceId` is set. A subsequence id is not accepted as a campaign id here (`404`). ## 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. A campaign's emails include its subsequences, and a person is counted once in the lead totals even if they went through several sequences of one 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 } } ``` > [!WARNING] > > 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 – by default together with its subsequences (`scope=main` for the main sequence only, `subsequenceId` for one subsequence). Ask for extra breakdowns with `breakdown=smtp,sequence,bounce` – each is an extra query, so request only what you will read. `sequence` lists the steps of the main sequence, or of the subsequence when `subsequenceId` is set. A campaign with subsequences also gets `subsequences`: entered / in progress / replied / positive per subsequence. 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. # Delete an API key Source: https://developer.mailbeast.ai/api-reference/api-keys/delete-an-api-key /openapi.json delete /v1/api-keys/{id} Delete a key by id. It stops working immediately and is removed from the list. Cannot be undone. 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. # 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 (with A/B variants), sending accounts, tags, 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. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. `409 campaign_state_changed` = it changed (for example completed) while the request ran; read it again. 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. Returns 400 if the workspace has no email account at all; an account that exists but cannot send right now (disconnected, reauthorizing, DNS below the sending floor) does not block the resume. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. `409 campaign_state_changed` = it changed (for example completed) while the request ran; read it again. 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. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. `409 campaign_state_changed` = it changed (for example completed) while the request ran; read it again. 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`, `tags` 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`. # 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 and provider-detected. 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, engagement: opened / clicked / not opened). 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`. # Create a subsequence Source: https://developer.mailbeast.ai/api-reference/subsequences/create-a-subsequence /openapi.json post /v1/campaigns/{id}/subsequences Create a subsequence of a campaign - OFF (it sends nothing until `POST /v1/subsequences/{id}/enable`). It sends from the same mailbox and in the same thread as the conversation with the lead, and inherits the campaign's mailboxes, tracking and stop rules. Name, settings and emails are all-or-nothing: if any part is invalid, nothing is created. Needs a plan with subsequences (Pro+ and above) - otherwise `403` with code `subsequences_plan_required`. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. `409 subsequence_limit_reached` = the campaign already has the maximum number of subsequences. Requires one of the following scopes: `campaigns:write`, `campaigns:all`, `all:all`. # Delete a subsequence Source: https://developer.mailbeast.ai/api-reference/subsequences/delete-a-subsequence /openapi.json delete /v1/subsequences/{id} Delete a subsequence. Its scheduled emails are canceled; leads in it do not return to the main sequence. Turning off (or deleting) the campaign's last subsequence that is on lets a campaign whose main sequence has finished complete on its own (unless it keeps active when leads run out); /enable then needs a restart in the dashboard. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. Requires one of the following scopes: `campaigns:write`, `campaigns:all`, `all:all`. # Get a subsequence Source: https://developer.mailbeast.ai/api-reference/subsequences/get-a-subsequence /openapi.json get /v1/subsequences/{id} One subsequence: trigger, start and sending settings, every email step with its A/B variants, whether it is on and why it is (not) sending, and its counters. Requires one of the following scopes: `campaigns:read`, `campaigns:all`, `all:read`, `all:all`. # List subsequences of a campaign Source: https://developer.mailbeast.ai/api-reference/subsequences/list-subsequences-of-a-campaign /openapi.json get /v1/campaigns/{id}/subsequences The follow-up flows of a campaign that start on a lead status, a reply, an open, a click or a finished sequence - each with its trigger, start and sending settings, its emails, whether it is on, and counters (entered, in progress, replied, positive). Requires one of the following scopes: `campaigns:read`, `campaigns:all`, `all:read`, `all:all`. # Move leads into a subsequence Source: https://developer.mailbeast.ai/api-reference/subsequences/move-leads-into-a-subsequence /openapi.json post /v1/subsequences/{id}/leads Move leads of the main campaign into the subsequence, by `leadIds` or `emails` (up to 1000). Their remaining main-sequence emails are canceled and the subsequence emails them instead (real email), from the same mailbox and in the same thread. Only leads that were already emailed can enter, each subsequence once, one subsequence at a time per lead; the response counts who entered and why the others did not. A subsequence that is off (paused or never turned on) takes them too: they wait and start when you turn it on. Needs a plan with subsequences (Pro+ and above) - otherwise `403` with code `subsequences_plan_required`. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`. # Pause a subsequence Source: https://developer.mailbeast.ai/api-reference/subsequences/pause-a-subsequence /openapi.json post /v1/subsequences/{id}/pause Turn a subsequence off: no new leads enter, and leads already in it wait (nothing more is sent to them) until it is resumed. The main sequence keeps sending. Turning off (or deleting) the campaign's last subsequence that is on lets a campaign whose main sequence has finished complete on its own (unless it keeps active when leads run out); /enable then needs a restart in the dashboard. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`. # Resume a subsequence Source: https://developer.mailbeast.ai/api-reference/subsequences/resume-a-subsequence /openapi.json post /v1/subsequences/{id}/resume Turn a paused subsequence back on - the same as `/enable`: leads that waited continue (real email), new matching leads enter again. `enrollExisting: true` also enrolls the leads that matched while it was off. Needs a plan with subsequences (Pro+ and above) - otherwise `403` with code `subsequences_plan_required`. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`. # Turn a subsequence on Source: https://developer.mailbeast.ai/api-reference/subsequences/turn-a-subsequence-on /openapi.json post /v1/subsequences/{id}/enable Turn a subsequence on: leads that match its trigger from now on enter it and get its emails (real email). With `enrollExisting: true` the leads that already match enter too. On a draft campaign it starts together with the campaign. A completed campaign must be restarted in the dashboard first (`409 parent_completed`); a stopped or deleted one cannot get new subsequence leads (`409 family_ended`). Needs at least one email (400). Needs a plan with subsequences (Pro+ and above) - otherwise `403` with code `subsequences_plan_required`. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`. # Update a subsequence Source: https://developer.mailbeast.ai/api-reference/subsequences/update-a-subsequence /openapi.json patch /v1/subsequences/{id} Partial update - only what you pass changes. Each `settings` section (`trigger`, `start`, `sending`) and `sequence` REPLACE the current one. The emails of a running subsequence cannot be replaced - pause it first. Needs a plan with subsequences (Pro+ and above) - otherwise `403` with code `subsequences_plan_required`. `409 campaign_busy` = the campaign is being changed right now; retry in a few seconds. Requires one of the following scopes: `campaigns:write`, `campaigns: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, deleted or expired 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 and their subsequences | | `campaigns:lifecycle` | Start / pause / resume sending, turn subsequences on and off, move leads into a subsequence (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 / delete 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`. > [!NOTE] > > `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 } } ``` > [!TIP] > > 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** (with A/B variants), the sending **accounts**, its **tags**, 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` | > [!NOTE] > > 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 > [!NOTE] > > By default a new campaign sends from **all** connected mailboxes of your workspace > (`settings.useAllAccounts: true`). To send only from specific mailboxes, pass their > ids in `accounts` and set `settings.useAllAccounts` to `false` - then `accounts` > must hold at least one id. While `useAllAccounts` is `true`, `accounts` does not > limit which mailboxes send. One call configures everything. The configuration - campaign, schedule, settings, sending accounts, tags, and sequence - is **all-or-nothing**: if any part fails validation (for example an SMTP account or a tag that isn't yours, or a step after the first with a delay under 1 hour), nothing is created. `accounts` and `tags` must be your workspace's own. `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, "useAllAccounts": false, "skipRiskyEmails": true, "unsubscribeLinkHeader": true, "abTestingOptimization": true }, "sequence": [ { "variants": [ { "subject": "Quick question about {{company_name}}", "body": "

Hi {{first_name}}, …

" }, { "subject": "{{first_name}}, an idea for {{company_name}}", "body": "

Hi {{first_name}}, …

" } ] }, { "subject": "Following up", "body": "

Just circling back.

", "delayDays": 3 } ], "accounts": ["123e4567-e89b-12d3-a456-426614174000"], "tags": ["8debf023-6c1e-4b7a-9f3d-2a1b0c9d8e7f"], "leads": ["jane@acme.com", "sam@globex.com"], "launch": false }' ``` > [!NOTE] > > `sendingMinutesStart` / `sendingMinutesEnd` are minutes from midnight in the > campaign timezone, in 30-minute steps - `540` = 09:00, `1020` = 17:00, `1440` = > midnight, and the start must be before the end. Step delays are counted from > the previous step; the first step sends immediately when a lead enters. > `endDate` is optional: the campaign sends until the end of that day in its > timezone and then completes, and it can't be before `startDate`. On `PATCH`, > `"endDate": null` removes it. ### Campaigns that keep receiving leads By default a campaign completes once every lead has received every step. Leads added after that are not sent until someone restarts the campaign in the dashboard - the API cannot restart a completed campaign. If leads keep arriving over time - for example an automation that calls `POST /v1/campaigns/{id}/leads` every day - set `"keepActiveWhenNoLeads": true` in `settings` (in the dashboard: Settings → Advanced → Campaign Completion): ```json theme={null} "settings": { "keepActiveWhenNoLeads": true } ``` The campaign then stays `active` when it runs out of leads and starts scheduling new leads as soon as they are added - no restart needed. It still completes on its `endDate`, and you can pause or delete it as usual. Switch it on for an existing campaign with `PATCH /v1/campaigns/{id}` before its leads run out - it does not reopen a campaign that has already completed. `/start` needs at least one lead, so add the first leads before starting; an empty campaign cannot be started to wait for them. ### Per-day windows (advanced schedule) Instead of one window for all `sendingDays`, you can give each day its own windows - the same "Advanced schedule" as in the dashboard. Pass `intervals` (this implies `"mode": "advanced"`); `day` is `1` = Monday … `7` = Sunday and times are minutes from midnight in the campaign timezone. A shifted week - Monday from 09:30, Tuesday from 10:30, and so on: ```json theme={null} "schedule": { "timezone": "America/New_York", "intervals": [ { "day": 1, "startMinutes": 570, "endMinutes": 1050 }, { "day": 2, "startMinutes": 630, "endMinutes": 1110 }, { "day": 3, "startMinutes": 690, "endMinutes": 1170 }, { "day": 4, "startMinutes": 750, "endMinutes": 1230 }, { "day": 5, "startMinutes": 810, "endMinutes": 1290 } ] } ``` Rules (a violation is a `400`, nothing is changed): * up to 50 windows; a day may have several, but they must not overlap; * start and end on a 30-minute step, each window at least 30 minutes long; * at least one window must be enabled (`enabled` defaults to `true`). `intervals` **replaces** all existing windows. `"mode": "simple"` switches back to `sendingDays` + `sendingMinutesStart`/`sendingMinutesEnd` and keeps the windows for later; `"mode": "advanced"` alone re-enables the saved ones. Every campaign response reports `schedule.mode` and `schedule.intervals`. ### A/B variants A step is either one email (`subject` + `body`) or an A/B test: `variants`, up to **5**, lettered `A`, `B`, `C`… in the order you pass them - the same as the sequence editor in the dashboard. Leads are split **evenly** between the active variants of a step (two → 50% each, three → 33.33%). A variant with `"isActive": false` is kept but not sent; at least one per step must stay active. A variant without `isActive` is on - when you send variants back, pass each one's `isActive` as the campaign shows it, or a switched-off variant starts sending again. ```json theme={null} "sequence": [ { "variants": [ { "subject": "Quick question about {{company_name}}", "body": "

…

" }, { "subject": "Idea for {{company_name}}", "body": "

…

" }, { "subject": "{{first_name}}?", "body": "

…

", "isActive": false } ] }, { "subject": "Following up", "body": "

…

", "delayDays": 3 } ] ``` Pass either `variants` or `subject` + `body` on a step, not both (`400`). A step read back from `GET` shows its variant A as `subject` + `body` next to `variants`; when you send the step back, send only its `variants`. With `settings.abTestingOptimization: true`, once every active variant of a step has at least 1000 sends, the variant with the best reply rate is kept and the others are switched off - `variants[].isActive` shows the result. ### Settings `settings` holds everything on the campaign's **Settings** page. Every field is optional; on `PATCH` only the fields you pass change. A new campaign starts with the defaults below. | Field | Default | What it does | | - | - | - | | `useAllAccounts` | `true` | Send from every connected mailbox, or (`false`) only from `accounts`. | | `stopOnReply` | `true` | Stop follow-ups to a lead once they reply. | | `stopOnBounce` | `true` | Stop sending to a lead whose email hard-bounced. | | `stopCompanyOnReply` | `true` | A reply stops the sequence for every lead of the same company email domain (public domains such as gmail.com never match). | | `highBounceAutoProtection` | `false` | Pause the campaign when its hard-bounce rate over the last 24 hours goes above 5%. | | `skipInvalidEmails` | `true` | Skip leads whose email verification is `invalid`. | | `skipUnknownEmails` | `true` | Skip leads whose email verification is `unknown`. | | `skipRiskyEmails` | `false` | Skip leads whose email verification is `risky` (catch-all domain). | | `trackOpens` | `false` | Track opens. Not available with `sendAsTextOnly`. | | `trackClicks` | `false` | Track link clicks. Not available with `sendAsTextOnly`. | | `sendAsTextOnly` | `false` | Send plain text only, no HTML part. | | `sendFirstAsText` | `false` | Send the first step as plain text, follow-ups as HTML. The first email is then not tracked and has no body unsubscribe link (the `List-Unsubscribe` header still applies). | | `emailProviderMatching` | `false` | Prefer a sending mailbox on the lead's provider (Google to Google, Outlook to Outlook). | | `unsubscribeLinkHeader` | `false` | Add the one-click `List-Unsubscribe` header (RFC 8058). | | `unsubscribeLinkBody` | `false` | Add an unsubscribe link at the bottom of the email body. | | `autoBlacklistOnUnsubscribe` | `false` | Also add a lead who unsubscribes to the workspace blacklist. | | `newLeadsRatio` | `80` | Percent (0-100) of daily sending that goes to first emails for new leads; the rest is follow-ups. `0` contacts no new leads. | | `abTestingOptimization` | `false` | Keep the best-replying variant of each step automatically (see above). | | `hubspotSyncEnabled` | `true` | Let the recurring HubSpot sync add contacts to this campaign (only matters when HubSpot is connected). | | `keepActiveWhenNoLeads` | `false` | Keep the campaign active when it runs out of leads and schedule leads added later (see [Campaigns that keep receiving leads](#campaigns-that-keep-receiving-leads)). | The text modes follow the dashboard's rules: `sendAsTextOnly` and `sendFirstAsText` exclude each other - turning one on turns the other off, and both `true` in one request is a `400`. A plain-text-only email has no HTML for an open pixel, tracked links or a body unsubscribe link, so turning `sendAsTextOnly` on turns `trackOpens`, `trackClicks` and `unsubscribeLinkBody` off, and asking for any of them as `true` while it is on is a `400`. To track the follow-ups but keep the first email plain, use `sendFirstAsText`. Every field may be left out, and `null` counts as left out (the current value stays) everywhere except `schedule.endDate`, where `null` removes the end date. 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, "stopOnReply": true, "useAllAccounts": false, "skipRiskyEmails": true, "…": "every field from the table above" }, "metrics": { "leadsCount": 2, "sent": 0, "opens": 0, "clicks": 0, "replies": 0 }, "sequence": [ { "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "subject": "Quick question about {{company_name}}", "body": "

Hi {{first_name}}, …

", "delayDays": 0, "delayHours": 0, "variants": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "label": "A", "subject": "Quick question about {{company_name}}", "body": "…", "isActive": true }, { "id": "6c1e8b2a-4f3d-4e5a-9b8c-7d6e5f4a3b2c", "label": "B", "subject": "{{first_name}}, an idea for {{company_name}}", "body": "…", "isActive": true } ] } ], "accounts": ["123e4567-e89b-12d3-a456-426614174000"], "tags": [ { "id": "8debf023-6c1e-4b7a-9f3d-2a1b0c9d8e7f", "name": "q4-launch", "color": "#10B981" } ] }, "leads": { "submitted": 2, "created": 2, "duplicates": 0, "invalid": 0 }, "launched": false } ``` ## Subsequences A campaign can have [subsequences](/subsequences): follow-up flows that start when a lead replies, gets a status (e.g. *Interested*, *Out of office*), opens, clicks, or finishes the sequence without replying. `GET /v1/campaigns/{id}` lists them as `"subsequences": [{ "id", "name", "status" }]` (an empty array when there are none); manage them under `/v1/campaigns/{id}/subsequences` and `/v1/subsequences/{id}`. A subsequence is not a campaign in this API: it is not in `GET /v1/campaigns`, and `/v1/campaigns/{id}…` answers `404` for its id. ## 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 (unless `settings.keepActiveWhenNoLeads` is on - then only the end date completes it). ```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`). > [!NOTE] > > 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`). `/start`, `/pause` and `/resume` answer `409 campaign_busy` when the campaign is being changed right now - retry after `Retry-After` - and `409 campaign_state_changed` when it changed (for example completed) while the request ran - read it again before retrying. ## 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. Take the `variantId` from `sequence[].variants[].id` on the campaign (`GET /v1/campaigns/{id}`, or the create response). A step written with `subject` + `body` has one variant, labelled `A`; an A/B step has one per variant. Replacing the `sequence` with `PATCH` rebuilds the steps, so step and variant ids change - read the campaign again before sending a test. ```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. `lead` uses a real lead, which you name with `leadId`. `dummy` uses no lead data at all: lead tags such as `{{first_name}}` render **empty** (only the sender tags resolve) - use `lead` to check personalization. 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. > [!NOTE] > > 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 API reads them, lets you filter by them - so "the campaigns for client X" is a single call rather than a full page-through - and puts them on a campaign: pass tag ids in `tags` on create, or on `PATCH` to replace the set (`"tags": []` removes them all). ```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" ``` > [!NOTE] > > Tags are **created, renamed, and deleted in the dashboard**, not through the API. > > 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. `isWithinSchedule` / `nextScheduleOpensAt` are computed for any status - a draft or paused campaign reports whether "now" falls inside its window, not whether it is sending. Campaign dates count: before `startDate` or after the end of the `endDate` day (campaign timezone) the window is closed, and `nextScheduleOpensAt` is never past that day (`null` instead): ```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`, `tags`, or `sequence` **replaces** that section entirely (the sequence is replaced in place - you always end up with exactly the steps and variants you sent; to keep a step's A/B test, send its `variants` again). A supplied `sequence` must have **at least one step** - an empty array is rejected (`400`); omit the field to leave the sequence unchanged. From step 2 on, `delayDays` + `delayHours` must add up to at least 1 hour (`400`, nothing is written). ```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 } }' ``` > [!NOTE] > > 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 - a `PATCH` that includes it is rejected (`400`) before anything is written. `accounts` and `settings.useAllAccounts` **can** be updated live; with > `useAllAccounts: false` the campaign sends only from `accounts`, which then may not > end up empty. # 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` | | Mark a thread read | `emails:write` | | Reply and forward | `emails:send` | > [!NOTE] > > **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", "subsequence": null, "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. An email of a [subsequence](/subsequences) has the main campaign's `campaignId`, the lead's id in that campaign as `leadId`, and `subsequence` (`id`, `name`); other emails have `"subsequence": null`. So `campaignId` and `leadId` filters include the emails of the campaign's subsequences. ### 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" ``` # 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 ``` > [!NOTE] > > 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. > [!NOTE] > > **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 - delete any extras ([list](/api-reference/api-keys/list-api-keys) them first). ## 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` | | `reply` / `forward` / `test-email` while duplicate protection is briefly unavailable | `503` with `Retry-After` - **nothing was sent**, retry with the same key | > [!NOTE] > > **Email sends never run unprotected.** Other writes go through even if duplicate > protection is briefly unavailable - a stray duplicate draft is easy to clean up. > An email cannot be unsent, so `POST /v1/emails/{id}/reply`, `/forward` and > `POST /v1/campaigns/{id}/test-email` sent with an `Idempotency-Key` refuse with > `503` instead. The retry is safe. 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. > [card] Base URL > > `https://api.mailbeast.ai` > [card] Auth > > `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. > [card] [Get started](/quickstart) > > 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. > [!NOTE] > > 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. ## 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`, `search` (email / name / company), or `engagement`. Keep requesting with `?cursor=meta.nextCursor` until `meta.hasMore` is `false` (then `nextCursor` is `null`). `limit` defaults to 50 (max 250). ### Engagement Every lead carries its engagement, counted **once per email**: | Field | Meaning | | - | - | | `metrics.sent` | Emails that left our servers for the lead (bounced ones included). | | `metrics.opened` | Emails the lead opened. | | `metrics.clicked` | Emails in which the lead clicked a tracked link. | | `lastOpenedAt` / `lastClickedAt` | Latest of the per-email first opens / first clicks: when the lead first opened (clicked in) the newest email it opened (clicked). Re-opens and repeat clicks don't move it; `null` when there is none. | Automated opens and clicks by security scanners and link prefetchers are excluded. Opens are approximate by nature: Apple Mail and some security gateways load images on their own, while clients that block images can hide opens. `?engagement=` narrows the list: | Value | Leads who… | | - | - | | `opened` | opened at least one email | | `opened_no_reply` | opened, and have not replied. Any reply counts, including out-of-office and other auto-replies. | | `clicked` | clicked a tracked link | | `not_opened` | were sent at least one email with open tracking and opened none | ```bash theme={null} curl "https://api.mailbeast.ai/v1/campaigns/{cid}/leads?engagement=opened_no_reply" \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` > [!NOTE] > > Engagement reaches the lead within about a minute of the open or click. ## 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" } }' ``` > [!NOTE] > > 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" ``` # MCP server Source: https://developer.mailbeast.ai/mcp-server Connect Claude, ChatGPT, Cursor and other AI assistants to your MailBeast workspace. The MailBeast MCP server lets an AI assistant work in your workspace: build and launch campaigns, add leads, check mailboxes and warmup, read and answer replies, and pull analytics - by asking in plain language. ``` https://mcp.mailbeast.ai/mcp ``` Add this URL to your AI app and sign in with your MailBeast account. You choose the workspace and what the assistant may do; no API key to copy. ## Requirements * A MailBeast workspace on the **Pro+** plan (or an enterprise plan with API access) - the same access as the [REST API](/authentication). * You are an **owner or admin** of that workspace. Other roles can't connect apps. * To send email (campaigns, replies, test emails): at least one connected sending mailbox. * An AI app that supports remote MCP servers: Claude (web, desktop, mobile, Claude Code), ChatGPT, Cursor, or any other MCP client that speaks Streamable HTTP and OAuth. ## How connecting works **1. Add the server** Add `https://mcp.mailbeast.ai/mcp` in your AI app (per-app steps below). **2. Sign in** The app opens a MailBeast page in your browser. Sign in if you aren't already. **3. Choose workspace and permissions** Pick the workspace, then the permissions: viewing campaigns, leads, mailboxes, inbox and analytics is always included, and **Make changes and send email** is on by default - switch it off if the assistant should only view. The page shows which app is asking and where you will be sent back. **4. Allow access** Select **Allow access**. You return to the app and the MailBeast tools are ready. Behind the scenes this is standard OAuth 2.1 with PKCE: the app discovers our sign-in from the server's `401` response and its [metadata](https://mcp.mailbeast.ai/.well-known/oauth-protected-resource/mcp). Each approved connection appears as an API key named **"\ (MCP)"** - for example "Claude (MCP)" - in [Settings → API Keys](https://app.mailbeast.ai/settings/api). ## Connect your app > [tab] Claude > > Add it on claude.ai or in Claude Desktop; it then works in the Claude mobile apps too. > > 1. Open **Customize → Connectors** ([claude.ai/customize/connectors](https://claude.ai/customize/connectors)). > 2. Select **Add custom connector**, name it `MailBeast` and enter > `https://mcp.mailbeast.ai/mcp`. Leave the OAuth client ID and secret empty. > 3. Select **Add**, then **Connect**, and approve access in the browser. > 4. In a chat, turn MailBeast on under **+ → Connectors**. > > On **Team and Enterprise** plans an organization Owner first adds the connector in > **Organization settings → Connectors** (**Add → Custom**, same URL); then each > member opens **Customize → Connectors** and selects **Connect**. > [tab] ChatGPT > > 1. Open [chatgpt.com/plugins](https://chatgpt.com/plugins). > 2. Select **+**, then **Add custom MCP server**. Name it `MailBeast`. > 3. Under **Connection**, enter `https://mcp.mailbeast.ai/mcp` and choose **OAuth** > for authentication. > 4. Confirm the risk notice and select **Create as a plugin**, then approve access > in the browser. > 5. In a chat, type `@MailBeast` to use it. > > Custom MCP servers in ChatGPT depend on your plan and workspace settings. On > Business, only workspace admins and owners can add them; on Pro, ChatGPT connects > custom servers for reading only. See > [OpenAI's help article](https://help.openai.com/en/articles/12584461). > [tab] Claude Code > > ```bash theme={null} > claude mcp add --transport http mailbeast https://mcp.mailbeast.ai/mcp > ``` > > Start Claude Code, run `/mcp`, select **mailbeast** and **Authenticate**. Your > browser opens; approve access and return to the terminal. > [tab] Cursor > > [![Add MailBeast MCP server to Cursor](https://cursor.com/deeplink/mcp-install-dark.png)](https://cursor.com/link/mcp/install?name=MailBeast\&config=eyJ1cmwiOiJodHRwczovL21jcC5tYWlsYmVhc3QuYWkvbWNwIn0%3D) > > Or add it to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project): > > ```json theme={null} > { > "mcpServers": { > "MailBeast": { > "url": "https://mcp.mailbeast.ai/mcp" > } > } > } > ``` > > In **Cursor Settings → MCP**, MailBeast shows **Needs login** - select it and > approve access in the browser. ## Permissions | You approve | The assistant can | Behind it | | - | - | - | | **View** (always included) | Read campaigns, sequences, leads, mailboxes and their health, emails and replies, analytics, Lead Finder searches, plan usage | API key with `all:read` | | **View + Make changes and send email** | Everything above, plus: create, change, start, pause and delete campaigns; add, update and delete leads; change, pause and remove mailboxes; connect Google / Microsoft mailboxes by link; mark conversations read; reply to and forward emails; send test emails | API key with `all:all` | **Read-only means read-only**: the assistant can't change anything or send email. It still sees the content it reads - your emails, replies and lead data - and passes it to the AI app you use. Whatever you approve, the assistant can't: * create or see API keys, or change your plan and billing; * see any password - over a sign-in connection it doesn't accept SMTP/IMAP passwords at all (connect those mailboxes in the dashboard); * go beyond your workspace's plan limits and the [rate limit](/authentication#rate-limits) - tool calls count against it like direct API requests (120 per minute per workspace). To change permissions, connect again with the other choice and delete the old "(MCP)" key. ## Disconnect * **In MailBeast** - delete the app's key ("Claude (MCP)", "ChatGPT (MCP)", …) in [Settings → API Keys](https://app.mailbeast.ai/settings/api). Access ends immediately: the app's next call is refused and it has to sign in again. * **In the app** - remove the connector (Claude: **Customize → Connectors → … → Remove**; ChatGPT: remove the plugin in **Plugins**; Claude Code: `claude mcp remove mailbeast`; Cursor and other clients: remove it from the MCP settings or config file). Removing the connector in the app alone may leave the key in MailBeast - delete it there to revoke access for sure. ## Connect with an API key For scripts, CI, and clients without OAuth sign-in. Create a key in [Settings → API Keys](https://app.mailbeast.ai/settings/api) - a **separate key for MCP**, with only the scopes you need (`all:read` for read-only, `all:all` for everything) - and send it as a header: ``` Authorization: Bearer mb_live_… ``` > [tab] Claude Code > > ```bash theme={null} > claude mcp add --transport http mailbeast https://mcp.mailbeast.ai/mcp \ > --header "Authorization: Bearer $MAILBEAST_API_KEY" > ``` > [tab] Cursor > > ```json theme={null} > { > "mcpServers": { > "MailBeast": { > "url": "https://mcp.mailbeast.ai/mcp", > "headers": { "Authorization": "Bearer mb_live_…" } > } > } > } > ``` The assistant sees only the tools the key's [scopes](/authentication#scopes) allow. A key connection also offers `connect_mailboxes` (SMTP/IMAP with passwords) and credential changes in `update_mailbox`, which sign-in connections don't. > [!WARNING] > > Never put an API key in the server URL. Older setups that used a URL containing the > key keep working for now, but switch them to sign-in or the header above and delete > that key - URLs end up in logs, browser history and screenshots. ## Tools **Access** shows what a tool needs: **Read** works on every connection; **Write** needs *Make changes and send email*; **Sends email** needs *Make changes and send email* and sends real email to real recipients. Every tool calls the matching endpoint of the REST API (see the API Reference tab) - the MCP server adds no permissions of its own. ### Workspace | Tool | What it does | Access | | - | - | - | | `get_workspace` | Workspace name and timezone, plan, and usage against every plan limit | Read | ### Mailboxes | Tool | What it does | Access | | - | - | - | | `list_mailboxes` | Sending mailboxes with status, health score, warmup and limits | Read | | `get_mailbox` | One mailbox: status and why it last failed, warmup, limits; optionally the latest connection test | Read | | `start_oauth_connect` | Starts connecting a Google Workspace or Microsoft 365 mailbox: returns a link you open to sign in - no password | Write | | `check_oauth_connect` | Checks whether that sign-in finished | Read | | `update_mailbox` | Changes sender name, daily cap, sending gap, signature, reply-to, BCC, tracking domain, warmup, language | Write | | `pause_mailbox` / `resume_mailbox` | Stops a mailbox from sending / brings it back (a failed one gets a fresh connection test) | Write | | `bulk_update_mailboxes` | One change - status or shared settings - for up to 1000 mailboxes | Write | | `delete_mailbox` | Removes a mailbox from the workspace | Write | | `connect_mailboxes` | Connects SMTP/IMAP mailboxes with passwords - **API key connections only** | Write | ### Campaigns | Tool | What it does | Access | | - | - | - | | `list_campaigns` | Campaigns with status and lead count | Read | | `get_campaign` | One campaign: schedule, settings, metrics, mailboxes, every sequence step and A/B variant, whether it's inside its sending window | Read | | `create_campaign` | Creates a campaign as a **draft** (nothing is sent): schedule, settings, sequence with A/B variants, tags, lead emails | Write | | `update_campaign` | Changes a campaign; a new sequence, mailbox list or tag list replaces the current one | Write | | `send_test_email` | Sends one preview of a sequence step to one address (12 per day per workspace) | **Sends email** | | `start_campaign` | Starts sending a draft campaign to its leads on its schedule | **Sends email** | | `pause_campaign` | Pauses a campaign - nothing more is sent until it's resumed | Write | | `resume_campaign` | Resumes a paused campaign - sending continues on its schedule | **Sends email** | | `delete_campaign` | Permanently deletes a campaign and stops its sending | Write | ### Subsequences Follow-up flows of a campaign that start on a lead status, a reply, an open, a click or a finished sequence - from the same mailbox, in the same thread. See [Subsequences](/subsequences). | Tool | What it does | Access | | - | - | - | | `list_subsequences` | A campaign's subsequences: trigger, start, emails, whether each is on, counters | Read | | `create_subsequence` | Creates a subsequence, turned **off** (it is turned on in the dashboard) | Write | | `update_subsequence` | Changes the name, order, trigger / start / sending settings or emails | Write | | `enroll_in_subsequence` | Moves leads of the campaign (by id or email) into a subsequence; it then emails them | **Sends email** | ### Leads | Tool | What it does | Access | | - | - | - | | `add_leads` | Adds up to 100 leads per call to a campaign (deduplicated by email) | Write | | `list_leads` | A campaign's leads with status and engagement (opened, clicked, replied) | Read | | `get_lead` | One lead with custom fields and sequence progress | Read | | `find_lead_by_email` | Finds leads by email across all campaigns | Read | | `update_lead` | Changes a lead's fields, tags, custom fields or status | Write | | `bulk_set_lead_status` | Sets the same status on up to 1000 leads | Write | | `bulk_add_lead_tags` | Adds tags to up to 1000 leads | Write | | `bulk_remove_lead_tags` | Removes tags from up to 1000 leads | Write | | `delete_leads` | Deletes leads from a campaign by ids, by filter, or all | Write | ### Emails | Tool | What it does | Access | | - | - | - | | `list_emails` | Emails across all mailboxes (unified inbox), newest first, without bodies | Read | | `get_email` | One email with its body as plain text; `includeHtml: true` adds the HTML version (returned anyway when the email has no text part; `htmlOmitted` marks that it was left out). Bodies over 25,000 characters are cut and marked `bodyTruncated` | Read | | `count_unread_emails` | Number of unread emails | Read | | `reply_to_email` | Sends a reply in the conversation, from the mailbox that owns it | **Sends email** | | `forward_email` | Forwards an email to recipients you choose | **Sends email** | | `mark_thread_read` | Marks a conversation as read | Write | ### Analytics and Lead Finder | Tool | What it does | Access | | - | - | - | | `get_campaign_analytics` | A campaign's sends, opens, clicks, replies, bounces and rates, by mailbox, step or bounce type, optionally over time | Read | | `get_workspace_analytics` | Totals and rates across all campaigns for a period | Read | | `list_lead_searches` / `get_lead_search` | Lead Finder searches and their progress (searches are started in the dashboard) | Read | ## Safety * **Campaigns start as drafts.** Starting is always a separate step, so you can review the sequence and get a test email first. * **No passwords in chat.** Google Workspace and Microsoft 365 mailboxes connect through `start_oauth_connect` - a link you open yourself. SMTP/IMAP mailboxes are connected in the dashboard (or with an API key connection); passwords are stored encrypted and never returned. * **Your AI app receives what the tools return.** Email content, lead data and analytics the assistant reads go to the AI provider you use (Anthropic, OpenAI, …) and are handled under its terms. MailBeast sees only the tool calls, not your conversation. See our [Privacy Policy](https://mailbeast.ai/privacy). * **Treat incoming emails as untrusted.** A reply from a lead can contain text written to steer an AI assistant. Check what the assistant proposes before you approve a send, a deletion or a bulk change. ## Troubleshooting | Symptom | What to do | | - | - | | "MCP is available on the Pro+ plan" next to a workspace | MCP needs Pro+ (or enterprise with API access). Pick another workspace or change the plan in the dashboard | | "Only workspace owners and admins can connect apps" | Ask an owner or admin of that workspace to connect, or to change your role | | "This connection request has expired" | A sign-in request lasts 30 minutes and works once - start connecting again from the app | | The assistant has no tools to create, change or send | You connected with **Make changes and send email** off. Connect again with it on and delete the old key | | `connect_mailboxes` is missing | Sign-in connections don't take passwords. Use `start_oauth_connect` for Google / Microsoft, or connect SMTP/IMAP in the dashboard | | The app shows **Reconnect** / "Authorization failed" / `401` | The key was deleted or the connection expired. Connect again | | Claude: "Couldn't reach the MCP server" | Check that your network allows `mcp.mailbeast.ai`, then reconnect. If it persists, send us the `ofid_…` reference Claude shows | | Claude Code: the browser didn't open | Copy the URL Claude Code prints, open it, approve, then run `/mcp` again | | `429` / "Too many requests" | The workspace rate limit (shared with the REST API) - wait for the time in the message | | `402` / "plan limit reached" | A plan limit is used up - ask "what's left on my plan?" (`get_workspace`) | Still stuck? Write to [support@mailbeast.ai](mailto:support@mailbeast.ai) or use the chat in the app. ## Example prompts * "How many emails can I still send this month?" * "Show me reply rates for my campaigns this week." * "Create a 3-step campaign for these leads, weekdays 9-17 their time, and send me a test of step 1." * "Which mailboxes are failing and why?" * "Summarize today's unread replies and draft answers to the interested ones." * "Compare reply rates of my last three campaigns by sequence step." > [!NOTE] > > `https://developer.mailbeast.ai/mcp` is a different server: it searches **this > documentation** and does not touch your workspace. It's useful for agents that > write code against the API. # Quickstart Source: https://developer.mailbeast.ai/quickstart From zero to your first authenticated call in two steps. **1. Create a key** 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. **2. Confirm it works** 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" } ] } ``` > [!WARNING] > > For security reasons, the API key will be displayed only once, and there is no way to recover it if you lose it. # Subsequences Source: https://developer.mailbeast.ai/subsequences Follow-up flows that start when a lead replies, gets a status, opens, clicks, or finishes a campaign without replying. A **subsequence** is a follow-up flow inside a campaign. It starts on its own when something happens to a lead: the lead replies "Interested", sends an out-of-office, clicks your pricing link, or gets through the whole campaign without replying. Typical uses: send a calendar link to interested leads, catch up with leads after their vacation, ask a wrong contact for a referral, re-engage leads who opened but never answered. How a subsequence behaves: * **It continues the conversation.** Emails go out from the same mailbox and, when a step's subject is empty, in the same email thread as the conversation with the lead. * **Entering ends the main sequence.** The lead's remaining campaign emails are canceled - the lead never gets the campaign's step 3 and the subsequence's first email on the same day. It does not go back to the main sequence. * **One at a time, each once.** A lead is in at most one subsequence at a time (a newer match replaces the current one) and enters each subsequence at most once. * **Any reply stops it.** A reply from the lead to any email of the campaign, or a manual reply you send from the inbox, ends the lead's subsequence. * **Only contacted leads.** A subsequence continues a conversation, so a lead that was never emailed cannot enter. Unsubscribed, bounced and blacklisted leads never enter. * **Settings are inherited.** Mailboxes, tracking, unsubscribe link, stop rules and verification skips come from the campaign and follow its changes. A subsequence has its own name, trigger, start, sending limits and emails. Subsequences need a plan that includes them (**Pro+ and above**); otherwise every write except pause and delete answers `403` with code `subsequences_plan_required`. ## Scopes | Action | Scope | | - | - | | List and read subsequences | `campaigns:read` | | Create, update, delete | `campaigns:write` | | Turn on / pause / resume, move leads in | `campaigns:lifecycle` | Turning a subsequence on and moving leads into it lead to real email, so they need `campaigns:lifecycle` - the same scope that starts a campaign. ## Create a subsequence ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/campaigns/{campaignId}/subsequences \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Interested - book a call", "settings": { "trigger": { "any": [ { "kind": "lead_status", "statuses": ["lead_replied_interested", "lead_replied_meeting_request"] } ], "sources": ["main"], "filters": [] }, "start": { "mode": "delay", "amount": 1, "unit": "hours" } }, "sequence": [ { "subject": "", "body": "

Hi {{first_name}}, great to hear from you! Pick any time here: [calendar link]

" }, { "subject": "", "body": "

Just making sure my last note did not get buried.

", "delayDays": 2 } ] }' ``` A new subsequence is **off** (`"enabled": false`, `status: "draft"`) - it sends nothing until you [turn it on](#turn-on-pause-resume). Name, settings and emails are all-or-nothing: if any part is invalid (`400`; `details` names the field when the error is about one), nothing is created. The response is the subsequence, the same shape as `GET /v1/subsequences/{id}`. ## Settings `settings` has three sections. Each one you send **replaces** that section as a whole; the sections you leave out keep their value (on create: the defaults). ### Trigger - who enters ```json theme={null} "trigger": { "any": [ { "kind": "replied" } ], "sources": ["main"], "filters": [ { "kind": "tag", "op": "has_none", "values": ["competitor"] } ] } ``` `any` holds up to 5 rules - **any** of them enrolls a lead; each kind can be used once. `"any": []` means *manual only*: leads enter only when you [move them in](#move-leads-in). | `kind` | A lead enters when | Extra fields | | - | - | - | | `lead_status` | its status becomes one of `statuses` (from the AI reply classification or set by you) | `statuses`: `lead_replied_interested`, `lead_replied_meeting_request`, `lead_replied_out_of_office`, `lead_replied_wrong_person`, `lead_replied_not_interested`, `lead_replied_uncategorized`, `lead_converted`, `lead_lost` | | `replied` | it replies | - | | `reply_contains` | its reply contains one of `phrases` (case-insensitive) | `phrases`: 1-20 phrases, 2-100 characters each | | `opened` | it opens an email (needs open tracking on the campaign) | `when` | | `clicked` | it clicks a link (needs click tracking) | `when`, optional `urlContains` - only links whose URL contains this text | | `sequence_finished_no_reply` | it got every email of the main sequence and did not reply within `waitDays` | `waitDays`: 0-60 | `when` decides what an open or a click does: * `{ "mode": "after_main_sequence", "waitDays": 0 }` (recommended) - the lead finishes the main sequence first; if it did not reply, it enters `waitDays` later. * `{ "mode": "right_away" }` - it enters at the open or click, and the rest of its main sequence is canceled. `sequence_finished_no_reply` and `"after_main_sequence"` rules watch the main sequence, so they need `"main"` in `sources` (`400` otherwise). `sources` says where the rules listen: `"main"` (the campaign's own sequence) and/or ids of **other subsequences of the same campaign**. That chains flows: a "Re-engage" subsequence can hand leads that reply "Interested" to a "Book a call" one. `trigger` always carries `any`, `sources` and `filters` - pass `[]` when there is nothing to list. `filters` (up to 5) are extra conditions - a lead enters only if **all** match: | `kind` | Fields | | - | - | | `tag` | `op`: `has_any` / `has_none`, `values`: tag names | | `custom_field` | `field`, `op`: `equals` / `contains` / `is_empty` / `is_not_empty`, `value` (for `equals` / `contains`) | | `email_provider` | `values`: `google` / `microsoft` / `other` | | `engagement` | `op`: `opened_at_least` (with `emails`) / `not_opened` / `clicked` / `not_clicked` | | `replied` | `value`: `true` / `false` | | `reached_step` | `atLeast`: step number of the main sequence | Filters are checked when a rule enrolls a lead, not when you move one in, so a manual-only subsequence ignores them. A filter no lead entering by the rules could pass is rejected (`400`): `replied: false` when every rule fires on a reply (`replied`, `reply_contains`, `lead_status` with reply statuses - and Converted - only), `replied: true` when every rule fires only without a reply (`sequence_finished_no_reply`, opens and clicks `"after_main_sequence"`), `engagement` `not_opened` when every rule is an open or a click (a click counts as an open), `not_clicked` when every rule is a click, and `reached_step` past the last step of the main sequence. Two filters of the same kind (`replied`, `email_provider`, `reached_step`) and filters that rule each other out are rejected too. When one event matches several subsequences, the lead enters the first by `position` (set it with `PATCH`). ### Start - when the first email goes out | `start` | First email | | - | - | | `{ "mode": "immediately" }` (default) | at the next sending window | | `{ "mode": "delay", "amount": 2, "unit": "days" }` | after the delay (`minutes` / `hours` / `days`, up to 365 days) | | `{ "mode": "ooo_return", "offsetDays": 1, "withoutDate": { "mode": "delay", "days": 7 }, "maxDaysAhead": 120 }` | `offsetDays` after the return date found in the out-of-office reply; without a date, `days` after the auto-reply (or `{ "mode": "skip" }`); a lead away longer than `maxDaysAhead` days is skipped (`null` = no limit) | `ooo_return` works only with a single `lead_status` rule for `lead_replied_out_of_office`. The first step has no delay of its own - `start` is it. Emails still go out only inside the sending schedule. ### Sending ```json theme={null} "sending": { "schedule": "inherit", "dailyLimit": { "mode": "inherit" } } ``` * `schedule`: `inherit` - the campaign's schedule (default); `custom` - the subsequence keeps its own schedule, edited in the dashboard. * `dailyLimit.mode`: `inherit` - its emails count toward the campaign's daily limit and go out first, the campaign's own emails use the rest (default); `custom` - its own limit `value` on top of the campaign's; `unlimited` - only the mailboxes' own daily limits apply. Whether a subsequence is on is **not** a setting: `settings.enabled` in a request is a `400`. Use `/enable` and `/pause`. ## Emails `sequence` follows the same rules as a campaign's sequence: a step is `subject` + `body` (one variant A) or `variants` (an A/B test, up to 5, leads split evenly). Two differences: * **An empty subject (`""`) replies in the same thread** as the conversation with the lead - recommended for the first step. A subject of its own starts a new thread. * **Step 1 has no delay** - `delayDays` / `delayHours` must be `0` or left out on it (`400` otherwise); when it goes out is set by `settings.start`. Later steps wait `delayDays` / `delayHours` after the previous one - at least 1 hour (`400` otherwise, nothing is created). ## Turn on, pause, resume ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/subsequences/{id}/enable \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enrollExisting": true }' ``` ```json Response theme={null} { "id": "5b2f3c4d-…", "status": "active", "enabled": true, "state": "active", "enrollingExisting": true, "success": true } ``` * `/enable` turns it on: from now on, leads that match the trigger enter it. `enrollExisting: true` also enrolls the leads that **already** match (in the background) - by a lead status, a reply, a finished main sequence, or an open / a click in the main sequence; `reply_contains` rules and clicks on a URL count only from now on. A lead going through another subsequence right now is not taken out of it. On a **draft** campaign the subsequence stays `draft` while on and starts together with the campaign. * `/pause` turns it off: no new leads enter, and leads already in it wait - nothing more is sent to them - until it is resumed. The main sequence keeps sending. If this was the campaign's last subsequence that is on, the main sequence has finished and no lead is still in a subsequence, the campaign then completes on its own (unless it keeps active when leads run out) - and `/enable` needs a restart in the dashboard. * `/resume` is the same as `/enable`: waiting leads continue, new ones enter again. A subsequence needs at least one email to be turned on (`400`). On a **completed** campaign `/enable` answers `409 parent_completed` - restart the campaign in the dashboard first. A deleted campaign answers `404`. `state` says in one word why a subsequence is (not) sending: | `state` | Meaning | | - | - | | `draft` | off | | `starts_with_campaign` | on, starts when the draft campaign starts | | `active` | on and sending | | `paused` | paused by you | | `paused_with_campaign` | on, but the campaign is paused - nothing is sent until it resumes | | `stopped_campaign_ended` | the campaign completed, was stopped or deleted | | `paused_requires_plan` | the plan does not include subsequences | | `cant_start` | on, but it cannot start: it has no email, or an email is missing content | | `waiting_restart` | on, waiting for the completed campaign to be restarted | `status` is the subsequence's campaign status: `active` = sending; `draft` / `paused` = not sending right now - it can still be on and wait for the campaign to start or restart; `enabled` and `state` tell which. ## Move leads in Move leads of the campaign into a subsequence by hand - by lead id or by email, up to 1000 per call: ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/subsequences/{id}/leads \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "emails": ["jane@acme.com", "sam@globex.com"], "start": { "mode": "delay", "days": 2 } }' ``` ```json Response theme={null} { "enrolled": 1, "skipped": { "not_contacted": 1 }, "notFound": 0 } ``` Pass `leadIds` (ids of the **campaign's** leads, from `GET /v1/campaigns/{id}/leads`) or `emails`, not both. `start` is `{ "mode": "now" }` (the default), `{ "mode": "delay", "days": N }` or `{ "mode": "date", "date": "YYYY-MM-DD" }` - it overrides the subsequence's own `settings.start` for these leads. Moving works whether the subsequence is on or off: into one that is paused or was never turned on, the leads wait and start when you `/enable` (or `/resume`) it. Each lead's remaining campaign emails are canceled, exactly as when a trigger fires. The response counts who entered, who did not and why, and which ids or emails are not leads of the campaign (`notFound`): | `skipped` reason | Why | | - | - | | `not_contacted` | the lead was never emailed - a subsequence continues a conversation | | `already_enrolled` | the lead was in this subsequence before - each lead enters once | | `suppressed` | unsubscribed, bounced or blacklisted | | `company_replied` | a colleague at the same company replied in this campaign or one of its subsequences, and the campaign stops the company on a reply | | `lead_removed` | the lead was removed from the campaign | | `not_accepting` | the campaign is not active or paused (draft, completed, stopped or deleted), the subsequence is completed or deleted, or your plan does not include subsequences. A subsequence that is paused or was never turned on still takes leads | | `thread_unavailable` | step 1 replies in the same thread (empty subject), but the conversation's thread could not be found - give step 1 a subject of its own | Treat any other key in `skipped` the same way - the lead did not enter (for example, `not_family_lead` when the lead was removed from the campaign while the call ran). ## Read ```bash theme={null} # all subsequences of a campaign, in position order curl https://api.mailbeast.ai/v1/campaigns/{campaignId}/subsequences \ -H "Authorization: Bearer $MAILBEAST_API_KEY" # one subsequence curl https://api.mailbeast.ai/v1/subsequences/{id} \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` The list answers `{ "data": [ … ] }` - an array of subsequences; one subsequence is the object itself: ```json Response theme={null} { "id": "5b2f3c4d-6e7f-4a81-9b0c-1d2e3f4a5b6c", "campaignId": "a7d4e2f1-08b3-4c65-9e7a-1b2c3d4e5f60", "name": "Interested - book a call", "position": 1, "status": "active", "enabled": true, "state": "active", "settings": { "trigger": { "any": [ { "kind": "lead_status", "statuses": ["lead_replied_interested"] } ], "sources": ["main"], "filters": [] }, "start": { "mode": "delay", "amount": 1, "unit": "hours" }, "sending": { "schedule": "inherit", "dailyLimit": { "mode": "inherit" } } }, "sequence": [ { "id": "9a8b7c6d-…", "subject": "", "body": "

Hi {{first_name}}, …

", "delayDays": 0, "delayHours": 0, "variants": [ { "id": "550e8400-…", "label": "A", "subject": "", "body": "

…

", "isActive": true } ] } ], "counters": { "entered": 42, "waiting": 3, "inProgress": 12, "replied": 9, "positive": 5, "ended": 30 }, "nextStartAt": "2026-10-29T09:00:00.000Z", "createdAt": "2026-10-28T08:00:00.000Z" } ``` `entered = inProgress + ended`: an entry ends when its emails are all sent, the lead replies, its status changes, it is removed or moves to another subsequence. `waiting` (part of `inProgress`) is leads whose first email is not due yet. `positive` counts leads that replied and whose status is positive now, when the lead became positive during this subsequence - a subset of `replied`. A lead that turned positive in the main sequence counts there, not here. The campaign lists its subsequences too - `GET /v1/campaigns/{id}` has `"subsequences": [{ "id", "name", "status" }]`, and `GET /v1/campaigns/{id}/analytics` adds `subsequences` with entered / in progress / replied / positive counters per subsequence (all time). The campaign's analytics `totals` cover the campaign with its subsequences by default; `scope=main` gives the main sequence and `subsequenceId={id}` one subsequence (see [Analytics](/analytics)). > [!NOTE] > > A subsequence id is **not** a campaign id in the API: it does not appear in > `GET /v1/campaigns`, and every `/v1/campaigns/{id}…` route answers `404` for it > (`/v1/leads/search` does not return subsequence lead rows either - it returns the > lead's row in the campaign). Use `/v1/subsequences/{id}`. > > Emails of a subsequence in [`/v1/emails`](/emails) come with > the **main campaign's** `campaignId` and the lead's id in that campaign, plus a > `subsequence` object (`id`, `name`) - so every id you receive works with > `/v1/campaigns/{id}…`. ## Update and delete `PATCH /v1/subsequences/{id}` changes only what you send: `name`, `position`, any `settings` section, `sequence`. A sent section or `sequence` replaces the current one; step and variant ids change when `sequence` is replaced. The emails of a running subsequence cannot be replaced - pause it first (`400`, nothing is written; see [`/pause`](#turn-on-pause-resume) for what turning off the last subsequence that is on does). Sections are written one after another, as with `PATCH /v1/campaigns/{id}`. `DELETE /v1/subsequences/{id}` deletes it: its scheduled emails are canceled, and leads in it do not return to the main sequence. Deleting the campaign's last subsequence that is on can let a campaign whose main sequence has finished complete, as `/pause` does. ## Errors Errors use the usual envelope. Codes specific to subsequences: | Status | `code` | Meaning | | - | - | - | | `400` | `validation_failed` | invalid settings or emails - `details` names the field when there is one, e.g. `settings.trigger.sources: …`; other checks (a loop of sources, a source from another campaign) explain it in `message` | | `403` | `subsequences_plan_required` | the plan does not include subsequences | | `404` | `not_found` | no such subsequence in your workspace (or a campaign id was used) | | `409` | `campaign_busy` | the campaign is being changed right now - retry in a few seconds (`Retry-After` is set) | | `409` | `campaign_state_changed` | the campaign changed (paused, completed, deleted) while the request ran - read it again | | `409` | `parent_completed` | `/enable` on a completed campaign - restart it in the dashboard first | | `409` | `family_ended` | the campaign ended (stopped, or deleted while the request ran) | | `409` | `subsequence_limit_reached` | the campaign already has as many subsequences as it can - delete one to add another | ## MCP The [MCP server](/mcp-server) has `list_subsequences`, `create_subsequence`, `update_subsequence` and `enroll_in_subsequence`. A subsequence created there is off; it is turned on in the [dashboard](https://app.mailbeast.ai) or with `POST /v1/subsequences/{id}/enable`. # 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 } } } ``` > [!TIP] > > 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" } ```