OCPP and OCPI were designed by different groups for different jobs, and nothing in either specification says how to connect them. Yet every CPO that roams has to: chargers speak OCPP to the CSMS, and the CSMS speaks OCPI to partners. This final part of the series is about that translation layer, the decisions it forces, and the places it usually breaks.
This is part 10 of the OCPI 2.2.1 series. It assumes familiarity with OCPP; if you need a refresher, start with the OCPP 1.6 tutorial or the OCPP 2.0.1 tutorial.
Architecture: never translate message by message
The tempting design is a direct relay: an OCPP StatusNotification comes in, an OCPI PATCH goes out. It works in a demo and fails in production, because the two protocols have different identity models, different timing and different failure modes. The robust design puts a domain model in the middle:
- OCPP handlers update the CSMS's own model of stations, EVSEs, connectors, transactions and meter readings.
- OCPI publishers watch that model for changes and decide what to send to which partner, with their own queues, retries and rate limits.
- OCPI receivers (tokens, commands, charging profiles) act on the model, which then drives OCPP calls.
This also makes mixed fleets manageable: 1.6 and 2.0.1 chargers both feed the same model, and OCPI never needs to know which protocol a charger speaks. See CSMS architecture at scale for how this fits into the wider platform.
Identity: stations, EVSEs and connectors
OCPI's hierarchy is Location, EVSE, Connector, and status lives on the EVSE. OCPP 2.0.1 has a near-identical Charging Station, EVSE, Connector model, so evseId maps naturally to an OCPI EVSE. OCPP 1.6 only has a charge point and numbered connectors, and a 1.6 "connector" is usually one charging point that can serve one car, which is an OCPI EVSE.
| OCPP | OCPI |
|---|---|
| Charge point / Charging Station | Usually part of a Location; several stations can share one Location |
OCPP 1.6 connectorId 1..n | One EVSE each (with one or more OCPI Connectors), unless the hardware shares one power module between outlets |
OCPP 2.0.1 evseId | EVSE |
OCPP 2.0.1 connectorId within an EVSE | Connector |
OCPP 1.6 connectorId 0 | No OCPI equivalent; station-wide status that you fold into each EVSE |
A dual-outlet 1.6 charger that can only charge one car at a time is the awkward case. Publishing it as one EVSE with two Connectors is more accurate for drivers, but then you must merge two OCPP connector statuses into one EVSE status. Keep the OCPI uid stable and independent of OCPP numbering, so a hardware swap does not change it.
Status mapping
OCPP 1.6 ChargePointStatus | OCPP 2.0.1 ConnectorStatusEnumType | OCPI EVSE status |
|---|---|---|
Available | Available | AVAILABLE |
Preparing | Occupied | CHARGING or BLOCKED (see below) |
Charging, SuspendedEV, SuspendedEVSE | Occupied | CHARGING |
Finishing | Occupied | CHARGING or BLOCKED |
Reserved | Reserved | RESERVED |
Unavailable | Unavailable | INOPERATIVE |
Faulted | Faulted | OUTOFORDER |
| Station offline / no heartbeat | Station offline | UNKNOWN after a grace period |
OCPI has no exact equivalent of Preparing or Finishing. What matters for drivers is that the EVSE is not free, so do not map them to AVAILABLE. Some CPOs use CHARGING for anything with a vehicle connected; others use BLOCKED for a plugged-in car not yet authorized. Pick one rule, apply it consistently, and document it for partners. Also remember that Preparing is sometimes reported when a driver merely presents a card without plugging in, which is a good reason to debounce before pushing. Status semantics on the OCPP side are covered in OCPP 1.6 StatusNotification and OCPP 2.0.1 boot and provisioning.
Authorization
When a charger sends OCPP Authorize (or a 1.6 StartTransaction with an unknown idTag), the CSMS has to decide whether the idTag belongs to a roaming partner:
- Normalise the
idTagand look it up in the CSMS's own customers first, then in tokens received via OCPI. - Apply the token's
whitelistvalue (see Part 5): answer locally, or call the eMSP'sauthorizeendpoint. - Store the returned
authorization_referenceagainst the EVSE and token, because OCPP 1.6 has nowhere to carry it. - Answer the charger.
OCPI AllowedType / status | OCPP 1.6 AuthorizationStatus | OCPP 2.0.1 AuthorizationStatusEnumType |
|---|---|---|
ALLOWED | Accepted | Accepted |
BLOCKED | Blocked | Blocked |
EXPIRED | Expired | Expired |
NO_CREDIT | Invalid | NoCredit |
NOT_ALLOWED | Invalid | NotAllowedTypeEVSE, NotAtThisLocation or NotAtThisTime if known, else Invalid |
Status 2004 (unknown token) | Invalid | Unknown |
For tokens with whitelist ALWAYS or ALLOWED, consider pushing them to chargers' local authorization lists so sessions can start while the charger is offline; never do this for NEVER. The OCPP mechanics are in OCPP 1.6 Authorize and id tags and OCPP 2.0.1 authorization.
Transactions to Sessions and CDRs
| OCPP event | OCPI action |
|---|---|
| Successful authorization or accepted remote start, before energy | Optional: PUT Session with status: PENDING |
1.6 StartTransaction / 2.0.1 TransactionEvent Started | PUT Session, status: ACTIVE, kwh: 0, auth_method and authorization_reference set |
MeterValues / TransactionEvent Updated with meter data | Throttled PATCH of kwh, charging_periods, total_cost |
EV stops drawing power (SuspendedEV, 2.0.1 chargingState change) | Start a new charging period; time now counts as PARKING_TIME |
1.6 StopTransaction / 2.0.1 TransactionEvent Ended | PATCH Session status: COMPLETED, end_date_time; then compute and POST CDR |
| Transaction never really started (no energy, rejected) | PATCH Session status: INVALID, no CDR (or per partner agreement) |
A few details make the difference between CDRs that reconcile and CDRs that get disputed:
- Energy comes from meter readings, not from adding up samples. 1.6
meterStop - meterStart(Wh) divided by 1000 gives kWh; in 2.0.1 use theEnergy.Active.Import.Registervalues at start and end. Background on measurands is in OCPP 1.6 meter values and OCPP 2.0.1 TransactionEvent. - Charging vs parking time needs a signal. OCPP 1.6 reports
SuspendedEVviaStatusNotification; 2.0.1 reportschargingStateinTransactionEvent. Some 1.6 chargers never reportSuspendedEV, so a fallback based on power dropping near zero inMeterValuesis often needed to splitTIMEfromPARKING_TIME. - Timestamps from the charger. Use the charger's timestamps for start and stop, not the CSMS receive time, but sanity-check them; chargers with a dead RTC report dates in 1970 or 2000. Time sync via
HeartbeatandBootNotificationis covered in OCPP 1.6 BootNotification and Heartbeat. - Offline transactions. A charger that was offline replays
StartTransaction/StopTransactionlater. The OCPI Session may then go straight from nothing toCOMPLETED, which is allowed; just make sure the CDR is still produced. Queuing behaviour is in OCPP 1.6 start and stop transaction. - Session IDs. Use your own stable OCPI session ID, not the OCPP
transactionId. 1.6 transaction IDs are integers assigned per CSMS and can collide across migrations; 2.0.1 IDs are generated by the charger.
Commands and charging profiles
| OCPI | OCPP 1.6 | OCPP 2.0.1 |
|---|---|---|
START_SESSION | RemoteStartTransaction (idTag, connectorId) | RequestStartTransaction (idToken, evseId, remoteStartId) |
STOP_SESSION | RemoteStopTransaction (transactionId) | RequestStopTransaction (transactionId) |
RESERVE_NOW | ReserveNow | ReserveNow |
CANCEL_RESERVATION | CancelReservation | CancelReservation |
UNLOCK_CONNECTOR | UnlockConnector | UnlockConnector |
Charging Profiles PUT | SetChargingProfile with TxProfile | SetChargingProfile with TxProfile |
Charging Profiles GET | GetCompositeSchedule | GetCompositeSchedule |
Charging Profiles DELETE | ClearChargingProfile | ClearChargingProfile |
For START_SESSION on OCPP 1.6, the idTag sent to the charger has to be something the charger will later report in StartTransaction, so the CSMS can match the transaction to the command. App-user UIDs longer than the 20-character idTag limit need a short alias that the CSMS maps back. In 2.0.1, remoteStartId exists exactly for this correlation and comes back in TransactionEvent. Either way, store the eMSP's authorization_reference against the pending start and copy it into the Session. The OCPP side is detailed in OCPP 1.6 remote start and stop and OCPP 1.6 smart charging.
Tariffs and cost
OCPP 1.6 has no tariff concept at all, so roaming cost is calculated entirely in the CSMS from the OCPI tariff, the charging periods and the meter data. OCPP 2.0.1 can show costs on the charger (see OCPP 2.0.1 display messages and costs), but the CDR amount should still come from the same CSMS calculation that produced the OCPI tariff, so the eMSP can reproduce it. One tariff engine, used for live session cost, CDR cost and anything displayed on the charger, avoids three slightly different answers.
Testing the bridge
- Drive end-to-end scenarios with a charge point simulator (see testing OCPP with a charge point simulator) and assert on the OCPI messages produced, not just the OCPP ones.
- Include offline replays, a charger with a wrong clock, a session crossing a tariff boundary,
SuspendedEVidle time, and a remote start the driver never plugs in for. - Compare your CDR totals against an independent calculation from the published tariff for every test session.
- Monitor the OCPI outbox: undelivered sessions and CDRs per partner should be close to zero at all times.
Key takeaways
- Bridge OCPP and OCPI through a domain model, not message-by-message relaying.
- OCPP 1.6 connectors usually become OCPI EVSEs; keep OCPI IDs independent of OCPP numbering.
- Never map
Preparing/FinishingtoAVAILABLE; debounce status before pushing. - Store
authorization_referenceand remote start correlation in the CSMS, because OCPP 1.6 cannot carry them. - Compute energy from register readings, split charging and parking time explicitly, and use one tariff engine for every cost figure.
That concludes the OCPI series. For the device side in depth, continue with the OCPP 1.6 and OCPP 2.0.1 tutorials. HashTech builds OCPP-certified CSMS platforms with OCPI roaming integration; see our EV charging work or get in touch.

