REST API basics
The REST API lets scripts and other systems do everything the web UI does. This page explains how every request works: signing in, request formats, answers, errors and lists. For each endpoint and field, see the REST API reference.
Where the API is
All endpoints are below /api/v1, on the same address and port as the web UI. The port is HTTP_PORT (default 8080).
API=http://localhost:8080/api/v1Behind a reverse proxy, use the public address instead.
The server describes itself in an OpenAPI document. It lists exactly the endpoints and plugin types of your installation:
curl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:8080/api/v1/openapi.yaml -o openapi.yamlAuthentication
Every request to /api/v1 needs a token, the OpenAPI document included. Send it in the Authorization header:
curl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:8080/api/v1/partnersYou can use two kinds of token:
- An API token of a service account. It starts with
mb2b_. The audit log names the callersa:<account name>. - An access token from an identity provider that MicroB2B knows (see identity providers). The token must belong to a MicroB2B user or to a federated service account. A script gets one with the provider’s client credentials grant.
A browser session cookie does not work on the API. Only GET /health needs no token.
When the server refuses a token, it answers 401 with one of these codes:
| Code | What it means |
|---|---|
MISSING_AUTH_HEADER | The request has no Authorization header. |
INVALID_AUTH_HEADER | The header is not Bearer <token>. |
INVALID_TOKEN | The token is unknown, revoked, for a disabled account, or not accepted for another reason. |
TOKEN_EXPIRED | The token was right, but it has expired. Make a new one. |
The answer never says what is wrong with a token. The server’s log does. See troubleshooting sign-in.
Request format
Requests and answers are JSON. A few rules apply:
- A request with a body sends
Content-Type: application/json. - Every
POST,PUT,PATCHandDELETEsendsContent-Type: application/json, even without a body. This covers a delete, a service action and a notifier test. - Two requests upload files and use
multipart/form-datainstead: the certificate upload (POST /certificates) and sending a message by hand (POST /messages). They also need anX-Requested-Withheader. Any value works;XMLHttpRequestis usual. - A
GETneeds noContent-Type.
A wrong or missing Content-Type is refused with 415 UNSUPPORTED_MEDIA_TYPE. A missing X-Requested-With is 400 MISSING_HEADER. These rules stop a web page in a browser from changing anything through the API.
Request bodies have a size limit. Most bodies may be up to 1 MiB, a certificate upload up to 20 MiB. A message sent by hand may hold a file of up to 64 MiB. A larger body is refused with 413 PAYLOAD_TOO_LARGE.
Times are RFC 3339 strings, such as 2026-10-05T00:44:14Z.
Answers
Every JSON answer has the same envelope. On success, data holds the result:
{"data": {"id": 1, "name": "acme", "display_name": "ACME Corporation"}, "error": null}On failure, error holds a code, a message and sometimes details:
{"data": null, "error": {"code": "NOT_FOUND", "message": "Partner not found"}}GET /health is the exception. It answers {"status": "healthy", "timestamp": ..., "database": "up"} without the envelope, or 503 with "unhealthy".
Creating, changing and deleting
- Create with
POST. A field you leave out gets the same default as the web form’s create page. The answer is201with the stored record. - Update with
PUT. A field you leave out keeps its stored value. The answer is200with the stored record. The path says which record you change. A body that names another record (anotheridorname) is refused. - Delete with
DELETE. The answer is204without a body.
The API checks your input with exactly the same rules as the web form. A submission that the form accepts, the API accepts, with the same messages.
Secrets are write-only. Passwords, extra HTTP headers, signing secrets, webhook URLs and client secrets are never returned. An update that leaves one out keeps it. If you change the URL a secret is sent to, you must send the secret again.
Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | The body, a path ID or a query parameter is not valid. |
| 400 | MISSING_HEADER | An upload without X-Requested-With. |
| 401 | see above | The request has no valid token. |
| 404 | NOT_FOUND | The record does not exist. |
| 409 | CONFLICT | A record with the same name already exists. |
| 409 | CERTIFICATE_EXISTS | The same certificate is already stored. details.existing_id names it. |
| 409 | CERTIFICATE_IN_USE | A profile still uses the certificate you want to delete. |
| 409 | NOT_REDISPATCHABLE, NOT_REDELIVERABLE | The message or delivery is not in a state that allows the action. |
| 413 | PAYLOAD_TOO_LARGE | The request body is too large. |
| 415 | UNSUPPORTED_MEDIA_TYPE | The Content-Type is wrong or missing. |
| 500 | INTERNAL_ERROR | Something failed on the server. The cause is in the server’s log, not in the answer. |
A few endpoints have codes of their own. The REST API reference lists them.
Validation errors
When input fails a check, the answer is 400 INVALID_INPUT with the message Validation failed. details has one entry per refused field, by its JSON name:
{
"data": null,
"error": {
"code": "INVALID_INPUT",
"message": "Validation failed",
"details": {
"partner_certificate_id": "Messages to this partner are encrypted to the partner's certificate, but none is chosen. Choose the partner's certificate, or set the encryption algorithm to None."
}
}
}A problem that belongs to no single field is under _form. A bad query parameter is reported under the parameter’s name. A body that is not a JSON object gets the message Invalid request body, with the reason under details.body.
Lists and paging
A list answer holds the records under a plural key, such as partners or messages. Next to it are total (all matching records), limit and offset:
{"data": {"partners": [...], "total": 42, "limit": 20, "offset": 0}, "error": null}| Parameter | What it does | Default |
|---|---|---|
limit | How many records to return, at most 500. A larger value counts as 500. | 20; 50 for messages, notifications and the audit log |
offset | How many records to skip. | 0 |
To read the next page, add limit to offset. A limit below 1 or an offset below 0 is a 400 naming the parameter.
Many lists take filters. Filters combine. An unknown filter value is a 400:
curl -H "Authorization: Bearer $ACCESS_TOKEN" "$API/messages?status=Error&limit=10"
curl -H "Authorization: Bearer $ACCESS_TOKEN" "$API/certificates?q=acme&key=absent"API resources lists the filters of each list.