Why a discovery module exists at all
OCPI is a peer-to-peer protocol between independent companies. There is no central registry that tells a CPO where an eMSP's Locations endpoint lives, and no guarantee that two parties run the same version of the spec. Hard-coding a partner's URLs would mean a code change every time they reorganise their API or upgrade a version.
The Versions module solves both problems with one rule: you are given exactly one URL out of band, and you discover everything else from it. That single URL is the versions endpoint. From there you learn which versions the partner supports, and for the version you agree on, the exact URL of every module they expose.
This is why the Versions module is the only part of OCPI whose location you are allowed to know in advance. Everything else is discovered.
The two endpoints
1. The version list
A GET against the versions URL returns every OCPI version this party can speak, each with the URL of its own details endpoint. It is deliberately minimal.
GET /ocpi/versions
Authorization: Token <base64-encoded-token>
{
"data": [
{ "version": "2.1.1", "url": "https://cpo.example.com/ocpi/2.1.1" },
{ "version": "2.2", "url": "https://cpo.example.com/ocpi/2.2" },
{ "version": "2.2.1", "url": "https://cpo.example.com/ocpi/2.2.1" }
],
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-08-11T09:04:12Z"
}2. The version details
A GET against one of those version URLs returns the endpoint directory: which modules exist, and for each, whether this party acts as the SENDER or the RECEIVER.
GET /ocpi/2.2.1
Authorization: Token <base64-encoded-token>
{
"data": {
"version": "2.2.1",
"endpoints": [
{ "identifier": "credentials", "role": "SENDER", "url": "https://cpo.example.com/ocpi/2.2.1/credentials" },
{ "identifier": "locations", "role": "SENDER", "url": "https://cpo.example.com/ocpi/2.2.1/locations" },
{ "identifier": "sessions", "role": "SENDER", "url": "https://cpo.example.com/ocpi/2.2.1/sessions" },
{ "identifier": "cdrs", "role": "SENDER", "url": "https://cpo.example.com/ocpi/2.2.1/cdrs" },
{ "identifier": "tariffs", "role": "SENDER", "url": "https://cpo.example.com/ocpi/2.2.1/tariffs" },
{ "identifier": "tokens", "role": "RECEIVER", "url": "https://cpo.example.com/ocpi/2.2.1/tokens" },
{ "identifier": "commands", "role": "RECEIVER", "url": "https://cpo.example.com/ocpi/2.2.1/commands" },
{ "identifier": "chargingprofiles","role": "RECEIVER", "url": "https://cpo.example.com/ocpi/2.2.1/chargingprofiles" }
]
},
"status_code": 1000,
"timestamp": "2026-08-11T09:04:13Z"
}
Sender and receiver — the field people misread
The role field was added in OCPI 2.2 and it trips up almost every first integration. It does not describe what the other party does. It describes the role this party plays for that module.
In OCPI, the Sender owns the data and is the source of truth. The Receiver consumes it. A CPO is the Sender of Locations because it owns the chargers; the eMSP is the Receiver. For Tokens it inverts — the eMSP owns its customer tokens and is the Sender, while the CPO is the Receiver.
| Module | Sender (owns data) | Receiver (consumes) |
|---|---|---|
locations | CPO | eMSP |
sessions | CPO | eMSP |
cdrs | CPO | eMSP |
tariffs | CPO | eMSP |
tokens | eMSP | CPO |
commands | eMSP | CPO |
chargingprofiles | SCSP / eMSP | CPO |
hubclientinfo | Hub | CPO / eMSP |
Because a single party can list the same module twice with different roles, the pair (identifier, role) — not identifier alone — is the unique key. Building your endpoint cache keyed on identifier only is a bug that surfaces the first time you connect to a hub or a party operating in both roles.
Version negotiation in practice
The spec does not mandate an algorithm for picking a version. The convention that works is: fetch the partner's list, intersect it with yours, and take the highest common version. If the intersection is empty, you cannot proceed and must report 3002 — Unsupported version.
Two operational realities make this less clean than it sounds:
- Version pinning is often contractual, not technical. A partner may support 2.2.1 on paper while their production deployment has only been certified against 2.2. Agreeing the version in the onboarding document and asserting it in code beats auto-negotiating to the highest available.
- Endpoint URLs change without warning. Partners reorganise their API gateways. If you cached the endpoint directory at registration and never refreshed it, you will find out through failed requests. Re-fetch version details on a schedule and on any unexplained 404.
Error handling
The Versions module is where authentication failures surface first, because it is the first authenticated call either party makes. Distinguishing the failure modes saves hours of partner email:
| Symptom | Almost always means |
|---|---|
| HTTP 401 on the versions URL | Wrong token, or the token was sent unencoded — OCPI 2.2.1 requires the credentials token to be base64-encoded in the Authorization: Token header |
| HTTP 404 on the versions URL | You were given the version details URL instead of the version list URL — an extremely common onboarding mix-up |
3002 Unsupported version | No overlap between the two version lists |
3003 No matching endpoints | The version details response omitted a module you require |
Empty endpoints array | Partner has deployed the version shell without wiring the modules — their side is incomplete |
Production lessons
- Store the versions URL, derive everything else. The only partner URL that belongs in your database is the versions endpoint. Every other URL should be a cached derivation you can rebuild on demand. Integrations that persist module URLs as configuration become unmaintainable at partner number ten.
- Re-discover on a schedule. Refreshing the endpoint directory daily costs two HTTP calls per partner and catches silent URL migrations before they become an incident.
- Log the negotiated version on every downstream request. When a partner reports a malformed payload, the first question is always which version each side thought it was speaking. Having it in the request log ends that conversation immediately.
- Validate the endpoint directory at registration. If a partner's role assignments contradict the module they are asking to use, catch it during onboarding rather than at the first live session.
- Expose all versions you genuinely support, and no more. Advertising a version you have not tested guarantees that some partner will negotiate to it.
Frequently asked questions
The version list, which returns every OCPI version a party supports along with a URL for each, and the version details endpoint, which returns the full endpoint directory for one specific version — every module identifier, its role, and its URL.
It states whether the party publishing the directory acts as SENDER or RECEIVER for that module. The Sender owns the data and is the source of truth; the Receiver consumes it. A CPO is Sender for Locations and Receiver for Tokens.
Each fetches the other's version list, intersects it with its own, and takes the highest common version. If there is no overlap, the request fails with status code 3002, Unsupported version. In practice the version is usually fixed contractually during onboarding.
Almost always because the credentials token was not base64-encoded in the Authorization header. OCPI 2.2.1 requires Token followed by the base64-encoded token value.