Skip to content
REST API basics

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/v1

Behind 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.yaml

Authentication

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/partners

You can use two kinds of token:

  • An API token of a service account. It starts with mb2b_. The audit log names the caller sa:<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:

CodeWhat it means
MISSING_AUTH_HEADERThe request has no Authorization header.
INVALID_AUTH_HEADERThe header is not Bearer <token>.
INVALID_TOKENThe token is unknown, revoked, for a disabled account, or not accepted for another reason.
TOKEN_EXPIREDThe 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, PATCH and DELETE sends Content-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-data instead: the certificate upload (POST /certificates) and sending a message by hand (POST /messages). They also need an X-Requested-With header. Any value works; XMLHttpRequest is usual.
  • A GET needs no Content-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 is 201 with the stored record.
  • Update with PUT. A field you leave out keeps its stored value. The answer is 200 with the stored record. The path says which record you change. A body that names another record (another id or name) is refused.
  • Delete with DELETE. The answer is 204 without 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

StatusCodeWhen
400INVALID_INPUTThe body, a path ID or a query parameter is not valid.
400MISSING_HEADERAn upload without X-Requested-With.
401see aboveThe request has no valid token.
404NOT_FOUNDThe record does not exist.
409CONFLICTA record with the same name already exists.
409CERTIFICATE_EXISTSThe same certificate is already stored. details.existing_id names it.
409CERTIFICATE_IN_USEA profile still uses the certificate you want to delete.
409NOT_REDISPATCHABLE, NOT_REDELIVERABLEThe message or delivery is not in a state that allows the action.
413PAYLOAD_TOO_LARGEThe request body is too large.
415UNSUPPORTED_MEDIA_TYPEThe Content-Type is wrong or missing.
500INTERNAL_ERRORSomething 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}
ParameterWhat it doesDefault
limitHow many records to return, at most 500. A larger value counts as 500.20; 50 for messages, notifications and the audit log
offsetHow 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.