The structure
Three nested concepts, and the nesting is the whole model.
| Concept | What it is |
|---|---|
| Tariff | The container: an ID, a currency, and a list of elements |
| TariffElement | A set of price components plus optional restrictions governing when they apply |
| PriceComponent | A type, a price, and a step_size |
The four component types
| Type | Charged per | Typical use |
|---|---|---|
ENERGY | kWh consumed | The main charge on DC fast charging |
TIME | Hour of charging | Common on AC, where energy rate is slow |
PARKING_TIME | Hour connected but not charging | Idle fees to free up bays |
FLAT | Session | A fixed connection or session fee |
A single tariff can carry several types at once — a flat connection fee, an energy rate, and an idle fee that begins after charging completes. That is normal, and it is why a driver's final bill rarely equals kWh multiplied by a headline rate.
Restrictions decide which element applies
Restrictions are what turn a static price list into time-of-use, power-tiered, or customer-specific pricing.
| Restriction | Applies the element when |
|---|---|
start_time / end_time | Within a time of day — the basis of peak/off-peak |
start_date / end_date | Within a date range — promotions, seasonal rates |
min_kwh / max_kwh | Consumption falls in a band |
min_power / max_power | Delivered power falls in a band — tiered DC pricing |
min_duration / max_duration | Session length falls in a band — how idle fees start after N minutes |
day_of_week | On listed days — weekday vs weekend |
reservation | The session came from a reservation |
Example: peak/off-peak energy tariff
{
"country_code": "IN", "party_id": "EFI",
"id": "TRF-DC-STD",
"currency": "INR",
"type": "REGULAR",
"elements": [
{
"price_components": [
{ "type": "ENERGY", "price": 22.00, "step_size": 1 }
],
"restrictions": { "start_time": "09:00", "end_time": "22:00" }
},
{
"price_components": [
{ "type": "ENERGY", "price": 14.50, "step_size": 1 }
],
"restrictions": { "start_time": "22:00", "end_time": "09:00" }
},
{
"price_components": [
{ "type": "PARKING_TIME", "price": 120.00, "step_size": 300 }
],
"restrictions": { "min_duration": 2700 }
}
],
"last_updated": "2026-08-01T00:00:00Z"
}Reading it: 22 INR/kWh during the day, 14.50 overnight, and after 45 minutes connected an idle fee of 120 INR per hour billed in 5-minute blocks. A session spanning 22:00 crosses the boundary and is billed at both rates, which is exactly why the CDR splits into charging periods.
step_size is where the money leaks
step_size is the rounding unit, and it is the least understood field in the module. It defines the increment in which a dimension is billed.
| Component type | step_size unit | Example |
|---|---|---|
ENERGY | Watt-hours | 1 = per Wh; 1000 = rounded up to whole kWh |
TIME | Seconds | 60 = per minute; 900 = per 15-minute block |
PARKING_TIME | Seconds | 300 = per 5-minute block |
FLAT | Not applicable | Charged once |
Rounding is upward. A step_size of 1000 on energy means a 24.55 kWh session bills as 25 kWh. On a per-session basis that is trivial; across a network it is a material revenue difference, and across a roaming relationship it is a reconciliation gap if the two sides implement rounding differently.
When your computed total disagrees with a partner's CDR by a small amount, step_size is the first thing to check.
How tariffs reach the driver
Tariffs connect to infrastructure through tariff_ids on the Connector object in the Locations module. That is the only link, and it has consequences:
- Pricing is per connector, not per site. A location's AC and DC connectors will normally carry different tariffs.
- A connector can list several tariff IDs, in which case restrictions decide which applies. This is how a CPO offers a member rate and a walk-up rate at the same socket.
- A missing
tariff_idsmeans you cannot show a price. Displaying “price unavailable” is correct; guessing is not.
OCPI 2.2.1 also supports type values including AD_HOC_PAYMENT, PROFILE_CHEAP, PROFILE_FAST and PROFILE_GREEN, which let a CPO publish alternative tariffs for smart-charging preferences.
Lessons from partner integrations
- Never cache a tariff without honouring
last_updated. Price changes propagate through this field, and a stale cached tariff means quoting a price you cannot honour. - Recompute and compare against every CDR. Independently calculating what you expected and diffing against the partner's figure catches tariff misinterpretation before customers do.
- Check element ordering during certification. A catch-all placed first is a common partner-side error and it silently flattens their own pricing.
- Show the driver the components, not just a rate. “22 INR/kWh, plus idle fee after 45 min” prevents the complaint that a headline rate cannot.
- Handle currency explicitly. Tariff currency is authoritative; never assume the driver's home currency.
- Expect a session to span multiple tariff elements. Any UI showing one price per session will misrepresent time-of-use tariffs.
Frequently asked questions
As composable data. A Tariff contains elements; each element holds price components (ENERGY, TIME, PARKING_TIME or FLAT) plus optional restrictions that decide when it applies. Cost is computed by walking the session and applying whichever elements match at each point.
The rounding increment for a price component, always rounded upward. For ENERGY it is in watt-hours, so 1000 means billing rounds up to whole kWh. For TIME and PARKING_TIME it is in seconds, so 900 means 15-minute blocks. Mismatched step_size handling is a common cause of small reconciliation gaps.
Elements are evaluated in order and the first match wins for a given dimension. An unrestricted catch-all element placed first will mask every restricted element after it, flattening time-of-use pricing into a single rate.
Through tariff_ids on the Connector object in the Locations module. Pricing is therefore per connector rather than per site, and a connector can list several tariff IDs with restrictions deciding which applies.
Yes. A session that crosses a peak/off-peak boundary is billed at both rates. This is exactly why the CDR splits a session into charging periods, each carrying the dimensions and tariff that applied.