In OCPP 1.6, every charging session is bracketed by two messages: StartTransaction at the beginning and StopTransaction at the end. Between them, MeterValues messages report the energy and other measurements flowing during the session.
These two messages are foundational to billing, reporting, and operational visibility. They are also a frequent source of bugs, because the data they carry has to match what your roaming partners, your user’s app, and your billing system each expect.
OCPP 2.0.1 unified the model into a single TransactionEvent message. But chargers speak the version they were commissioned against, and 1.6 hardware has been going into the ground since 2015, so the StartTransaction and StopTransaction pattern is what most operators still debug day to day. This article covers both.
The OCPP 1.6 flow
A typical session looks like this:
- User presents credentials. An RFID card is tapped, the app starts the session remotely, or ISO 15118 Plug & Charge handles it.
- Authorize message. The charger sends
Authorizewith the credential ID. The CSMS responds Accepted or Invalid, sometimes after querying the eMSP over OCPI. - StartTransaction message. If authorized, the charger sends
StartTransactionwith the initial meter reading, the connector ID, and the credential ID. - CSMS responds with a transactionId. This is the unique identifier for this session.
- MeterValues messages flow. Periodically during the session, the charger reports the current meter reading.
- User unplugs, or the session otherwise stops. The charger sends
StopTransactionwith the final meter reading and the reason for stopping. - CSMS acknowledges. The session is complete.
sequenceDiagram
participant CP as Charge Point
participant CS as CSMS
CP->>CS: Authorize idTag
CS-->>CP: Accepted
CP->>CS: StartTransaction
CS-->>CP: transactionId
loop During session
CP->>CS: MeterValues
end
CP->>CS: StopTransaction
CS-->>CP: Acknowledged
If you are new to the protocol itself, start with what OCPP is. For where these two messages sit in the wider conversation, including the connection-layer messages that precede them, see the full session message flow.
StartTransaction structure
{
"connectorId": 1,
"idTag": "ABC123XYZ",
"meterStart": 12450,
"timestamp": "2026-06-26T19:00:00Z",
"reservationId": null
}
The fields:
- connectorId. Which connector on this charger. Numbering starts at 1, and 0 is reserved for the charger as a whole.
- idTag. The user identifier: an RFID UID, or the token from the Authorize call.
- meterStart. Initial meter reading in Wh. Note the unit carefully, because it is Wh and not kWh.
- timestamp. When the transaction started, in UTC.
- reservationId. If this transaction fulfils a reservation, the reservation’s ID.
The CSMS response:
{
"transactionId": 12345,
"idTagInfo": { "status": "Accepted" }
}
The transactionId is generated by the CSMS and correlates every subsequent message for this session. The idTagInfo can also include expiryDate, parentIdTag, and a status of Accepted, Blocked, Expired, Invalid, or ConcurrentTx.
StopTransaction structure
{
"transactionId": 12345,
"idTag": "ABC123XYZ",
"meterStop": 22890,
"timestamp": "2026-06-26T20:00:00Z",
"reason": "EVDisconnected",
"transactionData": [ ... ]
}
The fields:
- transactionId. The ID assigned by the CSMS at StartTransaction.
- idTag. The user identifier. It may be omitted when the stop was not user-initiated, such as a CSMS remote stop.
- meterStop. Final meter reading in Wh.
- timestamp. When the transaction ended.
- reason. Why the session ended, from the enum below.
- transactionData. An optional array of MeterValues to include with the stop.
The Reason enum:
- EmergencyStop. The emergency stop button was pressed.
- EVDisconnected. The user unplugged.
- HardReset. Hard reset of the charger.
- Local. Local stop button or app.
- Other. Vendor-specific.
- PowerLoss. Power was lost.
- Reboot. Soft reboot.
- Remote. CSMS-initiated stop.
- SoftReset. Soft reset.
- UnlockCommand. An UnlockConnector (coming soon) command ended the session.
- DeAuthorized. The user’s authorization expired mid-session.
Each reason carries different operational implications. EVDisconnected is the normal flow. EmergencyStop and PowerLoss usually warrant investigation. DeAuthorized may trigger a refund or a customer notification.
The billing math
The total energy delivered in a session is:
Energy = meterStop - meterStart (in Wh; divide by 1000 for kWh)
This is the source of truth for billing. Periodic MeterValues are useful for dashboards and analytics, but they can have gaps. meterStart and meterStop are anchored to the events that actually bound the transaction.
For time-based tariffs, meaning per-minute rates or parking fees, session duration is:
Duration = StopTransaction.timestamp - StartTransaction.timestamp (in seconds)
To calculate cost, the CSMS combines:
- Energy delivered multiplied by the tariff’s energy rate, per kWh.
- Duration multiplied by the tariff’s time rate, per minute, where one applies.
- Any other tariff dimensions such as parking or reservation fees.
The exact calculation depends on the tariff structure, and it varies by market: per-kWh pricing is the norm across most of Europe, while several US states only permitted per-kWh retail sales relatively recently, which left a legacy of per-minute pricing in North American networks. Either way, the meter readings and timestamps from StartTransaction and StopTransaction are the canonical inputs.
Common failure modes
The things that actually go wrong.
Lost StopTransaction. The network died at the moment the user unplugged and StopTransaction never arrived, so the CSMS shows the session as active for hours or days. Chargers commonly retry on reconnect. If the buffer was lost, an operator has to close the session by hand and work out what to bill.
Negative energy. The charger reports a meterStart greater than meterStop. Either a firmware bug or a meter reset mid-session. The bill becomes nonsensical.
Wrong units. The charger sends meterStart in kWh instead of Wh, so the energy total is off by a factor of 1000. Catch this in validation, not in a customer complaint.
Mismatched transactionId. The charger sends MeterValues or StopTransaction with a transactionId the CSMS does not recognize. Either the CSMS lost state or the charger’s transactionId tracking is buggy.
Duplicate StartTransaction. The charger sends StartTransaction and gets back transactionId 12345. A network blip convinces it the response never arrived, so it sends StartTransaction again and the CSMS issues transactionId 12346. Now one physical session has two transactions.
Charging without StartTransaction. Some chargers will physically deliver energy whenever the contactor is closed, even with no transaction registered. That energy flowed to a real customer and cannot be billed to anyone.
StopTransaction without StartTransaction. The charger lost state mid-session and reports a stop for a transactionId the CSMS has never seen.
Mitigations:
- Idempotency on transactionId, so the same transaction is never processed twice.
- Reconciliation jobs that flag stuck-open transactions for human review.
- Defensive validation on every field: units, ranges, timestamps.
- Local buffering on the charger for messages that fail to send.
Several of these are worth rehearsing before they happen in production rather than discovering them in a billing dispute. Firing a duplicate StartTransaction, or a StopTransaction for a transactionId the CSMS has never seen, is a five-minute test against a simulated station and a very expensive surprise otherwise.
The Authorize message in context
Authorize is sent before StartTransaction. It asks the CSMS whether this idTag is allowed to start a transaction, and the CSMS answers Accepted, Blocked, Expired, Invalid, or ConcurrentTx.
If Accepted, the charger then sends StartTransaction. The two messages often travel within a few hundred milliseconds of each other, but they are logically distinct: Authorize asks permission, StartTransaction begins the work. Treating them as one step is a bug that surfaces the first time a credential is valid but the transaction fails to open, because you have no way to tell which half went wrong.
Some implementations skip the Authorize round trip when the identifier is locally cached in a LocalAuthList, because the charger has already determined the user is valid without asking. That is faster, and it keeps working when the backend is unreachable, but it is only as correct as your list is fresh.
The OCPP 2.0.1 unified model
OCPP 2.0.1 replaced StartTransaction, MeterValues, and StopTransaction with a single message type: TransactionEvent. For a broader look at how the versions differ, see the OCPP version comparison.
TransactionEvent has an eventType field:
- Started, equivalent to 1.6’s StartTransaction.
- Updated, equivalent to 1.6’s MeterValues during a transaction.
- Ended, equivalent to 1.6’s StopTransaction.
The same envelope carries different data depending on the event type, but the protocol surface is unified.
Benefits:
- One message handler instead of three.
- A cleaner state machine, since every transaction-related event flows through the same path.
- Room to add new events without inventing new message types, such as paused and resumed.
Caveats:
- Migrating from 1.6 means real code restructuring, not renaming.
- Your CSMS will usually support both models in one codebase for years, because your fleet is mixed.
The data semantics are equivalent: meter readings, timestamps, transactionId tracking, and billing math all work the same way. Only the envelope changed.
What CSMSes should do
For CSMS builders, a few principles:
Handle a transaction atomically. A transaction’s lifecycle of start, samples, and stop should be tracked as a unit. Use database transactions or a saga pattern so half-states cannot persist.
Idempotency by transactionId. Always. Every time.
Run reconciliation jobs. A nightly job that finds stuck-open transactions and either auto-closes them with the last known meter reading or queues them for human review.
Validate timestamps generously. Do not reject a message because its timestamp is thirty seconds in the future, since clock drift is normal. Do reject one that is hours off.
Keep an audit trail. Every state change on a transaction should record who, when, and why. This pays for itself the first time a customer disputes a bill.
Recover cleanly from restarts. If your CSMS bounces, in-flight transactions should be picked up from persisted state rather than forgotten.
What charge points should do
For firmware builders:
Persist transaction state locally. If the network dies, the charger should still know which session it is in and resume cleanly on reconnect.
Buffer messages while disconnected. Do not drop MeterValues or StopTransaction because the WebSocket is down. Buffer and send on reconnect.
Generate meter values from a calibrated source. Do not approximate them from somewhere else. This number becomes someone’s bill.
Handle the unhappy paths. Power loss mid-session, a stuck contactor, an unexpected EV disconnect, an unresponsive CSMS. Each needs defined behaviour, and each needs testing.
Report the reason accurately. EVDisconnected, Remote, and Local are operationally different, and an operator reading a dashboard cannot recover the distinction you threw away.
The honest summary
StartTransaction and StopTransaction are the brackets that define a session in OCPP 1.6, and the difference between their two meter readings is what a customer eventually pays. The state machine is simple in concept and exposes a long tail of failure modes in production: network drops, buffer overflows, duplicate messages, mismatched IDs.
Here is the part that catches teams out. Almost none of these failures announce themselves. A duplicated transaction, a lost stop, a unit mismatch: each one produces a plausible-looking number that flows into billing and is only discovered when a customer complains or a roaming partner disputes a CDR. The engineering that matters is not implementing the two messages, which takes an afternoon. It is the idempotency, persistence, reconciliation, and audit trail around them, which is what separates a CSMS that survives its first bad week from one that quietly bills wrong for a month.
OCPP 2.0.1’s unified TransactionEvent model is a cleaner envelope. Every concern above still applies to it unchanged.