In OCPP 1.6 a charging session is StartTransaction, a stream of MeterValues and a StopTransaction, and the CSMS hands out the transactionId. In 2.0.1 all of that becomes one message, TransactionEvent, sent by the station with an eventType of Started, Updated or Ended. It is the most important message in the protocol for billing, and the one with the most edge cases. This article is functional block E (Transactions) as you would implement it.
The anatomy of TransactionEventRequest
| Field | Notes |
|---|---|
eventType | Started, Updated or Ended. |
timestamp | When the event happened at the station (not when it was sent). |
triggerReason | Why this event was sent now. Required on every event. |
seqNo | Per-transaction sequence number, starting at 0 and incremented for every TransactionEvent of that transaction. |
transactionInfo | transactionId (required, string, generated by the station), plus optional chargingState, timeSpentCharging, stoppedReason, remoteStartId. |
offline | True if the event occurred while the station was offline. |
evse | EVSE (and connector) the transaction is on. Sent in the first event after the EV is connected; may be omitted in later events. |
idToken | The token that authorized (or stopped) the transaction, sent in the event where that happened. |
meterValue | Array of MeterValueType, each with a timestamp and sampledValue list. |
numberOfPhasesUsed, cableMaxCurrent, reservationId | Optional context. |
The transactionId is a string of up to 36 characters, typically a UUID, created by the station. The CSMS must key transactions by station identity plus transactionId, not by transactionId alone, and must not expect integers. This is the change that breaks the most 1.6-era database schemas.
When does a transaction start and stop?
In 1.6 a transaction started when the station said so, which in practice meant "after authorization and plug-in" with vendor variations. 2.0.1 makes this configurable through two device model variables on TxCtrlr:
TxStartPoint: a list of conditions. The transaction starts as soon as any of them becomes true.TxStopPoint: a list of conditions. The transaction ends as soon as any of them is no longer true.
| Value | Meaning |
|---|---|
ParkingBayOccupancy | A vehicle is detected in the bay (needs a sensor). |
EVConnected | The cable is plugged in at both ends. |
Authorized | The driver or EV has been authorized. |
DataSigned | A signed meter value has been obtained (for eichrecht-style metering). |
PowerPathClosed | Connected and authorized, so energy can flow (relay closed). |
EnergyTransfer | Energy is actually being transferred. |
A common configuration is TxStartPoint = EVConnected,Authorized and TxStopPoint = EVConnected,Authorized: the transaction exists from the first of plug-in or card tap, and ends when the cable is removed or authorization ends. With TxStartPoint = PowerPathClosed, a transaction only exists once both conditions hold, which means failed authorizations never create transactions at all. Neither is wrong, but billing, idle-fee and reporting logic must know which one each station uses. Read both variables after boot and store them.
A complete session: cable plugged in first (E02)
Station configured with TxStartPoint = EVConnected,Authorized. The driver plugs in, so the transaction starts immediately with triggerReason CablePluggedIn:
[2, "te-0001", "TransactionEvent", {
"eventType": "Started",
"timestamp": "2026-10-26T10:00:00.000Z",
"triggerReason": "CablePluggedIn",
"seqNo": 0,
"transactionInfo": {
"transactionId": "7f3c2a10-5e61-4b8e-9d2a-0c4b1e8f6a33",
"chargingState": "EVConnected"
},
"evse": {
"id": 1,
"connectorId": 1
},
"meterValue": [
{
"timestamp": "2026-10-26T10:00:00.000Z",
"sampledValue": [
{
"value": 1250345.0,
"context": "Transaction.Begin",
"measurand": "Energy.Active.Import.Register",
"unitOfMeasure": { "unit": "Wh" }
}
]
}
]
}]
[3, "te-0001", {}]The driver taps an RFID card. The station authorizes it (with Authorize, its cache or local list, see part 5) and reports it in an Updated event with triggerReason Authorized and the idToken. The CSMS answers with idTokenInfo, which is how it can still refuse the token at this point.
[2, "te-0002", "TransactionEvent", {
"eventType": "Updated",
"timestamp": "2026-10-26T10:00:20.000Z",
"triggerReason": "Authorized",
"seqNo": 1,
"transactionInfo": {
"transactionId": "7f3c2a10-5e61-4b8e-9d2a-0c4b1e8f6a33",
"chargingState": "EVConnected"
},
"idToken": {
"idToken": "04A2B3C4D5E680",
"type": "ISO14443"
}
}]
[3, "te-0002", {
"idTokenInfo": {
"status": "Accepted"
}
}]Energy starts flowing: an Updated with triggerReason ChargingStateChanged and chargingState Charging. Then periodic meter values arrive as Updated events with triggerReason MeterValuePeriodic, carrying the measurands configured in SampledDataCtrlr.TxUpdatedMeasurands every TxUpdatedInterval seconds:
[2, "te-0004", "TransactionEvent", {
"eventType": "Updated",
"timestamp": "2026-10-26T10:01:30.000Z",
"triggerReason": "MeterValuePeriodic",
"seqNo": 3,
"transactionInfo": {
"transactionId": "7f3c2a10-5e61-4b8e-9d2a-0c4b1e8f6a33",
"chargingState": "Charging"
},
"meterValue": [
{
"timestamp": "2026-10-26T10:01:30.000Z",
"sampledValue": [
{
"value": 1251890.0,
"context": "Sample.Periodic",
"measurand": "Energy.Active.Import.Register",
"unitOfMeasure": { "unit": "Wh" }
},
{
"value": 61400.0,
"context": "Sample.Periodic",
"measurand": "Power.Active.Import",
"unitOfMeasure": { "unit": "W" }
}
]
}
]
}]Unlike 1.6, value is a JSON number, not a string, and units come from unitOfMeasure (with an optional power-of-ten multiplier). Default unit for energy is Wh. If you need the 1.6 background on measurands and contexts, see MeterValues in 1.6; the concepts carry over.
Finally the driver taps the card again to stop. Because Authorized is in TxStopPoint, the transaction ends, with triggerReason StopAuthorized and stoppedReason Local:
[2, "te-0009", "TransactionEvent", {
"eventType": "Ended",
"timestamp": "2026-10-26T10:42:05.000Z",
"triggerReason": "StopAuthorized",
"seqNo": 8,
"transactionInfo": {
"transactionId": "7f3c2a10-5e61-4b8e-9d2a-0c4b1e8f6a33",
"chargingState": "EVConnected",
"stoppedReason": "Local"
},
"idToken": {
"idToken": "04A2B3C4D5E680",
"type": "ISO14443"
},
"meterValue": [
{
"timestamp": "2026-10-26T10:42:05.000Z",
"sampledValue": [
{
"value": 1288712.0,
"context": "Transaction.End",
"measurand": "Energy.Active.Import.Register",
"unitOfMeasure": { "unit": "Wh" }
}
]
}
]
}]The Ended event may also carry the meter values configured in SampledDataCtrlr.TxEndedMeasurands, sampled every TxEndedInterval across the whole transaction. That is how a station can deliver a complete energy profile at the end even if it was offline for part of the session.
triggerReason and stoppedReason
triggerReason (TriggerReasonEnumType) says why this event was sent. stoppedReason (ReasonEnumType) appears in Ended and says why the transaction ended. They are different enums and are often confused.
| triggerReason | Typical use |
|---|---|
CablePluggedIn, EVDetected, EVDeparted | Physical events (plug, parking sensor) |
Authorized, Deauthorized, StopAuthorized | Authorization started, was revoked, or a stop was authorized |
RemoteStart, RemoteStop | Result of RequestStartTransaction / RequestStopTransaction |
ChargingStateChanged | chargingState changed (Charging, SuspendedEV, SuspendedEVSE, EVConnected, Idle) |
MeterValuePeriodic, MeterValueClock | Sampled or clock-aligned meter values |
EVCommunicationLost, EVConnectTimeout | Cable removed or EV never connected within EVConnectionTimeOut |
EnergyLimitReached, TimeLimitReached, ChargingRateChanged | Limits and smart charging effects |
AbnormalCondition, ResetCommand, UnlockCommand, Trigger, SignedDataReceived | Faults, commands, TriggerMessage, signed meter data |
stoppedReason values include Local, Remote, EVDisconnected, DeAuthorized, EmergencyStop, EnergyLimitReached, GroundFault, ImmediateReset, LocalOutOfCredit, MasterPass, OvercurrentFault, PowerLoss, PowerQuality, Reboot, SOCLimitReached, StoppedByEV, TimeLimitReached, Timeout and Other. When stoppedReason is omitted the default is Local. Map these to your session-end reporting; they are what support teams need when a driver complains a session stopped early.
seqNo, offline and message delivery
Transaction messages are the ones the station must not lose. While offline, the station queues TransactionEvent messages and delivers them in order after reconnecting, with offline: true on events that happened while it was disconnected (use cases E04 and E12). Retries for transaction messages that fail or get a CALLERROR are governed by OCPPCommCtrlr.MessageAttempts and MessageAttemptInterval, both with instance TransactionEvent (E13).
seqNo lets the CSMS detect gaps and reorder. Do not reject an event because a previous seqNo is missing; store it and flag the transaction as incomplete until the gap fills or the transaction ends. GetTransactionStatus (E14) asks the station whether a transaction is still ongoing and whether it still has queued messages:
[2, "gts-17", "GetTransactionStatus", {
"transactionId": "7f3c2a10-5e61-4b8e-9d2a-0c4b1e8f6a33"
}]
[3, "gts-17", {
"ongoingIndicator": false,
"messagesInQueue": true
}]messagesInQueue: true means do not finalise the bill yet. This is the cleanest way to decide when a session with gaps can be closed for billing and for OCPI CDRs.
CSMS implementation checklist
- Idempotency: stations retry. Deduplicate on (station,
transactionId,seqNo) and return the same response. - Always respond: never return a CALLERROR for business reasons such as an unknown token or a closed account. Respond normally and use
idTokenInfo.statusto deauthorize. A CALLERROR makes the station retry the same message. - Unknown transaction: an
UpdatedorEndedfor a transaction you have never seen (theStartedis still queued or was lost) should create a placeholder, not fail. - Energy from registers: compute energy as the difference of
Energy.Active.Import.Registervalues, not by summing power samples. - Time: use event
timestampfor billing, not receive time. Offline events can arrive hours later. - Remote starts: correlate with
transactionInfo.remoteStartId, which echoes theremoteStartIdyou sent inRequestStartTransaction.
Key takeaways
- One message, three event types; the station owns the string
transactionId. TxStartPointandTxStopPointdefine what a transaction even is on each station. Read them.- Use
seqNo,offlineandGetTransactionStatusto handle gaps instead of rejecting messages. - Respond to every
TransactionEvent; deauthorize viaidTokenInfo, never via CALLERROR.
Next: authorization in 2.0.1. For the 1.6 model this replaces, see Start/StopTransaction in 1.6.

