Three messages become one
| OCPP 1.6J | OCPP 2.0.1 |
|---|---|
| StartTransaction | TransactionEvent with eventType: Started |
| MeterValues | TransactionEvent with eventType: Updated |
| StopTransaction | TransactionEvent with eventType: Ended |
The motivation is that in 1.6J these three messages describe one thing — a transaction — while carrying inconsistent context. MeterValues might or might not carry a transaction ID. The relationship between a meter sample and the session it belongs to was implicit. Ordering between messages was not guaranteed.
One message type with a consistent envelope fixes all three.
The message
// Charge point -> CSMS
[2, "b7f14a2c", "TransactionEvent", {
"eventType": "Updated",
"timestamp": "2026-08-11T09:47:10Z",
"triggerReason": "MeterValuePeriodic",
"seqNo": 14,
"transactionInfo": {
"transactionId": "770142",
"chargingState": "Charging",
"timeSpentCharging": 2110
},
"evse": { "id": 1, "connectorId": 1 },
"idToken": { "idToken": "DR88231ABCD", "type": "ISO14443" },
"meterValue": [{
"timestamp": "2026-08-11T09:47:10Z",
"sampledValue": [
{ "value": 4490220, "measurand": "Energy.Active.Import.Register",
"unitOfMeasure": { "unit": "Wh" } },
{ "value": 48.2, "measurand": "Power.Active.Import",
"unitOfMeasure": { "unit": "kW" } },
{ "value": 74, "measurand": "SoC", "unitOfMeasure": { "unit": "Percent" } }
]
}]
}]
// CSMS -> charge point
[3, "b7f14a2c", { "idTokenInfo": { "status": "Accepted" } }]
triggerReason: the field 1.6J lacked
In 1.6J, receiving a MeterValues message told you a sample arrived. It did not tell you why. triggerReason makes that explicit, and it turns the transaction stream into an event log you can actually reason about.
| triggerReason | Sent because |
|---|---|
Authorized | A credential was accepted |
CablePluggedIn | Physical connection made |
ChargingStateChanged | Charging started, paused or resumed |
MeterValuePeriodic | Routine sampling interval |
MeterValueClock | Clock-aligned sample |
EnergyLimitReached | A configured energy cap was hit |
TimeLimitReached | A configured time cap was hit |
EVCommunicationLost | Lost contact with the vehicle |
EVDeparted | Vehicle disconnected |
RemoteStart / RemoteStop | A cloud command |
AbnormalCondition | Something went wrong |
ResetCommand | Charger being reset |
seqNo and chargingState
seqNo
A strictly incrementing counter within a transaction. It solves an ordering problem 1.6J left open: messages arriving out of order, particularly after an offline period, could not be reliably sequenced by timestamp alone when the charger's clock had drifted.
With seqNo you can order events definitively and, just as usefully, detect gaps. A jump from 14 to 16 means event 15 was lost, and you know to go looking rather than silently billing incomplete data.
chargingState
Replaces inference from StatusNotification with an explicit field on the transaction itself:
| chargingState | Means |
|---|---|
Charging | Energy flowing |
EVConnected | Plugged in, not drawing |
SuspendedEV | Vehicle paused it |
SuspendedEVSE | Charger paused it |
Idle | No vehicle |
In 1.6J you correlated connector status against transaction state and hoped they agreed. Here the transaction reports its own charging state, which removes an entire class of reconciliation bug.
Migrating from 1.6J
The two versions are not wire-compatible and there is no upgrade path at the message level — a CSMS supports one, the other, or both side by side. In practice everyone runs both for years.
| Concern | Reality |
|---|---|
| Fleet | 1.6J hardware cannot be firmware-upgraded to 2.0.1 in most cases |
| CSMS | Must speak both, selected by WebSocket subprotocol at connection |
| Data model | Normalise both into one internal transaction model, or maintain two billing paths forever |
| Roaming | OCPI CDRs must be identical regardless of which version produced them |
| IDs | transactionId is an integer in 1.6J and a string in 2.0.1 |
The last row causes real bugs. A platform that typed transaction IDs as integers has to widen that type everywhere, including in stored data and partner-facing references.
The right approach is to normalise at ingestion: translate both 1.6J messages and 2.0.1 TransactionEvents into one internal representation immediately, and keep version-specific handling at the protocol edge. Everything downstream — billing, roaming, reporting — should never know which version a charger speaks.
Production lessons
- Normalise both versions at the edge into one internal transaction model.
- Type transaction IDs as strings from the start, even on a 1.6J-only platform.
- Use
seqNogaps as a data-integrity alarm. A missing event means incomplete billing data. - Index and dashboard
triggerReason. It is the best diagnostic signal either version offers. - Do not assume 2.0.1 means better data. Vendor implementations vary; certify each model.
- Keep CDR output identical across versions. A roaming partner must not be able to tell which OCPP version produced a charge.
Frequently asked questions
It replaces StartTransaction, StopTransaction and MeterValues from 1.6J with a single message whose eventType is Started, Updated or Ended. This gives all three a consistent envelope and removes the ambiguity in 1.6J about which meter samples belonged to which session.
An explicit statement of why the event was sent — MeterValuePeriodic, ChargingStateChanged, EVDeparted, RemoteStop, AbnormalCondition and others. In 1.6J you had to infer this, so it is the single most useful addition in 2.0.1 for fleet diagnostics.
It is a strictly incrementing counter within a transaction that gives definitive ordering independent of clock drift, and lets you detect lost events. A jump from 14 to 16 means an event was lost and the billing data for that transaction is incomplete.
No. They are not wire-compatible and 1.6J hardware generally cannot be upgraded, so a CSMS must support both side by side, selected by WebSocket subprotocol. Normalise both into one internal transaction model at ingestion so nothing downstream depends on the version.
They become strings, whereas 1.6J uses integers. Platforms that typed transaction IDs as integers must widen that type across code, stored data and any partner-facing references.