BeaconBox
Esc

Type to search.

Dashboard

Errors

One envelope, a stable dotted code, and the short list of codes worth branching on. Plus the three that are not errors at all.

Every error is the same shape.

Response · 404 Not Found
{ "error_code": "message.not_found" }

A validation failure adds a detail array describing which field and why.

Response · 422 Unprocessable Content
{
"error_code": "common.validation_failed",
"detail": [
  {
    "type": "string_too_long",
    "loc": ["body", "subject"],
    "msg": "String should have at most 300 characters",
    "input": "..."
  }
]
}

Status codes

Status When
401 Key missing, malformed or revoked.
403 Authenticated, but not permitted. Also a business that is no longer active.
404 No such id. Also what another business’s id looks like.
409 Idempotency key reused with a different body, or a request still in flight.
413 The request body is larger than the endpoint accepts.
422 A malformed field, or text that cannot be stored.
429 Rate limited.
5xx Our problem. Retry with the same idempotency key.

Codes worth handling

Most integrations need four branches and a fallback.

Code Status What to do
business.invalid_api_key 401 Stop and alert. Retrying will not help.
idempotency.key_reused 409 A bug in your key derivation. Alert, do not retry.
idempotency.request_in_progress 409 Retry shortly; it will replay the first result.
common.validation_failed 422 Log detail and fix the caller. Never retry.
message.not_found 404 The id is wrong, or belongs to another business.

Everything else is worth logging with its code and moving on.

Bodies too large, and text that cannot be stored

A body over the limit is refused before it is read, with request.too_large and a 413. The limit is 2 MiB, or 8 MiB on POST /messages/batch, which fits a full batch of maximum-length messages. It is checked before authentication, so it does not depend on your key.

Text holding a NUL character (\u0000 in JSON, %00 in a URL) or an unpaired UTF-16 surrogate is refused anywhere: any body field, path or query parameter. It is an ordinary common.validation_failed with the field in detail. If such text ever slips past that check, the database refuses it and the answer is a 422 with request.unstorable_input instead. Strip the character and send again.

Neither is worth retrying unchanged. Neither spends the idempotency key, so a corrected request can reuse it.

# The code is the thing to read. jq makes that explicit.
curl -s -o /tmp/r.json -w '%{http_code}\n' \
https://api.beaconbox.eu/api/v1/messages/nope \
-H "Authorization: Bearer $BEACONBOX_API_KEY"

jq -r '.error_code' /tmp/r.json

Exceptions in the SDKs

Both map the same statuses to the same shapes, so an integration with a service in each language handles a bad ten minutes the same way.

Python PHP When
AuthenticationError AuthenticationException 401
PermissionDeniedError PermissionException 403
ResourceMissingError ResourceMissingException 404
ConflictError ConflictException 409
InvalidRequestError InvalidRequestException 422
RateLimitError RateLimitException 429, after the SDK retried
ServerError ServerException 5xx, after the SDK retried
APIConnectionError ApiConnectionException No answer at all
WebhookVerificationError WebhookVerificationException A delivery could not be proven ours

All inherit from BeaconBoxError and BeaconBoxException respectively, and all carry error_code / errorCode.

Three things that are not errors

Code that treats any of these as a failure will retry something that already succeeded.

No answer at all

APIConnectionError is the one case that needs saying twice: it is not proof the work did not happen. The request may have been processed and the response lost.

Retry it with the same idempotency key. That is the whole reason the key is mandatory.