Documentation
Everything the API can do.
One REST API for transactional sending, campaigns, and automation — plus an SMTP front door for anything that already speaks mail. The base URL is https://api.volanea.com and the machine-readable spec lives at GET /v1/openapi.json.
Start here
Quickstart
Grab your secret key from project settings, verify a sending domain, and you can send with one request. The Idempotency-Key header makes retries safe — a repeated key replays the stored response instead of sending again.
curl
curl https://api.volanea.com/v1/send \
-H "Authorization: Bearer sk_..." \
-H "Idempotency-Key: order-4921" \
-H "Content-Type: application/json" \
-d '{
"to": "ada@example.com",
"subject": "Your order shipped",
"html": "<p>On its way, {{firstName}}.</p>",
"variables": { "firstName": "Ada" }
}'
# → { "messages": [{ "id": "…", "to": "ada@example.com", "status": "queued" }],
# "testMode": false }
# The response returns as soon as the message is durably queued; track final
# delivery with GET /v1/emails/{id} or the email.sent / email.delivered webhooks.TypeScript — fetch
const res = await fetch("https://api.volanea.com/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer undefined`,
"Content-Type": "application/json",
"Idempotency-Key": "order-4921",
},
body: JSON.stringify({
to: "ada@example.com",
subject: "Your order shipped",
html: "<p>On its way, {{firstName}}.</p>",
variables: { firstName: "Ada" },
}),
});
const { data } = await res.json();Keys
Authentication
Every request carries an API key as Authorization: Bearer <key>. Each project has three:
sk_…
Secret key
Full access to every endpoint. Server-side only — never ship it to a browser or mobile app.
sk_test_…
Test key
Same access as the secret key, but nothing is delivered: messages render, log, and return results, and domain verification is not enforced. Responses include testMode: true.
pk_…
Public key
Accepted only by POST /v1/events. Safe to embed in client-side code for product event tracking.
Conventions
Errors & pagination
Every error is the same envelope with an appropriate HTTP status — a machine-readable code and a human-readable message. Validation failures (422, code validation_error) add a details array of issues.
error envelope
{
"error": {
"code": "domain_not_verified",
"message": "The domain \"example.com\" is not verified for this project."
}
}List endpoints return { data, nextCursor }. Pass nextCursor back as the cursor query parameter until it is null.
Port 2525
SMTP relay
Anything that already speaks SMTP can send through Volanea — the relay parses the message and hands it to the same pipeline as POST /v1/send. Authenticate with any username and your secret key as the password. Unauthenticated sessions are treated as inbound mail: messages addressed to one of your verified domains are stored, the sender becomes a contact, and an email.received event fires (which can trigger webhooks and workflows).
Coming soon — the public SMTP endpoint isn't live yet. Use the REST API today; everything below will work unchanged the moment it opens.
smtp · port 2525 · coming soon
# Any SMTP client works — username is ignored, the password is your secret key.
swaks --server smtp.volanea.com:2525 \
--auth-user volanea --auth-password sk_... \
--from hello@yourdomain.com --to someone@example.com \
--header "Subject: Hello from SMTP"
# nodemailer
const transport = nodemailer.createTransport({
host: "smtp.volanea.com", port: 2525,
auth: { user: "volanea", pass: process.env.VOLANEA_SECRET_KEY },
});Signed deliveries
Webhook verification
Register an endpoint and Volanea POSTs each matching event as JSON, retrying with backoff for about 24 hours. Every delivery is signed with your endpoint's whsec_… secret in the X-Volanea-Signature header — verify it against the raw request body before trusting the payload.
verify X-Volanea-Signature
import { createHmac, timingSafeEqual } from "node:crypto";
// X-Volanea-Signature: t=<unix-ms>,v1=<hex hmac-sha256 of "<t>.<raw body>">
export function verifyWebhook(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
const timestamp = Number(parts.t);
if (!parts.v1 || !Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() - timestamp) > 5 * 60_000) return false; // reject replays
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
return expected.length === parts.v1.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Model Context Protocol
AI agents (MCP)
Volanea ships an MCP server, so an AI coding agent already working in your repo can do the whole integration: build the templates, segments, and workflows in your project, write the sending and webhook code into your codebase, then send test mail and read it back to prove the thing actually works.
There is no separate MCP token — authentication is a project API key as a bearer token, the same key the REST API takes. Use the sk_test_… key; the connect command is pre-filled with it in your project settings.
connect
# Claude Code — run this inside your own project
claude mcp add volanea --transport http https://api.volanea.com/mcp \
-H "Authorization: Bearer sk_test_..."
# Then just ask for what you want:
# "Set up Volanea in this app — a welcome email when someone signs up,
# and a webhook handler that unsubscribes hard bounces."
# Any MCP client works. Cursor, Windsurf, and friends take the same
# two values in mcpServers config: the URL and the Authorization header.Orient
get_project_status · get_api_reference · list_resources
Reads the plan, reputation, verified domains, and what already exists — so generated code matches the real API instead of a guess.
Build
create_template · create_segment · create_workflow · update_workflow · create_campaign_draft
Creates account state. Idempotent by name, so a retry doesn't leave duplicates behind.
Deliver
setup_domain · create_webhook_endpoint
Registers a sending domain and returns its DNS records; registers an endpoint and returns its whsec_ signing secret.
Verify
send_test_email · check_delivery · track_event · test_workflow
Proves the wiring end to end — fire the event your app would fire, then read the rendered message back.
The surface is deliberately narrower than the REST API. Test sends force test mode no matter which key authenticated, so an agent can never mail a real person. Campaigns are created as drafts for a human to launch. Workflows start paused. Nothing deletes. The code the agent writes into your app is what sends for real, once you deploy it.
Reference
Every endpoint
All 92 of them, each with its own page: parameters, request and response fields, error codes, and a working example in six languages. Generated from the OpenAPI document at /v1/openapi.json, so it never describes an API we do not serve.
Send
2Transactional sending, with batching, scheduling, and idempotency.
Events
6Product event ingestion (public key) and the event log.
Contacts
13The contact graph: CRUD, timeline, bulk operations, CSV import/export.
Emails
3The send log — every message across all sources.
Templates
9Reusable content with versioning, rollback, and test sends.
Domains
5Sending domains, DNS records, and verification.
Segments
10Dynamic (condition-driven) and static audiences.
Campaigns
13One-off broadcasts with scheduling, A/B subjects, and engagement stats.
Workflows
11Event-triggered automation graphs and their executions.
Webhooks
6Signed event deliveries to your endpoints, with retries and replay.
Suppressions
5The do-not-send list: bounces, complaints, unsubscribes, and manual blocks.
Stats
2Engagement aggregates: the project rollup and the per-workflow funnel.
Verify
1Email address validation.
Billing
2Usage, monthly caps, reputation, and invoices.
Inbound
2Inbound mail ingestion (used by the SMTP relay).
Meta
2API metadata.