OCPI Request Examples: Locations, Sessions & CDR JSON

Copy-paste OCPI 2.2.1 request examples: real Locations, Sessions, and CDR JSON payloads, with the HTTP method, endpoint path, and headers for each.

If you have ever stared at the OCPI 2.2.1 spec trying to reverse-engineer what an actual request looks like, this article is the shortcut. Below are copy-paste-ready examples for Locations, Sessions, and CDRs, the three modules a first integration has to get working. Each one gives you the HTTP method, the endpoint path, the headers that matter, and a fully populated JSON body.

OCPI request and response JSON shown in the interactive simulator

Reading a payload is one thing; watching it move between a CPO and an eMSP is another. Once you have the shapes below in your head, try the interactive OCPI Simulator to replay the credentials handshake and push these same objects across a live connection. The handshake is the part that is hardest to picture from a static example, and the credentials and versions walkthrough covers what the two parties swap before any request below is even legal.

The parts every OCPI request shares

Before the module-specific bodies, three things are constant across almost every OCPI call. Each of them fails in a way that looks like a body problem when it is not.

The endpoint is versioned and role-scoped. OCPI URLs are not fixed; you discover them from the other party’s versions endpoint during the handshake. They do follow a predictable shape:

https://cpo.example.com/ocpi/cpo/2.2.1/locations
https://emsp.example.com/ocpi/emsp/2.2.1/sessions

The cpo or emsp segment tells you which role hosts that copy of the module. Locations are hosted by the CPO and pulled or pushed to the eMSP; Sessions and CDRs are hosted by the eMSP and pushed to by the CPO.

The Authorization header is mandatory. After credentials are exchanged, every functional request carries:

Authorization: Token <base64-token>

The word Token is literal (it is the scheme name, not the value), and in OCPI 2.2 and later the token itself is base64-encoded. Skip the encoding and the server rejects the call with a 401 before it reads a single byte of your body.

Responses are wrapped in an envelope. You never get a bare object back. OCPI wraps every response body:

{
  "data": { },
  "status_code": 1000,
  "status_message": "Success",
  "timestamp": "2026-07-12T09:30:00Z"
}

data holds an object on single-object endpoints and an array on list endpoints. status_code is the OCPI-level verdict: 1000 is success, the 2xxx range is a client-side error, and 3xxx is a server-side one.

So check status_code, not just the HTTP status. A 200 OK carrying status_code: 2001 means your request arrived intact and was refused anyway. The OCPI overview explains where this two-layer design comes from, and the error response reference (coming soon) lists what each range actually signals.

Locations: a GET request and its response

Locations is the map: where the stations are, what connectors they have, whether they are live. The CPO hosts it and the eMSP consumes it, either by polling on a schedule or by receiving pushes (the tradeoff is laid out in pull versus push patterns (coming soon)). A paginated pull looks like this:

GET /ocpi/cpo/2.2.1/locations?date_from=2026-07-01T00:00:00Z&limit=50
Host: cpo.example.com
Authorization: Token bXktc2VjcmV0LXRva2Vu
Accept: application/json

The date_from filter and limit are how you page through a large catalog without re-pulling everything. The response wraps an array of Location objects; here is one, trimmed to the load-bearing fields:

{
  "country_code": "US",
  "party_id": "EVC",
  "id": "LOC_10021",
  "publish": true,
  "name": "Downtown Garage - Level 3",
  "address": "425 Market Street",
  "city": "San Francisco",
  "postal_code": "94105",
  "state": "CA",
  "country": "USA",
  "coordinates": {
    "latitude": "37.79130",
    "longitude": "-122.39690"
  },
  "evses": [
    {
      "uid": "EVSE_10021_1",
      "evse_id": "US*EVC*E10021001",
      "status": "AVAILABLE",
      "connectors": [
        {
          "id": "1",
          "standard": "IEC_62196_T1_COMBO",
          "format": "CABLE",
          "power_type": "DC",
          "max_voltage": 920,
          "max_amperage": 500,
          "max_electric_power": 350000,
          "tariff_ids": ["TARIFF_DC_PEAK"],
          "last_updated": "2026-07-11T18:04:00Z"
        }
      ],
      "last_updated": "2026-07-11T18:04:00Z"
    }
  ],
  "time_zone": "America/Los_Angeles",
  "last_updated": "2026-07-11T18:04:00Z"
}

A few things worth internalizing: evse_id (the roaming-visible identifier) is different from uid (the internal, opaque key you use in URLs). status lives on the EVSE, not the connector. And max_electric_power is in watts, so a 350 kW connector reads 350000, not 350. The full field map, including the many optional facility and image fields, is in the Locations module deep-dive.

Sessions: pushing a live session with PUT and PATCH

A Session is a living object. The CPO creates it on the eMSP with a full PUT the moment charging starts, then streams updates with PATCH. Here is the initial push:

PUT /ocpi/emsp/2.2.1/sessions/US/EVC/SESSION_45501
Host: emsp.example.com
Authorization: Token bXktc2VjcmV0LXRva2Vu
Content-Type: application/json
{
  "country_code": "US",
  "party_id": "EVC",
  "id": "SESSION_45501",
  "start_date_time": "2026-07-12T09:15:00Z",
  "kwh": 0,
  "cdr_token": {
    "country_code": "DE",
    "party_id": "IONE",
    "uid": "DE-IONE-C1234567890-1",
    "type": "RFID",
    "contract_id": "DE-IONE-C1234567890-1"
  },
  "auth_method": "AUTH_REQUEST",
  "location_id": "LOC_10021",
  "evse_uid": "EVSE_10021_1",
  "connector_id": "1",
  "currency": "USD",
  "status": "ACTIVE",
  "last_updated": "2026-07-12T09:15:00Z"
}

Note that the URL carries the country_code, party_id, and session id as path segments; that trio is what uniquely addresses the object. Once the session is live, the CPO does not re-send the whole thing. It sends only what changed:

PATCH /ocpi/emsp/2.2.1/sessions/US/EVC/SESSION_45501
Host: emsp.example.com
Authorization: Token bXktc2VjcmV0LXRva2Vu
Content-Type: application/json
{
  "kwh": 18.4,
  "status": "ACTIVE",
  "last_updated": "2026-07-12T09:34:12Z"
}

That last_updated field is doing real work. If two PATCH requests arrive out of order, which a busy connection makes entirely possible, the eMSP compares timestamps and discards the older one, so the session never appears to “go backwards.” Building the receiver correctly means treating last_updated as the tiebreaker, a point the Sessions module walkthrough covers in depth. The running kwh and total_cost in a Session are estimates; the authoritative billing number arrives later, in the CDR.

CDRs: posting the final billing record

When a session ends, the CPO posts a Charge Detail Record, the immutable billable summary. Unlike Sessions, a CDR is created once with a POST and never modified:

POST /ocpi/emsp/2.2.1/cdrs
Host: emsp.example.com
Authorization: Token bXktc2VjcmV0LXRva2Vu
Content-Type: application/json
{
  "country_code": "US",
  "party_id": "EVC",
  "id": "CDR_88120",
  "start_date_time": "2026-07-12T09:15:00Z",
  "end_date_time": "2026-07-12T09:42:33Z",
  "session_id": "SESSION_45501",
  "cdr_token": {
    "country_code": "DE",
    "party_id": "IONE",
    "uid": "DE-IONE-C1234567890-1",
    "type": "RFID",
    "contract_id": "DE-IONE-C1234567890-1"
  },
  "auth_method": "AUTH_REQUEST",
  "cdr_location": {
    "id": "LOC_10021",
    "evse_uid": "EVSE_10021_1",
    "evse_id": "US*EVC*E10021001",
    "connector_id": "1",
    "connector_standard": "IEC_62196_T1_COMBO",
    "connector_format": "CABLE",
    "connector_power_type": "DC",
    "coordinates": { "latitude": "37.79130", "longitude": "-122.39690" }
  },
  "currency": "USD",
  "tariffs": [ { } ],
  "charging_periods": [
    {
      "start_date_time": "2026-07-12T09:15:00Z",
      "dimensions": [ { "type": "ENERGY", "volume": 42.7 } ]
    }
  ],
  "total_cost": { "excl_vat": 18.90, "incl_vat": 20.60 },
  "total_energy": 42.7,
  "total_time": 0.459,
  "last_updated": "2026-07-12T09:42:40Z"
}

The CDR snapshots everything it needs to stand alone: the location, the tariff that applied, and each charging period. That is deliberate: a CDR must remain valid and auditable even after the live Location or Tariff it was built from has changed. And because it is immutable, corrections are not edits. To fix a CDR you post a credit CDR that references the original, a pattern explained fully in the CDRs module guide. Note that cdr_location is a compact snapshot with its own field set rather than a full Location object, so code that deserializes both into the same class will break on the first CDR it sees.

Why these break, and how to see it live

Reading three well-formed requests makes OCPI look easy. Everything above is correct, and that is precisely what hides the ways it goes wrong:

  • Missing or unencoded Authorization token. You get a 401, or a 400 when the scheme word itself is dropped.
  • Path/body mismatch, where the id in the URL disagrees with the id in the body on a Session PUT.
  • Wrong role in the path, such as pushing a Session to /cpo/ when Sessions are hosted at /emsp/.
  • Ignoring status_code: a 200 OK with status_code: 2001 is logged as a success and the object silently never lands.
  • Applying a stale PATCH, because the receiving side never compares last_updated.

None of these surface until you send a real request at a real counterparty. That is what the OCPI Simulator is for: paste these bodies in, watch the envelope come back, then break a header on purpose and see which status_code you get.

One thing worth carrying away from all this JSON: the payloads are the easy half. Every object above is a snapshot of state that was true at one last_updated instant, and most of the protocol is a set of rules about which snapshot wins when two of them disagree. Get that ordering right and malformed bodies become a schema problem you can catch once. Get it wrong and you ship an integration that validates perfectly and bills the wrong number, a failure no schema check can see. The common integration pitfalls writeup collects the ones that survive a clean validation pass.

Frequently asked questions

What does a valid OCPI request look like?

A valid OCPI request is a normal HTTP call to a versioned endpoint (e.g. /ocpi/cpo/2.2.1/locations) with a Token-scheme Authorization header and, for writes, a JSON body matching the module object. The response body is always wrapped in an envelope with data, status_code, status_message, and timestamp fields.

Why do OCPI GET responses wrap data in an envelope?

Every OCPI response uses a standard envelope so clients can check status_code (1000 means success) independently of the HTTP status. The actual payload lives under the data key, which is an array for list endpoints and an object for single-object endpoints.

Do I need the Authorization header on every OCPI call?

Yes. After the credentials handshake, every functional-module request carries Authorization: Token <base64-encoded-token>. The server checks the header before it parses anything else, so a bad token fails as a 401, or as a 400 if the literal scheme word Token is dropped.

Are Sessions pushed with PUT or PATCH?

Both. The CPO does a full PUT when a session first appears, then sends PATCH requests with only the changed fields (like kwh and last_updated) as the session progresses. The eMSP uses last_updated to reject stale updates that arrive out of order.

Found this useful? Share it.