Errors
The error shape, what each status means, and how to tell a blocked message from a failed call.
Every error is JSON with a stable code:
{"error":{"code":"invalid","message":"body/email must match format \"email\"","details":{"issues":[{"instancePath":"/email","message":"..."}]}}}Branch on code, not on message. Messages may change.
Status |
| Means |
|---|---|---|
400 |
| The request is wrong. |
401 |
| Missing, wrong or revoked key. |
403 |
| The key lacks a scope ( |
404 |
| Unknown id or route. |
409 |
| A duplicate, such as a name already in use. |
412 |
| Something must be set up first, such as working provider credentials. |
422 |
| The |
429 |
| Slow down. Wait for |
Blocked is not an error
A message that consent or suppression stops is not an error. The send answers 202. The message is recorded as blocked with a reason:
curl -s "$BASE/v1/messages/<jobId>" -H "Authorization: Bearer $HERMETICA_API_KEY"The decisionLog shows every check the message passed or failed. The same goes for deferred (quiet hours) and render_error (a template variable was missing).
Unknown fields are refused
With an API key, a body field the route does not know answers 400 and names it, for example Unknown field: standard with details.unknownFields: ["standard"]. Nothing is silently ignored, so a misspelled field never looks like a successful PATCH. Check the field names against the API reference.
Send Content-Type: application/json only when there is a body. A DELETE with that header and no body fails.
Dates
Analytics take dates as YYYY-MM-DD. Most other fields take ISO 8601 times, such as 2026-11-04T15:00:00Z.
Retry
Retry 429 and 5xx with backoff. Do not retry 4xx without changing the request. Make retries safe with idempotency.