If you’ve built an OCPP-based CSMS and now need to expose it to roaming partners, OCPI is a different kind of integration problem. OCPP is a single persistent WebSocket connection to a charger you control. OCPI is a set of versioned REST modules exchanged with a partner platform you don’t control, under a contract, with data that has to reconcile with money.
This is a practical walkthrough of what actually needs to be built: module registration, credentials, and the three modules that matter most in production — Locations, Sessions, and CDRs.
The shape of an OCPI integration
Every OCPI party (CPO or eMSP) exposes a versions endpoint and implements a subset of modules, each independently versioned. The core modules for a CPO exposing charging infrastructure to an eMSP are:
credentials— registration and token exchangelocations— charge point and connector data (CPO → eMSP, push or pull)sessions— live charging session state (CPO → eMSP)cdrs— final billing records (CPO → eMSP)tariffs— pricing (CPO → eMSP)commands— remote start/stop requests (eMSP → CPO)
Not every party implements every module. A pure eMSP won’t implement locations as a sender; a pure CPO won’t implement commands as a receiver of driver-initiated actions from its own app.
Step 1: Versions and credentials
Before any real data flows, both parties register with each other via the credentials module.
GET https://your-csms.example.com/ocpi/versions
{
"status_code": 1000,
"data": [
{ "version": "2.2.1", "url": "https://your-csms.example.com/ocpi/2.2.1/details" }
]
}
The partner fetches /2.2.1/details to get the list of modules and endpoints you support, then registers a token:
POST /ocpi/2.2.1/credentials
Authorization: Token CREDENTIALS_TOKEN_A
{
"token": "CREDENTIALS_TOKEN_B",
"url": "https://your-csms.example.com/ocpi/versions",
"roles": [
{ "role": "CPO", "party_id": "SIM", "country_code": "TH",
"business_details": { "name": "Simplico EV Network" } }
]
}
This is the part teams underestimate: OCPI auth is per-relationship, not a single API key. In production you’re storing and rotating a distinct token pair per roaming partner, plus tracking which module versions each partner supports — that’s state your CSMS needs a real data model for, not a config file.
Step 2: Locations — structure, not just content
A Location isn’t one flat record. It nests:
Location
└─ EVSEs (physical charge points)
└─ Connectors (plug types, power, status)
{
"id": "LOC001",
"party_id": "SIM",
"country_code": "TH",
"name": "Simplico Demo Site",
"address": "123 Sukhumvit Rd",
"city": "Bangkok",
"coordinates": { "latitude": "13.736717", "longitude": "100.523186" },
"evses": [
{
"uid": "EVSE001",
"evse_id": "TH*SIM*E001",
"status": "AVAILABLE",
"connectors": [
{ "id": "1", "standard": "IEC_62196_T2", "format": "SOCKET",
"power_type": "AC_3_PHASE", "max_voltage": 400, "max_amperage": 32 }
]
}
],
"last_updated": "2026-08-07T09:00:00Z"
}
Two implementation details cause most integration bugs here:
last_updateddrives everything. Partners often pull only records changed since their last sync (?date_from=). If your backend doesn’t reliably bump this timestamp on every relevant change — including nested EVSE/connector status — partners silently work with stale availability data.- Push vs pull is negotiated per partner, not fixed by the spec. Some eMSPs want you to
PUTupdates to theirlocationsendpoint on every status change; others prefer to poll yours. Your CSMS needs to support both directions from day one, because you won’t control which model each partner picks.
Step 3: Sessions — the part that has to be real-time
A Session object tracks a charge from authorization to completion. The field that trips people up is status, which needs to reflect reality within seconds, not minutes — an eMSP’s driver app is showing this live.
{
"id": "SESS001",
"location_id": "LOC001",
"evse_uid": "EVSE001",
"connector_id": "1",
"start_date_time": "2026-08-07T14:02:00Z",
"kwh": 12.4,
"auth_method": "WHITELIST",
"status": "ACTIVE",
"last_updated": "2026-08-07T14:15:00Z"
}
In practice, this means your OCPI sessions module can’t be a periodic batch job sitting on top of your OCPP session table — it needs to subscribe to the same events your OCPP layer emits (StartTransaction, MeterValues, StopTransaction) and push or update the OCPI-side session object in the same request cycle. If OCPP and OCPI session state drift apart, that’s the first thing a roaming partner will notice — and complain about.
Step 4: CDRs — where billing actually lives
The CDR (Charge Detail Record) is the object everything else exists to produce correctly. It’s sent once, after the session ends, and it’s the source of truth for settlement.
{
"id": "CDR001",
"session_id": "SESS001",
"start_date_time": "2026-08-07T14:02:00Z",
"end_date_time": "2026-08-07T14:48:00Z",
"cdr_token": { "uid": "TOKEN123", "type": "RFID", "contract_id": "TH-SIM-0001" },
"total_energy": 12.4,
"total_time": 0.77,
"total_cost": { "excl_vat": 62.00, "incl_vat": 66.34 },
"charging_periods": [
{ "start_date_time": "2026-08-07T14:02:00Z",
"dimensions": [ { "type": "ENERGY", "volume": 12.4 } ] }
]
}
A few things worth building deliberately rather than discovering in production:
- CDRs are immutable once sent. If your tariff calculation has a bug, you don’t edit the CDR — you issue a new one or handle it through your partner’s dispute process. Design your billing pipeline assuming corrections are the exception path, not routine.
total_costmust match what the tariff you published implies. If yourtariffsmodule says one price and your CDR calculates another, that mismatch is what actually generates support tickets — usually weeks later, in a batch reconciliation report neither side enjoys reading.- Idempotency matters at the transport level. Network retries on the CDR POST are common; make
idyour dedup key so a retried request doesn’t create a duplicate billing record on the partner’s side.
Testing before you connect to a real partner
Two practical steps before your first live roaming partner:
- Run against the OCA/EVRoaming validator or a sandbox partner rather than your first production counterpart — module version mismatches and field validation errors are far cheaper to find there.
- Simulate the full lifecycle end to end: register credentials, push a location, open a session, stream status updates, close the session, and generate the CDR — before wiring in a second real partner. Most integration bugs live in the transitions between modules, not inside any single one.
Where this sits relative to OCPP
If you’ve already built (or are running) an OCPP-based CSMS, the OCPI layer sits beside it, not inside it. OCPP owns the charger-facing state machine; OCPI translates a slice of that state into a partner-facing, versioned, contractually-defined interface. Treat them as two backends sharing a data model, not one backend with an extra endpoint bolted on — that’s the difference between an OCPI integration that survives your third roaming partner and one that needs a rewrite for it.
Frequently Asked Questions
Do we need a separate database for OCPI, or can we extend our OCPP session tables?
Extending is usually fine as long as OCPI-specific fields (party_id, country_code, CDR tokens, tariff references) don’t get bolted on as afterthought columns. Model Locations, Sessions, and CDRs as their own entities that reference your OCPP data, not the other way around.
How many roaming partners can bilateral connections realistically support before we need a Hub?
There’s no hard number, but most teams feel the pain around 4-6 bilateral partners, when token management, per-partner push/pull preferences, and independent module version support start requiring real orchestration rather than ad-hoc handling.
What breaks most often in production OCPI integrations?
Timestamp handling (last_updated not propagating from nested objects), CDR/tariff mismatches, and session status lag between the OCPP and OCPI layers — in that order.
If you’re scoping an OCPI build on top of an existing OCPP backend, or standing up both from scratch, that’s exactly what our EV CSMS — Charging Station Management System (OCPP/OCPI) service is for. For a technical scoping conversation, reach out at hello@simplico.net.
Latest Posts
- The Landlocked Sea Bass: Building an Automated Feeding System for Marine Fish Far From the Ocean July 31, 2026
- Your Shop Floor Speaks Five Dialects: Why OPC UA Doesn’t Solve Protocol Fragmentation July 30, 2026
- Field Notes: Finding a Vacuum Leak Before the Batch Leaves the Furnace July 30, 2026
- Field Notes: Running an E-Commerce Platform Across Backend, Web, and Mobile July 27, 2026
- EUDR Is Coming for Thai Rubber and Palm Oil — Is Your Depot’s Paperwork Ready? July 26, 2026
- Your SOC Watches Your Employees. It Doesn’t Watch Your Vendors. July 23, 2026
