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
{
"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 withprice_componentsand optionalrestrictions. - A PriceComponent has a
type(TariffDimensionType), apriceexcluding VAT, an optionalvatpercentage, and astep_size. typeon 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_priceandmax_pricecap the total;start_date_timeandend_date_timebound validity.tariff_alt_textis the human-readable description eMSPs show in apps. Write it; many apps fall back to it when they cannot render complex tariffs.
TariffDimensionType | price is per | step_size unit |
|---|---|---|
ENERGY | kWh | Wh |
TIME | hour of charging | seconds |
PARKING_TIME | hour connected but not charging | seconds |
FLAT | session | 1 (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:
| Restriction | Meaning |
|---|---|
start_time, end_time | Time of day, HH:MM, local time at the location. end_time before start_time wraps past midnight. |
start_date, end_date | Date range, YYYY-MM-DD, local time. |
day_of_week | List of MONDAY ... SUNDAY. |
min_kwh, max_kwh | Applies while session energy is within the range. |
min_current, max_current | Applies while charging current is within the range (A). |
min_power, max_power | Applies while charging power is within the range (kW). |
min_duration, max_duration | Applies while session duration is within the range, in seconds. |
reservation | RESERVATION 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:
{
"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).
| Component | Volume | Billed volume | Price excl. VAT |
|---|---|---|---|
FLAT | 1 session | 1 | 0.50 |
ENERGY | 24.0 kWh | 24.000 kWh (step 1 Wh) | 24.0 x 0.40 = 9.60 |
PARKING_TIME | 598 s | 600 s (2 x 300 s) = 0.1667 h | 0.1667 x 6.00 = 1.00 |
| Total | 11.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.
priceis always excl. VAT. Publishing VAT-inclusive prices doubles the tax on the eMSP side. - Missing `vat`. If
vatis 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.00onTIME. - 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_PAYMENTtariff alongsideREGULAR, eMSPs should use theREGULARone for their tokens. Check that partners actually select bytype.
Key takeaways
- Tariff = ordered elements; element = price components + restrictions; first matching element wins per dimension.
ENERGYpriced per kWh, stepped in Wh;TIME/PARKING_TIMEpriced 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.

