DRAFT — not indexed by search engines. Visible only via direct URL or the admin page.

OCPI Request Examples: Locations, Sessions & CDR JSON

Copy-paste OCPI 2.2.1 request examples — real Locations, Sessions, and CDR JSON payloads with headers, methods, and endpoints in one place.

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 request examples for the three modules people reach for first — Locations, Sessions, and CDRs — with the real HTTP method, the endpoint path, the headers that matter, and a fully populated JSON body for each. No hand-waving, no “the object contains the following fields.” Just the requests.

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 — it is the fastest way to see why a request succeeds or gets a 2001 back.

The parts every OCPI request shares

Before the module-specific bodies, three things are constant across almost every OCPI call, and getting them wrong causes most first-day failures.

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. But they 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. Forget the encoding and you get a 401. This trips up nearly everyone once.

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

{
  "data": { },                  // object, or array for list endpoints
  "status_code": 1000,          // 1000 = success; 2xxx = client error; 3xxx = server
  "status_message": "Success",
  "timestamp": "2026-07-12T09:30:00Z"
}

Check status_code, not just the HTTP status. A 200 OK with status_code: 2001 means your request was understood but rejected. This split between transport status and application status is one of the first things the OCPI overview makes clear, and it matters more than people expect.

Locations: a GET request and its response

Locations is the map — where the stations are, what connectors they have, whether they are live. The eMSP most often reads it. 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 — 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 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 — common on a busy connection — 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.458,
  "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 changes. 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. The cdr_location here is a compact snapshot, not a full Location object — a distinction that surprises people who expect the two to match.

Why these break, and how to see it live

Reading three well-formed requests makes OCPI look easy. In practice the failures are mundane and repetitive:

  • Missing or unencoded Authorization token — a 401, or a 400 if the scheme word is dropped.
  • Path/body mismatch — the id in the URL disagrees with the id in the body on a Session PUT.
  • Wrong role in the path — pushing a Session to /cpo/ instead of /emsp/.
  • Ignoring status_code — treating a 200 OK with status_code: 2001 as a success.
  • Applying a stale PATCH — no last_updated check on the receiving side.

None of these show up until you send a real request against a real counterparty. That is exactly what the OCPI Simulator is for: paste these bodies in, watch the envelope come back, break a header on purpose, and see which status_code you get. Seeing the handshake and the push happen end to end teaches the protocol far faster than the spec PDF does — and it is the difference between “I read the OCPI examples” and “I can debug an OCPI integration.”

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>. A missing or unencoded token is the single most common cause of 401 and 400 errors when people first hit an OCPI endpoint.

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.