OCPI Tariff Dimensions: ENERGY, TIME, FLAT & PARKING_TIME

The four OCPI tariff dimensions in depth: ENERGY, TIME, FLAT and PARKING_TIME, how step_size rounding works, and how restrictions gate each one.

OCPI tariff dimensions are the building blocks of every EV charging bill. When a driver in Amsterdam or Atlanta pays for a session, the amount is assembled from tariff dimensions: energy, time, a session fee, an idle fee, each applied under its own rules. Get the dimensions right and the bill is deterministic and can be disputed line by line. Get them wrong and you produce charges drivers notice. This guide covers every tariff dimension in OCPI 2.2.1, how step_size rounding works, how restrictions narrow when a dimension applies, and the pricing patterns they enable, with worked examples you can reconcile by hand.

This deep dive sits under the OCPI Tariffs module. If you are new to the protocol, start with what OCPI is; if you want to see how tariffs relate to the charger-side protocols, see OCPI vs OCPP vs ISO 15118.

Start with a tariff you can read

Before the definitions, here is a complete, valid OCPI tariff:

{
  "id": "tariff-1",
  "currency": "EUR",
  "type": "REGULAR",
  "elements": [
    {
      "price_components": [
        { "type": "ENERGY", "price": 0.35, "step_size": 1 },
        { "type": "FLAT", "price": 1.00, "step_size": 1 }
      ]
    }
  ]
}

That is €0.35 per kWh plus a €1 session fee. A 20 kWh session costs €1 + (20 × €0.35) = €8. Nothing else is required: no restrictions, no second element, no extra modules.

Everything in this article is a variation on that shape. The pricing lives in price_components, and the only thing that changes between a corner-shop AC point and a 350 kW hub on a motorway is which components are in the array and what narrows them.

The four tariff dimensions

Each price component has a type, drawn from the OCPI TariffDimensionType enum. The tariff above uses two of them. OCPI 2.2.1 defines exactly four:

typeWhat it charges forPrice is perstep_size counts
ENERGYElectricity deliveredkWhunits of 1 Wh
TIMETime spent actively charginghourunits of 1 second
PARKING_TIMETime plugged in but not charging (the basis for idle fees)hourunits of 1 second
FLATA fixed fee, once per sessionsessionno meaningful unit

Two of those columns encode the price differently from the way people say it out loud, so they are worth pausing on.

Price is per hour for the time dimensions, not per minute. An idle fee you think of as “20 cents a minute” is "price": 12.00 in the JSON. Writing 0.20 there charges 20 cents an hour, and nobody notices until a driver leaves a car plugged in overnight for three cents.

step_size is a count of base units, not a duration. For TIME and PARKING_TIME the base unit is one second, so billing in whole minutes is "step_size": 60. For ENERGY the base unit is one watt-hour, so whole-kWh billing is "step_size": 1000, and "step_size": 1 bills to the watt-hour. Misreading this one field is what produces a bill that is off by a whole unit on every session, so it gets its own section later.

That is the complete list. There is no RESERVATION_TIME, IDLE_TIME, or POWER price dimension. Those names do appear elsewhere in OCPI, as CDR measurement dimensions that record what happened rather than what it costs, but you cannot put them in price_components. Reservation pricing is built from TIME and FLAT with a reservation restriction; see Pricing a reservation below.

Alongside type, each price component carries:

  • price: per-unit cost in the tariff’s currency, in the units above.
  • step_size: the minimum billable increment, as a multiplier of the dimension’s base unit.
  • vat: the VAT percentage for this dimension, if the tariff prices tax per component.

A single element can hold several components at once, and they all apply to the same session. What a session actually incurs is what lands on the CDR:

flowchart LR
  S[Session] --> E[ENERGY<br/>per kWh]
  S --> T[TIME<br/>per hour charging]
  S --> F[FLAT<br/>per session]
  S --> P[PARKING_TIME<br/>idle fee]
  E --> C[CDR total cost]
  T --> C
  F --> C
  P --> C
  style C fill:#e0f2fe,stroke:#0369a1

The elements array can hold multiple elements with different restrictions. The usual reason to reach for a second element is time of use: one element for peak hours, another for off-peak.

Restrictions: narrowing when dimensions apply

The restrictions object on each element controls when its price_components apply.

Common restrictions:

day_of_week: the days (MONDAY, TUESDAY, and so on) on which this element applies.

start_time / end_time: a time-of-day range in HH:MM. This is the peak/off-peak lever.

start_date / end_date: a date range, for seasonal pricing.

min_kwh / max_kwh: applies only when session energy falls inside the range.

min_current / max_current: applies only at certain charging currents.

min_power / max_power: applies only at certain power levels.

min_duration / max_duration: applies only to sessions of a certain length.

reservation: a ReservationRestrictionType that marks an element as pricing the reservation rather than the charging session itself (covered in Pricing a reservation below).

A peak/off-peak tariff:

{
  "elements": [
    {
      "restrictions": {
        "day_of_week": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"],
        "start_time": "07:00",
        "end_time": "20:00"
      },
      "price_components": [
        { "type": "ENERGY", "price": 0.45, "step_size": 1 }
      ]
    },
    {
      "restrictions": {
        "day_of_week": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"],
        "start_time": "20:00",
        "end_time": "07:00"
      },
      "price_components": [
        { "type": "ENERGY", "price": 0.25, "step_size": 1 }
      ]
    },
    {
      "restrictions": {
        "day_of_week": ["SATURDAY", "SUNDAY"]
      },
      "price_components": [
        { "type": "ENERGY", "price": 0.30, "step_size": 1 }
      ]
    }
  ]
}

Three elements: weekday-peak (€0.45), weekday-off-peak (€0.25), weekend-flat (€0.30). The session’s actual cost depends on when energy flowed.

The second element is the one to test. It runs from 20:00 to 07:00, so it wraps past midnight, and code that compares the two times as plain numbers within a single day will match nothing at all. A wrapping window has to be evaluated as two intervals, or as “not inside the complementary window.”

Pricing a reservation

There is no RESERVATION_TIME price dimension. Instead, OCPI 2.2.1 prices a reservation by adding a tariff element with a reservation restriction, and that reserved-only element may use only the FLAT and TIME dimensions, where TIME here means the duration of the reservation rather than charging time. A flat reservation fee plus a per-hour hold charge looks like this:

{
  "elements": [
    {
      "restrictions": { "reservation": "RESERVATION" },
      "price_components": [
        { "type": "FLAT", "price": 0.50, "step_size": 1 },
        { "type": "TIME", "price": 5.00, "step_size": 60 }
      ]
    },
    {
      "price_components": [
        { "type": "ENERGY", "price": 0.35, "step_size": 1000 }
      ]
    }
  ]
}

The first element only applies to the reservation: a €0.50 flat hold plus €5.00 per hour of reserved time, billed in 60-second steps. The second element prices the actual charging. That is how you model “we hold the stall for you, and there is a fee if you reserve” without inventing a dimension the protocol does not have.

The reservation itself is placed through the Commands module, which carries a RESERVE_NOW from the eMSP to the CPO; the charger-side half of the same handshake is covered in how OCPP reservations work. The tariff only prices the hold. It has no opinion about whether the hold succeeded.

Step sizes and rounding

The step_size field sets billing granularity, and it counts base units rather than the unit you quote the price in.

For ENERGY, step_size is in watt-hours. 1000 bills per whole kWh; 1 bills to the watt-hour.

For TIME and PARKING_TIME, step_size is in seconds. 900 bills in 15-minute blocks; 60 bills per minute.

For FLAT, 1 is the conventional value, since a once-per-session fee has nothing to step through.

The rule is always round up. Each consumption unit is rounded up to the next step boundary, so a session that delivered 5.3 kWh on a step_size of 1000 Wh bills as 6 kWh, and a 10-minute session on a 900-second step bills as 15 minutes.

That makes step_size a pricing decision rather than an encoding detail. A coarse step is a blunt way to discourage very short stall occupancy; a fine step gives a driver a number they can check against the meter reading on the screen. Choose the step for the behavior you want, then make sure the receipt text explains it, because a driver who charged for ten minutes and paid for fifteen will otherwise read it as a fault.

Tariff types

The type field on the tariff classifies it, using the OCPI TariffType enum:

  • REGULAR: the default tariff, valid when a driver uses an RFID/token with no charging preference set.
  • AD_HOC_PAYMENT: for drive-up users paying directly at the charger, by card rather than through an eMSP.
  • PROFILE_CHEAP: the tariff that applies when the driver’s charging preference is CHEAP.
  • PROFILE_FAST: the tariff that applies when the charging preference is FAST.
  • PROFILE_GREEN: the tariff that applies when the charging preference is GREEN.

A CPO can publish several tariffs of different types for the same connector, and the type is what selects between them. AD_HOC_PAYMENT exists because a driver paying by card at the terminal has no roaming contract behind them: there is no eMSP to settle with and no roaming price to apply, so the drive-up price has to live somewhere separate. The PROFILE_* types are the other half of the charging preferences mechanism, covered next.

Tariff profiles and charging preferences

The PROFILE_CHEAP, PROFILE_FAST, and PROFILE_GREEN tariff types do not stand alone. They are the pricing half of OCPI 2.2.1’s Charging Preferences feature, which is defined in the Sessions module. The ProfileType enum a driver can express is CHEAP, FAST, GREEN, or REGULAR:

  • CHEAP: the driver wants the cheapest charging possible.
  • FAST: the driver wants to charge as quickly as possible and will pay a premium.
  • GREEN: the driver wants as much renewable energy as possible.
  • REGULAR: no special preference.

The flow ties the two modules together:

  1. The CPO publishes tariffs, at least one per ProfileType it supports (the spec requires that for every supported preference a matching tariff exists).
  2. The driver picks a preference in their eMSP app, say “cheapest.”
  3. The eMSP sends it on the session, via a PUT to the session’s charging_preferences endpoint, carrying a ChargingPreferences object (profile_type, and optionally departure_time, energy_need, discharge_allowed).
  4. The CPO applies the tariff whose type matches, here PROFILE_CHEAP, and confirms whether the preference can be honored.

So a road-tripper on I-95 who wants speed and a commuter in Munich who wants the cheapest overnight rate get different tariffs on the same charger, without the CPO proliferating separate parties or hidden discount fields. The preference selects the tariff; the tariff dimensions still do the pricing math.

Real-world tariff patterns

Here is how recognizable pricing structures map onto elements and components.

Pure per-kWh (simple)

ENERGY: 0.35/kWh

One element, one dimension. Easy.

Per-kWh + session fee

ENERGY: 0.35/kWh
FLAT: 1.00 per session

The flat fee covers the per-session overhead that does not scale with kWh: the payment authorization, the backend round trips, the fixed slice of transaction cost that a 3 kWh session incurs just as surely as a 60 kWh one.

Per-kWh + idle fee after a grace period

ENERGY: 0.35/kWh always
PARKING_TIME: 0.05/minute, restriction min_duration: 1800 (30 minutes after charging completes)

Drivers charge at the normal rate, and once charging finishes and the car is still plugged in, the idle fee starts after the grace period. The grace period is the parameter worth arguing about: too short and you bill a driver who is walking back across the car park, too long and the stall stays blocked at exactly the hours it is most contested. For the other half of site economics, where the cost driver is peak kW rather than stall minutes, see peak demand charges in EV charging (coming soon).

Peak/off-peak time-of-use

Two elements with day_of_week and start_time/end_time restrictions, different ENERGY prices.

Demand-based (per-power)

ENERGY: 0.30/kWh, restriction max_power: 50 kW
ENERGY: 0.45/kWh, restriction min_power: 50 kW

Slow charging is cheaper; fast charging costs more. It nudges drivers who do not need speed onto the slower hardware.

Tiered by session size

ENERGY: 0.40/kWh, restriction max_kwh: 20 (first 20 kWh)
ENERGY: 0.30/kWh, restriction min_kwh: 20 (above 20 kWh)

Encourages deeper sessions, and it is the natural shape for a volume agreement where the buyer commits to kWh depth rather than visit frequency.

Free for short sessions

FLAT: 0, restriction max_duration: 1800 (first 30 minutes free)
FLAT: 2.00, restriction min_duration: 1800 (€2 fee after 30 minutes)
ENERGY: 0.35/kWh always

Free for quick top-ups, charged once the stay runs long. Watch the boundary. Both elements can match a session of exactly 1800 seconds, so decide which side owns the boundary, write it into the tariff, and test that case explicitly.

CDR breakdown

A CDR (Charge Detail Record) reports the cost per dimension. Each element of the tariff that applied during the session produces a line in the CDR.

A CDR might look like:

{
  "id": "cdr-12345",
  "total_cost": { "excl_vat": 8.20, "incl_vat": 9.84 },
  "total_energy": 20.5,
  "total_time": 1.0,
  "charging_periods": [
    {
      "start_date_time": "2026-06-26T19:00:00Z",
      "dimensions": [
        { "type": "ENERGY", "volume": 12.5 },
        { "type": "TIME", "volume": 1800 }
      ],
      "tariff_id": "tariff-1"
    },
    {
      "start_date_time": "2026-06-26T19:30:00Z",
      "dimensions": [
        { "type": "ENERGY", "volume": 8.0 },
        { "type": "TIME", "volume": 1800 }
      ],
      "tariff_id": "tariff-1"
    }
  ]
}

Each charging_period documents what was consumed in that period. The eMSP can reconcile the math against the tariff to verify the cost calculation.

Currency and VAT

OCPI is currency-aware. Each tariff specifies a currency (ISO 4217 code).

VAT (Value-Added Tax) is expressed as a percentage on each price component, in the optional vat field:

{ "type": "ENERGY", "price": 0.35, "step_size": 1000, "vat": 19.0 }

Here the price (€0.35) is the base amount and vat (19%) is the tax applied to it. The tariff’s tax_included flag (YES, NO, or N/A) tells receivers whether the prices already include tax. The total_cost in the CDR is a Price object that breaks the figure into excl_vat and incl_vat so the tax is always explicit.

The reason vat sits on the component rather than the tariff is that one bill can carry components taxed at different rates: an energy sale and a parking service are not always the same tax object in the same jurisdiction. Design for that early. Correcting tax math after invoices have gone out means reissuing them, and in a roaming relationship it means reissuing someone else’s invoices too, which is a conversation about money between two companies rather than a code fix (see who pays whom in OCPI roaming).

Common implementation pitfalls

Each of these has a distinct signature in the resulting bill.

Hard-coded dimension types. Code that only knows about ENERGY and FLAT silently drops PARKING_TIME values (and any reservation pricing). Bills come out too low, and idle fees vanish.

Wrong step_size interpretation. Treating step_size as “round to nearest” instead of “round up to next” produces bills that are slightly off, and always in the direction that costs the CPO money.

Time zone confusion. Tariff restrictions are evaluated in the tariff’s timezone. Mismatch that against the session’s actual local time and you apply the wrong element, most visibly on either side of a peak boundary or a daylight-saving change.

Multiple applicable elements. Two elements both match because their restrictions overlap. Which one wins is not something you want to discover from a CDR. Design tariffs with non-overlapping elements and the question never arises.

Currency conversion. A tariff in EUR, a CDR in EUR, but the eMSP’s billing in USD. Conversion adds a question the protocol does not answer: at what rate, fixed as of when?

Tariff updates mid-session. A CPO updates a tariff while a session is in progress. Which price applies? Bill from the tariff_id the CDR records on each charging period, not from the tariff you re-fetch afterwards. The CDR is the record of what was applied; the tariff endpoint only tells you what applies now.

These are the tariff-specific traps. The wider set, across modules, is in OCPI integration pitfalls.

Best practices

Test tariff math against the CPO’s billing engine. The CPO issues the charge, so the CPO’s arithmetic is the one that has to be reproduced. If yours disagrees, yours is the one to explain.

Validate every CDR against the published tariff. Recomputing the total from the dimensions catches interpretation bugs while they are still cheap.

Maintain a per-partner tariff translation layer. Two partners can read the same restriction differently. Normalize on ingest so the difference lives in one place instead of scattered through your billing code.

Show the tariff before the session starts. OCPI lets an eMSP fetch the tariff ahead of authorization, so the app can show an estimate. An estimate seen up front turns a disputed charge into an expected one.

Audit on a sample basis. Pull a fixed sample of CDRs each month and reconcile them by hand against the published tariff. Drift shows up in the sample long before it shows up in a complaint.

Key takeaways

OCPI’s four tariff dimensions are expressive, and the cost of that expressiveness is that the same pricing intent can be encoded several ways, not all of which two implementations will read alike. The essentials to hold onto:

  • There are exactly four tariff price dimensions in OCPI 2.2.1: ENERGY, TIME, PARKING_TIME, FLAT. RESERVATION_TIME, POWER, and similar names are CDR measurement dimensions, not tariff price dimensions.
  • step_size always rounds up to the next multiple of the base unit (1 Wh for ENERGY, 1 second for TIME and PARKING_TIME). Never round to nearest.
  • Restrictions narrow when an element applies (time, day, power, energy, duration), and reservation pricing is done with FLAT/TIME under a reservation restriction rather than a separate dimension.
  • The PROFILE_CHEAP/FAST/GREEN tariff types are one half of Charging Preferences, defined in the Sessions module; the driver’s preference selects which tariff applies.
  • Bill from the tariff_id on each CDR charging period, break VAT out into excl_vat/incl_vat, and reconcile a sample of CDRs against the published tariff.

Underneath all of this sits a fact the spec never states outright: a tariff is a calculation that two companies perform separately and then have to agree on. The CPO computes the CDR, the eMSP recomputes it to check, and neither can see the other’s code. Every ambiguity left in the encoding turns into a dispute between two implementations that are each behaving correctly by their own reading: an overlapping element, an unowned boundary at exactly 1800 seconds, a window that wraps past midnight, an FX rate with no fixing date. That is why the real test of a tariff design is not whether it captures your pricing. It is whether a stranger’s code, working from the JSON alone, arrives at the same number you did.

OCPI 2.2.1 (June 2020) is the current mainstream release for this work, and 2.2 (March 2020) is a distinct release rather than a patch, so a partner still on 2.2 is not automatically compatible with what you assumed. OCPI 2.3.0 was published in February 2025. Before you rely on any version-specific behavior, check OCPI 2.2 vs 2.2.1, the OCPI version history, and the official OCPI changelog.

Quick check

Q1. Which tariff dimension charges for time when a car is plugged in but no longer actively charging?
Q2. A tariff is updated by the CPO while a session is in progress. Which tariff governs the session under OCPI?
Q3. How does OCPI apply step_size when billing a dimension?
Q4. Which tariff type is used for a drive-up customer paying directly at the charger without an eMSP?
Q5. Why should VAT and currency handling be designed correctly early in a tariff implementation?

Frequently asked questions

Why are there so many tariff dimensions?

Real-world pricing is multi-dimensional. A tariff might charge €0.35/kWh for energy, €0.05/minute after the first 30 minutes for occupancy, plus a €1 session fee. Each component is a different dimension. The flexibility lets a CPO model its actual pricing rather than approximating it.

Can I stack multiple dimensions in one tariff?

Yes. A single tariff element can hold ENERGY, TIME and FLAT price components active at the same time, each with its own price and step_size, and you split them across separate elements when a dimension needs its own restrictions. The CDR breaks out the cost per dimension so a driver can see what they paid for.

How do tariff restrictions work?

Restrictions narrow when a dimension applies, using day of week, time of day, date range, energy delivered, current, power or session duration. A single tariff can price peak and off-peak differently by putting two elements with different time restrictions around the same dimension.

How do the PROFILE_CHEAP, PROFILE_FAST, and PROFILE_GREEN tariff types work?

They are values of the tariff `type` field tied to OCPI 2.2.1 Charging Preferences. The driver picks a preference (CHEAP, FAST, or GREEN) in their app; the eMSP sends it on the session; and the CPO applies the tariff whose type matches. The spec requires that for every ProfileType a CPO supports, a matching tariff is provided.

Found this useful? Share it.