Sending
The BoostMail API has two send surfaces. Transactional send delivers one message to one recipient, for receipts, password resets, and other one-to-one mail. Campaign send delivers a prepared campaign to its whole audience. This guide covers both, plus the two mistakes that are easy to make: sending around suppression, and double-sending on a retry.
Transactional send
Section titled “Transactional send”POST /v1/transactional/send sends a single transactional message. It needs a key with the transactional:send scope.
The request body requires two fields, campaign_id and email. It also accepts an optional template_data object, a free-form set of merge values passed to the template.
{ "campaign_id": "txn_welcome", "template_data": { "first_name": "Alex", "order_number": "1024" }}The campaign_id is the id of a transactional campaign you have set up in your BoostMail dashboard, under Campaigns. You configure transactional campaigns outside the /v1 API: no /v1 endpoint creates one. An unknown or non-transactional campaign_id is rejected as a 4xx validation or not-found error (see the Errors guide).
The idempotency key
Section titled “The idempotency key”Every transactional send requires an idempotency-key header. A missing or blank key returns 400 with the wire code invalid_request.
curl -X POST https://api.boostmail.app/v1/transactional/send \ -H "Authorization: Bearer bm_live_..." \ -H "idempotency-key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "txn_welcome", "email": "[email protected]" }'The key protects you from sending twice on a retry. A retry with the same key and the same fingerprint (the campaign_id, the normalized email, and a hash of template_data) replays the original receipt and sends nothing. The email is normalized for case and whitespace, so a retry that only changes the letter case still replays. While the original request is still in flight, the same key returns a transient 409 instead of a replay. The stored receipt lives for about 24 hours.
The three-cause 409
Section titled “The three-cause 409”A transactional 409 always carries the generic wire code conflict. Three different causes share that one code:
- Idempotency-key reuse. The same key is sent with a different fingerprint (a different
campaign_id, email, ortemplate_data). This is permanent, and it usually means a client bug. - An erased recipient. The recipient identity has been erased (redacted). This check runs before suppression. It is permanent: the send will never succeed for that recipient.
- An in-flight original. The first request with this key is still being processed, and this call is a concurrent retry. This is transient. It resolves to a
200replay once the original completes.
Reuse and erased are not distinguishable from an in-flight race, not from the wire and not from your own request history (a well-behaved client resends the same key and fingerprint every time). They separate only operationally, by what happens when you retry.
On a transactional 409, retry a bounded number of times with backoff. If a retry returns 200, the original was in flight and you now have the receipt. If the 409 persists past your retry budget, it is permanent (reuse or erased), and the client should stop.
{ "error": { "code": "conflict", "message": "conflict" }}The double-send caveat
Section titled “The double-send caveat”The receipt and suppression
Section titled “The receipt and suppression”Suppression is checked only against a known subscriber. A fresh or non-subscriber recipient email has no suppression history, so it is simply sent.
An accepted request (a valid key, a valid idempotency-key, and a recipient who is not erased) returns a 200 receipt. Branch on the status field. Any rejection is a non-200 error (the 400 and 409 above, plus the shared codes in the Errors guide); a 200 never carries an error.
A non-suppressed send returns the scheduled receipt:
{ "status": "scheduled", "message_id": "msg_01H9Z8ABCDEF", "scheduled_at": "2026-08-13T14:32:00Z"}Suppression depends on the subscriber’s state. A subscriber who is marketing unsubscribed is still sent to, because a receipt or a reset must be delivered. A subscriber who has bounced or complained is suppressed: the receipt reports it, and nothing is enqueued. The receipt is still recorded under the idempotency key, so a later replay returns it.
{ "status": "suppressed", "reason": "hard_bounce"}The reason is hard_bounce or spam_complaint. Fields that do not belong to the returned branch are omitted, not set to null: read message_id and scheduled_at only on scheduled, and read reason only on suppressed. Treat an unrecognized status or reason with a default branch rather than an error, matching the forward-compatibility rule in the Versioning guide.
For the full precedence order, and how suppression differs from marketing consent, see the Suppression guide.
Campaign send
Section titled “Campaign send”POST /v1/campaigns/{id}/send starts sending a prepared campaign to its audience. It needs a key with the campaigns:send scope. There is no idempotency key on this endpoint.
Send an empty body against a Draft campaign. The API returns 202 and moves the campaign into sending:
curl -X POST https://api.boostmail.app/v1/campaigns/<campaign-id>/send \ -H "Authorization: Bearer bm_live_..."{ "status": "sending"}Double-send safety here is the campaign state machine, not an idempotency key. Only a Draft campaign can move to sending. A repeat send after that transition returns 409 conflict and does not restart the send. Treat a 409 on retry as “already sent”, not as a failure.
{ "error": { "code": "conflict", "message": "conflict" }}This already-sending 409 looks the same on the wire as a transactional 409: both carry the generic code conflict. You tell them apart by the endpoint you called and your own request history, never by code.
Erased recipients
Section titled “Erased recipients”An erased (redacted) shopper is handled differently on the two surfaces.
Transactional send checks for an erased recipient before it sends, and returns 409 conflict for that named recipient (this is the erased cause above, and the redaction note in the Errors guide).
Campaign send runs no send-time erasure check. When a shopper is erased, their row is hard-deleted, so they are simply absent from the campaign audience. They are silently skipped, with no per-recipient skip report. Nothing in the 202 response flags the omission.