The problem OCPI solves
A driver buys a contract from one company and charges at hardware owned by another. The company that owns the driver relationship — the eMSP, or e-Mobility Service Provider — usually owns no chargers. The company that owns the chargers — the CPO, or Charge Point Operator — usually has no relationship with that driver.
For a charge to happen, those two companies have to agree on five things across a network boundary: does this charger exist and is it free, is this driver allowed to use it, what does it cost, has charging started, and what is owed to whom afterwards. OCPI is the standard answer to all five.
Without it, every eMSP would need a bespoke integration with every CPO. OCPI turns that N×N problem into a single protocol each party implements once.
The roles
OCPI 2.2 introduced multiple roles per connection, and 2.2.1 keeps them. A single party can hold several — a company operating chargers and selling mobility contracts declares both.
| Role | Who it is | Typically owns |
|---|---|---|
CPO | Charge Point Operator | Locations, Sessions, CDRs, Tariffs |
EMSP | e-Mobility Service Provider | Tokens, and the driver relationship |
HUB | Roaming hub / broker | Routing between many parties — see HubClientInfo |
NSP | Navigation Service Provider | Consumes Locations for maps and routing |
SCSP | Smart Charging Service Provider | Shapes power via ChargingProfiles |
OTHER | Anything else | Escape hatch for roles the spec does not name |
Identity is the pair country_code + party_id — a two-character country code and a three-character party identifier. party_id is unique only within a country, so treating it as a global key breaks the first time you onboard across a border.
Sender and Receiver
Every module has a direction, and it is not the same direction for every module. The Sender owns the data and is the source of truth. The Receiver consumes it.
This inverts between modules. The CPO is Sender for Locations because it owns the chargers. The eMSP is Sender for Tokens because it owns the customers. Neither party is globally the client or the server — both run an API, and which one you call depends on which module you are using.
Each party publishes its role per module in its version details, which is why the pair (module, role) rather than module alone is the key when you cache a partner's endpoint directory.
The ten modules
Every module below has a full guide with worked JSON, field tables and production lessons.
| Module | Purpose | Sender |
|---|---|---|
| Versions | Discover supported versions and the endpoint directory. The only URL you are given in advance. | Both |
| Credentials | Register parties and exchange tokens — the Token A/B/C handshake. | Both |
| Locations | Publish sites, EVSEs and connectors, plus live availability status. | CPO |
| Sessions | The live, mutable view of a charge in progress. | CPO |
| CDRs | The immutable final record that billing and settlement run on. | CPO |
| Tariffs | Pricing as composable data — elements, components, restrictions. | CPO |
| Tokens | Who may charge, by whitelist or real-time authorisation. | eMSP |
| Commands | Remote start, stop, reserve, cancel and unlock. | eMSP |
| ChargingProfiles | Smart charging — shaping power across company lines. | SCSP / eMSP |
| HubClientInfo | Which parties on a hub are currently reachable. | Hub |
The five commands
The Commands module carries five actions, each with its own page:
| Command | Does |
|---|---|
START_SESSION | Begin charging at an EVSE for a given token |
STOP_SESSION | End an in-progress session by session ID |
RESERVE_NOW | Hold an EVSE for a token until an expiry time |
CANCEL_RESERVATION | Release a reservation early |
UNLOCK_CONNECTOR | Physically release a latched cable |
Transport, authentication and routing
OCPI is plain HTTPS with JSON bodies. There is no message broker, no custom framing, and no long-lived socket — that lives one layer down in OCPP.
Authentication
A single header carries a credentials token, base64-encoded:
Authorization: Token <base64-encoded-token>Forgetting the base64 encoding is the most common cause of a 401 on a brand-new integration. The token itself comes from the Credentials module handshake.
Routing headers
Four headers identify the two ends of a message. They are optional on direct connections and mandatory through a hub, because the hub has no other way to know where to forward:
OCPI-from-country-code: IN
OCPI-from-party-id: EMS
OCPI-to-country-code: IN
OCPI-to-party-id: EFI
X-Request-ID: 7c1a4e90
X-Correlation-ID: booking-88231X-Request-ID is unique per HTTP request. X-Correlation-ID stays constant across a logical transaction, including the asynchronous callbacks that commands generate. Populating both is the difference between a resolvable partner dispute and an unresolvable one — when a CPO asks what happened to a session three weeks ago, the correlation ID is the only thing that answers.
Push, pull and pagination
Most data modules support both directions, and a robust integration uses both.
| Mechanism | Verb | Use for |
|---|---|---|
| Push, full object | PUT | Creating or wholly replacing an object |
| Push, partial | PATCH | Field-level updates — status changes, running kWh |
| Pull | GET | Baseline sync, backfill, and repairing missed pushes |
Push alone is not enough. Pushes get lost, and a lost push is silent — there is no error, just stale data. Scheduled pulls with date_from are what repair the drift, and every production integration needs them.
Pagination
GET on a collection is paginated with offset and limit, plus optional date_from and date_to. The response carries the next page in a Link header alongside X-Total-Count and X-Limit:
GET /ocpi/2.2.1/locations?offset=0&limit=100&date_from=2026-08-10T00:00:00Z
Link: <https://cpo.example.com/ocpi/2.2.1/locations?offset=100&limit=100>; rel="next"
X-Total-Count: 3471
X-Limit: 100Follow the Link header rather than constructing the next URL yourself — a partner may cap limit below what you requested, and hand-built URLs then silently skip records.
Ordering
Every OCPI object carries last_updated. Network retries mean updates arrive out of order, so reject any update older than what you already hold. Without that guard a delayed “available” overwrites a newer “charging”, and your map lies to drivers.
Error code families
OCPI returns HTTP 200 with a status_code in the body for most application-level outcomes. The families are meaningful and worth handling distinctly:
| Code | Meaning | Whose problem |
|---|---|---|
1000 | Success | — |
2000 | Generic client error | Yours |
2001 | Invalid or missing parameters | Yours |
2002 | Not enough information | Yours |
2003 | Unknown location | Yours — or your Locations data is stale |
2004 | Unknown token | Yours — token not synced to this CPO |
3000 | Generic server error | Theirs |
3001 | Unable to use the client's API | Theirs calling you — usually a firewall |
3002 | Unsupported version | Negotiation — no common version |
3003 | No matching endpoints | Their endpoint directory is incomplete |
4000 | Generic hub error | The hub |
4001 | Unknown receiver | Party not on this hub |
4002 | Timeout on forwarded request | The far party, via the hub |
4003 | Connection problem | The hub's link to the far party |
4002 means the hub reached the partner and the partner did not answer — retrying immediately hits the same wall. A 4001 means the party is not on that hub at all, which is a configuration problem no retry will fix. Treating all failures identically produces retry storms against parties that were never going to respond.A complete roaming transaction
This is how the modules fit together. One driver, one charge, from onboarding to money.
- Registration — once per partner. The Versions module negotiates a version and returns the endpoint directory; the Credentials module completes the Token A/B/C handshake.
- Catalogue sync — continuous. The CPO publishes Locations and Tariffs; the eMSP pushes Tokens. Now each side knows what exists, what it costs, and who may use it.
- Discovery. The driver opens an app and sees chargers with live availability and a price — both from the CPO's data.
- Authorisation. Either the CPO authorises locally from its whitelist, or it calls the eMSP in real time and receives an
authorization_reference. - Start. The eMSP sends
START_SESSION. The CPO acknowledges, relays it to the charger over OCPP, and posts the real result to a callback. - Charging. A Session object appears and updates with accumulating energy and an estimated cost.
- Stop. The driver unplugs, or
STOP_SESSIONis sent. The session moves toCOMPLETED. - Settlement. The CPO finalises meter values, applies the tariff, and issues one immutable CDR. The eMSP invoices the driver from it and settles with the CPO against it.
The authorization_reference set at step 4 flows through steps 5 to 8, which is what lets you trace a line on an invoice back to the moment a driver was authorised. It is optional in the spec and indispensable in practice.
Version history
| Version | Year | What changed |
|---|---|---|
| 2.0 | 2016 | First widely circulated release |
| 2.1.1 | 2017 | Long the de facto standard; still live on many networks |
| 2.2 | 2019 | Roaming hubs, multiple roles per party, smart charging, sender/receiver roles on endpoints |
| 2.2.1 | 2021 | Final release with CDR and connector updates, additional types, improved descriptions |
| 2.2.1-d2 | 2023 | Documentation revision |
Documentation revisions (d2, d3, d4) do not change message content or add fields. They clarify descriptions and correct documentation issues. A partner advertising 2.2.1-d2 and one advertising plain 2.2.1 are wire-compatible.
2.1.1 remains the practical constraint on many integrations. It has no hub support, one role per connection, and no smart charging. If a partner is on 2.1.1, features built on 2.2+ simply are not available — confirm the version before promising functionality.
Where OCPI ends and OCPP begins
They are different layers and are frequently confused.
| OCPI | OCPP | |
|---|---|---|
| Between | Company and company | Charger and its own operator |
| Transport | HTTPS request/response | Persistent WebSocket |
| Crosses a company boundary | Yes — that is the point | No |
| Owned by | EVRoaming Foundation | Open Charge Alliance |
A remote start illustrates the split cleanly: the eMSP sends START_SESSION over OCPI to the CPO; the CPO sends RemoteStartTransaction over OCPP to the charger. The eMSP never touches OCPP, and the charger never knows OCPI exists. Full detail in the OCPP guide.
An implementation roadmap
The order matters — each phase depends on the one before it.
| Phase | Build | You can now |
|---|---|---|
| 1 | Versions + Credentials | Register with a partner and discover their endpoints |
| 2 | Locations + Tariffs | Show chargers and prices in an app |
| 3 | Tokens | Let your drivers authorise at partner chargers |
| 4 | Sessions + CDRs | Show live charges and bill for them — a complete product |
| 5 | Commands | Remote start and stop from the app |
| 6 | ChargingProfiles + HubClientInfo | Smart charging and hub connectivity |
Phases 1 to 4 are a shippable roaming integration. Phase 5 is what drivers notice most. Phase 6 is optional for many operators and, in the case of ChargingProfiles, not supported by every partner regardless.
Production lessons
- Store only the versions URL. Every other endpoint is a cached derivation you can rebuild. Persisting module URLs as configuration becomes unmaintainable around partner number ten.
- Never bill from a Session. The CDR is the financial record. This single rule prevents more revenue disputes than any other.
- Enforce
last_updatedordering everywhere. It applies to Locations, Sessions, Tokens and Tariffs alike. - Reconcile continuously. Sessions completed without CDRs are unbilled revenue; CDRs without sessions mean offline charging. Both ratios are partner quality metrics.
- Set
authorization_referenceon everything that accepts it. It is the only clean thread from authorisation to invoice. - Treat
NOT_SUPPORTEDas a capability flag, not an error. Cache it per partner and stop offering actions that will never work. - Log both routing headers on every request. Partner disputes are resolved by correlation IDs or not at all.
- Audit against the spec, not against what works. An integration that functions with one partner can still be non-conformant, and the second partner is where that surfaces.
Official reference
The normative source is the OCPI 2.2.1-d2 document published by the EVRoaming Foundation. Everything on this page and in the linked module guides is written against it, with the operational detail that comes from running it across partner networks in production rather than from reading it alone.
Frequently asked questions
The Open Charge Point Interface, a protocol that lets independent EV charging companies transact with each other. It defines how a charge point operator publishes locations, availability and prices, how an e-mobility provider authorises its drivers at someone else's chargers, and how the resulting session is billed and settled between the two companies.
OCPI runs between companies over HTTPS — a CPO and an eMSP exchanging locations, tokens, sessions and CDRs. OCPP runs between a charger and its own operator over a persistent WebSocket. A remote start crosses both: the eMSP sends START_SESSION over OCPI, and the CPO sends RemoteStartTransaction over OCPP.
Ten: Versions, Credentials, Locations, Sessions, CDRs, Tariffs, Tokens, Commands, ChargingProfiles and HubClientInfo. Versions and Credentials handle discovery and registration; the rest carry operational and financial data.
The Sender owns the data and is the source of truth; the Receiver consumes it. The direction inverts between modules — the CPO is Sender for Locations because it owns the chargers, while the eMSP is Sender for Tokens because it owns the customers.
No. Documentation revisions d2, d3 and d4 clarify descriptions and fix documentation issues without changing message content or adding fields. A party on 2.2.1-d2 is wire-compatible with one on plain 2.2.1.
1000 is success. The 2xxx family is client errors such as invalid parameters or an unknown token. The 3xxx family is server errors including unsupported version and inability to reach the client's API. The 4xxx family is hub errors such as unknown receiver or a timeout on a forwarded request.
Versions and Credentials first, since nothing works without registration. Then Locations and Tariffs to show chargers and prices, then Tokens for authorisation, then Sessions and CDRs to complete billing. Commands, ChargingProfiles and HubClientInfo come last.