OCPI 2.3 Onboarding Guide¶
The practical hookup guide for a partner CPMS. You operate charge points behind your own CPMS with an OCPI 2.3 interface; this document is what you need to connect it to our platform.
Status honesty, up front. Our OCPI surface is live on 2.2 with 2.3 endpoints mounted, and the genuine 2.3 payload semantics (see Version negotiation) are being rolled out this week. Everything marked [rolling out] below describes the contract as it will be at demo time; everything unmarked is running today. Ask us to re-confirm any [rolling out] item live — we'd rather show you than promise you.
Two OCPI planes, one platform¶
You will talk to two base URLs (concrete hostnames and sandbox credentials come with your onboarding pack; placeholders used below):
| Plane | Placeholder | What lives there |
|---|---|---|
| CPMS roaming surface (CPO + eMSP roles) | {CPMS_BASE} |
versions, credentials, locations, sessions, CDRs, tariffs, tokens, commands |
| Booking service (BSP) | {BOOKING_BASE} |
the OCPI 2.3 booking module |
Both are multi-tenant: each CPO tenant on our platform has its own OCPI
country_code + party_id, and your registration establishes a peer relationship per tenant.
You only ever see the locations, sessions and bookings of the tenants you are peered with.
Registration¶
1. Exchange Token A¶
Out of band (during onboarding), we exchange initial credentials tokens — you receive a Token A authorizing your one registration call against us, and give us one for the reverse direction.
2. Pull versions¶
GET {CPMS_BASE}/ocpi/versions
Authorization: Token {TOKEN_A}
returns the mutually supported versions:
{
"data": [
{ "version": "2.2", "url": "{CPMS_BASE}/ocpi/2.2" },
{ "version": "2.3", "url": "{CPMS_BASE}/ocpi/2.3" }
],
"status_code": 1000,
"timestamp": "2026-08-04T12:00:00Z"
}
GET {CPMS_BASE}/ocpi/2.3 returns the endpoint list for that version (module identifier, role,
URL) — always discover module URLs from there rather than hardcoding paths.
3. Credentials handshake (Token A → B → C)¶
Standard OCPI registration against the shared credentials endpoint:
POST {CPMS_BASE}/ocpi/2.2/credentials
Authorization: Token {TOKEN_A}
with your credentials object — your token for us to call you (Token B), your versions URL,
and your roles (country_code, party_id, business details). We validate, fetch your versions,
store the peer, and respond with our credentials object containing Token C — the token you
use on every subsequent call to us. Token A is then dead.
PUT re-issues (token rotation), DELETE unregisters. The credentials endpoint is shared
between 2.2 and 2.3 registrations.
4. eMSP-side registration [rolling out]¶
For flows where we consume your CPO surface (our drivers roaming to your chargers), our
eMSP receiver — /ocpi/emsp/versions + a credentials receiver, so your CPMS can onboard us with
the same handshake in the other direction — is rolling out this week, replacing an out-of-band
registration path. Token push for driver authorization runs over the standard tokens module
once peered.
Module set¶
CPO side (you consume, at {CPMS_BASE})¶
| Module | Role | Endpoints | Notes |
|---|---|---|---|
locations |
SENDER | GET /locations, /{id}, /{id}/{evse_uid} |
site topology incl. HDV bays |
sessions |
SENDER | GET /sessions, per-id GET |
session objects for your drivers |
cdrs |
SENDER | GET /cdrs, per-id GET |
CDR delivery [rolling out] — wired to the signed-CDR generator |
tariffs |
SENDER | GET /tariffs, /{tariff_id} |
reservation-fee tariff objects [rolling out] |
tokens |
RECEIVER | GET /tokens/{uid}, POST /tokens/{uid}/authorize |
full AuthorizationInfo response [rolling out] (today a minimal allowed-stub) |
commands |
RECEIVER | POST /commands/{START_SESSION\|STOP_SESSION\|RESERVE_NOW\|CANCEL_RESERVATION} |
the seam the booking layer drives on partner CPMSs |
bookings |
RECEIVER | at {BOOKING_BASE} — see below |
the booking module lives on the booking service |
eMSP side (we consume yours)¶
Versions + credentials receiver [rolling out], then: your locations (we sync your site
topology into the schedule), your commands receiver (we send RESERVE_NOW/START_SESSION
near arrival), your sessions/cdrs senders (we track charging progress against bookings).
Authentication¶
Every OCPI call carries the standard header:
Authorization: Token {TOKEN_C}
Unknown or missing token → HTTP 401 with OCPI status 2001. Peer scoping is strict: a token
only reaches the resources of its own peer relationship — a booking created by another EMP is a
2003 Not Found to you, not a 403.
Version negotiation¶
- 2.3 preferred, 2.2 legacy. Register on 2.3 if your stack supports it; 2.2 peers stay fully supported — no forced migration.
- Today the mounted 2.3 endpoints serve 2.2-shaped payloads (route-level aliasing). [rolling out] Genuine OCPI 2.3.0 semantics ship this week on typed 2.3 modules, including the deltas that actually matter on the wire:
Price={before_taxes, taxes[]}(2.2.1's{excl_vat, incl_vat}stays on the 2.2 plane), flowing throughSession.total_costand all CDRtotal_*fields;Tariff.tax_included(required in 2.3.0) andmin_price/max_priceasPriceLimit;CDR.booking_idlinking a CDR to its booking;Location.time_zonerequired;Parkingobjects and EVSE parking links for HDV bays;- OCPI
DateTimeleniency on input (designator-less UTC accepted), canonical…ZUTC output. - The two version planes never mix types: what you negotiate is what you parse.
The booking module¶
The booking module is served by the booking service at {BOOKING_BASE}, authenticated with the
same Authorization: Token scheme (booking-plane peer tokens are provisioned by us during
onboarding today; discovery via a booking-plane versions endpoint is planned).
POST {BOOKING_BASE}/ocpi/cpo/2.3.0/bookings
GET {BOOKING_BASE}/ocpi/cpo/2.3.0/bookings/{booking_id}
DELETE {BOOKING_BASE}/ocpi/cpo/2.3.0/bookings/{booking_id}
Create¶
{
"booking_id": "emp-000123",
"location_id": "LOC-HAM-01",
"evse_uid": "",
"start_date_time": "2026-08-14T18:00:00Z",
"end_date_time": "2026-08-14T19:00:00Z",
"token": { "uid": "DE-XYZ-C00112233", "type": "RFID" },
"vehicle_type": "TRUCK_40T",
"vehicle_dimensions": {
"height_cm": 400, "length_cm": 1650, "width_cm": 255, "weight_kg": 40000
}
}
booking_idis yours — you mint it, we key on it. Retrying a create with the same id is safe.evse_uidis optional: omit it and the scheduler places the vehicle (the concrete charge point is bound near arrival — for the driver the exact bay matters at the gate, not at booking).vehicle_type/vehicle_dimensionsare HDV extensions — send dimensions when you have them and bay compatibility (height, length, weight, access side, charging technology) is checked at admission.- Timestamps must be RFC 3339 with timezone designator (
…Zrecommended).
Response (OCPI envelope):
{
"data": {
"booking_id": "emp-000123",
"location_id": "LOC-HAM-01",
"evse_uid": "",
"start_date_time": "2026-08-14T18:00:00Z",
"end_date_time": "2026-08-14T19:00:00Z",
"status": "CONFIRMED",
"last_updated": "2026-08-04T12:00:01Z"
},
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-08-04T12:00:01Z"
}
Admission is synchronous: you get CONFIRMED (schedulable) or REJECTED (no feasible slot) in
the create response. The creation response additionally carries the driver's offline site-entry
credential (PIN/QR) — it is served exactly once and never again on reads.
[rolling out] total_cost on the booking response, echoing the applicable reservation fee
from the tenant's published tariff.
Statuses¶
| Status | Meaning |
|---|---|
REQUESTED |
received, admission in progress |
CONFIRMED |
slot guaranteed |
ATTENDING |
vehicle arrived on site (gate event) |
COMPLETED |
session done, booking fulfilled |
CANCELLED |
cancelled by you (DELETE) or operator |
EXPIRED |
lapsed without effect |
NO_SHOW |
grace period passed without arrival — slot released, no-show fee applies |
REJECTED |
admission refused — no feasible slot |
Always uppercase. GET returns the current state; DELETE cancels (inside the free-cancellation
window, at no charge) and returns the booking with status CANCELLED.
Errors¶
OCPI envelope with status_code 2001 (invalid request / auth) or 2003 (unknown booking),
alongside the matching HTTP status. Bookings belonging to another peer are indistinguishable
from nonexistent ones.
First 30 minutes (sandbox checklist)¶
- Exchange Token A — you'll have it from the onboarding pack, along with
{CPMS_BASE},{BOOKING_BASE}and a booking-plane token. - Register:
GET {CPMS_BASE}/ocpi/versions→POST {CPMS_BASE}/ocpi/2.2/credentialswith your Token B → store the Token C we return. - Pull the endpoint list:
GET {CPMS_BASE}/ocpi/2.3— confirm the module URLs you'll use. - GET locations:
GET {CPMS_BASE}/ocpi/cpo/2.3/locations— you should see the sandbox HDV site with its bays/EVSEs. - Create a test booking:
POST {BOOKING_BASE}/ocpi/cpo/2.3.0/bookingsfor tomorrow, noevse_uid, with truck dimensions → expectCONFIRMEDplus the entry credential. - Read it back:
GET …/bookings/{your-booking-id}→CONFIRMED(and note the credential is absent on reads — by design). - Cancel it:
DELETE …/bookings/{your-booking-id}→CANCELLED.
That's the whole loop. From there, point your CPMS's commands receiver at us and we can run the arrival flow (reserve → session → CDR) against your sandbox chargers.
Maintainer footnote (internal — not part of the partner narrative): booking receiver contract
internal/ocpi/inbound/receiver.go + ev-suite internal/ocpi23 (ev-suite ADR-141 — mirror,
booking-core owns the contract); statuses ADR-0007; credential-once + offline verification
ADR-0019/0020; peer scoping ADR-0028-0031 context; total_cost echo + tariffs publication ADR-0032
(proposed); CPO gateway realities & [rolling out] items: ev-suite plan/17 C1/C2 (typed 2.3, CDR
wiring D8, AuthorizationInfo, eMSP versions/credentials receiver, token push) — verify merge
state before the demo; per-tenant party IDs: tenants.cpo_party_id / ev-suite
organizations.party_id.