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:

StreamtypeCarries
Raw OCPP messagesocpp-rawEvery OCPP frame exchanged with a station, verbatim
Raw OCPI messagesOcpiInbound / OcpiOutboundEvery state-changing OCPI request exchanged with a roaming partner, with its response
Business eventscpo-eventNormalised 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.

TransportDestination per categoryOrderingAuthentication
Apache KafkatopicPer 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 SNSstandard topicNone — order by tsAWS IAM
AWS SQSqueuePer station on FIFO queues (MessageGroupId = charging station ID); none on standard queuesAWS 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):

FieldTypeDescription
categorystringTopic the message belongs to, e.g. OCPPRawInbound, ConnectorStatus
typestringocpp-raw or cpo-event
chargingStationIdstringOCPP identity of the station — the ID in its WebSocket URL
connectionIdstringOpaque ID of the station's WebSocket connection. Changes on every reconnect
tsstringISO 8601 UTC. When CPMS received the message from the station, or sent it to the station
metadata.sourcestringAlways Solidstudio
metadata.publishedAtstringISO 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

TopicDirectionContains
OCPPRawInboundstation → CPMSRequests the station sends (BootNotification, StatusNotification, …) and its replies to CPMS commands
OCPPRawOutboundCPMS → stationCommands CPMS sends (RemoteStartTransaction, Reset, …) and its replies to station requests

Additional fields:

FieldTypeDescription
rawstringThe OCPP frame exactly as it crossed the wire, as a JSON-encoded string. Parse it a second time
ocppVersionstringOptional. 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

raw is an OCPP frame:

FrameShape
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

TransportDifference
KafkaThe station ID is in chargerId instead of chargingStationId. Adds status: VALID on inbound, SENT on outbound
SNSHeartbeat 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:

AttributeTypeSet on
ocppVersionStringevery message
stationIdStringevery message
actionStringCALL frames (e.g. StatusNotification)
connectorIdNumberStatusNotification, StartTransaction, MeterValues, RemoteStartTransaction
transactionIdNumberStartTransaction reply, StopTransaction, MeterValues, RemoteStopTransaction
statusStringStatusNotification
errorCodeStringStatusNotification with an error other than NoError
vendorIdStringStatusNotification, DataTransfer
vendorErrorCodeStringStatusNotification
chargePointVendorStringBootNotification
eventCategoryStringStatusNotification: SECURITY when a vendor error code is set, otherwise DEVICE when an error code is set
severityStringStatusNotification 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.

DirectiontypeContains
partner → CPMSOcpiInboundCalls the partner makes to the CPMS OCPI API (commands, token pushes, credentials, …) and CPMS's response
CPMS → partnerOcpiOutboundCalls 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.

FieldTypeDescription
typestringOcpiInbound or OcpiOutbound
protocolVersionstring | nullOCPI version of the call: 2.1.1, 2.2 or 2.2.1. null when it cannot be derived from an inbound URL
tracing.idstringUUID unique to this message. Use it to deduplicate
tracing.correlationIdstringCurrently equal to tracing.id
tracing.causationIdstringCurrently equal to tracing.id
tracing.timestringISO 8601 UTC. When the exchange was captured, after the response
tracing.sourcestringAlways Solidstudio
request.methodstringHTTP method
request.urlstringInbound: path and query string. Outbound: full partner URL
request.headersobjectOCPI headers only: X-Request-ID, X-Correlation-ID and, when routing headers are enabled, OCPI-from-* / OCPI-to-*
request.bodyanyRequest body as parsed JSON
response.headersobjectOCPI headers of the response, as for request.headers
response.bodyanyResponse body as parsed JSON — the OCPI response envelope with status_code, status_message, data
versionstringVersion 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, OPTIONS and TRACE are filtered out, so pulls (a partner paginating GET /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 (OCPI 1xxx success, 2xxx client error, 3xxx server error).
  • Credentials are never streamed. Authorization, Cookie and 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.

TopicPublished whenOCPP 1.6OCPP 2.0.1 / 2.1
ConnectorStatusA connector reports a statusStatusNotificationStatusNotification
ChargingSessionA transaction starts or endsStartTransaction, StopTransactionTransactionEvent (Started, Ended)
MeterValuesThe station reports meter readingsMeterValuesMeterValues, TransactionEvent
EVChargingNeedsThe EV announces its charging needsOnly with the ISO 15118 (V2G) extensionNotifyEVChargingNeeds (OCPP 2.1)

Each event carries the envelope plus a payload object specific to the category.

ConnectorStatus

FieldTypeDescription
connectorIdintegerOCPP connector ID
evseIdintegerEVSE ID. For OCPP 1.6 stations, the EVSE ID configured for the charging point in CPMS
statusstringStatus 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).

FieldTypePresent onDescription
eventstringbothStarted or Ended
connectorIdintegerbothOCPP connector ID
evseIdintegerbothEVSE ID
ocppTransactionIdinteger | stringbothTransaction ID on the station: integer in OCPP 1.6, string in OCPP 2.x
idTagstringbothToken that authorised the transaction (RFID UID, contract ID, …)
meterStartnumberbothEnergy register at start, in Wh
meterStopnumberEndedEnergy register at stop, in Wh
reasonstringEndedStop 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

FieldTypeDescription
connectorIdintegerOCPP connector ID
evseIdintegerEVSE ID
meterValue[]arrayReadings, one entry per sampling timestamp
meterValue[].timestampstringISO 8601 UTC. When the station took the reading
meterValue[].sampledValue[]arrayIndividual measurands sampled at that timestamp
sampledValue[].valuenumberMeasured value
sampledValue[].measurandstringOCPP measurand, e.g. Energy.Active.Import.Register, Power.Active.Import, SoC
sampledValue[].unitstringUnit 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.

FieldTypeDescription
evseIdintegerEVSE the EV is connected to
timestampstringISO 8601 UTC. When the EV communicated its needs
chargingNeedsobjectThe 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 + messageId from raw for raw OCPP, chargingStationId + category + ts + the payload's identifiers for business events.
  • Order by timestamp, not by arrival. Use ts, or tracing.time for 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/connectors and POST /api/v1/cdrs/rsql-list.

Related

  • Webhooks — session and CDR updates over HTTP
  • Remote commands — ocppMessageId for 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

Did this page help you?