Documentation
One REST API for transactional sending, campaigns, and automation — plus an SMTP front door for anything that already speaks mail. The machine-readable spec lives at GET /v1/openapi.json.
Start here
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 http://localhost:4100/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": "sent" }],
# "testMode": false }TypeScript — @volanea/sdk
import { Volanea } from "@volanea/sdk";
const volanea = new Volanea({ apiKey: process.env.VOLANEA_SECRET_KEY! });
await volanea.send(
{
to: "ada@example.com",
subject: "Your order shipped",
html: "<p>On its way, {{firstName}}.</p>",
variables: { firstName: "Ada" },
},
{ idempotencyKey: "order-4921" },
);Keys
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
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
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).
smtp · localhost:2525
# Any SMTP client works — username is ignored, the password is your secret key.
swaks --server localhost: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: "localhost", port: 2525,
auth: { user: "volanea", pass: process.env.VOLANEA_SECRET_KEY },
});Signed deliveries
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));
}Reference
The full surface, grouped by resource. Request and response shapes live in the OpenAPI document at /v1/openapi.json.
Transactional sending with batching, scheduling, template variables, and idempotency keys.
/v1/sendSend one message (to one recipient — address or {email, name} — or up to 50). Supports templateId, variables, custom headers, attachments, sendAt scheduling, and the Idempotency-Key header.
sk_ · sk_test_/v1/send/batchSend up to 1,000 personalized messages in one call; each succeeds or fails independently.
sk_ · sk_test_Product event ingestion and the event log. Events feed segments, workflow triggers, and webhooks.
/v1/eventsTrack a product event for a contact (auto-created if unknown). Safe to call from client code with a public key.
any key/v1/eventsThe event log, newest first, cursor-paginated. Filter by name or contactId.
sk_ · sk_test_/v1/events/namesEvery distinct event name logged for the project, alphabetical.
sk_ · sk_test_/v1/events/statsPer-name counts and first/last-seen timestamps, most frequent first.
sk_ · sk_test_/v1/events/{name}/usageUsage detail for one event name: count, distinct contacts, first/last seen.
sk_ · sk_test_/v1/events/{name}Delete all events with this name. System event names (email./contact./segment./workflow.) are reserved.
sk_ · sk_test_The contact graph: one record per address, shared by every send.
/v1/contactsList contacts, newest first, cursor-paginated.
sk_ · sk_test_/v1/contactsCreate or update a contact by email; only provided fields change.
sk_ · sk_test_/v1/contacts/{id}Get one contact.
sk_ · sk_test_/v1/contacts/{id}Delete a contact.
sk_ · sk_test_/v1/contacts/{id}/timelineActivity timeline: events and emails merged, newest first (last 100).
sk_ · sk_test_/v1/contacts/bulkSubscribe, unsubscribe, delete, or suppress up to 10,000 addresses at once.
sk_ · sk_test_/v1/contacts/importCSV import with header-based column mapping and an update/skip dedupe strategy.
sk_ · sk_test_/v1/contacts/exportExport the full audience as CSV.
sk_ · sk_test_/v1/contacts/fieldsDiscover custom contact fields (data keys) with counts, for building segment filters dynamically.
sk_ · sk_test_/v1/contacts/fields/{field}/valuesUp to 100 distinct values seen for one custom field, most common first.
sk_ · sk_test_/v1/contacts/fields/{field}/usageHow many contacts have this custom field set.
sk_ · sk_test_/v1/contacts/fields/{field}Remove this key from every contact's data object. Irreversible.
sk_ · sk_test_The send log — every message across transactional, campaign, workflow, and inbound sources.
/v1/emailsList sent emails, newest first. Filter by status, source, or recipient. Bodies omitted for size.
sk_ · sk_test_/v1/emails/{id}Full message detail including the rendered HTML body and engagement counters.
sk_ · sk_test_Reusable content with {{variable}} placeholders, versioning, rollback, and test sends.
/v1/templatesList templates, most recently updated first.
sk_ · sk_test_/v1/templatesCreate a template (version 1 is snapshotted).
sk_ · sk_test_/v1/templates/{id}Get one template.
sk_ · sk_test_/v1/templates/{id}Update; content changes bump the version and snapshot it for rollback.
sk_ · sk_test_/v1/templates/{id}Delete a template.
sk_ · sk_test_/v1/templates/{id}/versionsList all version snapshots, newest first.
sk_ · sk_test_/v1/templates/{id}/rollbackRestore an old version's content as a new version.
sk_ · sk_test_/v1/templates/{id}/test-sendSend the template to one address with sample variables (subject prefixed "[Test]").
sk_ · sk_test_/v1/templates/{id}/duplicateClone as a new template with its own fresh version-1 history.
sk_ · sk_test_Sending domains with ownership, DKIM, SPF, DMARC, and tracking DNS records.
/v1/domainsList domains with their DNS records.
sk_ · sk_test_/v1/domainsAdd a domain and get the DNS records to install.
sk_ · sk_test_/v1/domains/{id}Get one domain with DNS records and per-record status.
sk_ · sk_test_/v1/domains/{id}/verifyCheck DNS now; the domain verifies once ownership + DKIM + SPF pass.
sk_ · sk_test_/v1/domains/{id}Remove a domain.
sk_ · sk_test_Dynamic (condition-driven) and static audiences over the contact graph.
/v1/segmentsList segments.
sk_ · sk_test_/v1/segmentsCreate a dynamic (conditions) or static segment.
sk_ · sk_test_/v1/segments/previewLive member-count preview for a condition tree, with a sample of matches.
sk_ · sk_test_/v1/segments/{id}Get one segment.
sk_ · sk_test_/v1/segments/{id}Update; changing conditions recomputes membership immediately.
sk_ · sk_test_/v1/segments/{id}Delete a segment.
sk_ · sk_test_/v1/segments/{id}/refreshRecompute membership now; returns entered/exited/memberCount.
sk_ · sk_test_/v1/segments/{id}/membersCurrent members joined to contacts, newest entries first.
sk_ · sk_test_/v1/segments/{id}/membersStatic segments: add up to 1,000 members by email.
sk_ · sk_test_/v1/segments/{id}/members/{contactId}Static segments: remove a member.
sk_ · sk_test_One-off broadcasts with scheduling, A/B subject experiments, and engagement stats.
/v1/campaignsList campaigns.
sk_ · sk_test_/v1/campaignsCreate a draft. Audience: everyone, a segment, or ad-hoc filter conditions.
sk_ · sk_test_/v1/campaigns/{id}Get one campaign, with a live audience estimate while in draft.
sk_ · sk_test_/v1/campaigns/{id}Update (drafts only).
sk_ · sk_test_/v1/campaigns/{id}Delete (cancel a sending campaign first).
sk_ · sk_test_/v1/campaigns/{id}/sendSend now, or schedule for later with scheduledFor.
sk_ · sk_test_/v1/campaigns/{id}/cancelStop a scheduled or in-flight campaign.
sk_ · sk_test_/v1/campaigns/{id}/recipientsRecipient snapshot with per-recipient outcome and A/B variant.
sk_ · sk_test_/v1/campaigns/{id}/linksPer-link click counts (top 50).
sk_ · sk_test_/v1/campaigns/{id}/resend-non-openersCreate a follow-up draft targeting recipients who never opened.
sk_ · sk_test_/v1/campaigns/{id}/test-sendSend the campaign content to one address (subject prefixed "[Test]").
sk_ · sk_test_/v1/campaigns/{id}/duplicateClone content and targeting as a new draft; stats and A/B decision are reset.
sk_ · sk_test_Event-triggered automation graphs: delays, branches, waits — every contact walks at their own pace.
/v1/workflowsList workflows.
sk_ · sk_test_/v1/workflowsCreate a draft workflow from a validated node/edge graph.
sk_ · sk_test_/v1/workflows/{id}Get one workflow with per-node aggregates (stepStats).
sk_ · sk_test_/v1/workflows/{id}Update; also activates or pauses via status.
sk_ · sk_test_/v1/workflows/{id}Delete a workflow.
sk_ · sk_test_/v1/workflows/{id}/enrollManually enroll a contact by email (workflow must be active).
sk_ · sk_test_/v1/workflows/{id}/executionsList executions with contact emails; filter by status.
sk_ · sk_test_/v1/workflows/executions/{executionId}One contact's walk through the graph with the ordered step log.
sk_ · sk_test_/v1/workflows/{id}/duplicateClone the graph/trigger/reentry as a new paused workflow (never auto-activated).
sk_ · sk_test_/v1/workflows/{id}/executions/{executionId}/cancelCancel one active or waiting execution.
sk_ · sk_test_/v1/workflows/{id}/executions/cancel-allCancel every active/waiting execution for this workflow.
sk_ · sk_test_Signed event deliveries to your endpoints, retried with backoff for ~24 hours.
/v1/webhooksList endpoints.
sk_ · sk_test_/v1/webhooksRegister an endpoint; the response includes the whsec_… signing secret.
sk_ · sk_test_/v1/webhooks/{id}Update URL, event patterns, or enabled state.
sk_ · sk_test_/v1/webhooks/{id}Delete an endpoint.
sk_ · sk_test_/v1/webhooks/{id}/deliveriesDelivery log for one endpoint, newest first, cursor-paginated.
sk_ · sk_test_/v1/webhooks/deliveries/{id}/replayReset a delivery (any status) and re-attempt it immediately.
sk_ · sk_test_The do-not-send list: hard bounces, complaints, unsubscribes, and manual blocks.
/v1/suppressionsList suppressions; filter by reason.
sk_ · sk_test_/v1/suppressions/checkIs this address suppressed, and why?
sk_ · sk_test_/v1/suppressionsManually suppress an address.
sk_ · sk_test_/v1/suppressions/{id}Remove a suppression.
sk_ · sk_test_Email address validation before you send.
/v1/verifyValidate an address: syntax, MX records, disposable domain, role account.
sk_ · sk_test_Usage, monthly caps, reputation, and invoices.
/v1/billingCurrent-month usage (overall and per-category), caps, computed reputation across 24h/7d/all-time, and the last 12 invoices.
sk_ · sk_test_/v1/billingSet or clear (null) the overall cap and/or any per-category cap (transactional/campaign/workflow/inbound). Provide any subset.
sk_ · sk_test_/v1/billing/invoices/{id}/orderCreate (or fetch) the Razorpay payment order for an issued invoice.
sk_ · sk_test_Inbound mail ingestion — normally called by the SMTP relay's unauthenticated listener.
/v1/inboundStore an inbound message, upsert the sender as a contact, and emit email.received.
sk_ · sk_test_API metadata.
/v1/openapi.jsonThe OpenAPI 3.1 description of this API.
no auth