Transactional API
Send receipts, alerts, and product messages from your application.
View as Markdown ↗Project key. Acts in the project the key belongs to. An all-access key works too — name the project with an X-Project-Id header. How keys and scopes work.
Endpoint
/api/v1/emailsAuthenticate every request with an Email Bump API key. The sender must use a verified sending domain — or, until you have verified one, the shared testing sender noreply@viaemailbump.com, which is limited (see Sending domains). Nothing sends until the account's email address is confirmed: until then a send is refused with 403.
Send an email
Body parameters
fromstringrequiredSender, optionally with a display name: Acme <hi@acme.com>. Must be your shared or a verified domain.
tostringrequiredRecipient email address. A one-element array (["jamie@example.com"]) is accepted too, for clients ported from an API that takes a list.
ccstring or arrayoptionalVisible copies — everyone on the message sees these addresses. See Cc and Bcc.
bccstring or arrayoptionalBlind copies: delivered, but named nowhere in the message.
subjectstringoptionalSubject line (or inherited from a template).
htmlstringoptionalHTML body.
textstringoptionalPlain-text body.
reply_tostringoptionalReply-To address.
templateobjectoptionalRender a saved template: { "id", "variables" } instead of raw content.
template_idstringoptionalFlat alternative to template.id.
variablesobjectoptionalFlat alternative to template.variables. Values are readable both at the top level ({{ order_id }}) and under contact.
attachmentsarrayoptionalFiles to send along. See Attachments.
scheduled_atstringoptionalRFC 3339 future time to send later instead of now.
streamstringoptionaltransactional (default) or marketing. See Marketing over the API.
curl -X POST https://emailbump.com/api/v1/emails \
-H "Authorization: Bearer ebk_your_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <orders@acme.com>",
"to": "jamie@example.com",
"reply_to": "support@acme.com",
"subject": "Your order has shipped",
"html": "<h1>On the way</h1><p>Tracking: EB-2048</p>",
"text": "Your order is on the way. Tracking: EB-2048"
}'Provide html, text, or both. When only HTML is supplied, Email Bump creates a text version. When only text is supplied, it creates a minimal HTML part.
This endpoint takes a single to address. Each send is metered, tracked and unsubscribed on its own, so five different people is five calls — pass more than one to and you'll get a 400 saying exactly that. To copy others on the same message, use cc and bcc; to reach an audience in one request, use a campaign.
The addresses in these docs are illustrations, and example.com is one of the recipients this endpoint refuses with a 400 — along with example.net, example.org and the .test / .invalid / .localhost / .local family. Those domains exist to be written down rather than delivered to, and example.com says so in DNS: it publishes a null MX record declaring that it accepts no mail, so every message to it hard-bounces and the bounce is charged to your account's reputation. When you run these examples, send to a mailbox you own — or, if what you want is a send that goes through the pipeline without reaching anybody, use the sandbox address below.
An address this workspace has already hard-bounced or been marked as spam by is refused on the transactional stream too, not only on marketing. The mailbox is gone or the person reported you, and neither of those changes because the next message happens to be a receipt — while bounce and complaint rates are measured per sending account, so the cost lands on everything else you send. Clear a suppression you believe is wrong by setting the contact's email_status back to active.
Cc and Bcc
cc and bcc copy the same message to other people. Each takes one address or an array of them, up to 50 between them.
curl -X POST https://emailbump.com/api/v1/emails \
-H "Authorization: Bearer ebk_your_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <orders@acme.com>",
"to": "jamie@example.com",
"cc": ["Dana <dana@example.com>", "ops@acme.com"],
"bcc": "archive@acme.com",
"subject": "Your order has shipped",
"html": "<h1>On the way</h1>"
}'Everyone on the message sees the Cc addresses in their copy. Nobody sees the Bcc ones — not the to recipient, and not the other blind copies.
Each copy is delivered on its own envelope, so it gets its own provider message id and therefore its own delivery, open, click and bounce events — which is what keeps a cc'd recipient's bounce attributable to them rather than to whoever was in To. It also means each copy counts against your monthly email allowance and appears as its own row in Developers → Transactional. A message to one person with two people copied is three emails.
The response carries a copies array, one entry per copy with its own id, so GET /v1/emails/{id} answers for a copy the same way it answers for the original.
"copies": [
{ "id": "…", "to": "dana@example.com", "kind": "cc", "status": "sent", "message_id": "…" },
{ "id": "…", "to": "ops@acme.com", "kind": "cc", "status": "sent", "message_id": "…" },
{ "id": "…", "to": "archive@acme.com", "kind": "bcc", "status": "sent", "message_id": "…" }
]A copy addressed to something this project has already hard-bounced or been marked as spam by is dropped rather than sent, and the message still goes to to. The dropped copy is named in warnings, appears in copies with "status": "failed", and is left out of the Cc header — a header naming somebody who never received it is a lie the other recipients can read. An address that appears twice, on to and cc or on cc and bcc, is delivered once. Addresses come back exactly as you wrote them, display names and all, and a scheduled send delivers the same Cc header an immediate one does.
A message addressed to the sandbox domain must have every cc and bcc on the sandbox domain too, or none at all. A sandbox message is dropped one envelope at a time, so a real address copied onto one would be the single part of a test send that really went out.
cc and bcc are refused with a 400 when stream is marketing. Every marketing recipient is consent-checked against their own contact record and needs their own unsubscribe link, and a Cc header would hand one subscriber another's address. Send each person their own message, or use a campaign.
SMTP accepts Cc and Bcc too, with the same key: the gateway takes ordinary MIME and lands in the same log.
Test sends without delivering them
Address a message to anything@sandbox.emailbump.comand it passes every gate a real send passes — authentication, sender validation, suspension and velocity checks, quota, rendering, abuse screening, and the transactional log — and is then dropped instead of being handed to the provider. The local part does not matter, and subdomains work too, so a per-run address like deploy-{git sha}@sandbox.emailbump.com is fine.
curl -X POST https://emailbump.com/api/v1/emails \
-H "Authorization: Bearer ebk_your_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@acme.com>",
"to": "deploy-check@sandbox.emailbump.com",
"subject": "Deploy smoke test",
"html": "<p>If this returns 200, the pipeline is up.</p>"
}'You get a normal 200 with an id you can fetch back, and a row under Transactional whose message id is prefixed sandbox- — that prefix is how you tell, in the log, that the message never left the building. Nothing is delivered, nothing can bounce, and no reputation is spent.
A smoke test that mails example.com is not a no-op: every run is a hard bounce, and bounce rate is measured per sending account, so the cost lands on your real mail. The sandbox address gives you the same green tick without that. It is metered like any other send, so it counts toward your plan's monthly emails — deliberately, since a smoke test that skipped the quota check would not be testing the pipeline your real mail goes through.
Marketing over the API
Add "stream": "marketing" to send a promotional one-off without building a campaign or flow — an announcement to one person, a hand-triggered win-back, your own sending logic driving the API. The message then carries what marketing mail must: the recipient is checked against their contact record and refused if they've unsubscribed or their address is suppressed, the marketing footer with its unsubscribe link is appended, one-click unsubscribe headers are set, and the send is classified under the marketing stream in analytics.
curl -X POST https://emailbump.com/api/v1/emails \
-H "Authorization: Bearer ebk_your_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@acme.com>",
"to": "jamie@example.com",
"subject": "Something new for you",
"html": "<p>We just launched…</p>",
"stream": "marketing"
}'A recipient who isn't a contact yet is created as one, the same way the Events API does — the unsubscribe link needs a record to point at. scheduled_at isn't supported on this stream yet; to schedule marketing email, use a campaign. Transactional sends (the default) are unchanged: no footer, no consent gate — receipts and password resets go to unsubscribed recipients too, as they should.
What happened to it
/api/v1/emails/{id}The id POST /v1/emails returned, and what became of the message:
curl https://emailbump.com/api/v1/emails/50b0b9a4-d447-480a-ad5b-3e1b999dd842 \
-H "Authorization: Bearer ebk_your_key"{
"object": "email",
"id": "50b0b9a4-…",
"to": "jamie@example.com",
"subject": "Your order has shipped",
"status": "sent",
"error": null,
"events": [
{ "type": "send", "occurred_at": "2026-08-03T01:27:57Z" },
{ "type": "delivery", "occurred_at": "2026-08-03T01:27:59Z" },
{ "type": "open", "occurred_at": "2026-08-03T01:31:12Z", "machine_open": false }
]
}status is our own record of the submission; events is what the provider reported afterwards. The distinction matters: "sent" means it was accepted for delivery, not that it arrived. A message to a domain with no MX record is "sent" and then bounces — the bounce event is where that shows up, usually within seconds.
Attachments
Send a receipt with its invoice, a report, a photo — anything, as base64. Up to 20 files totalling 25 MB per message.
Each attachment
filenamestringrequiredThe name the recipient sees when they save it.
contentstringrequiredThe file, base64-encoded. A whole data: URI works too, so a browser's FileReader result can be passed straight through.
content_typestringoptionalMIME type. Guessed from the filename when you leave it out.
content_idstringoptionalSet this to embed the file in the HTML instead of listing it at the bottom, and reference it as <img src="cid:the-id">.
curl -X POST https://emailbump.com/api/v1/emails \
-H "Authorization: Bearer ebk_your_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <billing@acme.com>",
"to": "jamie@example.com",
"subject": "Your invoice",
"html": "<p>Thanks! Your invoice is attached.</p><img src=\"cid:logo\">",
"attachments": [
{ "filename": "invoice-4821.pdf", "content": "JVBERi0xLjQK..." },
{ "filename": "logo.png", "content": "iVBORw0KGgo...", "content_id": "logo" }
]
}'A PDF invoice is normal and fine. Executables, archives and macro-enabled documents are what spam filters look for — if a message must carry one, a link to it delivers far better than the file itself.
Schedule for later
Replace FUTURE_RFC3339_TIMESTAMP with a future RFC 3339 timestamp. For one hour from now: node -p 'new Date(Date.now() + 3600000).toISOString()'. This schedules a real send with your recipient and key.
Add scheduled_at (RFC 3339, must be in the future) to send at a specific time. The response comes back with status: "scheduled"; Email Bump delivers it when it comes due. Attachments and cc/bcc are kept with it and go out with the message — copies are re-checked against suppression at the moment it sends, not when you queued it.
curl -X POST https://emailbump.com/api/v1/emails \
-H "Authorization: Bearer ebk_your_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <orders@acme.com>",
"to": "jamie@example.com",
"subject": "Your trial ends soon",
"html": "<p>Just a heads up…</p>",
"scheduled_at": "FUTURE_RFC3339_TIMESTAMP"
}'Use a template
Pass a saved template ID and variables instead of raw content. Variables are available at the top level and under contact.
{
"from": "Acme <orders@acme.com>",
"to": "jamie@example.com",
"template": {
"id": "9d7073b0-63b0-4ad0-925e-c43f04619ec2",
"variables": {
"first_name": "Jamie",
"order_id": "EB-2048"
}
}
}Response
{
"id": "6dde5322-c940-43e5-84cc-97a2d8c69c08",
"message_id": "provider-message-id",
"to": "jamie@example.com",
"from": "Acme <orders@acme.com>",
"subject": "Your order has shipped",
"status": "sent",
"created_at": "2026-07-20T18:42:10Z"
}cc, bcc and copies are present only when the send named somebody else.
Every accepted send is recorded in Developers → Transactional, including its rendered content, original payload, and event timeline.