OCPI MODULE · UPDATED 11 AUG 2026

CDR: the record money depends on

A CDR is the immutable, final record of a charging session — the object invoices, settlements and audits are built on. Get sessions wrong and drivers complain; get CDRs wrong and finance calls.

PART OF THE OCPI 2.2.1 COMPLETE GUIDE · 15 MODULE & COMMAND GUIDES

IN ONE PARAGRAPH

When a session ends the CPO issues exactly one Charge Detail Record to the eMSP. Unlike a Session it never changes: it binds measured energy, the charging periods and the exact tariff that applied into a final statement. Roaming settlement between companies happens CDR by CDR, which is why immutability is enforced rather than merely encouraged.

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

FieldWhy it matters
charging_periodsThe session sliced by dimension changes — energy per period, flat fees, parking time. The audit trail behind the total.
tariffsThe full tariff snapshot embedded, not referenced. The price that applied, immune to later tariff edits.
total_costThe invoice-ready figure, split into excl_vat and incl_vat.
total_energyTotal kWh. Must reconcile with the sum of period energy dimensions.
total_timeTotal session duration in hours, as a decimal.
session_idThe join key back to the live Session, and on our platforms to the OCPP transaction underneath.
cdr_tokenWho charged — the token and contract this bills to.
cdr_locationWhere, snapshotted. Not a reference — a copy, so the record survives the location being decommissioned.
last_updatedWhen the CDR was issued.
Notice the pattern: CDRs embed, they do not reference. The tariff is copied in, not linked. The location is copied in, not linked. This is deliberate — a financial record that changes meaning because someone edited a tariff six months later is not a financial record. Expect CDRs to be large, and do not try to normalise the embedded copies away in storage.

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:

CheckRule
Energy reconciliationSum of ENERGY dimensions across periods must equal total_energy within rounding tolerance
Time reconciliationend_date_time minus start_date_time should match total_time
Tariff presenceA non-zero total_cost with no embedded tariffs is unbillable — reject it
Currency consistencycurrency must match the embedded tariff's currency
Duplicate detectionid is unique per party; a repeat is a retry, not a second charge
Session linkagesession_id should match a known session, though absence is legitimate for offline charging
Period orderingPeriod 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_energy on 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

How is a CDR different from a Session?

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.

Who creates the CDR?

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.

Can a CDR be corrected?

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.

Why do OCPI CDRs embed the tariff instead of referencing it?

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.

How late can a CDR arrive?

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.