Errors
Every error response from the BoostMail API uses the same envelope, regardless of status code.
The error envelope
Section titled “The error envelope”{ "error": { "code": "...", "message": "...", "details": [ ... ] }}code: a fixed string identifying the error. See the table below.message: a human-readable summary.details: populated only on422responses (see below). Empty or absent on every other status.
Status codes
Section titled “Status codes”| Status | code | Meaning |
|---|---|---|
400 | invalid_request | The request is malformed: bad syntax, a bad parameter value, or a missing required field. |
401 | invalid_credential | The key is missing, invalid, revoked, or suspended. |
403 | insufficient_scope | The key is valid but lacks the scope the endpoint requires. |
404 | not_found | The resource does not exist. |
409 | conflict | The request conflicts with the resource’s current state. |
422 | validation_failed | The request is well-formed but fails validation on one or more fields. |
429 | rate_limited | You have exceeded a rate limit. |
500 | internal_error | Something went wrong on BoostMail’s side. |
code is per status, not per cause
Section titled “code is per status, not per cause”code is a fixed string for a given HTTP status. It does not vary by what specifically triggered the error. Every 409 response carries conflict, whether it came from:
- a transactional send whose idempotency key was reused with a different payload,
- a transactional send to a recipient whose identity has been erased,
- a transactional send that is still processing an earlier, identical request, or
- a campaign send repeated after that campaign already started sending.
Because code never changes, you tell a transactional 409 apart from a campaign 409 by endpoint and your own request history: a 409 from POST /v1/transactional/send is always one of the first three causes; a 409 from POST /v1/campaigns/{id}/send is always the fourth. Endpoint and request history do not, however, tell the three transactional causes apart from each other. Separating those needs a bounded retry with backoff. See the Sending guide for that rule.
Validation errors (422)
Section titled “Validation errors (422)”details is populated only on 422 validation_failed responses, one entry per offending field:
{ "field": "...", "code": "...", "message": "..." }On every other status, details is empty or absent.
For example, this request tries to create a subscriber with an email that already exists:
curl -X POST https://api.boostmail.app/v1/subscribers \ -H "Authorization: Bearer bm_live_..." \ -H "Content-Type: application/json" \{ "error": { "code": "validation_failed", "message": "validation_failed", "details": [ { "field": "email", "code": "already_exists", "message": "A subscriber with this email already exists" } ] }}The top-level code is always validation_failed on a 422. The specific cause, already_exists, lives inside details, never at the top level.
Redaction and the 409 on a named write
Section titled “Redaction and the 409 on a named write”A named write is a request that targets one specific recipient by email: creating or updating a subscriber (POST /v1/subscribers or PATCH /v1/subscribers/{id}), or sending a transactional email (POST /v1/transactional/send). If the target email’s identity has been erased, BoostMail rejects the write with 409 conflict instead of performing it.
Every one of these rejections is written to an audit log, so the erasure is provably honored. The audit entry stores metadata only, a hashed identifier, never the plaintext email.
This 409 applies only to a named write. A campaign send targets many recipients at once, and handles an erased recipient differently: silently skipped, not rejected, with no per-recipient error. See the Sending guide for that half of the split.
A note on the generated reference
Section titled “A note on the generated reference”The generated API reference marks every subscriber field as required. That is an artifact of how its schema is produced, not a guarantee about what you will get back. In practice, a subscriber response can omit non-essential fields. Do not write client code that assumes a field is always present just because the reference lists it as required.