Charger management

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.

RouteReturns
GET /v1/tenants/{tenant_id}/stations/{station_id}/ocpp-messagesOne page of messages, newest first
GET /v1/tenants/{tenant_id}/stations/{station_id}/ocpp-messages/exportThe same messages as NDJSON, oldest first
Webhook event station.ocpp_messageEach new message as it is recorded, filtered by OCPP action
flowchart diagram; its source follows
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
ParameterMeaning
since, untilISO 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.
actionRepeat to keep only these OCPP actions (up to 20). A reply carries its request's action.
cursornext_cursor of the previous page, with the same filters.
limit1 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"
}
  • direction is from_charger or to_charger (sent by the charging core).
  • message_type is the OCPP-J frame: call, its call_result, or a call_error whose error holds code and description. message_id pairs a call with its reply.
  • timestamp is when the charging core recorded the message.
  • Payload values are the charger's own: an OCPP 1.6 meterStart or meter value register is in Wh, as on the wire. Transaction reads state kWh separately.
  • next_cursor is null on 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.

WhatShown 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 blocksFirst 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_message delivery. Each message it misses is listed in its delivery log with status dropped and a note in last_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_message only to callers with stations: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.