> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# OCPI 2.2.1 (EV charging roaming)

> Client-mode REST API connector for OCPI 2.2.1 on the CPO side: push Locations and EVSE status, Sessions, CDRs and Tariffs to an eMSP or Hub, real-time token authorization, token list, and the Versions / Credentials handshake

Technical Preview

## Overview

**OCPI** (Open Charge Point Interface, maintained by the EVRoaming Foundation) is the open protocol EV charging networks use to share charge point data, sessions, billing records and tariffs. A **CPO** (charge point operator) runs the chargers; an **eMSP** (e-mobility service provider) owns the driver tokens and apps; a **Hub** (Gireve, Hubject's OCPI gateway, e-clearing.net and others) can sit in between.

The **Ocpi221Peer** connector puts a Coreflux broker on the **CPO side**. Chargers already speak OCPP or publish state over MQTT; LoT actions turn that state into OCPI calls to your roaming partner: EVSE status changes become `PATCH` on the eMSP's Locations receiver, a session start becomes `PUT /sessions`, a finished charge becomes `POST /cdrs`, and an RFID swipe can be checked in real time with `AuthorizeToken`. Each event publishes the peer's reply on `<topic>/response`.

| Event | Topic | Request | Use |
| - | - | - | - |
| `GetVersions` | `energy/ocpi/versions/list` | `GET /ocpi/emsp/versions` | Supported OCPI versions and their details URLs (Token A works here during registration) |
| `GetVersionDetails` | `energy/ocpi/version/details` | `GET /ocpi/emsp/2.2.1` | Module endpoint list for 2.2.1 (identifier, role SENDER/RECEIVER, url) — use it to set PREFIX |
| `GetCredentials` | `energy/ocpi/credentials/get` | `GET …/credentials` | Peer credentials object (token, url, roles with business details) |
| `RegisterCredentials` | `energy/ocpi/credentials/register` | `POST …/credentials` | WRITE: registration — send our credentials (Token B, versions url, roles); reply carries Token C |
| `UpdateCredentials` | `energy/ocpi/credentials/update` | `PUT …/credentials` | WRITE: rotate our token / url / roles; reply carries a new token for us |
| `DeleteCredentials` | `energy/ocpi/credentials/delete` | `DELETE …/credentials` | WRITE: unregister — our credentials on the peer become invalid |
| `GetLocation` | `energy/ocpi/location/get` | `GET …/locations/{country_code}/{party_id}/{location_id}` | Receiver GET: the peer's copy of our Location (validation) |
| `GetEvse` | `energy/ocpi/evse/get` | `GET …/locations/{country_code}/{party_id}/{location_id}/{evse_uid}` | Receiver GET: the peer's copy of one EVSE |
| `GetConnector` | `energy/ocpi/connector/get` | `GET …/locations/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}` | Receiver GET: the peer's copy of one Connector |
| `PutLocation` | `energy/ocpi/location/put` | `PUT …/locations/{country_code}/{party_id}/{location_id}` | WRITE: push a full Location object (new or replace) |
| `PutEvse` | `energy/ocpi/evse/put` | `PUT …/locations/{country_code}/{party_id}/{location_id}/{evse_uid}` | WRITE: push a full EVSE object |
| `PatchLocation` | `energy/ocpi/location/patch` | `PATCH …/locations/{country_code}/{party_id}/{location_id}` | WRITE: partial Location update (must include last\_updated) |
| `PatchEvseStatus` | `energy/ocpi/evse/status` | `PATCH …/locations/{country_code}/{party_id}/{location_id}/{evse_uid}` | WRITE: charger state -> EVSE status, body \{"status":"CHARGING","last\_updated":"..."} |
| `PatchConnector` | `energy/ocpi/connector/patch` | `PATCH …/locations/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}` | WRITE: partial Connector update (tariff\_ids, max power, last\_updated) |
| `GetSession` | `energy/ocpi/session/get` | `GET …/sessions/{country_code}/{party_id}/{session_id}` | Receiver GET: the peer's copy of a Session |
| `PutSession` | `energy/ocpi/session/put` | `PUT …/sessions/{country_code}/{party_id}/{session_id}` | WRITE: create or replace a Session (start of charging) |
| `PatchSession` | `energy/ocpi/session/patch` | `PATCH …/sessions/{country_code}/{party_id}/{session_id}` | WRITE: update kwh / status / charging\_periods / total\_cost during charging |
| `PostCdr` | `energy/ocpi/cdr/post` | `POST …/cdrs` | WRITE: send the final CDR; the peer returns the CDR URL in the Location header |
| `GetCdr` | `energy/ocpi/cdr/get` | `GET …/cdrs/{cdr_id}` | Receiver GET: CDR by the id path the eMSP returned (URL is eMSP-defined; spec example /cdrs/1234) |
| `GetTariff` | `energy/ocpi/tariff/get` | `GET …/tariffs/{country_code}/{party_id}/{tariff_id}` | Receiver GET: the peer's copy of a Tariff |
| `PutTariff` | `energy/ocpi/tariff/put` | `PUT …/tariffs/{country_code}/{party_id}/{tariff_id}` | WRITE: push a full Tariff object |
| `DeleteTariff` | `energy/ocpi/tariff/delete` | `DELETE …/tariffs/{country_code}/{party_id}/{tariff_id}` | WRITE: remove a Tariff from the peer |
| `ListTokens` | `energy/ocpi/tokens/list` | `GET …/tokens?{query}` | Sender GET list: eMSP tokens (query date\_from, date\_to, offset, limit; paginated via Link header) |
| `AuthorizeToken` | `energy/ocpi/token/authorize` | `POST …/tokens/{token_uid}/authorize?type={type}` | Real-time authorization of an RFID / app token at a charger; body LocationReferences (optional) -> AuthorizationInfo |

`…` stands for the module URL prefix, `/ocpi/emsp/2.2.1` in the template. 13 events change state on the peer and are marked `# WRITE` in the `.lot` file.

## Base URL and module URLs

OCPI does not fix URLs: each party publishes them. You get the **versions URL** and a **Token A** from your partner offline, then:

1. `GetVersions` lists supported versions with their details URLs.
2. `GetVersionDetails` returns the 2.2.1 module list: `identifier` (`credentials`, `locations`, `sessions`, `cdrs`, `tariffs`, `tokens`, …), `role` (`SENDER` or `RECEIVER`) and `url`.

The template uses the spec's own example layout: `BASE_ADDRESS` `https://ocpi-peer.example.com`, versions at `/ocpi/emsp/versions`, modules under `/ocpi/emsp/2.2.1/<module>`. If your partner's URLs differ, change `VERSIONS`, `DETAILS` and `PREFIX` in `gen_route.py` and regenerate, or edit the `WITH ENDPOINT` lines. Use the **RECEIVER** URLs for locations, sessions, CDRs and tariffs, and the **SENDER** URL for tokens.

## Authentication

Every request carries `Authorization: Token <encoded token>`. Since OCPI 2.2-d2 the token must be **Base64-encoded** (UTF-8 bytes, RFC 4648). Many older 2.1.1 / 2.2 peers still expect the raw token, so check with your partner.

| Step | Token | Events |
| - | - | - |
| Discovery | Token A (offline, from the peer) | `GetVersions`, `GetVersionDetails` |
| Registration | Token A | `RegisterCredentials` with our Token B, versions URL and roles; the reply holds **Token C** |
| Normal operation | Token C | everything else |

Keep the current value (`Token <base64>`) in Hub secret `OCPI_AUTHORIZATION`. `UpdateCredentials` rotates tokens; `DeleteCredentials` unregisters.

## Required headers

Every OCPI request must carry `X-Request-ID` and `X-Correlation-ID` (use a fresh UUID per request). The connector maps them from the payload fields `request_id` and `correlation_id`. The `OCPI-to-country-code` / `OCPI-to-party-id` / `OCPI-from-*` routing headers are only needed when you go through a Hub and are not in the template; add them to the events your Hub requires.

## Request payload

Publish JSON on the event's source topic:

| Field | Events | Meaning |
| - | - | - |
| `authorization` | all | `Token <base64>` |
| `request_id`, `correlation_id` | all | UUIDs for `X-Request-ID` / `X-Correlation-ID` |
| `country_code`, `party_id` | locations, sessions, tariffs | Your CPO country code and party id (client-owned objects) |
| `location_id`, `evse_uid`, `connector_id` | locations | Object path |
| `session_id`, `tariff_id`, `cdr_id` | sessions, tariffs, CDRs | Object id (`cdr_id` is the path the eMSP returned in `Location`) |
| `token_uid`, `type` | `AuthorizeToken` | Token uid and type (`RFID`, `APP_USER`, `AD_HOC_USER`, `OTHER`) |
| `query` | `ListTokens` | e.g. `date_from=2026-10-01T00:00:00Z&offset=0&limit=100` |
| `body` | POST / PUT / PATCH | OCPI JSON object as a string |

Example on `energy/ocpi/evse/status`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"authorization":"Token <base64>","request_id":"7f0c…","correlation_id":"1b2e…","country_code":"PT","party_id":"CFX","location_id":"LOC1","evse_uid":"PT*CFX*E1","body":"{\"status\":\"CHARGING\",\"last_updated\":\"2026-10-05T14:45:00Z\"}"}
```

Replies use the OCPI envelope `{"data": …, "status_code": 1000, "status_message": "Success", "timestamp": "…"}`. `1000` is success, `2xxx` are client errors (`2001` invalid parameters, `2003` unknown location, `2004` unknown token) and `3xxx` server errors. Unknown objects on receiver `GET` and on `AuthorizeToken` return HTTP `404`.

## Example actions

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Example actions for the Ocpi221Peer route (docs page). Not part of the route template.
# Secret: OCPI_AUTHORIZATION = "Token <base64(Token C)>" (Token A only for versions + registration).
# Inline JSON object literals are not supported when publishing to a topic, so the request payload is a COLLAPSED
#   model sent with PUBLISH MODEL. The body field is the OCPI JSON object as a string. Use fresh UUIDs for
#   request_id / correlation_id in production (static values shown here for readability).

DEFINE MODEL OcpiVersionsRequest COLLAPSED
    ADD STRING "authorization"
    ADD STRING "request_id"
    ADD STRING "correlation_id"

DEFINE MODEL OcpiEvseStatusRequest COLLAPSED
    ADD STRING "authorization"
    ADD STRING "request_id"
    ADD STRING "correlation_id"
    ADD STRING "country_code"
    ADD STRING "party_id"
    ADD STRING "location_id"
    ADD STRING "evse_uid"
    ADD STRING "body"

DEFINE ACTION OcpiVersionDetailsDaily
ON EVERY 1 DAY DO
    PUBLISH MODEL OcpiVersionsRequest TO "energy/ocpi/version/details" WITH
        authorization = (GET SECRET "OCPI_AUTHORIZATION")
        request_id = "cfx-version-details"
        correlation_id = "cfx-version-details"

DEFINE ACTION OcpiChargerStatusToEmsp
ON TOPIC "chargers/+/evse/status" DO
    PUBLISH MODEL OcpiEvseStatusRequest TO "energy/ocpi/evse/status" WITH
        authorization = (GET SECRET "OCPI_AUTHORIZATION")
        request_id = "cfx-evse-status"
        correlation_id = "cfx-evse-status"
        country_code = "PT"
        party_id = "CFX"
        location_id = "LOC1"
        evse_uid = (TOPIC POSITION 2)
        body = PAYLOAD
```

`OcpiChargerStatusToEmsp` expects the charger to publish an OCPI-shaped patch such as `{"status":"AVAILABLE","last_updated":"…"}` on `chargers/<evse_uid>/evse/status`; map your charger's own status values to OCPI `Status` (`AVAILABLE`, `BLOCKED`, `CHARGING`, `INOPERATIVE`, `OUTOFORDER`, `PLANNED`, `REMOVED`, `RESERVED`, `UNKNOWN`) first if they differ.

## Writes

`RegisterCredentials`, `UpdateCredentials`, `DeleteCredentials`, `PutLocation`, `PutEvse`, `PatchLocation`, `PatchEvseStatus`, `PatchConnector`, `PutSession`, `PatchSession`, `PostCdr`, `PutTariff` and `DeleteTariff` change your partner's data and can affect billing. Restrict which MQTT clients may publish to those topics with broker rules. Every `PATCH` body must include `last_updated`. Locations cannot be deleted in OCPI: `PATCH` the status to `REMOVED` instead.

## Not packaged

* CPO receiver interfaces the partner calls on you: **Commands** (`START_SESSION`, `STOP_SESSION`, `RESERVE_NOW`, `UNLOCK_CONNECTOR`, …), **ChargingProfiles**, Tokens push, and Credentials from the peer. These need REST API server mode.
* Async callbacks to an arbitrary `response_url`, and **HubClientInfo**.
* eMSP-role pulls (sender `GET` lists of Locations, Sessions, CDRs, Tariffs from a CPO). A second connector with `/ocpi/cpo/2.2.1` paths would cover that.
* **OCPP** (charger to CPO back office) uses WebSocket, not REST, and is out of scope.

## Sources

* [OCPI 2.2.1 specification (GitHub, release-2.2.1-bugfixes)](https://github.com/ocpi/ocpi/tree/release-2.2.1-bugfixes): `mod_locations`, `mod_sessions`, `mod_cdrs`, `mod_tariffs`, `mod_tokens`, `credentials`, `version_information_endpoint`, `transport_and_format`
* [EVRoaming Foundation](https://evroaming.org/)
* Hand-curated machine-readable cache: `openapi-cache/ocpi-2-2-1.json` (OCPI does not publish an official OpenAPI file)

## Status

**Blocked beta.** Smoke: `static 256/256, mock 26/26, controls 6/6, live 0/0`. Unblock with a partner's versions URL and Token A (eMSP, Hub test environment, or a self-hosted OCPI node), then re-run `smoke_ocpi.py` with `OCPI_BASE_URL` and `OCPI_AUTHORIZATION` (add `OCPI_WRITE_TEST=1` for location writes), or complete a broker MQTT round-trip.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.