Sessions and CDRs are where roaming turns into money. A Session tells the eMSP what is happening right now, so its app can show energy delivered and running cost. A CDR (Charge Detail Record) is the final, immutable record the eMSP bills the driver from and the CPO invoices the eMSP from. Getting CDRs wrong is the most expensive OCPI bug there is, because it surfaces weeks later in a settlement dispute.
This is part 6 of the OCPI 2.2.1 series. Both modules are owned by the CPO.
Sessions
The CPO creates a session with PUT on the eMSP's Sessions Receiver as soon as it knows about it, and updates it with PATCH:
PUT /ocpi/emsp/2.2.1/sessions/DE/CPX/SESS-000123 HTTP/1.1
Host: emsp.example.com
Authorization: Token ZWJmM2IzOTktNzc5Zi00NDk3LTliOWQtYWM2YWQzY2M0NGQy
Content-Type: application/json
X-Request-ID: 3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9
X-Correlation-ID: 3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9{
"country_code": "DE",
"party_id": "CPX",
"id": "SESS-000123",
"start_date_time": "2026-10-26T10:15:02Z",
"kwh": 0,
"cdr_token": {
"country_code": "NL",
"party_id": "EXA",
"uid": "04A1B2C3D4E5F6",
"type": "RFID",
"contract_id": "NL-EXA-C12345678-X"
},
"auth_method": "AUTH_REQUEST",
"authorization_reference": "AUTH-7f3c91",
"location_id": "LOC-0042",
"evse_uid": "3256",
"connector_id": "1",
"currency": "EUR",
"status": "ACTIVE",
"last_updated": "2026-10-26T10:15:04Z"
}| Enum | Values |
|---|---|
SessionStatus | ACTIVE, COMPLETED, INVALID, PENDING, RESERVATION |
AuthMethod | AUTH_REQUEST (real-time authorize), COMMAND (started by START_SESSION), WHITELIST (local decision) |
PENDING covers the period after authorization but before energy flows, such as a remote start waiting for the cable. INVALID marks a session that will never produce a CDR, for example one that never actually started. RESERVATION is used for a session representing an active reservation.
During charging, PATCH the fields that changed:
{
"kwh": 12.48,
"total_cost": { "excl_vat": 3.74, "incl_vat": 4.49 },
"last_updated": "2026-10-26T10:45:00Z"
}How often to send these is a negotiation. Every OCPP MeterValues message is far too often; once a minute or on significant change is reasonable for most partners, and some eMSPs specify a limit. End the session with a PATCH that sets status: COMPLETED and end_date_time.
Charging periods and dimensions
Both sessions and CDRs carry charging_periods: an ordered list of ChargingPeriod objects, each starting at start_date_time and lasting until the next period starts (or the session ends). A new period begins whenever something that affects price changes: the tariff element that applies, or the state of charging (charging vs parked). Each period lists the dimensions consumed in it:
{
"start_date_time": "2026-10-26T10:15:02Z",
"dimensions": [
{ "type": "ENERGY", "volume": 18.2 },
{ "type": "TIME", "volume": 0.75 },
{ "type": "MAX_POWER", "volume": 48.6 }
],
"tariff_id": "DC-BASIC"
}CdrDimensionType | Unit / meaning |
|---|---|
ENERGY | kWh delivered in the period |
TIME | Hours charging |
PARKING_TIME | Hours connected but not charging |
RESERVATION_TIME | Hours reserved |
MAX_CURRENT / MIN_CURRENT | A, max/min during the period |
MAX_POWER / MIN_POWER | kW, max/min during the period |
CURRENT | Average current in A |
POWER | Average power in kW |
ENERGY_EXPORT / ENERGY_IMPORT | kWh exported / imported (relevant for bidirectional charging) |
STATE_OF_CHARGE | Vehicle SoC in percent |
Time-based volumes are in hours, as decimals. 45 minutes is 0.75, not 45 and not 2700. Some dimension types are intended for live sessions only and are marked in the specification as not to be used in CDRs; check the spec's table before putting instantaneous values such as STATE_OF_CHARGE into a CDR.
CDRs
When the session is complete and the final meter reading is known, the CPO creates a CDR with POST to the eMSP's CDRs Receiver:
POST /ocpi/emsp/2.2.1/cdrs HTTP/1.1
Host: emsp.example.com
Authorization: Token ZWJmM2IzOTktNzc5Zi00NDk3LTliOWQtYWM2YWQzY2M0NGQy
Content-Type: application/json
X-Request-ID: 6f5e4d3c-2b1a-4098-8f7e-6d5c4b3a2918
X-Correlation-ID: 6f5e4d3c-2b1a-4098-8f7e-6d5c4b3a2918{
"country_code": "DE",
"party_id": "CPX",
"id": "CDR-000123",
"start_date_time": "2026-10-26T10:15:02Z",
"end_date_time": "2026-10-26T11:25:00Z",
"session_id": "SESS-000123",
"cdr_token": {
"country_code": "NL",
"party_id": "EXA",
"uid": "04A1B2C3D4E5F6",
"type": "RFID",
"contract_id": "NL-EXA-C12345678-X"
},
"auth_method": "AUTH_REQUEST",
"authorization_reference": "AUTH-7f3c91",
"cdr_location": {
"id": "LOC-0042",
"address": "Hafenstrasse 12",
"city": "Hamburg",
"postal_code": "20457",
"country": "DEU",
"coordinates": { "latitude": "53.543210", "longitude": "9.987650" },
"evse_uid": "3256",
"evse_id": "DE*CPX*E0042*1",
"connector_id": "1",
"connector_standard": "IEC_62196_T2_COMBO",
"connector_format": "CABLE",
"connector_power_type": "DC"
},
"currency": "EUR",
"charging_periods": [
{
"start_date_time": "2026-10-26T10:15:02Z",
"dimensions": [
{ "type": "ENERGY", "volume": 24.0 },
{ "type": "TIME", "volume": 1.0 }
],
"tariff_id": "DC-BASIC"
},
{
"start_date_time": "2026-10-26T11:15:02Z",
"dimensions": [
{ "type": "PARKING_TIME", "volume": 0.1661 }
],
"tariff_id": "DC-BASIC"
}
],
"total_cost": { "excl_vat": 9.70, "incl_vat": 11.54 },
"total_energy": 24.0,
"total_energy_cost": { "excl_vat": 9.60, "incl_vat": 11.42 },
"total_time": 1.1661,
"total_parking_time": 0.1661,
"total_parking_cost": { "excl_vat": 0.10, "incl_vat": 0.12 },
"last_updated": "2026-10-26T11:25:30Z"
}The eMSP answers with a success status and a Location HTTP header pointing to where the CDR can be retrieved in its system. Store that URL; it is how you later check what the eMSP actually received.
Some fields that deserve care:
cdr_locationis a snapshot of the location at charging time, not a reference. If the site is renamed next month, old CDRs keep the old name. Build it from your data at session end.tariffscan embed the Tariff objects that were used, so the eMSP can verify the price even if the tariff has changed since.total_timeincludes parking time;total_parking_timeis the non-charging part of it.total_costis mandatory. The separate totals (total_fixed_cost,total_energy_cost,total_time_cost,total_parking_cost,total_reservation_cost) are optional but most eMSPs want them for invoice breakdowns.signed_datacan carry signed meter values for calibration-law (Eichrecht) compliance where it applies.- Amounts are numbers in the session's
currency. Decide on rounding (normally to the currency's minor unit) and make sure the parts add up tototal_cost; eMSPs do check.
Correcting a CDR: credit CDRs
CDRs are immutable. There is no PUT or PATCH on the CDRs Receiver. If a CDR was wrong, the correction is two new CDRs:
- A credit CDR with a new
id,credit: true,credit_reference_idset to the original CDR's ID, and negative amounts that cancel the original. - A new, correct CDR with yet another
id.
Partners reconcile by summing everything for the session. Never reuse an ID, and never try to "fix" a CDR by posting a different body with the same ID; a well-behaved eMSP will reject it, and a less well-behaved one will silently keep both.
Delivery, retries and pull
The CDRs Sender interface lets the eMSP pull with GET {cdrs_url}?date_from=..., and many eMSPs do so daily as a reconciliation step even when pushes are in place. On the CPO side:
- Keep CDRs in an outbox and retry pushes with backoff until you get
1000. A CDR that was never delivered is revenue you will struggle to claim later. - Make retries idempotent: posting the same CDR ID twice should be harmless. If the eMSP rejects a duplicate, record it as delivered rather than retrying forever.
- Alert on CDRs stuck in the outbox. A partner who changed its endpoint and forgot to tell you shows up here first.
Common mistakes
- No CDR for zero-energy sessions, or CDRs for sessions that never started. Agree with partners on how to handle very short or zero-kWh sessions; many expect either no CDR or a zero-cost one, and
Session.status = INVALIDfor the ones that will not be billed. - Using local time in
start_date_time. CDR times are UTC. - Charging periods that do not cover the session, overlap, or are out of order.
- Cost in the session that never matches the cost in the CDR. A live estimate can differ, but the CDR is final and must be computed from the same tariff the eMSP has.
Key takeaways
- Sessions are live, mutable and pushed with
PUT/PATCH; CDRs are final, immutable and created withPOST. cdr_tokencarries the eMSP's identifiers; top-level identifiers are the CPO's.- Time dimensions are in hours; energy in kWh; power in kW.
- Fix a CDR with a credit CDR plus a new CDR, never by editing.
- Treat CDR delivery as a guaranteed-delivery outbox.
Next: Part 7, Tariffs, where we compute the costs that go into these CDRs.

