Base URL: ${origin}/v1. Authenticate with Authorization: Bearer ml_… (create keys in the console). Every error looks like {"error":{"code","message","fix","docs_url"}}.
Send an email
curl ${origin}/v1/emails \
-H "Authorization: Bearer ml_…" -H "Content-Type: application/json" -H "Idempotency-Key: order-1234" \
-d '{"from":"Acme <hi@acme.com>","to":"ada@example.com","subject":"Your receipt","html":"<p>Thanks!</p>","tags":{"order":"1234"}}'
Each send is checked against the key’s budgets and your plan when it is accepted. Suppressed recipients (bounced, complained, unsubscribed) are dropped. Set request_approval: true to have a person approve it first. The loop guard holds a key’s 6th email with the same subject to the same person within 10 minutes.
Endpoints
| POST | /v1/emails | Send one email Body: from, to, cc, bcc, reply_to, subject, html, text, headers, tags, attachments [{filename, content (base64), content_type}], scheduled_at, request_approval, topic, unsubscribe. Header: Idempotency-Key. Returns the email with its status: sent, queued, scheduled, held or suppressed. |
| POST | /v1/emails/batch | Send up to 500 emails Body: an array of email objects. Returns {data, errors}; each email is accepted or refused on its own. |
| GET | /v1/emails | List emails Query: status (comma separated), to, key_id, broadcast_id, before, limit (≤100). Returns {data, has_more}. |
| GET | /v1/emails/{id} | Get an email Status, reason and event timeline. |
| GET | /v1/emails/{id}/raw | Raw message The exact MIME message (message/rfc822). |
| POST | /v1/emails/{id}/cancel | Cancel Scheduled, queued or held emails. |
| POST | /v1/emails/{id}/approve | Approve a held email Console only (owner, admin or approver). Body: note (optional). Who approved it and the note are kept in the timeline and the audit log. |
| POST | /v1/emails/{id}/reject | Reject a held email Console only (owner, admin or approver). Body: note (the reason). |
| POST | /v1/inboxes | Create an inbox Body: username (optional; random if empty), domain (your verified domain with the receiving MX record, or empty for the shared domain), name. Use username "*" on your own domain for a catch-all. |
| GET | /v1/inboxes | List inboxes Addresses and unread conversation counts. |
| GET | /v1/inboxes/{id}/threads | List conversations Query: unread=true, limit, before. |
| POST | /v1/inboxes/{id}/send | Start a conversation Same body as /v1/emails; sent from the inbox address so replies come back to it. |
| GET | /v1/threads/{id} | Get a conversation Every received and sent message, oldest first. |
| PATCH | /v1/threads/{id} | Mark read / unread Body: {"read": true}. |
| GET | /v1/messages/{id} | Get a received message Text, HTML, attachments list, SPF/DKIM/DMARC/spam verdicts, is_auto (auto-replies, lists, bounces). |
| GET | /v1/messages/{id}/attachments/{index} | Download an attachment |
| POST | /v1/messages/{id}/reply | Reply in the conversation Body: text, html, reply_all, request_approval, attachments. Sets In-Reply-To and References. Same budgets as /v1/emails. |
| GET | /v1/budget | Remaining budget This key's per-minute/hour/day/month use and limits, and the account plan. |
| GET | /v1/domains | List domains With DNS records and per-record status. |
| POST | /v1/domains/{id}/check | Check DNS now |
| GET | /v1/webhooks | List webhooks |
| POST | /v1/webhooks | Add a webhook Body: url, events (array, or ["*"]). Returns the signing secret once. |
| GET | /v1/webhooks/deliveries | Delivery log Query: webhook_id, status (pending | delivered | dead). |
| POST | /v1/webhooks/replay | Replay deliveries Body: delivery_id, or webhook_id to replay every dead delivery. |
| POST | /v1/contacts | Add or update a contact Body: email, first_name, last_name, data, topics {topic_id: true|false}. Only people who opted in. |
| GET | /v1/contacts | List contacts Query: search, topic, limit, offset. |
| GET | /v1/topics | List topics |
| GET | /v1/broadcasts | List broadcasts |
| GET | /v1/suppressions | List suppressed addresses Query: search, reason (bounce | complaint | unsubscribed | manual). |
| POST | /v1/suppressions | Suppress an address Body: email. |
| GET | /v1/approvals | Recent approval decisions Console only. Who approved or rejected each email, their role and note. |
| GET | /v1/members | List team members Console only. Members, roles, pending invites and seats {used, limit}. |
| POST | /v1/members | Invite a member Console only (owner or admin). Body: email, role (admin | developer | approver | viewer). Emails a one-time invite link, or returns invite_url when the server has no NOTIFY_FROM. |
| PATCH | /v1/members/{id} | Change a role Console only (owner or admin). Body: role. Applies to the member's sessions right away. |
| DELETE | /v1/members/{id} | Remove a member Console only (owner or admin). Signs them out everywhere. |
| POST | /v1/members/{id}/link | Resend an invite / sign-in link Console only (owner or admin). Emailed, or returned as url when the server has no NOTIFY_FROM. |
| POST | /v1/login/email | Email me a sign-in link No auth. Body: email. Same answer whether or not the address is a member. |
| POST | /v1/login/link | Use a sign-in link No auth. Body: token. One use, 15 minutes. Sets the session cookie. |
| POST | /v1/invites/accept | Accept an invite No auth. Body: token, name. One use, 7 days. Sets the session cookie. |
| GET | /v1/audit | Audit log Console only (owner or admin); API keys have no scopes, so they can't read it. Query: action (e.g. key or key.created), actor, q, since, until (YYYY-MM-DD), before, limit (≤200), format=csv (up to 10,000 rows). |
Receiving email
Create an inbox, and email sent to it fires email.received with the message id, conversation id and sender. Read the conversation with GET /v1/threads/{id} and answer with POST /v1/messages/{id}/reply. Messages marked is_auto are auto-replies, mailing lists or bounces: don’t answer them automatically.
MCP for AI agents
URL ${origin}/mcp with the same Bearer key. Tools: send_email, get_email, list_emails, cancel_email, get_budget, list_domains, create_inbox, list_inboxes, list_threads, get_thread, reply_to_message, send_from_inbox, list_contacts, add_contact. Refusals come back as tool errors with the code and the fix.
claude mcp add --transport http sendbuzzpro ${origin}/mcp --header "Authorization: Bearer ml_…"
Webhooks
POST with JSON {"id","type","created_at","data"} and header ml-signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">. Failed deliveries retry 6 times over ~20 hours, then can be replayed. Events: email.queued email.scheduled email.held email.approved email.rejected email.canceled email.sent email.delivered email.delivery_delayed email.bounced email.soft_bounced email.complained email.opened email.clicked email.failed email.suppressed email.received contact.created contact.unsubscribed domain.verified domain.failed account.warned account.paused key.budget_exceeded
Teams, roles and the audit log
People sign in to the console; apps and agents use API keys, which belong to the workspace. The license key holder is the owner. Others are invited by email and sign in with one-time emailed links. Roles: admin (everything except billing), developer (keys, budgets, webhooks, domains, inboxes, sending), approver (approve or reject held emails, read the log and inbox), viewer (read-only). Every sign-in and change to keys, budgets, the kill switch, domains, webhooks, the team, approvals, suppressions and inboxes goes in an append-only audit log, with the person, IP and browser.
Email statuses
scheduled queued held sending sent delivered delivery_delayed opened clicked bounced complained failed suppressed rejected canceled
Errors
| Code | HTTP | Meaning | Fix |
|---|---|---|---|
agent_budget_exceeded | 429 | The API key went over one of its own budgets (minute, hour, day or month). The error says which window, the limit and what is used. | Wait for the window to pass, or ask the account owner to raise the budget in the console. A key can’t raise its own budget. |
plan_daily_limit | 429 | The Free plan’s 100 emails a day are used. | Wait until midnight UTC or upgrade. |
plan_monthly_limit | 429 | The plan’s monthly emails are used. There are no overage charges. | Sending resumes on the 1st (UTC), or upgrade. |
lifetime_pool_used | 429 | All emails in a lifetime plan are used. | Buy a larger tier or switch to a monthly plan. |
sending_paused | 403 | The account owner turned on the kill switch. | The owner resumes sending on the console Overview page. |
key_paused | 403 | This API key is paused. | Resume it under API keys. |
account_paused | 403 | Sending is paused because bounces or spam complaints were too high (4% / 0.08% over 30 days). | Remove bad addresses and appeal from the console. A person reviews every appeal. |
domain_not_added | 422 | The from address uses a domain that isn’t on this account. | Add it under Domains. |
domain_not_verified | 422 | The domain’s DNS records aren’t verified yet. | Add the records under Domains and press Check now. |
invalid_from | 422 | The from address is missing or invalid. | Use "Name <you@your-domain.com>". |
invalid_recipient | 422 | One address in to, cc, bcc or reply_to is invalid. The error names the field. | Fix or remove it. |
missing_recipient | 422 | There is no “to”. | Add at least one address. |
too_many_recipients | 422 | More than 50 recipients in one email. | Use /v1/emails/batch to send one email per person. |
missing_subject | 422 | The subject is empty. | |
subject_too_long | 422 | The subject is over 998 characters. | |
missing_content | 422 | There is no html or text. | Send html, text or both. |
invalid_header | 422 | A custom header is reserved, malformed or has a line break. | Use the dedicated field (from, to, reply_to…) instead. |
invalid_attachment | 422 | An attachment has no filename or isn’t base64. | |
message_too_large | 422 | The email is over 10 MB. | Link to large files instead. |
invalid_scheduled_at | 422 | scheduled_at isn’t an ISO 8601 time within 30 days. | |
invalid_batch | 422 | The batch body isn’t a JSON array. | |
batch_too_large | 422 | More than 500 emails in one batch. | Split it. |
idempotency_conflict | 409 | This Idempotency-Key was used for a different request in the last 24 hours. | Use a new key for a new email. Retrying the same request with the same key is always safe. |
invalid_idempotency_key | 400 | The Idempotency-Key is over 200 characters. | |
invalid_api_key | 401 | The key is missing, malformed or deleted. | Send “Authorization: Bearer ml_…”. |
unauthorized | 401 | Not signed in. | |
console_only | 403 | Keys, budgets, approvals, the kill switch, the team and the audit log are only for people signed in to the console. | |
role_forbidden | 403 | Your role can’t do this (e.g. a viewer creating a key, or a developer approving an email). | Ask an owner or admin to change your role under Settings → Team. |
seat_limit | 403 | Your plan’s seats are used (owner and pending invites count). | Remove a member or invite, or upgrade. |
member_exists | 409 | That person is already on the team. | |
member_not_found | 404 | ||
owner_protected | 403 | The owner can’t be changed or removed; ownership follows the license key. | |
invalid_role | 422 | Role must be admin, developer, approver or viewer. | |
invite_invalid | 400 | The invite link was used, replaced or is over 7 days old. | Ask the owner to resend it. |
link_invalid | 400 | The sign-in link was used or is over 15 minutes old. | Ask for a new one. |
invalid_email | 422 | Not a valid email address. | |
bad_origin | 403 | A cross-site request to the console API was blocked. | |
not_held | 409 | The email isn’t waiting for approval. | |
not_cancelable | 409 | Only scheduled, queued or held emails can be canceled. | |
email_not_found | 404 | No email with that id (old emails are removed after the plan’s history window). | |
provider_error | 502 | Amazon SES refused the request. | Retry with the same Idempotency-Key. |
invalid_domain | 400 | Not a valid domain name. | |
domain_taken | 409 | The domain is on another SendBuzzPro account. | Contact support if it is yours. |
domain_limit | 403 | Your plan’s domain limit is reached. | |
domain_not_found | 404 | ||
invalid_webhook_url | 400 | Webhook URLs must be public https addresses. | |
invalid_events | 400 | Unknown webhook event types. | |
consent_required | 422 | Imports need confirmation that everyone opted in. | |
invalid_contact_email | 422 | ||
empty_audience | 422 | Nobody would receive the broadcast. | |
broadcast_locked | 409 | Only draft broadcasts can be edited or sent. | |
complaint_suppression | 409 | That person marked your email as spam. | Only remove it with confirm=true if they asked to be emailed again. |
inbox_limit | 403 | Your plan’s inbox limit is reached. | Delete an inbox or upgrade. |
address_taken | 409 | That inbox address is taken. | Pick another username. |
username_reserved | 422 | Names like support, admin or billing are reserved on the shared domain. | Use a more specific name, or your own domain. |
invalid_username | 422 | Usernames use letters, numbers, dots, dashes and underscores. | |
no_inbound_domain | 400 | Receiving isn’t set up for that domain. | Use your own domain with the receiving MX record. |
catch_all_shared | 422 | Catch-all inboxes only work on your own domain. | |
thread_not_found | 404 | ||
message_not_found | 404 | ||
inbox_not_found | 404 | ||
attachment_not_found | 404 | ||
invalid_json | 400 | The body isn’t valid JSON. | |
not_found | 404 | Unknown endpoint. | |
server_error | 500 | Something failed on our side. | Retry with the same Idempotency-Key. |
What you can send
Transactional email and email to people who opted in. Cold email, bought or scraped lists are not allowed.