DRAFT — not indexed by search engines. Visible only via direct URL or the admin page.

OCPP StartTransaction and StopTransaction Explained

The two messages that bracket every charging session in OCPP 1.6, how they relate to billing, and how 2.0.1's unified TransactionEvent changes the model.

In OCPP 1.6, every charging session is bracketed by two messages: StartTransaction at the beginning, StopTransaction at the end. Between them, MeterValues messages report the energy and other measurements that flow during the session.

These two messages are foundational to billing, reporting, and operational visibility. They’re also a frequent source of bugs and inconsistencies because the data they carry needs to match the partner’s expectations across roaming, the user’s app, and your billing system.

OCPP 2.0.1 unified the model into a single TransactionEvent message, but billions of sessions still flow through 1.6’s StartTransaction/StopTransaction pattern. This article covers both.

The OCPP 1.6 flow

A typical session looks like:

  1. User presents credentials. RFID card tapped, app starts session remotely, or ISO 15118 PnC.
  2. Authorize message. Charger sends Authorize with the credential ID. CSMS responds Accepted or Invalid (sometimes after querying the eMSP via OCPI).
  3. StartTransaction message. If authorized, the charger sends StartTransaction with the initial meter reading, the connector ID, and the credential ID.
  4. CSMS responds with a transactionId. This is the unique identifier for this session.
  5. MeterValues messages flow. Periodically during the session, the charger sends MeterValues with the current meter reading.
  6. User unplugs (or otherwise stops). Charger sends StopTransaction with the final meter reading and the reason for stopping.
  7. CSMS acknowledges. 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. The periodic readings in step 5 are covered in depth in the MeterValues message (coming soon) article.

StartTransaction structure

{
  "connectorId": 1,
  "idTag": "ABC123XYZ",
  "meterStart": 12450,
  "timestamp": "2026-06-26T19:00:00Z",
  "reservationId": null
}

The fields:

  • connectorId — which connector on this charger (1, 2, etc.; 0 is reserved for “the whole charger”).
  • idTag — the user identifier (RFID UID, or the token from the Authorize call).
  • meterStart — initial meter reading in Wh (not kWh — be careful with units).
  • timestamp — when the transaction started, UTC.
  • reservationId — if this transaction fulfills a reservation, the reservation’s ID.

The CSMS response:

{
  "transactionId": 12345,
  "idTagInfo": { "status": "Accepted" }
}

The transactionId is generated by the CSMS and used to correlate all subsequent messages 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 (may be omitted if the stop wasn’t initiated by the user — e.g. CSMS remote stop).
  • meterStop — final meter reading in Wh.
  • timestamp — when the transaction ended.
  • reason — why the session ended (see below).
  • transactionData — optional array of MeterValues to include with the stop.

The Reason enum:

  • EmergencyStop — emergency stop button pressed.
  • EVDisconnected — 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 — UnlockConnector command.
  • DeAuthorized — the user’s authorization expired during the session.

Each reason maps to different operational implications. EVDisconnected is the normal flow; EmergencyStop or PowerLoss may need investigation; DeAuthorized may trigger a refund or 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 (coming soon) are nice for dashboards and analytics, but they may have gaps; meterStart and meterStop are anchored to actual transaction-bounding events.

For time-based tariffs (cents per minute, parking fees), session duration is:

Duration = StopTransaction.timestamp - StartTransaction.timestamp (in seconds)

For cost calculation, the CSMS combines:

  • Energy delivered × tariff energy-rate (€/kWh).
  • Duration × tariff time-rate (€/minute) if applicable.
  • Plus any other tariff dimensions (parking, reservation).

The exact calculation depends on the tariff structure but the meter and timestamp data from StartTransaction/StopTransaction are the canonical inputs.

Common failure modes

A list of things that go wrong.

Lost StopTransaction. Network died right when the user unplugged. StopTransaction never arrived. CSMS shows the session as still active for hours or days. Resolution: most chargers retry on reconnect; if buffer is lost, the operator has to manually close the session (and figure out what to bill).

Negative energy. Charger reports meterStart > meterStop somehow. Bug in firmware, or the charger had a meter reset mid-session. Bill becomes nonsensical.

Wrong units. Charger sends meterStart in kWh instead of Wh. Energy total off by 1000x. Catch this in validation.

Mismatched transactionId. Charger sends MeterValues or StopTransaction with a transactionId the CSMS doesn’t recognize. Either the CSMS lost state or the charger has a buggy transactionId tracker.

Duplicate StartTransaction. Charger sends StartTransaction, gets back transactionId 12345. Network blip; charger thinks it didn’t get a response. Sends StartTransaction again. CSMS issues a new transactionId 12346. Now there are two transactions for one logical session.

Charging without StartTransaction. Some chargers will physically deliver energy if the contactor is closed, even if no transaction has been registered. The energy is “lost” — flowed to the user but not billed.

StopTransaction without StartTransaction. Charger lost state mid-session. Reports StopTransaction for a transactionId the CSMS has no record of.

Mitigation strategies:

  • Idempotency on transactionId. Don’t double-process the same transactionId.
  • Reconciliation jobs that flag stuck-open transactions and surface them for human review.
  • Defensive validation on every field (units, ranges, timestamps).
  • Local buffering on the charger for messages that fail to send.

The Authorize message in context

A clarification on the relationship to Authorize.

Authorize is sent BEFORE StartTransaction. It asks the CSMS “is this idTag allowed to start a transaction?” The CSMS responds Accepted, Blocked, Expired, Invalid, or ConcurrentTx.

If Accepted, the charger then sends StartTransaction. The two messages can be sent close in time (often within a few hundred milliseconds), but they’re logically distinct: Authorize asks permission; StartTransaction begins the work.

Some implementations skip the Authorize step when the user identifier is locally cached (LocalAuthList) — the charger has already determined the user is valid without asking the CSMS. In that case StartTransaction can begin immediately. This is faster (less network round-trip) but requires the LocalAuthList to be kept up to date.

The OCPP 2.0.1 unified model

OCPP 2.0.1 replaced StartTransaction + MeterValues + StopTransaction with a single message type: TransactionEvent. (For a broader look at how the versions differ, see the OCPP version comparison (coming soon).)

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 TransactionEvent envelope carries different data depending on event type but the protocol surface is unified.

Benefits:

  • One message handler, not three.
  • Cleaner state machine — every transaction-related event flows through the same path.
  • Easier to add new events without inventing new message types (e.g. paused/resumed).

Caveats:

  • Migration from 1.6 requires significant code restructuring.
  • Backward compatibility — your CSMS often supports both 1.6 (separate messages) and 2.0.1 (unified) in the same codebase.

The data semantics — meter readings, timestamps, transactionId tracking, billing math — are equivalent. Only the message envelope changed.

What CSMSes should do

For builders of CSMSes, a few principles:

Atomic transaction handling. A transaction’s lifecycle (start, samples, stop) should be tracked atomically. Use database transactions or saga patterns to ensure half-states don’t persist.

Idempotency by transactionId. Always. Every. Time.

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.

Generous timestamp validation. Don’t reject messages because timestamps are “in the future” by 30 seconds (clock drift). Do reject if they’re hours off.

Audit trail. Every state change on a transaction should be logged with who, when, and why. Useful for debugging and regulatory compliance.

Recovery from CSMS restarts. If your CSMS restarts, in-flight transactions should be picked up correctly from persisted state. Don’t lose track of active sessions because the back-end bounced.

What charge points should do

For builders of charge point firmware:

Persist transaction state locally. If the network dies, the charger should still know what session it’s in and resume cleanly on reconnect.

Buffer messages on disconnect. Don’t drop MeterValues or StopTransaction because the WebSocket is down. Buffer them, send on reconnect.

Generate accurate meter values. The meter readings should come from a calibrated source. Don’t fake them, don’t approximate from elsewhere.

Handle the unhappy paths. Power loss mid-session, contactor stuck, EV disconnected unexpectedly, CSMS unresponsive — all need defined behaviors. Test them.

Report reason accurately. When StopTransaction fires, send the right reason enum. EVDisconnected vs Remote vs Local are operationally different.

The honest summary

StartTransaction and StopTransaction are the brackets that define a session in OCPP 1.6. Their MeterValues field carries the data that drives billing. The state machine around them is simple in concept but exposes many failure modes in production — network drops, buffer overflows, duplicate messages, mismatched IDs. Investing in idempotency, persistence, reconciliation, and good audit trails pays off in fewer billing disputes and more reliable operations. The OCPP 2.0.1 unified TransactionEvent model is cleaner but the underlying concerns remain the same.

Quick check

Q1. What identifier does the CSMS return in its StartTransaction response to correlate all subsequent messages in a session?
Q2. A charger sends meterStart and meterStop in kWh instead of Wh. What is the result?
Q3. How does a well-built CSMS handle a duplicate StartTransaction caused by a network blip?
Q4. What does the Authorize message do relative to StartTransaction?
Q5. Which StopTransaction reason represents the normal, expected end of a session?

Frequently asked questions

What is a transaction in OCPP?

A transaction is one charging session — from the moment a user authorizes charging to the moment they unplug or stop. In OCPP 1.6, a transaction has a unique transactionId that ties together StartTransaction, the MeterValues during the session, and StopTransaction. In 2.0.1, the same logical session is tracked via TransactionEvent messages.

Can a session have multiple transactions?

Not usually. A session is one transaction. Some advanced scenarios (paused-then-resumed sessions in 2.0.1, or sessions split across reconnection events) can result in multiple TransactionEvent updates within what feels like one user-facing session, but the transactionId stays the same.

What happens if StopTransaction never reaches the CSMS?

The charger should retry. Most implementations buffer the StopTransaction locally and resend on reconnect. The CSMS, when it eventually receives the StopTransaction, deduplicates by transactionId. If the message is truly lost (e.g. local buffer overflowed), the session shows as never-ended in the CSMS and operators need to manually reconcile.

Does StartTransaction authorize the user?

No. Authorization happens via a separate Authorize message before the transaction begins. StartTransaction is the charger telling the CSMS "I am starting a transaction for an already-authorized user." Some implementations conflate the two; that is a bug.

Found this useful? Share it.