OCPP message log
Read the OCPP messages a charger and the charging core exchanged, to find out why a charger rejects a command, never finishes booting or reports a fault. The log is read-only and redacted: secrets never leave the API, whichever way you read it.
| Route | Returns |
|---|---|
GET /v1/tenants/{tenant_id}/stations/{station_id}/ocpp-messages | One page of messages, newest first |
GET /v1/tenants/{tenant_id}/stations/{station_id}/ocpp-messages/export | The same messages as NDJSON, oldest first |
Webhook event station.ocpp_message | Each new message as it is recorded, filtered by OCPP action |
Diagram source
flowchart LR
Charger[Charger] <-->|OCPP-J frames| Core[Charging core journal]
Core --> Redact[Redaction in the API]
Redact --> Log[Message log and export]
Redact --> Hook[station.ocpp_message webhook]Access: stations:diagnostics
Every route needs the stations:diagnostics scope. Tenant admins and Ivora
support staff hold it; viewers (read-only roles) never do, even when signed in.
An API key gets it only when it is created with stations:diagnostics in its
scopes: no other scope implies it, and keys created before this release do not
receive it. A key whose owner is now a viewer of the tenant is refused (403).
Read the log
GET /v1/tenants/7/stations/12/ocpp-messages?action=BootNotification&action=StatusNotification&limit=50
| Parameter | Meaning |
|---|---|
since, until | ISO 8601 period, since inclusive and until exclusive. until defaults to now and since to 24 hours before until. One request covers at most 7 days. |
action | Repeat to keep only these OCPP actions (up to 20). A reply carries its request's action. |
cursor | next_cursor of the previous page, with the same filters. |
limit | 1 to 500 messages, 100 by default. |
{
"station_id": 12,
"since": "2026-10-13T09:00:00+00:00",
"until": "2026-10-14T09:00:00+00:00",
"data": [
{"id": 90412, "station_id": 12, "direction": "from_charger", "message_type": "call",
"action": "StartTransaction", "message_id": "4f1c", "timestamp": "2026-10-14T08:59:12+00:00",
"protocol": "ocpp1.6", "payload": {"connectorId": 2, "idTag": "****C3D4", "meterStart": 12500, "timestamp": "2026-10-14T08:59:11Z"},
"redacted": true, "truncated": false, "payload_size": 82, "error": null}
],
"next_cursor": "90412"
}
directionisfrom_chargerorto_charger(sent by the charging core).message_typeis the OCPP-J frame:call, itscall_result, or acall_errorwhoseerrorholdscodeanddescription.message_idpairs a call with its reply.timestampis when the charging core recorded the message.- Payload values are the charger's own: an OCPP 1.6
meterStartor meter value register is in Wh, as on the wire. Transaction reads state kWh separately. next_cursorisnullon the last page.
Errors: 422 time_range_invalid when since is not before until or the
period is longer than 7 days, 422 invalid_request for an unreadable
timestamp, action name or cursor, and 404 station_not_found for a station of
another tenant.
Redaction
Redaction is applied to the log, the export and the webhook alike.
redacted says whether it changed the message.
| What | Shown as |
|---|---|
Any value (text, number, boolean, or a whole object or list) of a field whose name contains password, secret, key, token or pin, in any case and at any depth: AuthorizationKey, apiKey, accessKey, token, pin, apnPassword, a VPN key, … The OCPP configuration key name fields key and unknownKey stay readable, configurationKey and idTokenInfo objects are searched like any other, and ID tokens follow their own rule below | [redacted] |
Values of configuration keys and variables whose name contains any of the words above, psk, basicauth or credential (AuthorizationKey, BasicAuthPassword, SimPin, WifiKey, CentralAuthKey, …; WebSocketPingInterval stays readable) in ChangeConfiguration, GetConfiguration, SetVariables, GetVariables and NotifyReport | [redacted] |
| HTTP Basic and Bearer credentials in text, user names and passwords in URLs, URL query strings (signed upload links) | Basic [redacted], Bearer [redacted], https://[redacted]@host/path?[redacted] |
ID tokens (idTag, parentIdTag, idToken, additionalIdToken) | Last 4 characters behind **** |
| Certificates, certificate chains, CSRs, EXI and OCSP data, signatures, PEM blocks | First 32 characters and the length |
JSON inside a text value (a DataTransfer data string, JSON inside that, …) | Parsed, redacted with these rules and written back. Inside it no field name is exempt: a key field there is [redacted] |
| JSON quoted once more (a JSON string holding JSON) | Unquoted, then redacted like JSON inside text |
Text that looks structured but is not JSON redaction can read: JSON after a byte order mark or other invisible character, invalid JSON, a quoted field name followed by :, form-encoded name=value pairs, markup such as XML, and a secret word followed by a delimiter in free text (password: …, apiKey=…, a tab, a line break, a vertical bar, or = and : URL-encoded as in pin%3D…) | [redacted] as a whole |
| Anything nested more than 64 levels deep, counting through JSON inside text | [redacted] |
A payload longer than 32,768 characters after redaction is omitted:
payload is null, truncated is true and payload_size gives its length.
A payload redaction cannot read is omitted the same way, with payload_size 0.
So is a message whose OCPP frame is nested more than 100 levels deep (some chargers'
customData can reach that). The message is still listed, with its action,
message_id and timestamp, but message_type is null. The
station.ocpp_message webhook skips such a message and does not deliver it.
Export
GET …/ocpp-messages/export takes the same since, until, action and
cursor and answers application/x-ndjson: one message per line, oldest
first, at most 10,000 lines. If more remain, the last line is
{"truncated": true, "next_cursor": "…"}. If the charging core fails during the
export, the last line is {"error": "…", "next_cursor": "…"}. Pass
next_cursor as cursor with the same filters to continue. A failure before
the first line is an ordinary error response. Each export is recorded in the
audit log.
Webhook events
Subscribe an endpoint to station.ocpp_message (it needs
stations:diagnostics as well as webhooks:write) and choose the actions you
need, so frequent MeterValues and Heartbeat messages do not flood it:
{"url": "https://app.example.com/ivora/ocpp", "events": ["station.ocpp_message"],
"stations": [12], "ocpp_messages": {"actions": ["BootNotification", "StatusNotification"]}}
data is one message as the log shows it, with the same redaction;
resource_id is its id. Without ocpp_messages.actions every action is
delivered. Signing, retries and the delivery log are those of every
webhook.
Volume limits:
- Ivora reads the charging core's journal about every 15 seconds, up to 500 messages per read, only for tenants with such an endpoint and only the actions they subscribed to. Messages are read about 10 seconds after the core records them and are never replayed from before the subscription. A burst is read in the following sweeps. Reading never holds up charger traffic.
- Backpressure: an endpoint with 1,000 or more pending deliveries gets no new
station.ocpp_messagedelivery. Each message it misses is listed in its delivery log with statusdroppedand a note inlast_error, and is never attempted. Read missed messages from the event log (GET /v1/tenants/{tenant_id}/events?type=station.ocpp_message, kept for 3 days) or from the message log. - The event log shows
station.ocpp_messageonly to callers withstations:diagnostics. It records only messages that one of the tenant's endpoints accepts.
AI agents
The MCP tool list_station_ocpp_messages reads the log with the same scope and
redaction. The export is not an agent tool.