The message
// Charge point -> CSMS
[2, "19223204", "StartTransaction", {
"connectorId": 1,
"idTag": "DR88231ABCD",
"meterStart": 4471820,
"timestamp": "2026-08-11T09:12:00Z",
"reservationId": 8823
}]
// CSMS -> charge point
[3, "19223204", {
"transactionId": 770142,
"idTagInfo": { "status": "Accepted" }
}]| Field | Notes |
|---|---|
connectorId | Never 0 here — a transaction belongs to a physical connector |
idTag | The credential; may differ from the one used to stop, if they share a parentIdTag |
meterStart | Watt-hours, not kWh. Cumulative register reading, not zero. |
timestamp | From the charger's clock — see Heartbeat |
reservationId | Present only if this session claims a reservation |
transactionId | Assigned by the CSMS; the identity for everything downstream |
meterStart is a cumulative register in watt-hours. It is the odometer, not the trip counter. Energy delivered is meterStop minus meterStart, and treating either as a per-session value produces bills in the thousands of kWh. Dividing by 1000 in the wrong place is the other half of this bug.The idTagInfo can still refuse
The response carries an idTagInfo even though the driver was usually authorised moments earlier by Authorize. This is not redundant — it is the CSMS's last chance to stop a session, and it fires in real cases: a token blocked in the seconds between authorisation and start, a ConcurrentTx the charger could not know about, an offline-authorised session the CSMS now rejects.
Critically, the transaction still exists. The CSMS assigned an ID. A charger receiving a non-Accepted status stops the session it just started, and that produces a StopTransaction with reason DeAuthorized. Your platform has to handle a transaction that opens and closes within seconds having delivered almost no energy.
Offline transactions
A charger that cannot reach the CSMS still has a driver in front of it. Assuming the offline authorisation policy permits it, the charger starts the session anyway — which means it needs a transaction ID before the CSMS can give it one.
The convention is a locally generated temporary ID, commonly negative, replaced once the queued StartTransaction is delivered and the real ID comes back.
Offline: charger assigns transactionId = -3312
MeterValues and StopTransaction reference -3312
Reconnect: StartTransaction replayed → CSMS assigns 770143
charger remaps -3312 → 770143 in queued messagesWhat this means for the platform:
- Expect StartTransaction messages with timestamps hours old. Trust the message timestamp, not receipt time.
- Expect out-of-order arrival. A queued StopTransaction can arrive before its StartTransaction if the charger flushes its queue oddly.
- Expect duplicates. A charger unsure whether delivery succeeded will resend.
- Never reject on timestamp age. A day-old transaction is real revenue.
Idempotency here is not optional. Key on the charger identity plus connector plus meterStart plus timestamp, and de-duplicate before creating a billable record.
From transaction to OCPI Session
On a CPO platform this message is what creates the OCPI Session pushed to roaming partners.
| OCPP | OCPI Session |
|---|---|
transactionId | Internal join key behind session_id |
timestamp | start_date_time |
idTag | cdr_token.uid |
connectorId | connector_id, plus the EVSE it belongs to |
meterStart | Baseline for kwh — never published directly |
Where a remote start came in through OCPI START_SESSION, the authorization_reference from that command must be carried onto the Session and the eventual CDR. That is the only clean thread from an eMSP's original request to the invoice, and it has to be stitched here — at the moment the transaction is created — because nothing later has both pieces.
Production lessons
- Treat
meterStartas watt-hours from a cumulative register. Unit and baseline errors here are the most expensive arithmetic bugs in EV charging. - Handle negative and temporary transaction IDs. They are normal, not corrupt data.
- Make ingestion idempotent before anything becomes billable.
- Trust the message timestamp over receipt time for anything billing-related.
- Handle immediate
DeAuthorizedstops — zero-energy transactions that must not produce an invoice. - Stitch the OCPI correlation reference at creation time. It cannot be recovered later.
- Alert on StartTransaction without a matching StopTransaction beyond a plausible session length — that is either a stuck session or a lost message, and both cost money.
Frequently asked questions
The charge point reports the connector, credential, meter reading and timestamp at the moment charging begins, and the CSMS responds with a transactionId that identifies the session for every subsequent message, including MeterValues and StopTransaction.
No. It is a cumulative register reading in watt-hours — an odometer, not a trip counter. Energy delivered is meterStop minus meterStart. Treating it as a per-session value produces bills in the thousands of kWh.
The charger generates a temporary local transaction ID, commonly negative, and queues the messages. When it reconnects it replays StartTransaction, receives a real ID from the CSMS, and remaps the queued messages. Platforms must therefore expect old timestamps, out-of-order arrival and duplicates.
Yes. The idTagInfo can return a non-Accepted status if the token was blocked in the intervening seconds or a concurrent transaction exists. The transaction still exists and has an ID, so the charger stops it and sends a StopTransaction with reason DeAuthorized.