PHP2Shell alert: critical PHP flaws CVE-2026-109618 / 109619 — check your servers and get patched fast
HashTech Systems
EV Charging
Part 7 of 10

OCPI 2.2.1 Tariffs: Elements, Restrictions and Price Calculation

How OCPI 2.2.1 tariffs are structured, how TariffDimensionType, step_size and restrictions work, and a worked price calculation with VAT.

HashTech Engineering · October 26, 2026 · 6 min read

Tariffs are the part of OCPI most often implemented "nearly right". The structure looks simple: a list of elements, each with price components and optional restrictions. The calculation rules are where CPOs and eMSPs quietly disagree, and a disagreement of a few cents per session becomes a reconciliation problem across thousands of CDRs.

This is part 7 of the OCPI 2.2.1 series. For the business side of tariff design, see EV charging pricing models.

What an OCPI tariff represents

A Tariff in OCPI is the price the CPO charges, published so the eMSP can show it to drivers and verify CDRs. What the eMSP charges its own driver is the eMSP's business and is not expressed in OCPI. Tariffs are owned by the CPO and linked to connectors via tariff_ids (see Part 4).

Structure

json
{
  "country_code": "DE",
  "party_id": "CPX",
  "id": "DC-STANDARD",
  "currency": "EUR",
  "type": "REGULAR",
  "tariff_alt_text": [
    { "language": "en", "text": "0.50 EUR per session, 0.40 EUR/kWh, 6.00 EUR/hour idle fee after charging" }
  ],
  "elements": [
    {
      "price_components": [
        { "type": "FLAT", "price": 0.50, "vat": 19.0, "step_size": 1 }
      ]
    },
    {
      "price_components": [
        { "type": "ENERGY", "price": 0.40, "vat": 19.0, "step_size": 1 }
      ]
    },
    {
      "price_components": [
        { "type": "PARKING_TIME", "price": 6.00, "vat": 19.0, "step_size": 300 }
      ]
    }
  ],
  "last_updated": "2026-10-01T00:00:00Z"
}
  • A Tariff has a list of elements (TariffElement), each with price_components and optional restrictions.
  • A PriceComponent has a type (TariffDimensionType), a price excluding VAT, an optional vat percentage, and a step_size.
  • type on the Tariff (TariffType) is optional: AD_HOC_PAYMENT, PROFILE_CHEAP, PROFILE_FAST, PROFILE_GREEN, REGULAR. It lets a CPO publish different tariffs for ad-hoc payment or for smart charging profiles on the same connector.
  • min_price and max_price cap the total; start_date_time and end_date_time bound validity.
  • tariff_alt_text is the human-readable description eMSPs show in apps. Write it; many apps fall back to it when they cannot render complex tariffs.
TariffDimensionTypeprice is perstep_size unit
ENERGYkWhWh
TIMEhour of chargingseconds
PARKING_TIMEhour connected but not chargingseconds
FLATsession1 (not meaningful)

Since 2.2, TIME means time spent charging and PARKING_TIME means time connected without charging. A tariff that wants to charge for total plugged-in time needs both components, typically at the same price.

Restrictions

A TariffElement may carry restrictions. The element only applies while all of its restrictions are satisfied:

RestrictionMeaning
start_time, end_timeTime of day, HH:MM, local time at the location. end_time before start_time wraps past midnight.
start_date, end_dateDate range, YYYY-MM-DD, local time.
day_of_weekList of MONDAY ... SUNDAY.
min_kwh, max_kwhApplies while session energy is within the range.
min_current, max_currentApplies while charging current is within the range (A).
min_power, max_powerApplies while charging power is within the range (kW).
min_duration, max_durationApplies while session duration is within the range, in seconds.
reservationRESERVATION or RESERVATION_EXPIRES: element applies to reservations.

Times are evaluated in the Location's time_zone, which is why that field is mandatory. A tariff with start_time: "18:00" means 18:00 at the charger, not UTC.

How elements are chosen

The evaluation rule is per dimension and per moment in time: for each dimension, the first element in the list whose restrictions are currently satisfied and that contains a price component of that dimension is the one that applies. Order therefore matters. Put restricted elements (peak hours, idle fee after 2 hours) before the unrestricted fallback, or the fallback will always win.

Example: an energy price of 0.50 EUR/kWh between 08:00 and 20:00 and 0.35 EUR/kWh otherwise:

json
{
  "elements": [
    {
      "price_components": [
        { "type": "ENERGY", "price": 0.50, "vat": 19.0, "step_size": 1 }
      ],
      "restrictions": { "start_time": "08:00", "end_time": "20:00" }
    },
    {
      "price_components": [
        { "type": "ENERGY", "price": 0.35, "vat": 19.0, "step_size": 1 }
      ]
    }
  ]
}

A session that spans 20:00 is split into charging periods at 20:00, each priced with its own element. This is why CDR charging_periods must start a new period whenever the applicable tariff element changes; without that split, the eMSP cannot reproduce the price.

step_size and rounding

step_size is the minimum billing block: amounts are rounded up to whole steps. With TIME and step_size: 300, 6 minutes of charging is billed as 10 minutes. When the same dimension is priced by several elements over a session, the 2.2.1 text applies the step rounding to the total for that dimension using the step size of the last applicable component, rather than rounding every period separately. This is one of the most commonly misread rules; agree the interpretation explicitly with each partner and test it with a multi-period session.

Worked example

Using the DC-STANDARD tariff above (flat 0.50, energy 0.40/kWh, parking 6.00/hour in 5-minute steps, 19% VAT). Session: charging from 10:15:02 to 11:15:02 delivering 24.0 kWh, then the car stays connected without charging until 11:25:00 (9 minutes 58 seconds).

ComponentVolumeBilled volumePrice excl. VAT
FLAT1 session10.50
ENERGY24.0 kWh24.000 kWh (step 1 Wh)24.0 x 0.40 = 9.60
PARKING_TIME598 s600 s (2 x 300 s) = 0.1667 h0.1667 x 6.00 = 1.00
Total11.10

VAT at 19%: 11.10 x 1.19 = 13.209, rounded to 13.21 EUR incl. VAT. Note how PARKING_TIME rounds up from 598 to 600 seconds; with a 1-second step the parking cost would have been 0.997, rounded to 1.00 anyway, but a 15-minute step would bill a full 1.50.

Distribution: push and pull

The CPO pushes tariffs to the eMSP's Tariffs Receiver with PUT {tariffs_url}/{country_code}/{party_id}/{tariff_id}, and can remove one with DELETE on the same URL; tariffs are one of the few objects with a DELETE. eMSPs can also pull from the CPO's Sender with GET {tariffs_url}?date_from=.... Change tariffs by publishing a new tariff with a future start_date_time (or a new ID) rather than editing prices in place, so CDRs for sessions that straddle the change remain explainable.

Gotchas

  • Prices exclude VAT. price is always excl. VAT. Publishing VAT-inclusive prices doubles the tax on the eMSP side.
  • Missing `vat`. If vat is omitted, the eMSP cannot compute the incl. VAT amount. Include it unless VAT genuinely does not apply.
  • Per-minute pricing. Convert to per-hour: 0.05 EUR/min is price: 3.00 on TIME.
  • Element order. Unrestricted elements first means restricted elements never apply.
  • Restrictions on unsupported dimensions. Power- or current-based restrictions require that the CPO reports those values per period. If your CSMS does not track them, do not publish such tariffs.
  • Ad-hoc tariffs. If you publish an AD_HOC_PAYMENT tariff alongside REGULAR, eMSPs should use the REGULAR one for their tokens. Check that partners actually select by type.

Key takeaways

  • Tariff = ordered elements; element = price components + restrictions; first matching element wins per dimension.
  • ENERGY priced per kWh, stepped in Wh; TIME/PARKING_TIME priced per hour, stepped in seconds; prices exclude VAT.
  • Restrictions use local time at the location.
  • Split charging periods whenever the applicable element changes, and agree on step-size and rounding rules with each partner.

Next: Part 8, the Commands module.

Let's Talk

Ready to build something that lasts?

Whether it's a charging network, an AI rollout, or a core enterprise system — let's talk about what you're building.