Event streaming
CPMS publishes what happens at your charging stations and on your roaming connections to a message broker you consume from in real time. Use it when you need station- or roaming-level data as it happens — connector status, transactions, meter values, OCPI pushes and commands — at a volume and latency that polling the REST API or Webhooks cannot serve.
There are three streams:
| Stream | type | Carries |
|---|---|---|
| Raw OCPP messages | ocpp-raw | Every OCPP frame exchanged with a station, verbatim |
| Raw OCPI messages | OcpiInbound / OcpiOutbound | Every state-changing OCPI request exchanged with a roaming partner, with its response |
| Business events | cpo-event | Normalised connector status, transaction start/stop, meter values and EV charging needs |
Which one do I need?
- Webhooks — you want the CPMS view of a session or CDR (
SessionDto,CdrDto), pushed over HTTP. - Business events — you want station events in one shape, whatever OCPP version the station speaks.
- Raw OCPP — you want full fidelity: every action, every field, CPMS commands and station replies. You parse OCPP yourself.
- Raw OCPI — you want to audit or mirror what CPMS exchanges with eMSPs and hubs: location and session pushes, CDRs, tokens, commands.
flowchart LR
CS[Charging station] <-->|OCPP| CPMS
P[Roaming partner] <-->|OCPI| CPMS
CPMS -->|ocpp-raw| RI[OCPPRawInbound]
CPMS -->|ocpp-raw| RO[OCPPRawOutbound]
CPMS -->|OcpiInbound / OcpiOutbound| OI[OCPI inbound<br/>OCPI outbound]
CPMS -->|cpo-event| BE[ConnectorStatus<br/>ChargingSession<br/>MeterValues<br/>EVChargingNeeds]
RI --> You[Your consumer]
RO --> You
OI --> You
BE --> You
Transports
Each category is published to its own Kafka topic, SNS topic or SQS queue.
| Transport | Destination per category | Ordering | Authentication |
|---|---|---|---|
| Apache Kafka | topic | Per station: the message key is the charging station ID. Raw OCPI: per partner (key = partner ID) | SASL/SCRAM over TLS, or mutual TLS (OCPI) |
| AWS SNS | standard topic | None — order by ts | AWS IAM |
| AWS SQS | queue | Per station on FIFO queues (MessageGroupId = charging station ID); none on standard queues | AWS IAM |
Use SNS when you want to fan out to several subscribers (SQS, Lambda, HTTPS); use SQS FIFO when you need per-station order without running Kafka.
Getting access
Streaming is enabled per environment by Solidstudio; it is not configured through the Public API. Agree the following with your Solidstudio contact:
- Transport — Kafka, SNS or SQS.
- Destinations — Kafka topic names, SNS topic ARNs or SQS queue URLs, one per category you want. Kafka topic names for OCPP and business events default to the category names on this page; raw OCPI topic names (for example
raw-ocpi-inbound/raw-ocpi-outbound) are agreed during onboarding. - Credentials — for Kafka, the broker addresses and either a SCRAM username/password or, for raw OCPI, optionally a client certificate for mutual TLS. For SNS/SQS in your own AWS account, the topic or queue access policy must allow Solidstudio's publisher role to call
sns:Publish/sqs:SendMessage; we share the role ARN during onboarding.
Envelope
Every raw OCPP message and business event is a JSON object with these fields (raw OCPI messages use their own envelope):
| Field | Type | Description |
|---|---|---|
category | string | Topic the message belongs to, e.g. OCPPRawInbound, ConnectorStatus |
type | string | ocpp-raw or cpo-event |
chargingStationId | string | OCPP identity of the station — the ID in its WebSocket URL |
connectionId | string | Opaque ID of the station's WebSocket connection. Changes on every reconnect |
ts | string | ISO 8601 UTC. When CPMS received the message from the station, or sent it to the station |
metadata.source | string | Always Solidstudio |
metadata.publishedAt | string | ISO 8601 UTC. When the message was handed to the broker; publishedAt − ts is the pipeline delay |
Treat the envelope as open: ignore fields you don't recognise. New fields and new categories may be added without notice.
Raw OCPP messages
| Topic | Direction | Contains |
|---|---|---|
OCPPRawInbound | station → CPMS | Requests the station sends (BootNotification, StatusNotification, …) and its replies to CPMS commands |
OCPPRawOutbound | CPMS → station | Commands CPMS sends (RemoteStartTransaction, Reset, …) and its replies to station requests |
Additional fields:
| Field | Type | Description |
|---|---|---|
raw | string | The OCPP frame exactly as it crossed the wire, as a JSON-encoded string. Parse it a second time |
ocppVersion | string | Optional. OCPP version of the connection, e.g. 1.6 |
{
"category": "OCPPRawInbound",
"chargingStationId": "ACE00007",
"type": "ocpp-raw",
"connectionId": "740ac88a-6be6-407a-a2c5-67b51964cb6d",
"raw": "[2,\"c0b5e4d2-1f3a-4a8e-9a77-2b1f0f3d9e10\",\"StatusNotification\",{\"connectorId\":1,\"errorCode\":\"NoError\",\"status\":\"Available\"}]",
"ts": "2025-01-01T00:00:00.000Z",
"metadata": {
"source": "Solidstudio",
"publishedAt": "2025-01-01T00:00:00.012Z"
}
}Reading raw
rawraw is an OCPP frame:
| Frame | Shape |
|---|---|
| CALL | [2, "<messageId>", "<action>", { payload }] |
| CALLRESULT | [3, "<messageId>", { payload }] |
| CALLERROR | [4, "<messageId>", "<errorCode>", "<description>", { details }] |
A request and its reply share the messageId and travel on opposite topics: a station's CALL is on OCPPRawInbound and the CPMS reply on OCPPRawOutbound, and the other way round for CPMS commands. The two topics are not ordered relative to each other — join them on chargingStationId + messageId.
Commands sent through the v2 commands API return an ocppMessageId. It is the messageId of the CALL on OCPPRawOutbound, so you can follow a command you sent to the station's reply.
Transport differences
| Transport | Difference |
|---|---|
| Kafka | The station ID is in chargerId instead of chargingStationId. Adds status: VALID on inbound, SENT on outbound |
| SNS | Heartbeat messages may be omitted. Messages may carry SNS message attributes for subscription filter policies (below) |
SNS message attributes
When present, these attributes let you filter in the SNS subscription instead of in your consumer:
| Attribute | Type | Set on |
|---|---|---|
ocppVersion | String | every message |
stationId | String | every message |
action | String | CALL frames (e.g. StatusNotification) |
connectorId | Number | StatusNotification, StartTransaction, MeterValues, RemoteStartTransaction |
transactionId | Number | StartTransaction reply, StopTransaction, MeterValues, RemoteStopTransaction |
status | String | StatusNotification |
errorCode | String | StatusNotification with an error other than NoError |
vendorId | String | StatusNotification, DataTransfer |
vendorErrorCode | String | StatusNotification |
chargePointVendor | String | BootNotification |
eventCategory | String | StatusNotification: SECURITY when a vendor error code is set, otherwise DEVICE when an error code is set |
severity | String | StatusNotification with status Faulted: CRITICAL |
Example filter policy — only faulted connectors:
{ "action": ["StatusNotification"], "status": ["Faulted"] }Raw OCPI messages
Every state-changing OCPI call between CPMS and a roaming partner (eMSP or hub) is published as one message containing both the request and the response.
| Direction | type | Contains |
|---|---|---|
| partner → CPMS | OcpiInbound | Calls the partner makes to the CPMS OCPI API (commands, token pushes, credentials, …) and CPMS's response |
| CPMS → partner | OcpiOutbound | Calls CPMS makes to the partner (location, session and CDR pushes, real-time authorisation, …) and the partner's response |
Inbound and outbound go to separate topics. On Kafka, the message key is the CPMS identifier of the OCPI partner, so one partner's traffic stays in order within a topic.
| Field | Type | Description |
|---|---|---|
type | string | OcpiInbound or OcpiOutbound |
protocolVersion | string | null | OCPI version of the call: 2.1.1, 2.2 or 2.2.1. null when it cannot be derived from an inbound URL |
tracing.id | string | UUID unique to this message. Use it to deduplicate |
tracing.correlationId | string | Currently equal to tracing.id |
tracing.causationId | string | Currently equal to tracing.id |
tracing.time | string | ISO 8601 UTC. When the exchange was captured, after the response |
tracing.source | string | Always Solidstudio |
request.method | string | HTTP method |
request.url | string | Inbound: path and query string. Outbound: full partner URL |
request.headers | object | OCPI headers only: X-Request-ID, X-Correlation-ID and, when routing headers are enabled, OCPI-from-* / OCPI-to-* |
request.body | any | Request body as parsed JSON |
response.headers | object | OCPI headers of the response, as for request.headers |
response.body | any | Response body as parsed JSON — the OCPI response envelope with status_code, status_message, data |
version | string | Version of this message format, currently 1.0 |
{
"type": "OcpiOutbound",
"protocolVersion": "2.2.1",
"tracing": {
"id": "3f6c2a1e-8b7d-4c55-9f1a-0d2e6b7c8a91",
"correlationId": "3f6c2a1e-8b7d-4c55-9f1a-0d2e6b7c8a91",
"causationId": "3f6c2a1e-8b7d-4c55-9f1a-0d2e6b7c8a91",
"time": "2025-01-01T00:00:00.250Z",
"source": "Solidstudio"
},
"request": {
"method": "PATCH",
"url": "https://emsp.example.com/ocpi/emsp/2.2.1/locations/PL/SOL/LOC1/PL*SOL*E1",
"headers": {
"X-Request-ID": "6b1f8e0c-2d4a-4f3b-8c9e-1a2b3c4d5e6f",
"X-Correlation-ID": "a7e3c9d1-5b2f-4e8a-9c0d-7f6e5d4c3b2a"
},
"body": { "status": "CHARGING", "last_updated": "2025-01-01T00:00:00Z" }
},
"response": {
"headers": {
"X-Request-ID": "6b1f8e0c-2d4a-4f3b-8c9e-1a2b3c4d5e6f",
"X-Correlation-ID": "a7e3c9d1-5b2f-4e8a-9c0d-7f6e5d4c3b2a"
},
"body": { "status_code": 1000, "status_message": "Success", "timestamp": "2025-01-01T00:00:00.200Z" }
},
"version": "1.0"
}Notes:
- Read-only calls are not streamed by default.
GET,HEAD,OPTIONSandTRACEare filtered out, so pulls (a partner paginatingGET /locations, CPMS fetching tokens) do not appear; pushes, commands and authorisation calls do. - No HTTP status code. Judge the outcome from
response.body.status_code(OCPI1xxxsuccess,2xxxclient error,3xxxserver error). - Credentials are never streamed.
Authorization,Cookieand similar headers are dropped or replaced with.... - Non-JSON bodies are wrapped as
{ "body": "<text>" }. - Correlate with partner logs on
X-Request-ID/X-Correlation-ID— they are the values sent on the wire.
Business events
Business events are published for every OCPP version, as long as the station sends the underlying message.
| Topic | Published when | OCPP 1.6 | OCPP 2.0.1 / 2.1 |
|---|---|---|---|
ConnectorStatus | A connector reports a status | StatusNotification | StatusNotification |
ChargingSession | A transaction starts or ends | StartTransaction, StopTransaction | TransactionEvent (Started, Ended) |
MeterValues | The station reports meter readings | MeterValues | MeterValues, TransactionEvent |
EVChargingNeeds | The EV announces its charging needs | Only with the ISO 15118 (V2G) extension | NotifyEVChargingNeeds (OCPP 2.1) |
Each event carries the envelope plus a payload object specific to the category.
ConnectorStatus
| Field | Type | Description |
|---|---|---|
connectorId | integer | OCPP connector ID |
evseId | integer | EVSE ID. For OCPP 1.6 stations, the EVSE ID configured for the charging point in CPMS |
status | string | Status as reported by the station, in the enum of its OCPP version. 1.6: Available, Preparing, Charging, SuspendedEVSE, SuspendedEV, Finishing, Reserved, Unavailable, Faulted. 2.x: Available, Occupied, Reserved, Unavailable, Faulted |
{
"category": "ConnectorStatus",
"chargingStationId": "ACE00007",
"type": "cpo-event",
"connectionId": "740ac88a-6be6-407a-a2c5-67b51964cb6d",
"ts": "2025-01-01T00:00:00Z",
"payload": {
"connectorId": 1,
"evseId": 1,
"status": "Available"
},
"metadata": {
"source": "Solidstudio",
"publishedAt": "2025-01-01T00:00:00Z"
}
}ChargingSession
One event when the transaction starts (Started) and one when it ends (Ended).
| Field | Type | Present on | Description |
|---|---|---|---|
event | string | both | Started or Ended |
connectorId | integer | both | OCPP connector ID |
evseId | integer | both | EVSE ID |
ocppTransactionId | integer | string | both | Transaction ID on the station: integer in OCPP 1.6, string in OCPP 2.x |
idTag | string | both | Token that authorised the transaction (RFID UID, contract ID, …) |
meterStart | number | both | Energy register at start, in Wh |
meterStop | number | Ended | Energy register at stop, in Wh |
reason | string | Ended | Stop reason reported by the station, e.g. Local, Remote, EVDisconnected |
Transaction started:
{
"category": "ChargingSession",
"chargingStationId": "ACE00007",
"type": "cpo-event",
"connectionId": "740ac88a-6be6-407a-a2c5-67b51964cb6d",
"ts": "2025-01-01T00:00:00Z",
"payload": {
"event": "Started",
"connectorId": 1,
"evseId": 1,
"ocppTransactionId": 12345,
"idTag": "DEADBEEF",
"meterStart": 0
},
"metadata": {
"source": "Solidstudio",
"publishedAt": "2025-01-01T00:00:00Z"
}
}Transaction ended:
{
"category": "ChargingSession",
"chargingStationId": "ACE00007",
"type": "cpo-event",
"connectionId": "740ac88a-6be6-407a-a2c5-67b51964cb6d",
"ts": "2025-01-01T00:00:00Z",
"payload": {
"event": "Ended",
"connectorId": 1,
"evseId": 1,
"ocppTransactionId": 12345,
"idTag": "DEADBEEF",
"meterStart": 0,
"meterStop": 100,
"reason": "Remote"
},
"metadata": {
"source": "Solidstudio",
"publishedAt": "2025-01-01T00:00:00Z"
}
}MeterValues
| Field | Type | Description |
|---|---|---|
connectorId | integer | OCPP connector ID |
evseId | integer | EVSE ID |
meterValue[] | array | Readings, one entry per sampling timestamp |
meterValue[].timestamp | string | ISO 8601 UTC. When the station took the reading |
meterValue[].sampledValue[] | array | Individual measurands sampled at that timestamp |
sampledValue[].value | number | Measured value |
sampledValue[].measurand | string | OCPP measurand, e.g. Energy.Active.Import.Register, Power.Active.Import, SoC |
sampledValue[].unit | string | Unit as reported by the station, e.g. Wh, kWh, W, kW, Percent |
Units are passed through as the station reports them; normalise before aggregating across stations.
{
"category": "MeterValues",
"chargingStationId": "ACE00007",
"type": "cpo-event",
"connectionId": "740ac88a-6be6-407a-a2c5-67b51964cb6d",
"ts": "2025-01-01T00:00:00Z",
"payload": {
"connectorId": 1,
"evseId": 1,
"meterValue": [
{
"timestamp": "2025-01-01T00:00:00Z",
"sampledValue": [
{ "value": 32, "measurand": "Power.Active.Import", "unit": "kW" },
{ "value": 100, "measurand": "Energy.Active.Import.Register", "unit": "kWh" }
]
}
]
},
"metadata": {
"source": "Solidstudio",
"publishedAt": "2025-01-01T00:00:00Z"
}
}EVChargingNeeds
Only published for stations on OCPP 2.1 or OCPP 1.6 with the ISO 15118 (V2G) extension.
| Field | Type | Description |
|---|---|---|
evseId | integer | EVSE the EV is connected to |
timestamp | string | ISO 8601 UTC. When the EV communicated its needs |
chargingNeeds | object | The OCPP 2.1 ChargingNeedsType: requestedEnergyTransfer, departureTime, mobilityNeedsMode, controlMode, and the parameters object for the requested transfer (acChargingParameters, dcChargingParameters, v2xChargingParameters, …). Powers in W, energies in Wh |
{
"category": "EVChargingNeeds",
"chargingStationId": "ACE00007",
"type": "cpo-event",
"connectionId": "740ac88a-6be6-407a-a2c5-67b51964cb6d",
"ts": "2025-01-01T00:00:00Z",
"payload": {
"evseId": 1,
"timestamp": "2025-01-01T00:00:00Z",
"chargingNeeds": {
"mobilityNeedsMode": "EVCC",
"requestedEnergyTransfer": "AC_three_phase_BPT",
"controlMode": "Dynamic",
"departureTime": "2026-01-01T00:00:00Z",
"v2xChargingParameters": {
"evMaxEnergyRequest": 9300,
"evTargetEnergyRequest": 4800,
"maxDischargePower": -11000,
"minChargePower": 0,
"evMinEnergyRequest": -32800,
"maxChargePower": 11000,
"minDischargePower": 0
}
}
},
"metadata": {
"source": "Solidstudio",
"publishedAt": "2025-01-01T00:00:00Z"
}
}Semantics you must design for
- Out of band. Events are published asynchronously, after CPMS has handled the OCPP message or OCPI call. A slow or unavailable broker never delays the station or the partner — and an event can therefore arrive after the REST API already reflects the change.
- At-most-once, with possible duplicates. A publish that keeps failing is logged and dropped; some publishers retry a few times first, which can produce a duplicate. Deduplicate raw OCPI messages on
tracing.id. Raw OCPP messages and business events have no event ID: deduplicate on natural keys —chargingStationId+messageIdfromrawfor raw OCPP,chargingStationId+category+ts+ the payload's identifiers for business events. - Order by timestamp, not by arrival. Use
ts, ortracing.timefor raw OCPI. Arrival order only holds per station (OCPP) or per partner (OCPI) on Kafka, per station on SQS FIFO, and never across topics. - Reconnects change
connectionId. Use it to group a connection's traffic, not to identify a station. - Reconcile from the REST API. For completeness, backfill from
GET /api/v1/charging-stations/{chargingStationId}/ocpp-logs,POST /api/v1/sessions/rsql-list,GET /api/v1/connectorsandPOST /api/v1/cdrs/rsql-list.
Related
- Webhooks — session and CDR updates over HTTP
- Remote commands —
ocppMessageIdfor correlating commands with raw OCPP traffic - Session flows — how OCPP transactions become CPMS sessions
- OCPI partners — the roaming partners whose traffic appears on the raw OCPI stream
Updated 16 days ago