Conventions
Versioning, pagination, idempotency, limits and the error envelope.
Versioning
The API is /v1. A breaking change gets /v2 rather than being made in place.
Breaking means: removing or renaming a URL, a field, a header or an error code; changing a field's type or what a status code means; making an optional request field required; or changing how authentication works.
Not breaking, and therefore possible without warning: new endpoints, new optional fields, new error codes for new failure modes, and new values in an existing enumeration. Write clients that ignore fields they do not recognise and that do not fall over on an unfamiliar status value.
Pagination
Lists return a page and, where more exists, a cursor:
{ "messages": [ … ], "nextCursor": "…" }Pass it back as cursor for the next page. When nextCursor is absent, you have everything.
| Query | Meaning |
|---|---|
cursor | Token from the previous response |
before | The previous page instead of the next; not combined with cursor |
limit | 1–100, default 50 |
from / to | RFC 3339 timestamps, or YYYY-MM-DD dates |
q | Free-text search, up to 64 characters |
counts=true | Adds a breakdown by status alongside the results |
format=csv | The same filter as a CSV download, where the endpoint supports it |
companyId | Narrows to one company the credential reaches |
Do not construct cursors yourself or store them long-term; they are opaque and only meaningful against the query that produced them.
Idempotency
POST /v1/messages accepts an Idempotency-Key header of up to 128 characters, unique within your account.
- Same key, same body → the original document, with
200instead of202. - Same key, different body →
409 IDEMPOTENCY_CONFLICT. - No key → a new document every time, including on a retry.
Use one on every send. It is the only thing that makes a retry after a timeout safe.
Limits
| Limit | Size |
|---|---|
| JSON request body | ~1 MB |
Inline payload | ~1 MB — above this, use the upload flow |
| A document | ~40 MB, inbound and outbound |
Exceeding them is 413 PAYLOAD_TOO_LARGE.
Rate limits
Sign-in and other unauthenticated routes are limited per source address, and repeated failed password attempts lock an account for an increasing interval. Authenticated routes are limited generously enough that ordinary use does not reach them.
Over the limit is 429 with a Retry-After header. Honour it — retrying immediately makes it worse.
Errors
Every failure has the same shape:
{ "error": { "code": "VALIDATION_FAILED", "message": "…" } }Branch on code, which is stable. Show message to people; it is written for them and its wording may change. The full list is in Error codes.
An id in a path that is not a well-formed UUID is 404 NOT_FOUND, the same as one that is well-formed and does not exist.
Times, ids and addresses
Times are RFC 3339 in UTC. Ids are UUIDs. Peppol addresses are scheme:value — 0007:5561234567.