OCPP 2.0.1 · UPDATED 11 AUG 2026

TransactionEvent: the 2.0.1 rewrite

OCPP 2.0.1 collapses three of the busiest messages in 1.6J into one. It is the largest single change between the versions, and understanding why it was done explains most of what else changed.

PART OF THE OCPP COMPLETE GUIDE · 14 MESSAGE GUIDES

IN ONE PARAGRAPH

TransactionEvent replaces StartTransaction, StopTransaction and MeterValues with a single message carrying an eventType of Started, Updated or Ended. It adds an explicit triggerReason saying why the message was sent and a seqNo giving strict ordering — both of which fix real ambiguities in 1.6J.

Three messages become one

OCPP 1.6JOCPP 2.0.1
StartTransactionTransactionEvent with eventType: Started
MeterValuesTransactionEvent with eventType: Updated
StopTransactionTransactionEvent 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.

triggerReasonSent because
AuthorizedA credential was accepted
CablePluggedInPhysical connection made
ChargingStateChangedCharging started, paused or resumed
MeterValuePeriodicRoutine sampling interval
MeterValueClockClock-aligned sample
EnergyLimitReachedA configured energy cap was hit
TimeLimitReachedA configured time cap was hit
EVCommunicationLostLost contact with the vehicle
EVDepartedVehicle disconnected
RemoteStart / RemoteStopA cloud command
AbnormalConditionSomething went wrong
ResetCommandCharger being reset
This is the single most useful addition in 2.0.1 for operations. In 1.6J, working out why a session ended meant inferring from the optional stop reason and surrounding messages. In 2.0.1 every event states its own cause, which makes fleet diagnostics a query rather than an investigation.

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:

chargingStateMeans
ChargingEnergy flowing
EVConnectedPlugged in, not drawing
SuspendedEVVehicle paused it
SuspendedEVSECharger paused it
IdleNo 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.

ConcernReality
Fleet1.6J hardware cannot be firmware-upgraded to 2.0.1 in most cases
CSMSMust speak both, selected by WebSocket subprotocol at connection
Data modelNormalise both into one internal transaction model, or maintain two billing paths forever
RoamingOCPI CDRs must be identical regardless of which version produced them
IDstransactionId 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 seqNo gaps 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

What does TransactionEvent replace in OCPP 2.0.1?

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.

What is triggerReason in OCPP 2.0.1?

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.

What is seqNo used for?

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.

Are OCPP 1.6J and 2.0.1 compatible?

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.

What changes about transaction IDs in OCPP 2.0.1?

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.