What a CDR is
When a charging session ends, the CPO issues exactly one Charge Detail Record to the eMSP. Unlike a Session object, which updates live, a CDR is final: it binds the measured energy, the charging periods and the exact tariff that applied into a signed statement of what happened and what it costs.
OCPI 2.2.1 dedicates an entire module to it because roaming settlement between companies happens CDR by CDR. The eMSP invoices its driver from the CDR and settles with the CPO against the same object. Two companies' accounts reconcile through it, which is why the design prioritises auditability over convenience.
The fields that matter
| Field | Why it matters |
|---|---|
charging_periods | The session sliced by dimension changes — energy per period, flat fees, parking time. The audit trail behind the total. |
tariffs | The full tariff snapshot embedded, not referenced. The price that applied, immune to later tariff edits. |
total_cost | The invoice-ready figure, split into excl_vat and incl_vat. |
total_energy | Total kWh. Must reconcile with the sum of period energy dimensions. |
total_time | Total session duration in hours, as a decimal. |
session_id | The join key back to the live Session, and on our platforms to the OCPP transaction underneath. |
cdr_token | Who charged — the token and contract this bills to. |
cdr_location | Where, snapshotted. Not a reference — a copy, so the record survives the location being decommissioned. |
last_updated | When the CDR was issued. |
Example: a real-world CDR
Trimmed to the essential shape — one session, two charging periods where a power tier changed mid-session, energy-based tariff:
{
"country_code": "IN", "party_id": "EFI",
"id": "cdr-2026-081142",
"start_date_time": "2026-08-11T09:12:00Z",
"end_date_time": "2026-08-11T10:03:41Z",
"session_id": "sess-77c1",
"cdr_token": { "uid": "DR-88231", "type": "APP_USER", "contract_id": "IN-EMS-C0142" },
"auth_method": "COMMAND",
"authorization_reference": "auth-4471",
"cdr_location": {
"id": "LOC-ND-014", "name": "Sector 62 Hub",
"address": "Block A, Sector 62", "city": "Noida", "country": "IND",
"coordinates": { "latitude": "28.627400", "longitude": "77.372200" },
"evse_uid": "EVSE-014-02", "evse_id": "IN*EFI*E014002",
"connector_id": "1",
"connector_standard": "IEC_62196_T2_COMBO",
"connector_format": "CABLE", "connector_power_type": "DC"
},
"charging_periods": [
{ "start_date_time": "2026-08-11T09:12:00Z",
"dimensions": [{ "type": "ENERGY", "volume": 18.400 }],
"tariff_id": "TRF-DC-STD" },
{ "start_date_time": "2026-08-11T09:47:10Z",
"dimensions": [{ "type": "ENERGY", "volume": 6.150 }],
"tariff_id": "TRF-DC-STD" }
],
"tariffs": [{ "id": "TRF-DC-STD", "currency": "INR", "elements": [{
"price_components": [{ "type": "ENERGY", "price": 18.50, "step_size": 1 }] }] }],
"total_energy": 24.550,
"total_time": 0.8614,
"total_cost": { "excl_vat": 454.18, "incl_vat": 535.93 },
"currency": "INR",
"last_updated": "2026-08-11T10:04:02Z"
}
Validate on receipt, not at invoice time
A malformed CDR discovered during month-end billing is a crisis. The same CDR rejected at ingestion is a support ticket. Validate these on arrival:
| Check | Rule |
|---|---|
| Energy reconciliation | Sum of ENERGY dimensions across periods must equal total_energy within rounding tolerance |
| Time reconciliation | end_date_time minus start_date_time should match total_time |
| Tariff presence | A non-zero total_cost with no embedded tariffs is unbillable — reject it |
| Currency consistency | currency must match the embedded tariff's currency |
| Duplicate detection | id is unique per party; a repeat is a retry, not a second charge |
| Session linkage | session_id should match a known session, though absence is legitimate for offline charging |
| Period ordering | Period start times must be ascending and fall within the session window |
Reject with a clear OCPI error code rather than silently accepting. A partner cannot fix a problem you never told them about, and a CDR you accepted is a CDR you have implicitly agreed to settle.
Immutability and credit CDRs
OCPI treats CDRs as immutable. There is no edit. When a CDR is wrong — a metering fault, a misapplied tariff, a duplicate — the correction is a new credit CDR that references the original.
This preserves the audit trail: the original stands as issued, the credit records the correction, and the net is the truth. Both companies' books can be reconstructed at any point in time.
Practically this means:
- Never update a stored CDR in place. Your storage should be append-only for this object.
- Handle negative amounts. Credit CDRs carry negative
total_cost. Billing code that assumes positive values will produce nonsense. - Net at the account level, not the record level. Show the driver both entries or a corrected total — but keep both.
- Expect corrections weeks later. Settlement windows must tolerate a credit against a period already closed.
Late CDRs and settlement windows
CDRs do not arrive promptly and cannot be made to. A charger that was offline during a session delivers its CDR when it reconnects — hours or days later. Your settlement design must absorb this rather than assume same-day delivery.
- Do not close a settlement period the moment it ends. Leave a grace window before finalising.
- Bill by session date, not receipt date. A CDR arriving on the 3rd for a session on the 30th belongs to the previous month.
- Alert on ageing gaps. Sessions completed with no CDR after a defined window are unbilled revenue — that report pays for itself.
- Track per-partner delivery latency. The distribution tells you which partner is about to cause a month-end problem.
Production lessons
- Reconcile every CDR against its OCPP StopTransaction meter reading. The mismatch rate is your platform's honesty metric, and it is the number a partner audit will ask for.
- CDRs are immutable — corrections are credit CDRs, never edits. Auditors and partners both depend on that.
- Validate that period volumes sum to
total_energyon receipt and reject with a clear error rather than invoicing a discrepancy. - Design settlement windows to tolerate late CDRs instead of assuming same-day delivery.
- Store the raw CDR exactly as received alongside your parsed version. When a partner disputes a figure, the original payload ends the argument.
- Never derive a CDR from a Session. If a CDR has not arrived, the correct state is unbilled, not estimated.
Frequently asked questions
A Session is the live, updating view of an ongoing charge; the CDR is the single final record issued after completion, with the binding tariff snapshot and totals used for settlement.
The CPO creates and pushes it to the eMSP, or the eMSP pulls it. The eMSP invoices the driver from it and settles with the CPO against it.
Not by editing — OCPI treats CDRs as immutable. Corrections are handled with a new credit CDR referencing the original, preserving the audit trail. Credit CDRs carry negative totals, so billing code must handle negative amounts.
So the record cannot change meaning later. If a CDR merely pointed at a tariff ID, editing that tariff months afterwards would silently rewrite historical invoices. Embedding the full snapshot makes the price that applied permanent.
Hours or days. A charger that was offline during a session only delivers its CDR when it reconnects. Settlement windows should include a grace period, and billing should be keyed on session date rather than receipt date.