SendRaven

Changelog

What changed, and why.

Dated entries, newest first, written for someone deciding whether to depend on us. Each says what the change does and the failure it prevents. Subscribe with the RSS feed, or read the docs for how things work today.

The site, the docs, and a tool count that cannot drift

The public site now says what the product is: a homepage built around a real send, reply and read, product pages for the one API, replies, guardrails, campaigns and deliverability, and a dated comparison against eight providers that says where each of them wins.

The docs fill their gaps — MCP, templates, campaigns, automations, topics and approvals — and are served from the same origin as /openapi.json and /llms.txt, so an agent that lands anywhere can find the rest.

The MCP tool count on the homepage, in llms.txt and on the install page is generated from the server's registry at build time. It had been typed by hand, and was wrong by six.

  • site
  • docs
  • agents

Plans, one meter, and Stripe

Three plans: Free with 3,000 emails a month and no overage, Pro and Scale with an included allowance and metered overage per thousand. One meter, emails sent; marketing and transactional share it, contacts are never counted, and receiving is free.

The limit is enforced in the send path rather than the API layer, so the scheduler, campaigns and automations all read the same check. A campaign that would cross the line is refused whole before the first message goes out, because a half-sent campaign cannot be re-run without mailing the first half twice.

Only a plan without overage can block a send. A paid workspace runs into metered overage instead — stopping password resets to protect a few dollars of margin is the wrong trade. Running out answers 402, not 429: retrying will not help, and a client backing off on 429 would retry forever. GET /v1/usage says what is left before you commit a batch.

Checkout, the billing portal and the webhook are wired to Stripe, with the included allowance carried as a free first tier on each metered price so it shows on the invoice and is applied per billing period rather than per calendar month. A failed payment does not downgrade you; a cancelled subscription returns the workspace to Free rather than deleting it.

  • billing

Per-key limits had never fired. They do now.

The daily cap, the recipient allowlist and the approval hold were configured, stored, and never loaded by the API's auth middleware, so every check evaluated against an absent value and read it as no limit. A key set to hold everything for review sent immediately. Found by configuring all three and watching a send to a non-allowlisted address get refused by the layer underneath rather than by us. The middleware now loads them, and the request type names each field so omitting one is a compile error rather than a feature that quietly does nothing.

Two more fixes worth knowing about. A CR or LF in a subject or header value could close the header block and replace the body of a message still signed by your domain; every header value is now sanitised in the MIME builder, and a subject with a line break is a 422. And request logs were recording the Authorization header verbatim; credential-bearing paths are now redacted at the logger. If you had a key before 1 September, rotate it.

  • agents
  • sending

Attachments on replies, kept for thirty days

Received mail is written to object storage instead of being carried inline in the notification, which had a 150KB ceiling — any reply with a photo or a PDF exceeded it and was lost with nothing recorded. Attachments now survive, and the thread lists each one with its name, type and size.

The bytes stay in storage rather than the database: documents cap at 16MB and mail arrives up to 40MB, so inlining a large attachment failed the insert and lost the whole message. They are served through the app, scoped to the workspace in the query itself, with attachment disposition and nosniff so an HTML or SVG attachment cannot run against a signed-in session.

Raw mail expires after thirty days. The message row is permanent — sender, subject, body and the attachment list all stay — but a download of an older attachment answers 410 rather than pretending it was never there. Thirty days covers the window in which someone actually acts on a reply; keeping every customer's mail forever is a liability rather than a feature.

  • inbound

Add a domain, get both identities, and receive on it

Adding a domain used to ask what it sends and create one identity for that answer. Now you give the domain you send from and both subdomains are provisioned beneath it — mail. for transactional, news. for campaigns — with one combined set of DNS records. The send path already knows each message's kind, so it picks the identity; nobody declares anything.

The split is not cosmetic. Mailbox providers score reputation on the From domain, so a campaign's complaint rate on a shared domain can push password resets into spam. The cost is that From addresses sit on the subdomain.

The generated records now include an optional inbound MX, and receiving works for any verified domain rather than one configured by hand. Publish it and replies to that domain land in Conversations. The dashboard gained Check DNS now, a From written as Name <address> resolves correctly, and a domain the provider already knows is adopted rather than refused.

  • sending
  • inbound

A contact is a person, and every list pages the same way

A contact was one row per audience, so the same address on three lists was three people: an unsubscribe had to write three times and could half-apply, and engagement was recorded three times. Now one contact per address per workspace, with audiences as membership. Deleting an audience removes memberships, not people, and deleting a person keeps their suppression — the record of an opt-out has to outlive the record of the person.

Tags are the label you apply without deciding on a schema first, normalised on the way in so VIP and vip cannot split a segment in two. Segments filter on them: several tags mean all of them, and exclusions win over inclusions. GET /v1/contacts finds a person by address without enumerating audiences, and the contacts and suppressions screens can now edit what the data layer could always do.

Every v1 list answers with the same envelope — object, data, has_more, next_cursor — with cursors rather than offsets, because these collections are written to while you page through them and an offset makes new rows shift everything down. An agent re-reading a duplicate acts on it twice. A malformed cursor is ignored rather than rejected: it means a stale value, and the first page is more useful than a 400.

  • campaigns
  • agents

Connect to MCP over HTTP, with OAuth instead of a pasted key

The MCP server speaks Streamable HTTP at mcp.sendraven.ai, so it is connected rather than installed. It holds no database or provider credentials of its own: every tool goes through the public REST API, so it reaches exactly what your key can reach and nothing more. initialize and tools/list answer without a key on purpose — registries probe with an unauthenticated list, and a blanket 401 makes a server look like it exposes nothing.

OAuth lets a client connect without anyone pasting an API key: dynamic registration, authorization code with PKCE, refresh rotation, revocation, and the discovery documents a client reads after a 401. Scopes are the same vocabulary as API keys, so an access token resolves to the same workspace and scopes a key does and there is no second list to drift.

Authorization codes are single-use by conditional update, so two clients racing an intercepted code cannot both redeem it. Presenting a rotated refresh token revokes the whole grant, because at that point two parties hold it and we cannot tell which is legitimate. Nothing is stored in the clear.

  • agents

MCP covers the whole API, and the API covers the migration

The MCP server went from 24 tools to 39: topics and preferences, audiences and contacts, segments with a live count, metrics, scheduled mail, batch suppression, webhook delivery attempts, broadcast recipients and API keys. Tool descriptions are written for the model that reads them — which call to reach for, and which mistakes cannot be undone.

Batch suppression import is the migration path: an existing provider's unsubscribe list has to land here before the first campaign, or everyone who already opted out gets mailed again. GET /v1/webhook-endpoints/:id/events answers why a webhook did not fire, which was otherwise invisible. Metrics compute open and click rates over delivered rather than sent, so a bounce problem does not read as an engagement problem.

Webhooks can be configured from the dashboard, which refuses a non-https endpoint — payloads carry recipient addresses and bounce diagnostics. New API keys and signing secrets are shown once and handed to the page in a short-lived cookie rather than a query string, so a live credential never lands in browser history or an access log.

  • agents
  • docs

Topics: opt down, not just out

A person can unsubscribe from newsletters and keep receiving receipts. Given only all-or-nothing, someone who wants less mail often presses spam instead, and a complaint costs the sending domain far more than an opt-out does.

Preferences belong to the person, keyed on (workspace, address, topic), so someone on two audiences has one opinion about newsletters. An absent preference means the topic's default applies — nothing is materialised per contact, so changing a default later moves everyone who never chose. An unknown topic key blocks the send rather than bypassing every preference someone has set.

The one-click unsubscribe link carries the topic: a click opts out of that kind of mail and lands on a preference page showing the rest. Transactional mail ignores topics.

  • campaigns

Automations that end themselves

A sequence is defined once here rather than as scheduled sends in the calling app, and one API call enrols someone. Steps are read live rather than snapshotted at enrolment, so a fix to step three reaches people already partway through.

Three exits, all automatic: unsubscribe, reply, and suppression. Cancelling queued sends without cancelling the enrolment stops today's email and lets the sequence generate tomorrow's, which reads to the recipient as an unsubscribe that did not work. Exit on reply is possible only because this platform receives mail — a drip still arriving after someone has answered is the clearest way a sequence reads as broken.

One active enrolment per person per automation is guaranteed by a unique index, not a check, so a double signup or a retried call cannot deliver the sequence twice. Pausing cancels in-flight enrolments rather than freezing them; a sequence resumed days later arrives with no context. Scheduled mail also has its own view now, with overdue rows flagged.

  • campaigns

Limits on the key, and a person between the draft and the send

An API key can carry a daily send cap, a recipient allowlist — exact addresses or whole domains, with lookalike domains rejected — and a hold for human approval. All three are checked per send, before anything is persisted, and they live on the key rather than the workspace: a workspace quota only trips after an agent has already mailed ten thousand people.

A held send returns pending_approval with an approval id. It is not an error and there is nothing to retry; the message waits in a queue where a person sees exactly what would go out before releasing it. Releasing is conditional on still-pending, so two reviewers cannot send the same message twice, and approvals expire after 72 hours so a forgotten draft never sends days later out of context.

Alongside: templates with {{variables}} whose values are HTML-escaped on substitution and whose rendering fails on a missing variable rather than mailing a visible placeholder; batch send of up to 100 messages, each succeeding independently, answering 207 when some failed so you retry only those; and attachments.

  • agents
  • sending

Replies come back as threads

Every outbound message carries a Message-ID we stamp; a reply echoes it, and the thread is an exact join on that header. Subject matching is deliberately absent — Re: Invoice from two customers would merge two conversations, and an agent reading the merged thread would answer one customer with the other's context. An unmatched reply starts a new thread rather than guessing.

The body of a third reply is mostly the first two quoted back. The stripped text is what an agent reads; it handles Gmail, Outlook, Apple Mail and bare quote runs, and returns the original whenever trimming would leave nothing, because over-trimming silently loses what the person wrote. Both versions are stored.

GET /v1/threads?awaiting_reply=true answers what has someone said to me that I have not answered in one call. Each inbound message carries SPF, DKIM and spam verdicts, and the thread view flags a reply that failed them — a forged reply is exactly the input an agent should not treat as instructions.

  • inbound
  • agents

Campaigns that are refused before they can hurt you

Audiences, contacts, CSV import and broadcasts. The importer accepts a Mailchimp export's column names as readily as anyone else's, upserts rather than duplicates so re-running a file is safe, and reports rows with no usable address instead of dropping them.

Before any campaign starts, the last fourteen days are read and the send is refused above a 4% bounce rate or a 0.08% complaint rate. Accounts get reviewed at around 5% and 0.1%; we stop short because the alternative to refusing one campaign is every workspace losing the ability to send. The preview endpoint reports the recipient count and the verdict before anyone commits, since a campaign cannot be recalled.

Two workers, or a worker and an impatient send-now, cannot both start the same campaign: the claim is a conditional status flip. Segments can exclude people who have not engaged in a window, and treat never-sent contacts as engaged — they have no history to judge, and excluding new signups would be worse than including them.

  • campaigns

Send, schedule, and stop mailing people who bounced

Every outbound message goes through one send path, which is where the suppression check lives, so no caller can mail someone who opted out or hard-bounced. Transactional and marketing are separate sending identities on separate subdomains with separate reputation, so a campaign's complaint rate cannot stop password resets. Fan-out is chunked and paced to the limits of the layer underneath rather than retried into throttling.

Delivery events are ingested idempotently, since they are delivered at least once. Permanent bounces and complaints suppress the address for all mail; an unsubscribe suppresses marketing only, so it never blocks a receipt. Transient bounces — a full mailbox, greylisting — are left alone, because suppressing them silently shrinks a healthy list.

Scheduling accepts an ISO timestamp or plain words like in 3 days, and refuses input it cannot parse rather than falling back to now, which would fire a whole sequence at once. Suppression is re-checked at send time, so an unsubscribe on day two stops the day-three email, and an unsubscribe is two writes: suppress the address and cancel everything queued for it.

  • sending