Before two OCPI parties can exchange any business data — locations, sessions, CDRs, anything — they need to set up the connection. The Credentials and Versions modules are how this happens.
They’re often overlooked in OCPI articles because they’re not “the interesting bit,” but skipping them means nothing else works. This is the OCPI handshake explained.
The setup problem
When a CPO and an eMSP want to roam with each other (whether directly or via a hub), they need to know two things about each other:
-
What OCPI versions and modules does the other side support? Version 2.1.1 with only Locations? 2.2.1 with the full module set? 2.3.0 with AFIR additions? Knowing this lets them negotiate the highest common version.
-
How do we authenticate API calls? Each OCPI call carries an Authorization header with a token. Both sides need to know each other’s tokens — and how to rotate them when needed.
The Versions module handles (1). The Credentials module handles (2). Together they form the OCPI handshake.
The Versions module
The Versions module is simple in concept: each party exposes one endpoint that lists which OCPI versions it speaks. For each version, it lists the URLs where its module endpoints live.
The Versions endpoint
Every OCPI party (CPO or eMSP) exposes:
GET /ocpi/versions
Authorization: Token <credentials token>
→ returns:
{
"data": [
{
"version": "2.2.1",
"url": "https://cpo.example.com/ocpi/2.2.1"
},
{
"version": "2.3.0",
"url": "https://cpo.example.com/ocpi/2.3.0"
}
],
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-03T09:00:00Z"
}
This list tells the partner: “I support these OCPI versions; here are the URLs for the version-specific details.”
The Version Details endpoint
For each version listed above, the partner GETs the details. The example below is a CPO’s Version Details — note the cpo.example.com URLs and that the CPO advertises itself as SENDER for the modules it owns:
GET https://cpo.example.com/ocpi/2.2.1
Authorization: Token <credentials token>
→ returns:
{
"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"
}
]
},
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-03T09:00:01Z"
}
Each module has its own URL. The role field says whether this party is the Sender or Receiver for that module — and which side plays which is fixed by the protocol, not chosen. Per the spec, the Sender is always the party that owns the data (the Receiver pulls from, or is pushed to by, the owner).
Here’s the full role map for a CPO ↔ eMSP connection:
| Module | CPO is | eMSP is |
|---|---|---|
| Locations | Sender | Receiver |
| Sessions | Sender | Receiver |
| CDRs | Sender | Receiver |
| Tariffs | Sender | Receiver |
| Tokens | Receiver | Sender |
| Commands | Receiver | Sender |
| Charging Profiles | Receiver | Sender¹ |
| Hub Client Info | Receiver² | Receiver² |
| Credentials | n/a³ | n/a³ |
¹ For Charging Profiles the Sender is a Smart Charging Service Provider (SCSP) — a role an eMSP or a dedicated smart-charging party can hold; the CPO always receives. ² Hub Client Info only appears when a hub sits in the middle: the hub is the Sender, and both connected parties are Receivers. ³ Credentials (and the Versions discovery endpoint) are implemented identically by every party. The spec states the role field “is not relevant, as this module is the same for all roles,” so it’s the one module listed just once, with no Sender/Receiver split.
The pattern is easy to remember: the CPO owns the physical infrastructure, so it’s the Sender for everything about charging — Locations, Sessions, CDRs, Tariffs. The eMSP owns the driver relationship, so it’s the Sender for Tokens and for the Commands that remotely start and stop a charge.
The Credentials module
Authentication in OCPI uses bearer tokens. Every API call carries an Authorization: Token <secret> header. The Credentials module is how parties initially exchange those tokens.
The two-step credential exchange
The exchange follows a specific sequence to allow each party to set up auth in their own infrastructure:
Step 1: Party A (let’s say a CPO) generates an initial token (Token A) out-of-band. Through email, signed agreement, secure portal — somewhere outside OCPI. Party A gives this Token A to Party B (the eMSP) along with their /ocpi/versions URL.
Step 2: Party B uses Token A to fetch Party A’s Versions and Version Details endpoints. Now Party B knows what URLs and modules Party A supports.
Step 3: Party B generates Token B and POSTs it to Party A’s Credentials endpoint:
POST https://cpo.example.com/ocpi/2.2.1/credentials
Authorization: Token <Token A>
{
"token": "<Token B>", // Party B's new token for Party A to use
"url": "https://emsp.example.com/ocpi/versions",
"roles": [
{
"role": "EMSP",
"business_details": {
"name": "The Mobility Network",
"website": "https://themobility.network",
"logo": {
"url": "https://themobility.network/assets/logo.png",
"thumbnail": "https://themobility.network/assets/logo-thumb.png",
"category": "OPERATOR",
"type": "png",
"width": 512,
"height": 512
}
},
"party_id": "TNM",
"country_code": "NL"
}
]
}
Step 4: Party A responds with Token C and Party A’s own details:
{
"data": {
"token": "<Token C>", // Party A's new token for Party B to use
"url": "https://cpo.example.com/ocpi/versions",
"roles": [
{
"role": "CPO",
"business_details": {
"name": "Fast Charge Networks",
"website": "https://fastcharge.example.com",
"logo": {
"url": "https://fastcharge.example.com/brand/logo.svg",
"thumbnail": "https://fastcharge.example.com/brand/logo-64.png",
"category": "OPERATOR",
"type": "svg",
"width": 240,
"height": 80
}
},
"party_id": "ABC",
"country_code": "NL"
}
]
},
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-03T09:00:02Z"
}
Step 5: Token A is now invalidated. Going forward:
- Party A uses Token B to authenticate calls to Party B
- Party B uses Token C to authenticate calls to Party A
This three-token dance (A → B → C) ensures both sides have control over their own authentication and that the initial out-of-band token (A) is short-lived.
sequenceDiagram
participant A as Party A (CPO)
participant B as Party B (eMSP)
Note over A,B: Out-of-band: signed agreement
A->>B: Token A + /ocpi/versions URL<br/>(email or portal)
B->>A: GET /ocpi/versions (auth: Token A)
A-->>B: Supported versions list
B->>A: GET /ocpi/2.2.1 (auth: Token A)
A-->>B: Module endpoints
B->>A: POST /credentials<br/>Token B + B's roles
A-->>B: Response: Token C + A's roles
Note over A,B: Token A invalidated
Note over A,B: A→B calls use Token B<br/>B→A calls use Token C
Token rotation
The Credentials module supports rotation. Either party can update their token by PUTing a new credentials object. The old token is invalidated; the new one is used going forward.
In practice, token rotation happens:
- After a security event (suspected compromise)
- On a schedule (some implementations rotate every 90 days)
- During credential migration to a new system
Modules within the Version Details — what’s available
The Version Details response lists all modules each party supports. Standard module identifiers:
credentialsversionslocationssessionscdrstariffstokenscommandschargingprofiles(smart charging)hubclientinfo(for hubs)
Not every party implements every module. An eMSP might not implement chargingprofiles. A small CPO might not implement commands. The Version Details endpoint is how each party publishes what they actually do.
How this all connects
The full setup flow when two parties (say a CPO and an eMSP) want to roam:
-
Out-of-band: they sign a roaming agreement. The CPO emails the eMSP a one-time Token A and the
/ocpi/versionsURL. -
eMSP fetches Versions: uses Token A to call
GET /ocpi/versions. Learns the CPO supports 2.1.1 and 2.2.1. -
eMSP picks the highest common version: the eMSP also supports 2.2.1, so that’s the version they’ll use.
-
eMSP fetches Version Details: calls
GET /ocpi/2.2.1to get the list of module endpoints. -
eMSP POSTs credentials: generates Token B and POSTs it to the CPO with their own details.
-
CPO responds with Token C: the CPO generates Token C and responds.
-
The connection is established. From now on, every API call carries either Token B (eMSP→CPO) or Token C (CPO→eMSP). The initial Token A is invalidated.
-
They start exchanging real data: Locations, Tariffs, Sessions, CDRs, Tokens, Commands — whatever modules both parties support.
A complete worked example: the full handshake, wire by wire
The snippets above are trimmed for clarity. Here is the whole handshake end to
end — every request and its response — with the details that trip people up
called out in the comments. Party A is the CPO; Party B is the eMSP. (Comments
use // and # for teaching; real JSON has no comments.)
1. eMSP fetches the CPO’s supported versions (GET)
# ── eMSP ──▶ CPO ────────────────────────────────────────────────────────────
# Out-of-band, the CPO already handed the eMSP a one-time registration token
# (Token A) and its /ocpi/versions URL — by signed email or a portal, never
# over OCPI. The eMSP's first move is to discover which versions the CPO speaks.
GET /ocpi/versions HTTP/1.1
Host: cpo.example.com
# Auth scheme is the literal word "Token" — NOT "Bearer" — then the token value.
# In OCPI 2.2+ the token VALUE is base64-encoded; here that is Token A, the
# single-use registration token. This is the ONLY thing Token A is used for.
Authorization: Token <base64(Token A)>
# Recommended so both sides can trace one call across their logs; echo them back.
X-Request-ID: 12345
X-Correlation-ID: 67890
# ── CPO ──▶ eMSP (the response) ─────────────────────────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-ID: 12345
X-Correlation-ID: 67890
{
"data": [ // array of Version objects — one per version the CPO speaks
{
"version": "2.1.1", // required — VersionNumber the CPO supports
"url": "https://cpo.example.com/ocpi/2.1.1" // required — where THAT version's details live
},
{
"version": "2.2.1", // required — VersionNumber the CPO supports
"url": "https://cpo.example.com/ocpi/2.2.1" // required — where THAT version's details live
},
{
"version": "2.3.0", // required — VersionNumber the CPO supports
"url": "https://cpo.example.com/ocpi/2.3.0" // required — where THAT version's details live
}
// Each element has exactly two fields (version, url), both required — nothing
// else lives on a Version object. The eMSP picks the highest version BOTH speak.
],
// THE key OCPI gotcha: HTTP 200 only means the HTTP call arrived. You must
// ALSO check status_code INSIDE the envelope. 1000 = success. You can get
// HTTP 200 with, say, status_code 2001 (invalid parameters) — always read the
// envelope, never trust the HTTP status alone. 2xxx = client error, 3xxx = server.
"status_code": 1000,
"status_message": "Success",
// RFC 3339, UTC ("Z").
"timestamp": "2026-07-03T09:00:00Z"
}
The eMSP picks the highest version both sides support and follows that version’s
url.
2. eMSP fetches the version details (GET)
# ── eMSP ──▶ CPO ────────────────────────────────────────────────────────────
# The URL comes straight from the "url" in the previous response — do NOT
# hand-build it. This returns the per-module endpoints for version 2.2.1.
GET /ocpi/2.2.1 HTTP/1.1
Host: cpo.example.com
# Still Token A — the credentials handshake below has not happened yet.
Authorization: Token <base64(Token A)>
# ── CPO ──▶ eMSP (the response) ─────────────────────────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": { // a VersionDetails object
"version": "2.2.1", // required — the VersionNumber these endpoints belong to
"endpoints": [ // required — one Endpoint per module/interface-role the CPO exposes
{
"identifier": "credentials", // required — ModuleID; the module we POST to next
"role": "SENDER", // required — InterfaceRole. For credentials the role is NOT meaningful (spec: "the same for all roles"); it's listed once, and SENDER here is just a conventional placeholder
"url": "https://cpo.example.com/ocpi/2.2.1/credentials" // required — where to reach this module
},
{
"identifier": "locations", // required — ModuleID
"role": "SENDER", // required — the CPO is the Sender for locations (it owns the data)
"url": "https://cpo.example.com/ocpi/2.2.1/locations"
},
{
"identifier": "sessions", // required — ModuleID
"role": "SENDER", // required — the CPO is the Sender for sessions
"url": "https://cpo.example.com/ocpi/2.2.1/sessions"
},
{
"identifier": "cdrs", // required — ModuleID
"role": "SENDER", // required — the CPO is the Sender for CDRs
"url": "https://cpo.example.com/ocpi/2.2.1/cdrs"
},
{
"identifier": "tariffs", // required — ModuleID
"role": "SENDER", // required — the CPO is the Sender for tariffs
"url": "https://cpo.example.com/ocpi/2.2.1/tariffs"
},
{
"identifier": "tokens", // required — ModuleID
"role": "RECEIVER", // required — the CPO RECEIVES tokens (the eMSP owns them, so it is the Sender)
"url": "https://cpo.example.com/ocpi/2.2.1/tokens"
},
{
"identifier": "commands", // required — ModuleID
"role": "RECEIVER", // required — the CPO RECEIVES remote commands (start/stop/reserve)
"url": "https://cpo.example.com/ocpi/2.2.1/commands"
},
{
"identifier": "chargingprofiles", // required — ModuleID (smart charging)
"role": "RECEIVER", // required — the CPO RECEIVES charging-profile requests
"url": "https://cpo.example.com/ocpi/2.2.1/chargingprofiles"
}
// Every Endpoint has exactly three required fields (identifier, role, url) and
// nothing else. A single module can appear TWICE — once as SENDER and once as
// RECEIVER — when a party implements both interfaces; this CPO does not, so
// each module is listed once. hubclientinfo would appear here only for a hub.
]
},
"status_code": 1000, // again: check this, not just the HTTP 200
"status_message": "Success",
"timestamp": "2026-07-03T09:00:01Z"
}
3. eMSP POSTs its credentials to the CPO (the token exchange)
# ── eMSP ──▶ CPO ────────────────────────────────────────────────────────────
# This is the heart of the handshake. The eMSP sends the CPO a NEW token
# (Token B) for the CPO to use on future calls TO the eMSP, plus the eMSP's own
# /ocpi/versions URL so the CPO can discover the eMSP in turn.
POST /ocpi/2.2.1/credentials HTTP/1.1
Host: cpo.example.com
# Still authenticated with Token A — this POST is the LAST time Token A is valid.
Authorization: Token <base64(Token A)>
Content-Type: application/json
{
"token": "<Token B>", // required — eMSP's token; the CPO will send THIS on calls to the eMSP
"url": "https://emsp.example.com/ocpi/versions", // required — gotcha: the eMSP's OWN versions URL, not the CPO's
"roles": [ // required — array of CredentialsRole; one per role this party plays
{
"role": "EMSP", // required — CPO | EMSP | HUB | NAP | NSP | OTHER | SCSP
"business_details": { // required — BusinessDetails
"name": "The Mobility Network", // required — display name (max 100 chars)
"website": "https://themobility.network", // optional — the party's website URL
"logo": { // optional — an Image object; shown here in full
"url": "https://themobility.network/assets/logo.png", // required within Image
"thumbnail": "https://themobility.network/assets/logo-thumb.png", // optional — smaller preview
"category": "OPERATOR", // required within Image — CHARGER | ENTRANCE | LOCATION | NETWORK | OPERATOR | OTHER | OWNER
"type": "png", // required within Image — file extension, e.g. png | jpeg | gif | svg
"width": 512, // optional — pixel width
"height": 512 // optional — pixel height
}
},
"party_id": "TNM", // required — the eMSP's party id (CiString, 3 chars)
"country_code": "NL" // required — ISO 3166-1 alpha-2 (CiString, 2 chars)
}
// A party that plays multiple roles (e.g. CPO + EMSP) lists one CredentialsRole
// per role in this array; this eMSP plays a single role, so there is one entry.
]
}
# ── CPO ──▶ eMSP (the response) ─────────────────────────────────────────────
# The response body is the OTHER party's credentials: the CPO hands back its
# OWN new token (Token C) and its OWN roles/url. This single response completes
# the exchange.
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"token": "<Token C>", // required — CPO's token; the eMSP will send THIS on calls to the CPO
"url": "https://cpo.example.com/ocpi/versions", // required — the CPO's own versions URL
"roles": [ // required — array of CredentialsRole
{
"role": "CPO", // required — CPO | EMSP | HUB | NAP | NSP | OTHER | SCSP
"business_details": { // required — BusinessDetails
"name": "Fast Charge Networks", // required — display name (max 100 chars)
"website": "https://fastcharge.example.com", // optional — the party's website URL
"logo": { // optional — an Image object; shown here in full
"url": "https://fastcharge.example.com/brand/logo.svg", // required within Image
"thumbnail": "https://fastcharge.example.com/brand/logo-64.png", // optional — smaller preview
"category": "OPERATOR", // required within Image — CHARGER | ENTRANCE | LOCATION | NETWORK | OPERATOR | OTHER | OWNER
"type": "svg", // required within Image — file extension
"width": 240, // optional — pixel width
"height": 80 // optional — pixel height
}
},
"party_id": "ABC", // required — the CPO's party id (CiString, 3 chars)
"country_code": "NL" // required — ISO 3166-1 alpha-2 (CiString, 2 chars)
}
]
},
"status_code": 1000, // 1000 here means the registration succeeded
"status_message": "Success",
"timestamp": "2026-07-03T09:00:02Z"
}
The moment this response is sent, Token A is invalidated. From here on: the eMSP authenticates its calls to the CPO with Token C, and the CPO authenticates its calls to the eMSP with Token B. A → B → C, and A never works again.
4. Later: rotating or tearing down the connection
# ── Rotate: replace the token you gave the other side ───────────────────────
# Authenticate with your CURRENT token, and send a fresh credentials object.
# The old token is invalidated once this returns 1000.
PUT /ocpi/2.2.1/credentials HTTP/1.1
Host: cpo.example.com
Authorization: Token <base64(current token)>
Content-Type: application/json
{
"token": "<new token>", // required — replaces the previous one
"url": "https://emsp.example.com/ocpi/versions", // required — your OWN versions URL
"roles": [ // required — a full CredentialsRole, identical shape to the POST above
{
"role": "EMSP", // required — CPO | EMSP | HUB | NAP | NSP | OTHER | SCSP
"business_details": { // required — BusinessDetails
"name": "The Mobility Network", // required — display name (max 100 chars)
"website": "https://themobility.network", // optional — the party's website URL
"logo": { // optional — an Image object
"url": "https://themobility.network/assets/logo.png", // required within Image
"thumbnail": "https://themobility.network/assets/logo-thumb.png", // optional — smaller preview
"category": "OPERATOR", // required within Image — CHARGER | ENTRANCE | LOCATION | NETWORK | OPERATOR | OTHER | OWNER
"type": "png", // required within Image — file extension
"width": 512, // optional — pixel width
"height": 512 // optional — pixel height
}
},
"party_id": "TNM", // required — party id (CiString, 3 chars)
"country_code": "NL" // required — ISO 3166-1 alpha-2 (CiString, 2 chars)
}
]
}
# ── Unregister: tear the whole connection down ──────────────────────────────
DELETE /ocpi/2.2.1/credentials HTTP/1.1
Host: cpo.example.com
Authorization: Token <base64(current token)>
# After this, both tokens are dead and the parties must handshake again from
# Token A to reconnect.
Common implementation gotchas
A few things that catch real-world implementations:
The three-token dance ordering — the #1 mistake
The single most common failure is mishandling the token sequence. Token A — the out-of-band registration token — is single-use. It exists only to bootstrap the handshake, and it MUST be invalidated the instant the credentials exchange completes. Going forward, each side uses the token the OTHER side gave it:
Out-of-band: CPO ──▶ eMSP Token A (email / portal — single-use bootstrap)
Handshake: eMSP ──▶ CPO POST /credentials { token: Token B, … }
CPO ──▶ eMSP response { token: Token C, … }
Afterwards: eMSP ──▶ CPO auth with Token C (the token the CPO gave it)
CPO ──▶ eMSP auth with Token B (the token the eMSP gave it)
Token A is now DEAD. A → B → C.
Where people go wrong:
- Reusing Token A for ongoing calls after the handshake — including for the
Versions endpoint. Token A is not “kept alive” for discovery. During
registration it authenticates exactly three calls (
GET /versions,GETthe version details, and thePOST /credentials); the instant that POST returns, it is invalidated for everything — Versions included. All later calls use Token C (eMSP→CPO) or Token B (CPO→eMSP). Reconnecting after aDELETE /credentialsneeds a fresh Token A, issued out-of-band. It’s invalidated by design — a compromise of the original email must not grant lasting API access. - Authenticating with your OWN token instead of the one the other side issued. The eMSP calls the CPO with Token C (the CPO’s token), NOT with Token B (its own). Swapping these is the classic “401 Unauthorized” cause.
- Sending the wrong
url. In the POST body you send your OWN/ocpi/versionsURL, not the other party’s — it is how they discover you back.
The Authorization header carries the credentials token
Every OCPI call authenticates with Authorization: Token <value> — the literal
word Token, never Bearer. The value is whichever credentials token applies
to the direction of the call (A, then B or C). In OCPI 2.2 and later the token
value is base64-encoded before it goes in the header; forgetting to encode
(or double-encoding) it is a common source of silent auth failures.
last_updated and status_code still apply
As with every module: HTTP 200 only means the call arrived — you must ALSO check
the status_code inside the OCPI envelope (1000 = success, 2xxx = client error,
3xxx = server), and all timestamps are RFC 3339 in UTC (the trailing Z).
For the authoritative field-by-field definitions, see the OCPI 2.2.1 specification’s Credentials and Versions modules in the OCPI GitHub repository.
Real-world gotchas
Token A leakage
The out-of-band Token A is the most fragile link. If it’s emailed in plaintext and intercepted, an attacker could complete the handshake themselves. Mitigations: rotate Token A frequently, deliver it through secure channels (signed emails, secure portals), expire it quickly.
Version mismatch
If two parties don’t share a common version, they can’t connect. Workaround: either upgrade one side, or run a translation proxy (some hubs do this).
Endpoint mismatch
A party might advertise a module endpoint in Version Details that they don’t actually support correctly. Calls fail with 404 or 501. Testing is essential before going to production.
Token rotation while sessions are in flight
If you rotate tokens while there are in-flight requests, some requests may fail. Best practice: graceful overlap (accept both old and new tokens for a transition window).
Hub routing complexity
When a hub sits between two parties, the credentials dance happens twice — once between each party and the hub. The hub maintains independent credentials with each side. This is standard but adds complexity.
Three things to take from this
-
Credentials and Versions are the foundation. Nothing else works without them.
-
The three-token dance is the security pattern. A → B → C, with A then invalidated. It gives both sides control and limits the lifetime of any single token.
-
Each version’s module endpoints are independent. A party can support different modules at different versions. The Version Details is the source of truth.
These modules are unglamorous but the OCPI ecosystem depends on them. Without working Credentials and Versions, you can’t even start to roam.