# SendRaven SendRaven is email infrastructure for AI agents. Applications and agents use it, over REST or its MCP server, to send, receive and act on email: transactional email, campaigns, automations, inbound threads, contacts, topics, templates and agent safety controls, under one platform and one usage meter. Priced per outbound email sent, on one meter. 3,000 emails a month free; Standard is from $19/month for 50,000 emails, then automatic volume pricing — $19 at 50K, $35 at 100K, $109 at 250K, $199 at 500K, $379 at 1M, $699 at 2M, $999 at 3M — and $0.50 per 1,000 beyond 3 million. Contacts cost $0 and inbound replies are included on every plan. A payment method is required before sending, and nothing is charged until paid sending is switched on. Free covers 1 workspace per account; any further workspace is on Standard from the day it is created. Site: https://sendraven.ai API base URL: https://api.sendraven.ai OpenAPI: https://api.sendraven.ai/openapi.json Short orientation: https://sendraven.ai/llms.txt MCP server: https://mcp.sendraven.ai/mcp (63 tools) Install for Claude Code: claude mcp add --transport http sendraven https://mcp.sendraven.ai/mcp ## What SendRaven does that a send endpoint does not - **Priced per email sent, never per contact.** One meter. A contact is free. Pricing by list size charges most to the customer whose list is growing, which is the point of the product. - **Transactional, campaigns and automations on one plan.** One key, one plan, one dashboard, one suppression list. Two products means two suppression lists that disagree and a price gap that rewards mislabelling campaigns as transactional. - **The reply is a thread, not a webhook.** Joined on Message-ID to the message it answers. A webhook hands you a raw message and leaves you to work out which conversation it belongs to. - **Quoted history stripped from replies.** text is what they typed; raw_text is the whole body. The third reply in a thread is mostly the first two quoted back. A model reading it pays for that and gets confused by it. - **One query for what is awaiting a reply.** GET /v1/threads?awaiting_reply=true This is the poll an agent actually runs. Without it you build a state machine over inbound events. - **Per-key daily cap and recipient allowlist.** daily_send_limit and allowed_recipients on the key, checked on every send. A workspace quota trips after an agent has mailed thousands of people. A key limit trips before. - **Human approval hold on a key.** requires_approval drafts the message and waits for a person. The only way to let an agent write mail without letting it send mail. - **Campaign refused before it starts on bounce or complaint rate.** Checked in the send path; the preview call says whether it will be allowed. Reputation dashboards report the damage. A gate prevents it. - **Unsubscribe cancels queued sends and ends the sequence.** Two writes: suppress the address, cancel what is scheduled for it. Doing only the first is how a working unsubscribe still delivers day three of a welcome series. - **Opt down by topic, per person.** Preferences on (email, topic), not on a contact row. Given only all-or-nothing, people press spam instead, and a complaint costs the domain far more than an opt-out. - **Remote MCP server with no stored credentials.** mcp.sendraven.ai, OAuth or a Bearer key, reaches exactly what the key can. A server that holds its own database credentials is a shared identity for every caller. - **llms.txt and an unauthenticated OpenAPI document.** Both public, written by hand, so an agent can read them before it has a key. A model decides what it is holding from the description document. Generated ones say what, not which. - **Errors written for an agent.** 402 for out of plan, not 429. pending_approval is a status, not an error. A client that backs off on 429 retries a payment problem forever. A model that sees an error retries a held draft. - **Inbound included and free.** Replies cost nothing and land in the thread. Charging for the reply discourages the half of email that makes it a conversation. - **Free tier that does not expire or throttle by the day.** 3,000 emails a month, inbound included, no daily cap. A trial that expires is a deadline, not a tier, and a 100-a-day cap is a tier you cannot test a campaign on. - **Rate per 1,000 past the entry paid tier.** $0.50, and it stops applying at each published price. The rate past the allowance is the real price for anyone who grows. - **Usage pricing without tier cliffs.** One curve, applied to what you actually sent. No tier to choose and none to outgrow. Tiers do not hurt at the boundaries. They hurt in the middle, which is where most senders live. ## Pricing One meter: outbound emails sent. No charge for contacts, no separate marketing plan, inbound replies included, and no separate transactional and marketing plans. A verified payment method is required before any outbound email, including on Free; it is never charged on Free and no account is ever upgraded automatically. | Plan | Price | Included | Domains | Log retention | | --- | --- | --- | --- | --- | | Free | $0/mo | 3,000 emails, then sending pauses | 1 | 7 days | | Standard | from $19.00/mo | 50,000 emails in the minimum, then automatic volume pricing | unlimited | 90 days | | Enterprise | custom | negotiated | unlimited | custom | Standard is one curve, not a set of tiers to choose between: SendRaven applies the best rate for what was actually sent. The published points are $19 at 50,000, $35 at 100,000, $109 at 250,000, $199 at 500,000, $379 at 1,000,000, $699 at 2,000,000, $999 at 3,000,000. Between them the rate is $0.50 per 1,000, and above 3,000,000 it stays at that rate on self-service pricing. One outbound recipient is one billable email: a message with three addresses across To, Cc and Bcc counts three, a campaign to 10,000 counts 10,000. Received mail counts zero. Retries never count twice. Enterprise is worth a conversation from around 1,000,000 emails a month, where a negotiated rate, an annual commitment or dedicated sending usually beats the published curve. It is never a sending wall and never a plan a customer is moved onto: self-service pricing keeps applying at every volume, including above 3,000,000. contact@sendraven.ai. Full page: https://sendraven.ai/pricing --- # Documentation ## Quickstart https://sendraven.ai/docs/quickstart From nothing to a delivered email. Three things have to be true before a message can go out: a verified sending domain, an API key with the right scope, and a recipient who isn't suppressed. This page walks through the first two. ### 1. Verify a sending domain Use a subdomain per purpose: `mail.example.com` for transactional mail, `news.example.com` for campaigns. Mailbox providers score reputation per domain, so keeping them apart is what stops a campaign's complaint rate affecting password reset delivery. Two records are optional and marked `optional: true`: an MX on the subdomain so replies arrive in Conversations, and a `link.` CNAME for click tracking. Publish the CNAME and, once its certificate is issued, links in mail from that subdomain are rewritten to `link.mail.example.com/c/…`, a click is recorded, and the recipient is redirected. Links stay on your name throughout; nothing goes through a SendRaven domain. A bare URL in the text is turned into a link first, so it is tracked too. Add `data-track="off"` to an anchor to leave one link alone. Adding a domain returns the DNS records to publish. `GET /v1/domains` then shows each record beside what is resolving now, so a stuck verification tells you which record is missing rather than just failing. The bounce subdomain is verified by SES separately, and later. `mail_from.status` is `pending` while SES polls the MX record, which can take up to 72 hours after the record appears; `active` once the Return-Path is on your domain. A domain is `verified` and sends fine before then. The Return-Path is amazonses.com meanwhile, so SPF aligns with Amazon rather than you, and DMARC passes on DKIM. If SES gave up before the MX existed the status is `failed`, and the next check restarts verification once the record resolves. ```bash curl -X POST https://api.sendraven.ai/v1/domains \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "mail.example.com", "risk_class": "transactional"}' ``` ### 2. Send Always pass an `Idempotency-Key`, a new one for each message. If the request times out, retrying with the same key and the same body returns the original result instead of delivering a second copy, which matters more here than in most APIs, because the duplicate lands in someone's inbox. A retry that arrives while the first request is still running waits up to 8 seconds for the first to finish and then returns its response (or, if the first failed, runs itself); only if the first is still running after that does it answer `409 idempotency_in_progress`; wait a moment and retry again. Reusing a key with a different body answers `422 idempotency_key_reused` and sends nothing. ```bash curl -X POST https://api.sendraven.ai/v1/emails \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "from": "Team ", "to": "someone@example.com", "subject": "Hello", "html": "

Hi there.

" }' ``` ### 3. Watch what happened `GET /v1/emails/{id}` returns the message with its full event timeline: send, delivery, bounce, complaint, open, click. This is the call to reach for when someone asks why an email didn't arrive. ### Runnable examples [CommonNinja/sendraven-examples](https://github.com/CommonNinja/sendraven-examples) has the whole round trip as code you can run: send, list threads awaiting a reply, read one, reply in the same thread and verify signed webhooks, in [Node](https://github.com/CommonNinja/sendraven-examples/tree/main/node-quickstart) and [Python](https://github.com/CommonNinja/sendraven-examples/tree/main/python-quickstart). It also has a support inbox triaged by Claude over the MCP server with every reply held for approval, and an OpenAI Agents follow-up agent. Each one was run against the production API before it was published, and each README shows that run's output. ## Authentication https://sendraven.ai/docs/authentication API keys, scopes, and what a key can be limited to. Every request carries a bearer token: `Authorization: Bearer sk_live_…`. Keys are created in the dashboard under API keys, or with `POST /v1/api-keys`, shown once, and stored only as a hash. If you lose one, revoke it and make another. The API also accepts an OAuth access token (`srv_oat_…`), which is what an MCP client holds after signing in through the consent screen. It carries the scopes that were granted and none of a key's limits: no daily limit, no recipient allowlist and no approval hold, which belong to a key someone configured. Because nothing limits it, it also cannot decide an approval: `POST /v1/approvals/{id}` answers `403 forbidden` for an OAuth token, and approvals are decided by a person in the dashboard or by an API key without guardrails. See [API keys](/docs/api/api-keys#guardrails) and the [MCP server](/docs/mcp). ### Scopes A key carries only the scopes you grant it. A key that only sends transactional mail has no business reading your contact list, and scoping it accordingly limits what a leak costs you. A route called without its scope answers `403 forbidden`, naming the scope. | Scope | Allows | | --- | --- | | emails:send | Send one message or a batch, cancel a scheduled send, decide an approval (API keys only, and not a key with guardrails), mark a thread handled, add and remove suppressions, enrol someone in an automation, and record an event | | emails:read | Read sent emails, their event timelines and metrics, the suppression list, the approvals queue and usage | | threads:read | Read conversations and inbound replies, and download inbound attachments | | domains:read | List sending domains and read one | | domains:write | Add, verify and delete sending domains, and list, create, edit and revoke API keys | | contacts:read | Read audiences, contacts, tags, contact properties, segments, topics and a person's topic preferences | | contacts:write | Create and delete audiences, add, edit, tag and remove contacts, manage contact properties, segments and topics, and set or import topic preferences | | broadcasts:read | Read campaigns, their preview, recipients and clicked links, and read automations | | broadcasts:write | Create, edit, send, resume, decide and delete campaigns, and create, edit, switch on or off and delete automations | | templates:read | List and read templates, and render one | | templates:write | Create, update, duplicate and delete templates | | webhooks:read | List webhook endpoints and their recent deliveries | | webhooks:write | Create, edit and delete webhook endpoints | `domains:write` is the closest thing to an administrative scope, because it is the one that manages keys. Grant it only to a key that should be able to mint others. A key it mints can hold only scopes the minting credential holds, and a key that carries a daily limit, a recipient allowlist or an approval hold can only mint keys at least as restricted. No key can edit or revoke itself: either answers `403 forbidden`. See [API keys](/docs/api/api-keys). A key's limits also apply to campaigns. A key with an approval hold or a recipient allowlist cannot send, schedule, reschedule or resume a campaign, or decide an A/B test's winner (`403 forbidden`), and a key with a daily limit can start one only when its recipients fit what is left of that limit today (`429 daily_limit`). See [Guardrails](/docs/api/api-keys#guardrails). ### Idempotency `POST` requests accept an `Idempotency-Key` header. Replaying the same key on the same path in the same workspace within 24 hours returns the stored response rather than acting again, and the response carries `Idempotent-Replay: true` so you can tell. Only a successful response is stored: after a `4xx` or `5xx`, a retry with the same key runs the request again. A retry that arrives while the first request is still running waits up to 8 seconds for it and returns its answer, and answers `409 idempotency_in_progress` only if it is still running after that. A key must be reused only with the identical request: the same key with a different body answers `422 idempotency_key_reused`, once the endpoint's scope has been checked (a credential without the scope gets `403`). Responses that carry a secret shown once (a new API key, a new webhook endpoint) are never stored, so retrying those creates another. See [Idempotency](/docs/api#idempotency). ## Sending https://sendraven.ai/docs/sending One message, a batch, a schedule, or a template. ### Scheduling `scheduled_at` accepts an ISO 8601 timestamp or a relative phrase: `in 2 min`, `in 3 days`, `in 2 weeks`. Anything it cannot parse is an error rather than a fallback to now, because a silent fallback would fire an entire sequence at once. A scheduled message can be cancelled with `DELETE /v1/emails/{id}` while its status is still `scheduled`. The message becomes `canceled` and does not go out, even if its time has already come. When it is due, suppressions and topic opt-outs are checked again for every recipient, so someone who unsubscribes after it was scheduled does not receive it. An unsubscribe also takes that address off the scheduled message straight away, from `to`, `cc` or `bcc`, and everyone else on it still receives it. If no `to` address is left, the message becomes `canceled` instead of going out. A message can carry at most 50 recipients across `to`, `cc` and `bcc`, the most the delivery provider accepts on one message. More is refused with `422 invalid_request` before anything is stored; send separate messages, or use a [campaign](/docs/broadcasts) for a list. A marketing message (`risk_class: "marketing"`) carries exactly one recipient. Its unsubscribe link and `List-Unsubscribe` header belong to one person, so a marketing send that names more than one address across `to`, `cc` and `bcc` is refused with `422 invalid_request` before anything is stored. Send one message per recipient, which a batch does in one call, or use a [campaign](/docs/broadcasts). ### Recipient names `to`, `cc` and `reply_to` take a bare address or `Name
`, so the recipient sees their name in the To line instead of an address. A name may be quoted, as in `"Lima, Ana" `, and is quoted for you in the header either way, so a comma is safe. Names outside ASCII are encoded for you. `bcc` accepts the same form and discards the name, since Bcc never reaches a header. The name only reaches the headers. Suppressions, topic opt-outs, an API key's recipient allowlist and the message log all match on the address, so adding a name never changes who is reached, and `GET /v1/emails` still returns bare addresses. A name longer than 100 characters, or with a line break, `<` or `>` in it, is refused with `422`. Campaigns and automations do this on their own: when a contact has a first or last name, it goes in the To line. ```json { "from": "Team ", "to": ["Ana Lima ", "bo@example.com"], "subject": "Your invoice", "text": "Attached." } ``` ### Batches `POST /v1/emails/batch` takes an array of 1 to 100 messages, up to 25 MB of JSON in all (a single send allows 15 MB, enough for one 10 MB attachment), and the whole array is validated before anything is sent. An entry that fails validation (a malformed address, a name with a line break, a subject with a line break, an oversized attachment) rejects the entire batch with `422 invalid_request`, its `details` pointing at the entry by index, and nothing is sent. Check addresses before building the batch. Past validation, entries are sent one at a time in order and each succeeds or fails on its own: an unknown template or topic, missing variables, no subject or body, a marketing entry with more than one recipient, a guardrail, the plan allowance or an SES error fails only that entry. The response is `{ "object": "list", "data": [...], "failed": n }`, with `data` in input order and each entry carrying `ok` and either the send result or its `error`. It is `202` when every entry succeeded and `207` when any failed; retry only those. A skipped entry, one whose recipients were all suppressed, counts as a success. An entry that fails on missing variables names them in its message but carries no `missing` array. ### Templates Pass `template` and `variables` instead of `html`. Values are HTML-escaped when they are substituted into the HTML body, so text supplied by a user or an agent cannot rewrite the message around it. A missing variable is an error, not a half-rendered email with a visible placeholder. ```json { "from": "Team ", "to": "ana@example.com", "template": "welcome", "variables": { "name": "Ana", "plan": "Pro" } } ``` ### Suppression > A send whose `to` addresses are all suppressed returns 202 with `skipped: true` and a reason. It is recorded in the log as `rejected` rather than silently dropped, so support can answer why something didn't arrive. Every recipient is checked, in `to`, `cc` and `bcc`: a suppressed or opted-out address is dropped wherever it appears and the message goes to the rest, as long as at least one `to` address is left. ## Templates https://sendraven.ai/docs/templates Stored copy with `{{variables}}`, rendered safely at send time. A template is a stored subject and body with `{{name}}` placeholders. Prefer sending through one over composing HTML in the caller: the copy is reviewed once, and the values filled in at send time cannot rewrite the message around them. ### Creating one `POST /v1/templates` takes a `slug` (lower-case letters, digits and hyphens), a `name`, a `subject`, `html` and an optional `text` body. Posting a slug that already exists updates it in place and returns `200`; a new one returns `201`. On an update, leaving `text` out keeps the stored text body and `"text": null` removes it. Placeholders are extracted from the subject and both bodies and returned as `variables`, so `GET /v1/templates` tells you what each template needs before you render it. `POST /v1/templates/{slug}/duplicate` copies one under a new slug (`409 conflict` if the new slug is in use), and `DELETE /v1/templates/{slug}` removes one. A delete answers `404` for a slug that does not exist, and `409 invalid_state` while an active or paused automation still renders a step from the template, naming each one. ```bash curl -X POST https://api.sendraven.ai/v1/templates \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "welcome", "name": "Welcome", "subject": "Welcome to {{plan}}, {{name}}", "html": "

Hi {{name}}, your {{plan}} workspace is ready.

" }' ``` ### Rendering without sending `POST /v1/templates/{slug}/render` with `{ "variables": { … } }` (strings, or numbers and booleans, which are turned into strings) returns the rendered `subject`, `html` and `text` without mailing anyone. `text` is `null` when the template has no plain-text body; a send through such a template gets a plain-text part derived from the HTML. It exists so an agent can check that its values produce sensible copy before a real person sees it. ```bash curl -X POST https://api.sendraven.ai/v1/templates/welcome/render \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"variables": {"name": "Ana", "plan": "Pro"}}' ``` ### Sending with one Pass `template` and `variables` on `POST /v1/emails` instead of `subject` and `html`. `subject` becomes optional and overrides the template's own when given. `POST /v1/emails/batch` accepts the same fields per entry and resolves them identically: a batch that quietly ignored templates would be a trap. On a send, every value in `variables` must be a string; a number or boolean fails validation with `422 invalid_request`, unlike the render endpoint, which converts them. ```json { "from": "Team ", "to": "ana@example.com", "template": "welcome", "variables": { "name": "Ana", "plan": "Pro" } } ``` ### Values are escaped > Values are HTML-escaped when substituted into the HTML body. They come from agents and end users, and a template is HTML: interpolated raw, a name like `