Transactional sends
Send receipts, password resets and other messages people asked for: inline, from a saved definition, or raw MIME.
Transactional messages are ones a person asked for. They do not need marketing consent and ignore quiet hours. They still stop at hard bounces, complaints, manual and do-not-contact suppressions, and blocked domains. Never send marketing this way.
Transactional is its own stream, kept apart from marketing the way Postmark and Amazon SES keep their message streams apart. Because it still reaches people who unsubscribed from marketing, it refuses marketing content with a 400:
a template in a preference topic, or a template a campaign uses (make a separate transactional template);
a
topicIdonPOST /v1/messages/sendwith purposetransactionaloroperational;copy that prints the marketing unsubscribe link,
{{ unsubscribe_url }}(or*|UNSUB|*).{{ preferences_url }}is allowed.
Send those with purpose marketing through POST /v1/messages/send, where consent and unsubscribes apply.
What you need first
A connection and a verified sender (see Connect a sending provider).
A key with
messages:send.
1. Send one
curl -s -X POST "$BASE/v1/transactional/send" \
-H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-usr_ada" \
-d '{"to":[{"email":"ada@example.com","name":"Ada","vars":{"plan":"Growth"}}],
"subject":"Welcome, {{ vars.recipient.name }}",
"html":"<p>Hi {{ vars.recipient.name }}, your {{ vars.plan }} workspace is ready.</p>",
"from":{"email":"hello@example.com","name":"Acme"},
"tags":["welcome"],"metadata":{"source":"signup"}}'
# 202 {"results":[{"status":"queued","jobId":"..."}]}The Idempotency-Key is required. See Idempotency.
Variables
vars.<key>: from the call'svarsand each recipient'svars.vars.recipient.email,vars.recipient.phone,vars.recipient.name.profile.*: the stored person (firstName,lastName,emailPrimary, ... or a custom attribute).unsubscribe_urlis not available here (see above).
Bare names such as {{ name }} are not defined; write {{ vars.recipient.name }}. Before anything is queued, every recipient's content is checked against its vars: a missing vars.* value (or a bare name) comes back for that recipient as "status":"invalid" with the tag in reason, and the others still go out. A missing profile.* attribute depends on the stored person, so that message is blocked later with render_error (add | default: "..."); check with GET /v1/messages/<jobId>.
Options
ccandbccarrays. Suppressed copies are dropped."scheduledFor":"2026-11-20T15:00:00Z"schedules it. Move it withPOST /v1/messages/<id>/reschedule, stop it withPOST /v1/messages/<id>/cancel.
2. Use a saved definition
Keep the content in a template and send by name:
curl -s -X POST "$BASE/v1/templates" \
-H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Password reset","kind":"html",
"version":{"subject":"Reset your password","body":{"kind":"html","html":"<a href=\"{{ vars.reset_url }}\">Reset</a>"}}}'
curl -s -X POST "$BASE/v1/transactional/definitions" \
-H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"password-reset","templateId":"<templateId>",
"settings":{"fromEmail":"hello@example.com","fromName":"Acme","tags":["auth"],
"tracking":{"opens":false,"clicks":false},"route":{"connectionId":"<connectionId>"}}}'
curl -s -X POST "$BASE/v1/transactional/definitions/<definitionId>/send" \
-H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: reset-req-8812" \
-d '{"to":[{"email":"ada@example.com","vars":{"reset_url":"https://example.com/reset/abc"}}]}'Turn tracking off for security messages. GET /v1/transactional/definitions/<id>/deliveries lists what went out.
3. Send raw MIME
curl -s -X POST "$BASE/v1/transactional/send-raw" \
-H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: raw-1" \
-d '{"rawMime":"From: Acme <hello@example.com>\r\nTo: ada@example.com\r\nSubject: Hello\r\n\r\nHi Ada"}'From must be a verified sender.
4. Follow up
GET /v1/messages/<jobId>: status, attempts, events and the full decision log.GET /v1/messages/<jobId>/content: the stored body, kept 30 days.POST /v1/messages/<jobId>/resend: send again (checks consent again).GET /v1/transactional/statsand/summary: totals.
For delivery updates as they happen, subscribe to message.status webhooks.
Try it without sending
Turn on test mode (PUT /v1/workspace/test-mode {"enabled":true}). Sends are accepted and logged with a test_... provider id and never reach your provider.