# sendbunny > sendbunny is a self-hosted, all-in-one email platform — marketing campaigns, > a transactional email API, a shared team inbox, and analytics — that installs > into the customer's own AWS account with one click. It is a one-time purchase > ($199 once; optional yearly updates subscription at $99/yr), with sending > billed directly by AWS at Amazon SES's raw rate of > about $0.10 per 1,000 emails. Unlimited contacts and sending domains, no > monthly platform fee, and sendbunny never has access to the customer's AWS > credentials or data (zero-access by design). ## Key facts - Category: self-hosted email marketing platform / Amazon SES dashboard and UI - Runs entirely in the customer's own AWS account (Amazon SES + serverless infrastructure); no third party can suspend sending or hold data hostage, and the platform keeps running even if sendbunny disappears - Pricing: $199 one-time; no per-contact fees, no monthly fees; AWS usage billed by Amazon at cost - Common comparison points: Mailchimp, Sendy, Resend, Mailgun - Includes: campaign editor with live preview and templates, audience management with background CSV imports, REST transactional email API, DKIM/SPF/DMARC authenticated sending setup, RFC 8058 one-click unsubscribe, automatic bounce/complaint suppression, deliverability dashboard, invite-only team roles, shared inbox, audit log - Install: one pre-filled CloudFormation button in the AWS console, about ten minutes from checkout to sending; updates apply with one click in-app - The transactional API endpoint is unique per install (an AWS Lambda function URL shown in the customer's dashboard) — there is no shared api.sendbunny.co ## Docs Complete developer documentation with runnable examples. An AI agent integrating sendbunny should start at the quickstart; the full docs content is also inlined in https://sendbunny.co/llms-full.txt and the API contract is machine-readable at https://sendbunny.co/openapi.json (OpenAPI 3.1). - [Documentation index](https://sendbunny.co/docs) - [Quickstart: Send Your First Email via the API](https://sendbunny.co/docs/quickstart): Send a transactional email through your self-hosted SendBunny install in four steps: verify a domain, create an API key, copy your endpoint URL, POST JSON. - [Transactional Email API Reference](https://sendbunny.co/docs/send-email-api): Complete reference for SendBunny's transactional send endpoint: authentication, every request field, template sends, all error responses, and rate behavior. - [Email Templates and Merge Variables](https://sendbunny.co/docs/email-templates): How SendBunny templates work: merge-tag syntax with fallbacks, required-variable enforcement, aliases, and sending templates through the transactional API. - [OpenAPI 3.1 spec for the transactional API](https://sendbunny.co/openapi.json) - [llms-full.txt — this file plus the full docs content inline](https://sendbunny.co/llms-full.txt) # Full documentation content Everything an integration needs is below, verbatim from https://sendbunny.co/docs. The machine-readable API contract is at https://sendbunny.co/openapi.json. # Quickstart: send your first email Canonical URL: https://sendbunny.co/docs/quickstart This guide takes you from a fresh SendBunny install to a delivered transactional email. Every step is explicit — no context is assumed beyond the prerequisites listed below. ## Prerequisites - **A SendBunny install in your AWS account.** SendBunny is self-hosted: there is no shared SaaS endpoint. If you have not installed it yet, see [Deploy](https://sendbunny.co/deploy) — installation is a pre-filled CloudFormation template and takes about 10 minutes. - **Admin access to your SendBunny dashboard.** The install emails the first admin a temporary password. API keys can only be created by users in the ADMIN role. - **A verified sender domain.** In the dashboard, open your domain's onboarding page and complete DNS setup (DKIM/SPF/DMARC records). The API refuses to send from unverified addresses with HTTP 403. - **SES production access (for real recipients).** New AWS accounts start in the SES sandbox, which only delivers to verified recipient addresses. The dashboard includes a wizard for requesting production access. In the sandbox, sends to unverified recipients fail with HTTP 502. ## Step 1 — Create an API key - In your SendBunny dashboard, go to **API Keys** (left sidebar). - Click **New key**, name it, and select the sending domain(s) the key may use. A key can only send from the root domains it was issued for. - Copy the key immediately. It starts with `sb_` and is **shown exactly once** — SendBunny stores only a hash. If you lose it, revoke it and create a new one. ## Step 2 — Copy your API URL Because SendBunny runs in your own AWS account, your API endpoint is unique to your install. The same **API Keys** page in the dashboard shows your **Transactional API URL** — an AWS Lambda function URL that looks like `https://abc123xyz.lambda-url.us-east-1.on.aws/`. All requests are a `POST` to that URL. ## Step 3 — Send an email Replace the URL, the key, and `from` (must be an address on a domain you verified — see prerequisites — and allowed for this key in Step 1). In the SES sandbox, `to` must be a verified recipient or an [SES mailbox simulator](https://docs.aws.amazon.com/ses/latest/dg/send-an-email-from-console.html) address like `success@simulator.amazonses.com`. **curl:** ```bash curl -X POST 'https://YOUR-INSTALL.lambda-url.us-east-1.on.aws/' \ -H 'Authorization: Bearer sb_YOUR_KEY_HERE' \ -H 'Content-Type: application/json' \ -d '{ "from": "hello@yourdomain.com", "to": "success@simulator.amazonses.com", "subject": "Hello from SendBunny", "html": "
It works!
" }' ``` A successful send returns HTTP 200 with the SES message id: **Response:** ```json { "messageId": "0100018f2ab4c123-..." } ``` ## Troubleshooting | Status | Error | Fix | | --- | --- | --- | | 401 | `Unauthorized` / `Invalid API key` | Send the key as `Authorization: Bearer sb_...`. Check the key was not revoked. | | 403 | `This API key is not allowed to send from that domain` | The `from` domain is not in this key's allowed domains. The response lists `allowedRootDomains`; use one of those or issue a new key. | | 403 | `From address is not a verified sender identity` | Complete domain verification in the dashboard for the `from` domain (DNS records must be live). | | 400 | `'subject' is required` and similar | Field validation failed — the error message names the exact field. See the [API reference](https://sendbunny.co/docs/send-email-api) for the full request contract. | | 422 | `Recipient is on the suppression list` | The recipient previously bounced, complained, or was manually suppressed. Remove them in dashboard → Suppression only if you are certain the address is deliverable. | | 502 | SES error message | SES rejected the send — most often sandbox mode (unverified recipient) or sending-quota limits. The message is passed through from SES. | ## Next steps - [Full API reference](https://sendbunny.co/docs/send-email-api) — every field, every error, rate behavior. - [Email templates](https://sendbunny.co/docs/email-templates) — store templates in the dashboard, send with `templateAlias` + `data` merge variables instead of inline HTML. - [OpenAPI spec](https://sendbunny.co/openapi.json) — machine-readable contract for codegen and AI agents. --- # Transactional email API Canonical URL: https://sendbunny.co/docs/send-email-api SendBunny exposes one transactional endpoint: `POST` a JSON body to your install's API URL to send a single email. This page documents the complete contract — every field and every error the endpoint can return. A machine-readable version is published at [/openapi.json](https://sendbunny.co/openapi.json). ## Endpoint and base URL SendBunny is self-hosted, so there is no shared API host. Your endpoint is an AWS Lambda function URL unique to your install, shown as **Transactional API URL** on the dashboard's **API Keys** page. It looks like `https://abc123xyz.lambda-url.us-east-1.on.aws/`. - Method: `POST` only. Any other method returns `405 { "error": "Method not allowed" }`. - Body: JSON, UTF-8. Send `Content-Type: application/json`. AWS Lambda caps the request body at ~6 MB; Amazon SES separately caps total message size (10 MB by default). - One recipient per request. To send to N recipients, make N requests. - Responses from the API are JSON with `Content-Type: application/json`. The one exception is `429`, which is emitted by AWS Lambda itself when concurrency is capped — don't rely on its body shape. - **Not supported** (today): multiple recipients, `cc`/`bcc`, `replyTo`, attachments, and custom headers. Unrecognized JSON fields are silently ignored, so a `cc` field does not error — it just does nothing. ## Authentication Every request needs an API key in the `Authorization` header: `Authorization: Bearer sb_...`. Keys are created by an ADMIN user in dashboard → **API Keys**, are shown exactly once at creation (only a SHA-256 hash is stored), and can be revoked at any time. Each key is scoped to one or more **root domains** at creation. The key may only send from addresses whose registrable domain is in that list, and may only use templates that belong to those domains. A missing or malformed header returns `401 { "error": "Unauthorized" }`; an unknown or revoked key returns `401 { "error": "Invalid API key" }`. Treat keys as server-side secrets. The endpoint sends permissive CORS headers, but calling it from browser JavaScript would expose your key to every visitor — always call it from your backend. ## Request fields There are two send modes: **raw** (you provide `subject` + `html`/`text` inline) and **template** (you reference a stored template by `templateId` or `templateAlias`, optionally with `data` merge variables). Fields from the two modes cannot be mixed. | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string | always | Sender address as a **bare address** (`hello@yourdomain.com` — display-name forms like `Ann ` are rejected). Must be on a **verified** sender identity, and its registrable domain must be in the API key's allowed domains. | | `to` | string | always | Recipient address (exactly one, bare address). Lowercased before processing. Checked against the account suppression list. | | `subject` | string | raw mode | Email subject, non-empty. In template mode it is optional — a non-empty `subject` overrides the template's stored subject. | | `html` | string | raw mode* | HTML body. *At least one of `html` / `text` is required in raw mode. Forbidden in template mode. Raw sends ship exactly the parts you provide — a plain-text part is **not** auto-derived from `html` (that only happens for stored templates without a text body). | | `text` | string | raw mode* | Plain-text body. Same rule as `html`. Providing both is recommended for deliverability. | | `templateId` | string | template mode* | Id of a stored template. *Provide exactly one of `templateId` / `templateAlias`. | | `templateAlias` | string | template mode* | Alias of a stored template. Lowercased; resolved within the `from` address's root domain (aliases are unique per domain). | | `data` | object | no | Merge variables for template mode only (sending `data` without a template is a 400). See [value rules](#template-data) below. | ## Example: raw send **curl:** ```bash curl -X POST 'https://YOUR-INSTALL.lambda-url.us-east-1.on.aws/' \ -H 'Authorization: Bearer sb_YOUR_KEY_HERE' \ -H 'Content-Type: application/json' \ -d '{ "from": "receipts@yourdomain.com", "to": "customer@example.com", "subject": "Your order shipped", "html": "Order #1042 shipped today.
", "text": "On its way — order #1042 shipped today." }' ``` **Node.js (built-in fetch, Node 18+):** ```javascript const res = await fetch(process.env.SENDBUNNY_API_URL, { method: "POST", headers: { Authorization: `Bearer ${process.env.SENDBUNNY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "receipts@yourdomain.com", to: "customer@example.com", subject: "Your order shipped", html: "Order #1042 shipped today.
", }), }) const body = await res.json() if (!res.ok) throw new Error(`SendBunny ${res.status}: ${body.error}`) console.log("sent", body.messageId) ``` **Python (standard library only):** ```python import json, os, urllib.request req = urllib.request.Request( os.environ["SENDBUNNY_API_URL"], method="POST", headers={ "Authorization": f"Bearer {os.environ['SENDBUNNY_API_KEY']}", "Content-Type": "application/json", }, data=json.dumps({ "from": "receipts@yourdomain.com", "to": "customer@example.com", "subject": "Your order shipped", "html": "Order #1042 shipped today.
", }).encode(), ) with urllib.request.urlopen(req) as res: print(json.load(res)) # {"messageId": "..."} ``` ## Example: template send Templates are created in dashboard → **Templates** and can be given an alias (e.g. `password-reset`). Reference one by alias or id, and pass merge variables in `data`: **curl:** ```bash curl -X POST 'https://YOUR-INSTALL.lambda-url.us-east-1.on.aws/' \ -H 'Authorization: Bearer sb_YOUR_KEY_HERE' \ -H 'Content-Type: application/json' \ -d '{ "from": "hello@yourdomain.com", "to": "priya@example.com", "templateAlias": "password-reset", "data": { "name": "Priya", "reset_url": "https://app.example.com/reset/abc123" } }' ``` If the template has no stored plain-text body, SendBunny derives one from the rendered HTML automatically. Template syntax (`{{name}}`, `{{name|fallback}}`) and variable rules are documented in [Email templates](https://sendbunny.co/docs/email-templates). ## Template data rules - `data` must be a JSON **object**. Values may be strings, numbers, booleans, or nested objects of those. Arrays and `null` are rejected with a 400 that names the offending paths. - Nested objects are flattened to dot paths: `{ "user": { "name": "Priya" } }` fills `{{user.name}}`. Numbers and booleans are converted to strings. - `{{email}}` is always available and defaults to the `to` address; an explicit `data.email` overrides it. - Every template variable **without an inline fallback** must be present in `data`, otherwise the send is rejected with `400 { "error": "Missing required template variables: ...", "missing": [...] }` — SendBunny never sends an email with blank merge fields. ## Responses Success is `200` with the SES message id — the same id that later appears in delivery/bounce events in the dashboard: **200 OK:** ```json { "messageId": "0100018f2ab4c123-2d1e8a77-..." } ``` All errors are JSON: `{ "error": "