# Email Accounts Source: https://developer.mailbeast.ai/accounts Connect and manage the sending mailboxes your campaigns send from. Email accounts are the sending mailboxes your campaigns use. Connect them two ways: * **SMTP/IMAP.** Any provider, including Gmail and Outlook using an [app password](https://support.google.com/mail/answer/185833) (host `smtp.gmail.com` / `imap.gmail.com`, etc.). See [Connect a mailbox](#connect-a-mailbox). * **Native OAuth.** Google Workspace and Microsoft, with tokens refreshed for you. See [Connect Google / Microsoft over OAuth](#connect-google-microsoft-over-oauth). ## Scopes | Action | Scope | | --------------------------------------- | ---------------- | | Read mailboxes and verification status | `accounts:read` | | Connect, update, pause / resume, delete | `accounts:write` | ## Connect a mailbox `POST /v1/accounts` connects one mailbox or up to 50. Each is handled **independently**, so the response tells you which connected and which failed (for example, an email already connected to another workspace). A new mailbox starts out unverified and is checked automatically. Poll `verification.overallStatus` (or `status`) to know when it is ready to send. ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/accounts \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "accounts": [ { "emailAddress": "jane@acme.com", "firstName": "Jane", "lastName": "Doe", "smtpHost": "smtp.acme.com", "smtpPort": 587, "smtpUsername": "jane@acme.com", "smtpPassword": "•••", "imapHost": "imap.acme.com", "imapPort": 993, "imapUsername": "jane@acme.com", "imapPassword": "•••" } ] }' ``` ```json Response theme={null} { "summary": { "submitted": 1, "created": 1, "failed": 0 }, "accounts": [ { "id": "…", "emailAddress": "jane@acme.com", "status": "inactive", "healthScore": 100, "smtp": { "host": "smtp.acme.com", "port": 587, "username": "jane@acme.com" }, "verification": { "overallStatus": "pending", "smtp": "inactive", "imap": "not_checked", "dns": { "mx": "pending", "spf": "pending", "dkim": "pending", "dmarc": "pending" } } } ], "errors": [] } ``` ## Connect Google / Microsoft over OAuth For a native Google Workspace or Microsoft mailbox (managed token refresh, no app password), use the OAuth flow. It's a three-step handshake because a person has to grant consent in a browser: `POST /v1/accounts/oauth/{google|microsoft}/init` returns a consent URL and a `state` handle. ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/accounts/oauth/google/init \ -H "Authorization: Bearer $MAILBEAST_API_KEY" # → { "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth?…", # "state": "…", "expiresInSeconds": 600 } ``` Open `authorizationUrl` in a browser. After they grant access, the provider redirects back to MailBeast, which exchanges the code and connects the mailbox. `GET /v1/accounts/oauth/{provider}/status?state=…` returns `pending`, then `connected` (with the new `accountId`) or `failed`. ```bash theme={null} curl "https://api.mailbeast.ai/v1/accounts/oauth/google/status?state=…" \ -H "Authorization: Bearer $MAILBEAST_API_KEY" # → { "status": "connected", "accountId": "…", "error": null } ``` The `state` is valid for 10 minutes; once it resolves, the result stays pollable for 30 minutes after. Like any new mailbox it is verified automatically and flips to `active` once verification passes (expect `inactive` / `checking` briefly right after connecting). ## List, get, update `GET /v1/accounts` is **cursor-paginated**, newest-first, and filterable by `status` (`active`, `inactive`, `checking`, `failed`, `revoked`) and by `tags`. Keep requesting with `?cursor=meta.nextCursor` until `meta.hasMore` is `false`. `PATCH /v1/accounts/{id}` updates settings/limits **and** rotates credentials in one call. `emailAddress` is immutable. Any `smtp*` / `imap*` field present rotates the credentials and re-runs verification. ```bash theme={null} # raise the daily cap and rotate the SMTP password (triggers re-verification) curl -X PATCH https://api.mailbeast.ai/v1/accounts/{id} \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "maxEmailsPerDay": 60, "smtpPassword": "•••" }' ``` `POST /v1/accounts/bulk` applies **one** settings `patch` **or** one `status` change (`active` / `inactive`) across many ids. The two are mutually exclusive. ## Check verification Mailboxes are verified for you: when you connect one, when you rotate its credentials, when you `resume` a failed one, and periodically after that. Read the latest result at any time: ```bash theme={null} curl https://api.mailbeast.ai/v1/accounts/{id}/verification \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ```json Verification result theme={null} { "overallStatus": "verified", "smtp": { "status": "verified", "error": null }, "imap": { "status": "verified", "error": null }, "dns": { "mx": { "status": "verified", "error": null }, "spf": { "status": "failed", "error": "No SPF record found" }, "dkim": { "status": "verified", "error": null }, "dmarc":{ "status": "verified", "error": null }, "a": { "status": "verified", "error": null } }, "lastVerifiedAt": "2026-07-02T00:00:00.000Z" } ``` ## Pause and resume Pause sending on a mailbox (for a suspected deliverability issue, say) and resume it later. A paused mailbox reports `status: "inactive"` and stays that way until you resume it - campaigns and warmup both skip it, and nothing re-activates it on its own. ```bash theme={null} # pause (only an active mailbox can be paused; 400 otherwise). No body. curl -X POST https://api.mailbeast.ai/v1/accounts/{id}/pause \ -H "Authorization: Bearer $MAILBEAST_API_KEY" # resume curl -X POST https://api.mailbeast.ai/v1/accounts/{id}/resume \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ## Delete a mailbox Delete a mailbox with `DELETE /v1/accounts/{id}`. Sending from it stops immediately. # Analytics Source: https://developer.mailbeast.ai/analytics Engagement reporting – per campaign, over time, and across the workspace. Three endpoints cover reporting: one campaign's totals, one campaign over time, and a workspace-wide roll-up. They all draw from the same source as your dashboard, so the numbers you get here match the ones you see there. ## Scopes | Action | Scope | | ------------------ | -------------- | | Read any analytics | `metrics:read` | ## Campaign analytics ```bash theme={null} curl "https://api.mailbeast.ai/v1/campaigns/{id}/analytics?startDate=2026-06-01T00:00:00Z" \ -H "Authorization: Bearer mb_live_..." ``` `opens` and `clicks` come in **two cardinalities**: * `total` – counted **per email**, bots excluded. The same email opened five times counts once; opening two different steps counts twice. * `uniqueLeads` – distinct leads who did it at least once. Rates are computed from `uniqueLeads`, which is why a single enthusiastic reader cannot push your open rate above 100%. `replies` is a flat number – distinct leads who replied. There is no raw reply-event count, so it is deliberately not dressed up as two cardinalities. ```json theme={null} { "totals": { "sent": 1200, "failed": 8, "delivered": 1186, "leadsContacted": 640, "opens": { "total": 240, "uniqueLeads": 180 }, "clicks": { "total": 31, "uniqueLeads": 24 }, "replies": 26, "bounces": 14, "unsubscribed": 5, "positiveLeads": 21, "conversions": 6, "conversionValue": 18000, "rates": { "open": 28.1, "click": 3.7, "reply": 4.0, "bounce": 1.1, "delivery": 98.8, "unsubscribe": 0.4, "conversion": 0.9 } } } ``` **A rate of `null` means "no denominator", not "zero percent".** A campaign that has not sent anything yet returns `"open": null` – not `0`. If we returned `0`, a chart would show your open rate collapsing to zero on days you simply weren't sending. Check for `null` before plotting. ### Out-of-office replies `replies` counts distinct leads who replied. Pass `excludeOOO=true` to exclude out-of-office auto-replies and count only replies from humans: ```bash theme={null} curl ".../analytics?excludeOOO=true" -H "Authorization: Bearer mb_live_..." ``` This is not a cosmetic switch. Out-of-office replies can inflate reply rate noticeably, and they inflate it *unevenly* across campaigns – a campaign that happened to run over a holiday looks better than it was. Pick one setting and use it consistently when you compare campaigns. ## Breakdowns Ask for extra detail with `breakdown`. Repeat the parameter or pass a comma-separated list. Each breakdown is a separate query, so request only what you will actually read. ```bash theme={null} curl ".../analytics?breakdown=sequence,bounce" -H "Authorization: Bearer mb_live_..." ``` | Value | Adds | Answers | | ---------- | ----------------- | ----------------------------------------------------------------------------- | | `smtp` | `bySmtp[]` | Which mailbox is carrying the campaign – and which one is dragging it down. | | `sequence` | `bySequence[]` | Per step **and** per A/B variant. Which follow-up actually earns the replies. | | `bounce` | `bounceBreakdown` | Hard vs soft, plus the reason. | ### Bounce breakdown Most outreach APIs give you a single `bounced` number. That number cannot tell you what to *do*. This one can: ```json theme={null} { "bounceBreakdown": { "total": 14, "hard": 9, "soft": 5, "senderBounces": 3, "recipientBounces": 11, "unknownBounces": 0, "byCategory": [ { "category": "invalid-mailbox", "label": "Invalid mailbox", "type": "recipient", "count": 9, "percentage": 64.3 } ] } } ``` `recipientBounces` are their problem – the address is dead, so clean your list. `senderBounces` are **yours** – reputation, authentication, or content got you refused. Scrubbing your list will not fix those, and treating them as one number is how a deliverability problem hides behind a "normal" bounce rate. ## Over time ```bash theme={null} curl ".../analytics/timeseries?startDate=2026-06-01T00:00:00Z&interval=day" \ -H "Authorization: Bearer mb_live_..." ``` Each point carries `sent`, `delivered`, `opens`, `clicks`, `replies`, **`bounces`** and **`unsubscribed`** – so bounce rate and unsubscribe rate over time are chartable directly, without a second call. `interval` accepts `day`, `week` or `month`. Long windows are automatically coarsened to keep the series readable; the response echoes the `interval` it actually used, which may be coarser than the one you asked for. Read it back rather than assuming. ## Workspace roll-up ```bash theme={null} curl "https://api.mailbeast.ai/v1/campaigns/analytics/overview" \ -H "Authorization: Bearer mb_live_..." ``` Totals across every campaign, plus `activeCampaigns`, `sendingAccounts`, and the highest-volume campaigns. Use it for a top-level view, then drill into a campaign. ## Reporting windows and retention `startDate` and `endDate` are ISO-8601. Passing only `startDate` means "from then until now". Your plan retains a fixed window of history. If you ask for a start date older than that, the window is moved forward and the response **tells you**: ```json theme={null} { "period": { "start": "2026-04-16T00:00:00.000Z", "end": "2026-07-15T00:00:00.000Z", "clamped": true, "retentionDays": 90 } } ``` When `clamped` is `true`, the data is real but the window is **shorter than you asked for**. Do not label such a result "all time" – check `clamped` before you do. ## What this API does not report These are deliberately absent rather than silently zero: * **Spam-complaint rate.** We do not ingest feedback loops, so we cannot count complaints. Returning `0` would read as "no complaints" instead of "not measured". * **Hourly granularity.** The smallest bucket is a day. * **Per-link click-through.** Clicks are counted, but not attributed to individual links. # Campaign analytics Source: https://developer.mailbeast.ai/api-reference/analytics/campaign-analytics /openapi.json get /v1/campaigns/{id}/analytics Aggregate engagement for one campaign. Ask for extra breakdowns with `breakdown=smtp,sequence,bounce` – each is an extra query, so request only what you will read. Requires one of the following scopes: `metrics:read`, `all:read`, `all:all`. # Campaign analytics over time Source: https://developer.mailbeast.ai/api-reference/analytics/campaign-analytics-over-time /openapi.json get /v1/campaigns/{id}/analytics/timeseries One point per interval so you can chart a campaign – this series carries bounces AND unsubscribes, so those rates are chartable without extra calls. Long windows auto-coarsen (day → week → month); the response says which interval it used. Requires one of the following scopes: `metrics:read`, `all:read`, `all:all`. # Workspace analytics roll-up Source: https://developer.mailbeast.ai/api-reference/analytics/workspace-analytics-roll-up /openapi.json get /v1/campaigns/analytics/overview Engagement rolled up across every campaign in the workspace, plus the highest-volume campaigns. Use it for a top-level dashboard; drill into a campaign for detail. Requires one of the following scopes: `metrics:read`, `all:read`, `all:all`. # Create an API key Source: https://developer.mailbeast.ai/api-reference/api-keys/create-an-api-key /openapi.json post /v1/api-keys Mint a new key for your workspace. The full token is returned **once**, so store it immediately. Requested scopes must be a subset of the calling key’s own scopes, and cannot include `apikeys:manage`. Requires the `apikeys:manage` scope, which no wildcard grants and which can only be created from the dashboard. # List API keys Source: https://developer.mailbeast.ai/api-reference/api-keys/list-api-keys /openapi.json get /v1/api-keys All keys in the caller’s workspace, with masked prefixes (no secrets). Requires the `apikeys:manage` scope, which no wildcard grants and which can only be created from the dashboard. # Revoke an API key Source: https://developer.mailbeast.ai/api-reference/api-keys/revoke-an-api-key /openapi.json delete /v1/api-keys/{id} Revoke a key by id - effective immediately. Cannot be undone. Requires the `apikeys:manage` scope, which no wildcard grants and which can only be created from the dashboard. # Campaign send status Source: https://developer.mailbeast.ai/api-reference/campaigns/campaign-send-status /openapi.json get /v1/campaigns/{id}/status Lightweight live send-state (running/paused/completed), schedule-window state, and headline metrics. Deep reporting lives under the analytics endpoints. Requires one of the following scopes: `campaigns:read`, `campaigns:all`, `all:read`, `all:all`. # Create a campaign Source: https://developer.mailbeast.ai/api-reference/campaigns/create-a-campaign /openapi.json post /v1/campaigns Create a campaign with its schedule, settings, sequence, sending accounts, and optionally its leads, in one call. Set `launch: true` to start it right away, which also needs the `campaigns:lifecycle` scope. If it cannot launch, the campaign is still created and `launchError` says why. Requires one of the following scopes: `campaigns:write`, `campaigns:all`, `all:all`. # Delete a campaign Source: https://developer.mailbeast.ai/api-reference/campaigns/delete-a-campaign /openapi.json delete /v1/campaigns/{id} Delete a campaign to clean up programmatically-created resources. Requires one of the following scopes: `campaigns:write`, `campaigns:all`, `all:all`. # Get a campaign Source: https://developer.mailbeast.ai/api-reference/campaigns/get-a-campaign /openapi.json get /v1/campaigns/{id} Full config - schedule, settings, sequence steps, attached accounts, headline metrics. Requires one of the following scopes: `campaigns:read`, `campaigns:all`, `all:read`, `all:all`. # List campaigns Source: https://developer.mailbeast.ai/api-reference/campaigns/list-campaigns /openapi.json get /v1/campaigns Cursor-paginate the workspace campaigns (id, name, status, lead count), newest-first and stable under concurrent changes. Requires one of the following scopes: `campaigns:read`, `campaigns:all`, `all:read`, `all:all`. # Pause campaign Source: https://developer.mailbeast.ai/api-reference/campaigns/pause-campaign /openapi.json post /v1/campaigns/{id}/pause Pause an active campaign. In-flight sends finish; nothing new is scheduled. Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`. # Resume campaign Source: https://developer.mailbeast.ai/api-reference/campaigns/resume-campaign /openapi.json post /v1/campaigns/{id}/resume Resume a paused campaign from where it left off. Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`. # Send a test email Source: https://developer.mailbeast.ai/api-reference/campaigns/send-a-test-email /openapi.json post /v1/campaigns/{id}/test-email Send yourself a rendered preview of one step variant before launching. It goes from a real mailbox and counts toward that mailbox's daily limit, but not your monthly quota. Capped at 12 per day per workspace. Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`. # Start campaign Source: https://developer.mailbeast.ai/api-reference/campaigns/start-campaign /openapi.json post /v1/campaigns/{id}/start Begin sending (or resume a paused campaign). Needs a lead list and a sequence - returns 400 if the campaign has nothing to send. Requires one of the following scopes: `campaigns:lifecycle`, `campaigns:all`, `all:all`. # Update a campaign Source: https://developer.mailbeast.ai/api-reference/campaigns/update-a-campaign /openapi.json patch /v1/campaigns/{id} Partial update - only the supplied sections are touched. `accounts` and `sequence`, when present, REPLACE the existing set. Requires one of the following scopes: `campaigns:write`, `campaigns:all`, `all:all`. # Bulk update mailboxes Source: https://developer.mailbeast.ai/api-reference/email-accounts/bulk-update-mailboxes /openapi.json post /v1/accounts/bulk Apply ONE settings/limits `patch` OR one `status` change (`active`/`inactive`) across many mailboxes. `patch` and `status` are mutually exclusive. Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`. # Connect mailboxes Source: https://developer.mailbeast.ai/api-reference/email-accounts/connect-mailboxes /openapi.json post /v1/accounts Connect one SMTP/IMAP mailbox, or up to 50 at once. Any provider works, including Gmail and Outlook with an app password. There is no limit on how many mailboxes a workspace may connect. Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`. # Delete a mailbox Source: https://developer.mailbeast.ai/api-reference/email-accounts/delete-a-mailbox /openapi.json delete /v1/accounts/{id} Delete a mailbox from your workspace. Sending from it stops immediately. Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`. # Get a mailbox Source: https://developer.mailbeast.ai/api-reference/email-accounts/get-a-mailbox /openapi.json get /v1/accounts/{id} Fetch one mailbox with its embedded status, health score, and verification snapshot. Requires one of the following scopes: `accounts:read`, `accounts:all`, `all:read`, `all:all`. # List mailboxes Source: https://developer.mailbeast.ai/api-reference/email-accounts/list-mailboxes /openapi.json get /v1/accounts List your mailboxes, newest first, with stable cursor pagination. Filter by `status` or `tags` (any-of), or pass `email` to look one mailbox up by its address instead of by `id`. Requires one of the following scopes: `accounts:read`, `accounts:all`, `all:read`, `all:all`. # Pause a mailbox Source: https://developer.mailbeast.ai/api-reference/email-accounts/pause-a-mailbox /openapi.json post /v1/accounts/{id}/pause Stop sending from an active mailbox. It stays paused until you `resume` it - campaigns and warmup both skip it, and nothing re-activates it on its own. Returns `400` if the mailbox is not currently active. Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`. # Poll OAuth connect Source: https://developer.mailbeast.ai/api-reference/email-accounts/poll-oauth-connect /openapi.json get /v1/accounts/oauth/{provider}/status Poll a connect started with `init`. `pending` until the mailbox owner finishes consent, then `connected` (with `accountId`) or `failed`. Requires one of the following scopes: `accounts:read`, `accounts:all`, `all:read`, `all:all`. # Resume a mailbox Source: https://developer.mailbeast.ai/api-reference/email-accounts/resume-a-mailbox /openapi.json post /v1/accounts/{id}/resume Start sending from a mailbox again. One you paused resumes immediately; one we stopped after repeated failures is re-verified first and only resumes if the check passes. Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`. # Start OAuth connect Source: https://developer.mailbeast.ai/api-reference/email-accounts/start-oauth-connect /openapi.json post /v1/accounts/oauth/{provider}/init Start connecting a Google Workspace or Microsoft mailbox over OAuth. Returns a consent URL to open in a browser, plus a `state` handle. Once the owner grants access, poll `GET /v1/accounts/oauth/{provider}/status?state=…`. Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`. # Update a mailbox Source: https://developer.mailbeast.ai/api-reference/email-accounts/update-a-mailbox /openapi.json patch /v1/accounts/{id} Update a mailbox’s settings and limits, and rotate its SMTP/IMAP credentials. The email address cannot be changed. Including any `smtp*` or `imap*` field rotates the credentials and re-runs verification. Requires one of the following scopes: `accounts:write`, `accounts:all`, `all:all`. # Verification result Source: https://developer.mailbeast.ai/api-reference/email-accounts/verification-result /openapi.json get /v1/accounts/{id}/verification The latest SMTP + IMAP + DNS verification result for a mailbox. Requires one of the following scopes: `accounts:read`, `accounts:all`, `all:read`, `all:all`. # Count unread emails Source: https://developer.mailbeast.ai/api-reference/emails/count-unread-emails /openapi.json get /v1/emails/unread/count Number of unread emails, optionally narrowed to specific mailboxes or a campaign. Emails outside a conversation have no read state and are not counted. Requires one of the following scopes: `emails:read`, `emails:all`, `all:read`, `all:all`. # Delete an email Source: https://developer.mailbeast.ai/api-reference/emails/delete-an-email /openapi.json delete /v1/emails/{id} Remove an email from your inbox. It stops appearing in listings. The copy on the mail server is not touched. Requires one of the following scopes: `emails:write`, `emails:all`, `all:all`. # Forward an email Source: https://developer.mailbeast.ai/api-reference/emails/forward-an-email /openapi.json post /v1/emails/{id}/forward Forward an email to recipients you choose. It is sent from the mailbox that owns the thread, with the original quoted below your note; all recipients are blocklist-checked. Requires one of the following scopes: `emails:send`, `emails:all`, `all:all`. # Get an email Source: https://developer.mailbeast.ai/api-reference/emails/get-an-email /openapi.json get /v1/emails/{id} Fetch one email with its body – the only endpoint that returns the body. A `null` body means none was stored; if the stored body can't be read right now the call returns `503` rather than a false empty. Requires one of the following scopes: `emails:read`, `emails:all`, `all:read`, `all:all`. # List emails Source: https://developer.mailbeast.ai/api-reference/emails/list-emails /openapi.json get /v1/emails List emails across your mailboxes, newest first. Results carry a `snippet` preview but never the message body; fetch a single email to read it. Filter by mailbox, campaign, conversation, lead, read state, direction, folder, date, or full-text search. Requires one of the following scopes: `emails:read`, `emails:all`, `all:read`, `all:all`. # Mark a conversation read Source: https://developer.mailbeast.ai/api-reference/emails/mark-a-conversation-read /openapi.json post /v1/threads/{id}/read Mark every email in a conversation as read in one call. Read state lives on the conversation, not the individual email - this is the one call that clears it. Requires one of the following scopes: `emails:write`, `emails:all`, `all:all`. # Reply to an email Source: https://developer.mailbeast.ai/api-reference/emails/reply-to-an-email /openapi.json post /v1/emails/{id}/reply Reply within the email's conversation. It is sent from the mailbox that owns the thread to the other party – you supply only the body (plus optional cc/bcc, which are blocklist-checked). Requires one of the following scopes: `emails:send`, `emails:all`, `all:all`. # Get a lead search Source: https://developer.mailbeast.ai/api-reference/lead-finder/get-a-lead-search /openapi.json get /v1/lead-searches/{id} One search’s status and live progress counters (companies found, companies processed, emails found) – the single target to poll while a run is in flight. Requires one of the following scopes: `leadfinder:read`, `all:read`, `all:all`. # List lead searches Source: https://developer.mailbeast.ai/api-reference/lead-finder/list-lead-searches /openapi.json get /v1/lead-searches Cursor-paginate the workspace’s lead searches (newest-first, stable under concurrent changes) for history and to discover ids to poll. In-progress chat drafts are not listed. Requires one of the following scopes: `leadfinder:read`, `all:read`, `all:all`. # Add leads Source: https://developer.mailbeast.ai/api-reference/leads/add-leads /openapi.json post /v1/campaigns/{cid}/leads Add 1-100 leads to a campaign. Each is deduplicated by email within the campaign, provider-detected, and counts toward the monthly-imports quota. The response summarizes the outcome and returns the created leads. Requires one of the following scopes: `leads:write`, `leads:all`, `all:all`. # Bulk mutate leads Source: https://developer.mailbeast.ai/api-reference/leads/bulk-mutate-leads /openapi.json post /v1/campaigns/{cid}/leads/mutate Apply one operation (`set_status`, `add_tags`, `remove_tags`, or `delete`) across many leads. Target them by `ids`; `delete` also accepts a `filter`, or `all: true` to remove every lead. Reply-based statuses can only be set on leads that have already replied. Requires one of the following scopes: `leads:write`, `leads:all`, `all:all`. # Delete a lead Source: https://developer.mailbeast.ai/api-reference/leads/delete-a-lead /openapi.json delete /v1/campaigns/{cid}/leads/{leadId} Remove a single lead. Contacted leads are soft-deleted with a retention window. Requires one of the following scopes: `leads:write`, `leads:all`, `all:all`. # Get a lead Source: https://developer.mailbeast.ai/api-reference/leads/get-a-lead /openapi.json get /v1/campaigns/{cid}/leads/{leadId} One lead including live sequence progress (steps sent/planned, next send). Requires one of the following scopes: `leads:read`, `leads:all`, `all:read`, `all:all`. # List campaign leads Source: https://developer.mailbeast.ai/api-reference/leads/list-campaign-leads /openapi.json get /v1/campaigns/{cid}/leads Page through a campaign’s leads with filters (status, tags, search). Requires one of the following scopes: `leads:read`, `leads:all`, `all:read`, `all:all`. # Search leads by email Source: https://developer.mailbeast.ai/api-reference/leads/search-leads-by-email /openapi.json get /v1/leads/search Find leads across all of the workspace’s campaigns by email (or fragment), for dedupe and cross-campaign lookup. Returns up to 10 matches. Requires one of the following scopes: `leads:read`, `leads:all`, `all:read`, `all:all`. # Update a lead Source: https://developer.mailbeast.ai/api-reference/leads/update-a-lead /openapi.json patch /v1/campaigns/{cid}/leads/{leadId} Update a lead’s fields, tags, custom fields, or status. The email address can only be changed while the lead has not been contacted yet. Reply-based statuses can only be set once the lead has replied. Requires one of the following scopes: `leads:write`, `leads:all`, `all:all`. # Current usage Source: https://developer.mailbeast.ai/api-reference/workspace/current-usage /openapi.json get /v1/usage Everything you have used and everything you have left, grouped by product: your Email Outreach plan limits and your Lead Finder credits. Check it before a large send or import. For billing cycles and renewal dates, see `GET /v1/account/subscription`. Requires one of the following scopes: `usage:read`, `all:read`, `all:all`. # Current usage for one limit Source: https://developer.mailbeast.ai/api-reference/workspace/current-usage-for-one-limit /openapi.json get /v1/usage/{limitType} Just one limit (used/limit/remaining/percentage/isUnlimited) - handy when you only care about the one you’re about to spend. Requires one of the following scopes: `usage:read`, `all:read`, `all:all`. # Subscription Source: https://developer.mailbeast.ai/api-reference/workspace/subscription /openapi.json get /v1/account/subscription Everything you pay for, grouped by product: your Email Outreach subscription and your separate Lead Finder subscription. Each has its own plan, status, and billing cycle. Requires one of the following scopes: `usage:read`, `all:read`, `all:all`. # Workspace identity Source: https://developer.mailbeast.ai/api-reference/workspace/workspace-identity /openapi.json get /v1/workspace Who this key belongs to - your workspace name and timezone. (Your plan lives in `GET /v1/usage` and `GET /v1/account/subscription`.) Requires one of the following scopes: `usage:read`, `all:read`, `all:all`. # Authentication Source: https://developer.mailbeast.ai/authentication API keys, scopes, and how the workspace is resolved. Every request carries an API key as a Bearer token: ```bash theme={null} curl https://api.mailbeast.ai/v1/api-keys \ -H "Authorization: Bearer mb_live_7Fq2Ka9Lm3Xb0ZpH_…" ``` The key identifies **both** the caller and the workspace - you never put an organization id in the path. A missing or revoked key returns `401`. ## Scopes Keys are least-privilege: each carries only the scopes it needs, and every route declares the one scope it requires. A key missing a required scope returns `403`. ### Granular scopes Every scope maps to a live capability - there are no scopes without an endpoint behind them. | Scope | Grants | | ---------------------------------------------- | --------------------------------------------------------------------- | | `campaigns:read` / `campaigns:write` | Read / create & update campaigns | | `campaigns:lifecycle` | Start / pause / resume sending (spends quota) | | `leads:read` / `leads:write` | Read / ingest campaign leads | | `emails:read` / `emails:send` / `emails:write` | Read emails / send replies & forwards (billable) / mark-read & delete | | `accounts:read` / `accounts:write` | Read / connect & update sending mailboxes | | `metrics:read` | Read campaign & workspace analytics | | `leadfinder:read` | Read Lead Finder searches | | `usage:read` | Read plan usage, subscription & workspace identity | | `apikeys:manage` | Create / list / revoke API keys | ### Wildcard scopes A key can also hold a wildcard that covers many scopes at once. A route always requires a **granular** scope; any wildcard that covers it is accepted just the same. | Wildcard | Covers | | ------------------------------------------------------------- | ----------------------------------------- | | `campaigns:all` / `leads:all` / `emails:all` / `accounts:all` | Every action within that one domain | | `all:read` | Every read-only scope, across all domains | | `all:all` | Everything - **except** `apikeys:manage` | For example, a route requiring `emails:read` accepts any of `emails:read`, `emails:all`, `all:read`, or `all:all`. `all:read` never covers a write, send, or lifecycle scope - those need the matching `*:write` / `*:all` / `all:all`. `apikeys:manage` is opt-in and **never** covered by any wildcard (not even `all:all`). It is grantable only from the dashboard - never delegable through the API - and a key holding it can mint only keys whose scopes are a **subset** of its own, so there is no privilege escalation. ## Rate limits The API is rate-limited **per workspace** - the limit is shared across every API key in the workspace, so minting extra keys does not raise your ceiling. | Limit | Window | Scope | | ---------------- | ---------- | ---------------------------- | | **120 requests** | 60 seconds | Per workspace (organization) | Exceed the limit and you get `429` with a `Retry-After` header (seconds) and the standard error envelope: ```json theme={null} { "error": { "type": "rate_limit_error", "code": "rate_limited", "message": "Rate limit exceeded: 120 requests per minute per workspace. Retry in 60s.", "status": 429, "requestId": "…", "retryAfter": 60 } } ``` Watch `X-RateLimit-Remaining` and slow down before you hit `0`. On a `429`, wait `Retry-After` seconds before retrying; back off exponentially on repeated hits. For bulk lead loads, batch and space your calls rather than firing them all at once. ## Managing keys Create your first key in the dashboard ([**Settings → API Keys**](https://app.mailbeast.ai/settings/api)), where the full secret is shown **once**. After that you can rotate keys programmatically with the [API Keys endpoints](/api-reference/api-keys/list-api-keys) using a key that has `apikeys:manage`. # Campaigns Source: https://developer.mailbeast.ai/campaigns Create, configure, launch and monitor email campaigns. A campaign bundles a **schedule**, **settings**, an email **sequence**, the sending **accounts**, and its **leads**. The API is task-shaped: you create a fully-configured campaign in one call rather than stitching together six. ## Scopes | Action | Scope | | ------------------------------- | --------------------- | | Read campaigns and status | `campaigns:read` | | Create / update / delete config | `campaigns:write` | | Start / pause / resume sending | `campaigns:lifecycle` | Lifecycle is deliberately a **separate** scope from `campaigns:write`: starting a campaign spends send quota and sender reputation. A key with only `campaigns:write` can build and edit a campaign but cannot start sending. Creating with `launch: true` therefore needs **both** scopes. ## Create a campaign One call configures everything. The configuration - campaign, schedule, settings, sending accounts, and sequence - is **all-or-nothing**: if any part fails validation (for example an SMTP account that isn't yours), nothing is created. `accounts` must be your workspace's own accounts. `leads` are best-effort and summarized in the response. If `launch: true` is set and the campaign can't start, the call still returns `201` - the campaign is created as a draft, with `launched: false` and a `launchError` explaining why (start it later with `POST /v1/campaigns/{id}/start`). Because the response is a success, a retry with the same `Idempotency-Key` replays it instead of creating a second draft. ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/campaigns \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Q4 Product Launch Outreach", "schedule": { "timezone": "America/New_York", "sendingDays": [1, 2, 3, 4, 5], "sendingMinutesStart": 540, "sendingMinutesEnd": 1020, "dailyLimit": 50 }, "settings": { "trackOpens": true, "trackClicks": true, "stopOnReply": true }, "sequence": [ { "subject": "Quick question about {{companyName}}", "body": "

Hi {{firstName}}, …

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

Just circling back.

", "delayDays": 3 } ], "accounts": ["123e4567-e89b-12d3-a456-426614174000"], "leads": ["jane@acme.com", "sam@globex.com"], "launch": false }' ``` `sendingMinutesStart` / `sendingMinutesEnd` are minutes from midnight in the campaign timezone - `540` = 09:00, `1020` = 17:00. Step delays are counted from the previous step; the first step sends immediately when a lead enters. The response returns the created campaign plus a lead-ingest summary: ```json Response theme={null} { "campaign": { "id": "333e9ef7-734d-40b2-9e3f-cbce63411b69", "name": "Q4 Product Launch Outreach", "status": "draft", "schedule": { "timezone": "America/New_York", "dailyLimit": 50, "…": "…" }, "settings": { "trackOpens": true, "trackClicks": true, "stopOnReply": true, "sendAsTextOnly": false }, "metrics": { "leadsCount": 2, "sent": 0, "opens": 0, "clicks": 0, "replies": 0 }, "sequence": [ { "subject": "Quick question about {{companyName}}", "body": "

Hi {{firstName}}, …

", "delayDays": 0, "delayHours": 0 } ], "accounts": ["123e4567-e89b-12d3-a456-426614174000"] }, "leads": { "submitted": 2, "created": 2, "duplicates": 0, "invalid": 0 }, "launched": false } ``` ## Launch and control sending Drive the send lifecycle through explicit routes - `/start`, `/pause`, or `/resume`. To end a campaign, pause it (or delete it); completion otherwise happens automatically when the end date passes or every lead has been sent. ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/campaigns/{id}/start \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` You can also launch at creation time by passing `"launch": true` in the create body (requires `campaigns:lifecycle`). A campaign must have **at least one lead and a sequence** to start. A launch attempt without them is rejected `400` on `POST /start`; on create-with-`launch` the campaign is still created and returned as a draft (`201`, `launched: false`, with a `launchError`). ## Send a test email Before you launch, send yourself a rendered preview of one step variant, from a real mailbox, exactly as a lead would receive it. ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/campaigns/{id}/test-email \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "variantId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "3f1c2b7e-9a4d-4e18-b2c1-6d8f5a0e7c93", "recipient": "you@acme.com", "personalization": "dummy" }' ``` ```json Response theme={null} { "sent": true, "messageId": "" } ``` `personalization` decides where merge variables come from. `dummy` fills them with placeholders. `lead` uses a real lead, which you name with `leadId`. There is no fallback: asking for `lead` without a `leadId` is rejected, so a test can never quietly leak the wrong person's data into an email. The content is the campaign's own: you cannot supply a body here, so this is not a way to send arbitrary mail. A test goes out from a real mailbox and counts toward **that mailbox's daily limit**, but not toward your monthly email quota. It is capped at **12 tests per day per workspace** (`429` beyond that). The recipient must not be blocked in your workspace. A test carries your campaign content, so the same do-not-email rules apply to it as to a real send: it is refused for anyone on your blacklist, by address or by domain. That list is your own instruction not to contact someone, and a test is not an exception to it. A `400` means something about the request needs changing – the mailbox rejected the recipient, for example – and the message says what. If the send fails on our side instead, you get a **`503`**: the request was fine, it costs you no quota, and retrying shortly is the right move. Distinguishing the two matters if you retry automatically: a `400` is worth giving up on, a `503` is not. ## Tags Every campaign in `GET /v1/campaigns` carries its `tags` – the ones you set in the dashboard. The API reads them and lets you filter by them, so "the campaigns for client X" is a single call rather than a full page-through. ```bash theme={null} # every campaign, with its tags curl https://api.mailbeast.ai/v1/campaigns \ -H "Authorization: Bearer $MAILBEAST_API_KEY" # → "tags": [ { "id": "8debf023-…", "name": "q4-launch", "color": "#10B981" } ] # only campaigns carrying at least ONE of these tags (comma-separated ids) curl "https://api.mailbeast.ai/v1/campaigns?tags=8debf023-…,1f2e3d4c-…" \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` Tags are **created, renamed, and deleted in the dashboard**, not through the API. There is no `/v1/tags` resource. Campaign tags and mailbox tags are **separate sets**. Two tags with the same name on the two pages are different tags, and their ids never cross over: a campaign tag id passed to `GET /v1/accounts?tags=` matches nothing. ## Check status A lightweight poll of live send-state, the schedule window, and headline metrics: ```bash theme={null} curl https://api.mailbeast.ai/v1/campaigns/{id}/status \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ```json Response theme={null} { "id": "333e9ef7-734d-40b2-9e3f-cbce63411b69", "status": "active", "isWithinSchedule": true, "nextScheduleOpensAt": null, "metrics": { "leadsCount": 2, "sent": 2, "opens": 1, "clicks": 0, "replies": 0 } } ``` ## Update `PATCH` touches only the sections you supply. Supplying `accounts` or `sequence` **replaces** that section entirely (the sequence is replaced in place - you always end up with exactly the steps you sent). A supplied `sequence` must have **at least one step** - an empty array is rejected (`400`); omit the field to leave the sequence unchanged. ```bash theme={null} curl -X PATCH https://api.mailbeast.ai/v1/campaigns/{id} \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schedule": { "dailyLimit": 100 } }' ``` Unlike create, `PATCH` applies each supplied section independently - it is not a single cross-section transaction, so a later section failing does not roll back an earlier one. Send one section at a time if you need to isolate failures. The `sequence` of a campaign that is **actively sending** cannot be changed; pause it first. Sending `accounts` **can** be updated live - the change takes effect on the next sends. # Emails Source: https://developer.mailbeast.ai/emails Read the replies your campaigns get, and answer them. Emails are flat: one object per message, across every mailbox you have connected. Conversations are not fetched as a whole. Instead, `threadId` ties messages together, and you can ask for just the newest message of each conversation. ## Scopes | Action | Scope | | ----------------------------------- | -------------- | | List and read emails | `emails:read` | | Delete an email, mark a thread read | `emails:write` | | Reply and forward | `emails:send` | **List results never include the message body.** They carry a `snippet` preview instead, which keeps paging fast no matter how large the messages are. Fetch a single email when you need to read it. ## List emails `GET /v1/emails` is cursor-paginated, newest first. Keep requesting with `?cursor=meta.nextCursor` until `meta.hasMore` is `false`. ```bash theme={null} # unread replies from one mailbox curl -G https://api.mailbeast.ai/v1/emails \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -d mailbox=jane@acme.com \ -d type=received \ -d isUnread=true ``` ```json Response theme={null} { "data": [ { "id": "8c5a1e07-2b93-4d6f-a0e8-7c4b9d1f2a36", "threadId": "13a265fd-f0f2-b1d1-d6fc-154e483183e7", "messageId": "", "mailbox": { "id": "3f1c2b7e-9a4d-4c8e-b1f6-5d2a7e0c3b94", "email": "jane@acme.com" }, "from": { "email": "john@prospect.io", "name": "John Carter" }, "to": ["jane@acme.com"], "cc": [], "bcc": [], "subject": "Re: Pricing question", "snippet": "Thanks for the details, this works for us.", "isUnread": true, "type": "received", "campaignId": "c1f3b0a2-6d4e-4f8b-9a1c-2e5d7f0a3b6c", "leadId": "9d4e2f10-5b6a-4c73-8e19-0f2a7c4d8b31", "hasAttachments": false, "receivedAt": "2026-07-13T09:24:11.000Z", "sentAt": null } ], "meta": { "hasMore": false, "nextCursor": null } } ``` Filters combine freely: `mailbox` (comma-separated addresses), `campaignId`, `threadId`, `leadId`, `isUnread`, `type` (`sent` or `received`), `folder` (`inbox`, `untracked`, `important`, `snoozed`, or `archived`), `since`, `until`, and `search`. `search` is full text over subject, preview text and sender address. Quoted phrases, `OR` and `-exclude` all work. ### Conversations without fetching them Pass `latestOfThread=true` to get one email per conversation. That gives you a conversation-style view at the cost of an ordinary list. ```bash theme={null} curl -G https://api.mailbeast.ai/v1/emails \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -d latestOfThread=true -d limit=25 ``` Your other filters are applied **first**, and the newest email that survives them is the one you get back. So this returns the last message each contact sent you – including in conversations you have already answered, where the newest message in the thread is your own reply: ```bash theme={null} curl -G https://api.mailbeast.ai/v1/emails \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -d latestOfThread=true -d type=received ``` An email that belongs to no conversation is a conversation of one, and is always kept. To open one conversation, list its messages with `?threadId=...`. ## Read one email `GET /v1/emails/{id}` is the only endpoint that returns the body. ```bash theme={null} curl https://api.mailbeast.ai/v1/emails/{id} \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` It returns the same object as the list, plus: ```json theme={null} { "body": { "html": "

Thanks for the details, this works for us.

", "text": "Thanks for the details, this works for us." } } ``` ## Reply and forward `POST /v1/emails/{id}/reply` answers in the conversation the email belongs to. You supply the body; the sender and the recipient are decided for you. The reply goes out from the mailbox that owns the conversation and is addressed to the other party, so a reply can never turn into a message to someone else. ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/emails/{id}/reply \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "body": { "text": "Happy to set up a call. Does Thursday work?" }, "cc": ["cto@acme.com"] }' ``` ```json Response theme={null} { "id": "5e9a3c81-7d24-4b60-9f13-8a6c2e0d4b57", "threadId": "13a265fd-f0f2-b1d1-d6fc-154e483183e7", "status": "queued" } ``` `POST /v1/emails/{id}/forward` sends the message on to recipients you choose, with the original quoted below your note. It also goes out from the mailbox that owns the conversation. ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/emails/{id}/forward \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": ["colleague@acme.com"], "body": { "text": "FYI, worth a look." } }' ``` ### Recipients you add are checked against your blocklist Anyone you name yourself – `cc` and `bcc` on a reply, `to`, `cc` and `bcc` on a forward – is checked against your workspace blocklist. If one of them is on it, the whole request is refused with `400` and nothing is sent; the message names every blocked address, so you can fix the list in one pass. ## Read state `isUnread` on an email means "this message arrived after the last time its conversation was read". A conversation can therefore hold both read and unread messages at once, which is exactly what you want when polling: it tells you which replies are new, not merely which conversations have something new in them. You **read** that state per email, and you **clear** it per conversation. `POST /v1/threads/{id}/read` marks every email in a conversation as read. It is the only write there is for read state, and it is the same action a person takes by opening the conversation in the dashboard, so what you do through the API and what your team sees stay in step. ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/threads/{id}/read \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ## Delete an email `DELETE /v1/emails/{id}` removes an email from your inbox. It stops appearing in listings. The copy on the mail server is left alone. # Idempotency Source: https://developer.mailbeast.ai/idempotency Retry writes safely - 24h response replay, plus durable dedup for quota and leads. Network calls fail in ambiguous ways - a request times out, but you can't tell whether the server processed it. Sending an `Idempotency-Key` lets you retry safely: within a 24-hour window the API replays the original response instead of running the operation again. Send the header on any mutating request (`POST`, `PATCH`, `DELETE`). If you retry with the **same key**, you get the original response back: ```bash theme={null} curl https://api.mailbeast.ai/v1/campaigns \ -H "Authorization: Bearer mb_live_…" \ -H "Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ -H "Content-Type: application/json" \ -d '{ "name": "Q3 outreach" }' ``` Retry that request within the window and the second call returns the same body with an extra header instead of creating another campaign: ``` X-Idempotency-Replayed: true ``` Idempotency is **opt-in**. A request with no `Idempotency-Key` header behaves normally - nothing is cached and every call runs. Reads (`GET`) are already idempotent and ignore the header. ## How it works * **Generate a unique key per operation** - a UUID is ideal. Use one key for one logical write, and reuse it only when retrying that same write. * **Keys are scoped to your workspace** - a key you use never collides with another workspace's, and the scope is derived server-side from your API key. * **Only successful (`2xx`) responses are cached.** If the first attempt failed, retrying with the same key genuinely re-runs the operation. **Exception - creating an API key is *not* idempotent.** Its response contains a one-time token that is never stored in retrievable form, so it cannot be replayed. An `Idempotency-Key` on `POST /v1/api-keys` is ignored, and a retry mints a **new** key - revoke any extras from the [list endpoint](/api-reference/api-keys/list-api-keys). ## Edge cases | Situation | Result | | -------------------------------------------- | ------------------------------------------------------------ | | Same key, **same** body | Original response replayed (`X-Idempotency-Replayed: true`) | | Same key, **different** body | `422` - the key is already bound to a different request | | Same key, two requests **in flight** at once | The second returns `409` while the first is still processing | | Key longer than 255 characters | `400` | A `422` from a body mismatch looks like this: ```json theme={null} { "error": { "type": "invalid_request_error", "code": "idempotency_key_reused", "message": "Idempotency-Key already used with a different request body.", "status": 422, "requestId": "…" } } ``` # Overview Source: https://developer.mailbeast.ai/introduction One API for the full local lead-gen & cold-outreach loop. The MailBeast API is a **task-shaped** REST API: each endpoint maps to a job you actually do - connect a mailbox, launch a campaign, load leads, read replies, discover new leads - not to an internal database table. `https://api.mailbeast.ai` `Authorization: Bearer mb_live…` ## Design principles These principles shape every endpoint as it ships: * **Composites over ceremony.** Create a fully-configured, optionally-live campaign in **one** call instead of six. * **One-or-many bodies.** Ingest endpoints accept a single object *or* an array of the same shape. * **Cursor pagination** on every list. **Least-privilege scopes** on every route. * **One error envelope** everywhere: `{ "error": { "type", "code", "message", "status", "requestId" } }`, plus an optional `details[]` on validation errors and `retryAfter` on `429`s. Create a key and make your first authenticated call in two steps. # Lead Finder Source: https://developer.mailbeast.ai/lead-finder List past lead searches and poll a running search's progress. Lead Finder discovers new companies and leads from a natural-language query. This first release exposes the two **reads** you need to follow a search that was started in the dashboard: list your past searches, and poll one search's status and progress while it runs. Starting, stopping, and pulling results from a search over the API is coming in a later release. For now you create and drive a search in the dashboard, and read its state here. ## Scopes | Action | Scope | | ------------------------------ | ----------------- | | List searches, read one search | `leadfinder:read` | ### Pagination The list is cursor-paginated and stable under concurrent changes. When `meta.hasMore` is `true`, pass `meta.nextCursor` back as `?cursor=` to fetch the next page; it is `null` on the last page. Cursors are opaque – do not parse or build them by hand. ```bash theme={null} curl "https://api.mailbeast.ai/v1/lead-searches?cursor=eyJ0cyI6..." \ -H "Authorization: Bearer mb_live_..." ``` `limit` defaults to 50 and caps at 100. ## Get one search ```bash theme={null} curl "https://api.mailbeast.ai/v1/lead-searches/{id}" \ -H "Authorization: Bearer mb_live_..." ``` This is the **single poll target** while a search runs. It returns the search's status plus live progress counters. ```json theme={null} { "id": "a7d4e2f1-08b3-4c65-9e7a-1b2c3d4e5f60", "query": "dental clinics in Berlin", "status": "processing", "searchMode": "companies", "maxResults": 500, "discoveryFinished": false, "progress": { "companiesFound": 84, "companiesProcessed": 51, "emailsFound": 137 }, "errorMessage": null, "createdAt": "2026-07-10T12:00:00.000Z", "updatedAt": "2026-07-10T12:15:00.000Z", "completedAt": null } ``` # Leads Source: https://developer.mailbeast.ai/leads Add, read, update and bulk-manage a campaign's leads. Leads live under a campaign. Add them, sync their state to your CRM, and manage them in bulk. Adding leads counts toward your monthly-imports quota. ## Scopes | Action | Scope | | ----------------------------------- | ------------- | | List / get / search leads | `leads:read` | | Add / update / delete / bulk-mutate | `leads:write` | ## Add leads Add 1-100 leads in one call. Each is **deduplicated by email** within the campaign and provider-detected. The response summarizes the outcome and returns the leads that were created. ```bash theme={null} curl -X POST https://api.mailbeast.ai/v1/campaigns/{cid}/leads \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "leads": [ { "email": "jane@acme.com", "firstName": "Jane", "companyName": "Acme", "tags": ["vip"], "customFields": { "industry": "SaaS" } }, { "email": "sam@globex.com", "firstName": "Sam" } ] }' ``` ```json Response theme={null} { "summary": { "submitted": 2, "created": 2, "duplicates": 0, "invalid": 0 }, "leads": [ { "id": "…", "email": "jane@acme.com", "status": "not_contacted", "…": "…" } ] } ``` ## List and filter **Cursor-paginated**, newest-first, and stable under concurrent inserts/deletes - the right shape for syncing leads to a CRM (offset pages would skip or duplicate rows as leads change). ```bash theme={null} # first page curl "https://api.mailbeast.ai/v1/campaigns/{cid}/leads?limit=50&status=lead_replied_interested" \ -H "Authorization: Bearer $MAILBEAST_API_KEY" # next page - pass the previous response's meta.nextCursor curl "https://api.mailbeast.ai/v1/campaigns/{cid}/leads?limit=50&cursor=eyJ0cyI6IjIwMjYtMDctMDJUMDA6MDA6MDAuMDAwWiIsImlkIjoiM2YxYzJiN2UtOWE0ZC00ZTE4LWIyYzEtNmQ4ZjVhMGU3YzkzIn0" \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ```json Response theme={null} { "data": [ { "id": "…", "email": "jane@acme.com", "…": "…" } ], "meta": { "hasMore": true, "nextCursor": "eyJ0cyI6IjIwMjYtMDctMDJUMDA6MDA6MDAuMDAwWiIsImlkIjoiM2YxYzJiN2UtOWE0ZC00ZTE4LWIyYzEtNmQ4ZjVhMGU3YzkzIn0" } } ``` Filter by `status`, `tags`, or `search` (email / name / company). Keep requesting with `?cursor=meta.nextCursor` until `meta.hasMore` is `false` (then `nextCursor` is `null`). `limit` defaults to 50 (max 250). ## Get one lead Returns the lead plus **live sequence progress** (steps sent/planned, next send). ```bash theme={null} curl https://api.mailbeast.ai/v1/campaigns/{cid}/leads/{leadId} \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ## Update a lead ```bash theme={null} curl -X PATCH https://api.mailbeast.ai/v1/campaigns/{cid}/leads/{leadId} \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "firstName": "Janet", "customFields": { "tier": "gold" } }' ``` Changing a lead's **email** safely re-runs the add / dedup / provider-detect pipeline, and is only allowed while the lead has not been contacted. ## Bulk mutate One operation over an id list - `set_status`, `add_tags`, `remove_tags`, or `delete`. `delete` also accepts a `filter` instead of ids. ```bash theme={null} # add a tag to specific leads curl -X POST https://api.mailbeast.ai/v1/campaigns/{cid}/leads/mutate \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operation": "add_tags", "ids": ["…", "…"], "tags": ["hot"] }' # delete every lead matching a filter curl -X POST https://api.mailbeast.ai/v1/campaigns/{cid}/leads/mutate \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operation": "delete", "filter": { "status": "lead_replied_unsubscribed" } }' # delete EVERY lead - explicit opt-in (an empty filter will NOT do this) curl -X POST https://api.mailbeast.ai/v1/campaigns/{cid}/leads/mutate \ -H "Authorization: Bearer $MAILBEAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operation": "delete", "all": true }' ``` ## Search across campaigns Find a lead by email across every campaign in the workspace - one row per campaign the email appears in. ```bash theme={null} curl "https://api.mailbeast.ai/v1/leads/search?email=jane@acme.com" \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` # Quickstart Source: https://developer.mailbeast.ai/quickstart From zero to your first authenticated call in two steps. In the dashboard, open [**Settings → API Keys**](https://app.mailbeast.ai/settings/api), click **Create API key**, include the **`apikeys:manage`** scope (plus any scopes you want to delegate, e.g. `campaigns:read`), and copy the secret - it is shown only once. List the keys in your workspace. A `200` with your key in the response confirms the token and scope are resolved correctly. ```bash theme={null} curl https://api.mailbeast.ai/v1/api-keys \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ```json Response theme={null} { "data": [ { "id": "b6fd2c26-…", "name": "Bootstrap", "keyPrefix": "mb_live_7Fq2Ka9Lm3Xb0ZpH_••••", "scopes": ["apikeys:manage", "campaigns:read"], "environment": "live", "lastUsedAt": null, "expiresAt": null, "revokedAt": null, "createdAt": "2026-07-11T09:12:04.000Z" } ] } ``` For security reasons, the API key will be displayed only once, and there is no way to recover it if you lose it. # Workspace & Usage Source: https://developer.mailbeast.ai/workspace Read your limits, subscription, and plan - know what's left before you hit a limit. These endpoints let you read your own limits, subscription, and plan. They're all read-only. Use them to see how much you have left **before** a request would hit a plan limit (`402`) or the rate limit (`429`) - so nothing catches you off guard. ## Scopes | Action | Scope | | --------------------------------------- | ------------ | | Read usage, subscription, and workspace | `usage:read` | ## Check your usage before you spend `GET /v1/usage` returns how much you've used and have left, grouped by product. Email Outreach and Lead Finder are separate products, so each is reported on its own - your Email Outreach plan limits and your Lead Finder credit balance. ```bash theme={null} curl https://api.mailbeast.ai/v1/usage \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ```json theme={null} { "emailOutreach": { "planName": "pro", "limits": [ { "limitType": "monthly_emails", "used": 1240, "limit": 100000, "remaining": 98760, "isUnlimited": false, "usagePercentage": 1.24 }, { "limitType": "active_contacts", "used": 5000, "limit": 25000, "remaining": 20000, "isUnlimited": false, "usagePercentage": 20 } ] }, "leadFinder": { "credits": { "limitType": "lead_finder_credits", "used": 0, "limit": 500, "remaining": 500, "isUnlimited": false, "usagePercentage": 0 } } } ``` Before a large lead import or a send, read the relevant limit and only proceed while `remaining` is comfortable. This turns a `402` from a surprise into a decision you control. ## Subscription: everything you pay for, in one place `GET /v1/account/subscription` groups your billing by product. **Email Outreach** and **Lead Finder** are independent subscriptions, each with its own billing cycle and renewal date. ```bash theme={null} curl https://api.mailbeast.ai/v1/account/subscription \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ```json theme={null} { "emailOutreach": { "planName": "pro", "status": "active", "billingInterval": "monthly", "currentPeriodEnd": "2026-08-01T00:00:00.000Z", "cancelAtPeriodEnd": false, "trial": null, "addons": { "contacts": 20000, "monthlyEmails": 10000, "aiCredits": 5000, "emailValidation": 0 } }, "leadFinder": { "tier": "pro", "status": "active", "billingInterval": "annual", "currentPeriodEnd": "2027-02-01T00:00:00.000Z", "cancelAtPeriodEnd": false } } ``` * **Your Email Outreach status** - `active`, `on_hold`, or `inactive` when you have no active subscription (for example, on the free plan). * **`currentPeriodEnd`** - when each product's current paid period ends. It renews then, or ends if `cancelAtPeriodEnd` is `true`, and is `null` when there's no active subscription. Each product has its own, so they can differ. * **Your Email Outreach add-ons** - the extra capacity you've purchased, in units. They're already folded into the caps in `GET /v1/usage`, so this is just the breakdown. `null` when you have no add-ons, and while the plan is on hold, since the extra capacity isn't active then either. * **Your Lead Finder subscription** - billed separately from Email Outreach. `null` if you don't have one. The Lead Finder credit balance itself lives in `GET /v1/usage` under `leadFinder.credits`. ## Workspace identity `GET /v1/workspace` returns the workspace resolved from your key: org name and timezone. Your plan lives in [`GET /v1/usage`](#check-your-usage-before-you-spend) and [`GET /v1/account/subscription`](#subscription-everything-you-pay-for-in-one-place). ```bash theme={null} curl https://api.mailbeast.ai/v1/workspace \ -H "Authorization: Bearer $MAILBEAST_API_KEY" ``` ```json theme={null} { "organizationId": "…", "name": "Acme Inc", "timezone": "America/New_York" } ```