Tariffs: price blocks, rules and quotes
A tariff says what charging costs. Every bill freezes its tariff when you create the bill and prices the completed transaction with it. The same calculation answers quotes and simulations, so a quote, the bill's total and the receipt agree.
A tariff is either simple or complex:
- A simple tariff charges one price per kWh:
rate_minor_per_kwh× kWh, rounded half up to integer minor units. This has not changed. - A complex tariff uses elements: price blocks in the OCPI 2.2.1 format, with restrictions such as time of day or energy charged. It can also carry Ivora billing rules: discounts, a cap and a prepaid report.
Elements
Each element has one or more price components and optional restrictions.
Prices are major units excluding tax, as OCPI publishes them (0.35 is $0.35).
Component type | Price per | step_size unit |
|---|---|---|
ENERGY | kWh | Wh |
TIME | hour while charging | seconds |
PARKING_TIME | hour idle: connected but not charging | seconds |
FLAT | session (reservation, in a reservation element) | ignored |
For each moment of a session, the first element that prices a dimension and
whose restrictions all hold sets that dimension's price. Put specific elements
first and the fallback last. FLAT is charged once, by the first element with
FLAT that applies when the session starts.
step_size rounds a dimension's total up to a multiple of the step of the last
element that billed it. For example, PARKING_TIME with step_size: 300 bills
idle time per started 5 minutes. A step size of 1 bills the metered quantity
exactly.
Restrictions
Restrictions are evaluated in the tariff's local time (time_zone, an IANA
name; without it, the station location's time zone, else UTC). Minimums are
inclusive, maximums and end times or dates are exclusive. An end_time before
its start_time wraps midnight.
Daylight-saving changes follow the local clock. When clocks fall back, a window such as 01:00–01:30 applies both times the clock shows it, and the time before and after the change is priced separately. When clocks spring forward past a window's start time, the window starts at the moment the clock jumps.
| Restriction | Meaning |
|---|---|
start_time, end_time | Local time of day, HH:MM |
start_date, end_date | Local date, YYYY-MM-DD |
day_of_week | Local days, such as ["MONDAY", "FRIDAY"] |
min_kwh, max_kwh | Energy already charged in the session, kWh |
min_power, max_power | Charging power, kW |
min_current, max_current | Charging current, A. An element that restricts current never applies when the current is unknown |
min_duration, max_duration | Session duration so far, seconds |
reservation | RESERVATION prices reserved time (TIME) and a reservation fee (FLAT); RESERVATION_EXPIRES applies when a reservation expires unused |
Example: peak and off-peak with an idle fee
{
"currency": "USD",
"authorization_minor": 2500,
"time_zone": "America/Los_Angeles",
"elements": [
{"price_components": [{"type": "ENERGY", "price": 0.45, "step_size": 1}],
"restrictions": {"start_time": "16:00", "end_time": "21:00",
"day_of_week": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"]}},
{"price_components": [{"type": "ENERGY", "price": 0.30, "step_size": 1},
{"type": "FLAT", "price": 1.00, "step_size": 1}]},
{"price_components": [{"type": "PARKING_TIME", "price": 24.00, "step_size": 300}]}
]
}
Send it to POST /v1/tenants/{tenant_id}/tariffs. Without tariff_alt_text,
Ivora generates the driver-facing description:
$0.45/kWh Mon–Fri, 16:00–21:00; otherwise $0.30/kWh + $1.00 session fee; $24.00/hour idle fee after charging (billed per started 5 min).
The generated text calls TIME "per hour while charging" and PARKING_TIME an "idle
fee after charging", as bills charge them (see
Charging and idle time). Supply your own
tariff_alt_text if you want different wording.
Validation
Tariff creation, edits and simulations refuse tariffs a driver could not read
clearly. They return 422 tariff_invalid, with every problem in
details.errors (element, field, message):
- an element that can never apply, because an earlier element applies whenever it would;
- two elements that price the same dimension in partly overlapping times or dates (nest them with the narrower one first, or make them disjoint);
- a dimension priced twice in one element, a reservation element with
ENERGYorPARKING_TIME, or a minimum not below its maximum; - a tariff whose generated description would exceed 512 characters, unless
you supply
tariff_alt_text.
Billing rules
rules are applied after the elements, in billing only. They are never
published to roaming partners, who see the elements and tariff_alt_text.
| Rule | Effect |
|---|---|
group_discounts | Percent off for sessions whose pricing_group or token matches. The largest matching group discount applies; group discounts never stack with each other |
merchant_validation | percent_off or amount_off_minor when the session carries a validation code in codes (any code when codes is empty) |
max_session_cost_minor | Caps the session's total |
prepaid_limit | On quotes with prepaid_balance_minor, reports how much the balance covers and whether the session reaches it. It never changes the total |
Rules apply in this order, each to the amount left by the one before:
- the group discount;
- the merchant validation (so a group discount and a merchant validation
compound: 10 % off and then 20 % off is 28 % off in all, and an
amount_off_minorvalidation comes off the already-discounted amount); - the session cost cap.
Amounts stay exact through every line and rule. The total is rounded half up once, at the end, and is never below 0.
Pass the driver's group or a validation code when you create the bill
(pricing_group, merchant_validation). The bill freezes them with the
tariff.
Quotes and simulations
POST /v1/tenants/{tenant_id}/tariffs/{tariff_id}/quote prices a session
profile under a stored tariff. POST /v1/tenants/{tenant_id}/tariffs/simulate
validates a draft tariff and prices the profile without storing anything. Both
need billing:read.
{
"session": {
"start_at": "2026-10-15T15:00:00-07:00",
"duration_minutes": 120,
"energy_kwh": 20,
"idle_minutes": 10,
"group": "staff"
}
}
A profile gives energy as energy_kwh (spread evenly over the charging time)
or energy_curve (cumulative kWh at minutes into charging). It can add
power_kw, current_a, reservation_minutes, reservation_expired,
id_token, merchant_validation and prepaid_balance_minor. The response
lists each priced line (type, the element that priced it, quantity,
unit, price_minor, exact_minor, amount_minor), the rule adjustments
and total_minor.
Bills and receipts
A bill on a complex tariff has billing_version: "elements-v1" and
rate_minor is the tariff's base rate. A final bill carries
cost_breakdown, the itemised result of the same calculation, for receipts.
Bills on simple tariffs keep billing_version: "energy-v1" and the same total
as before.
Bills see less of a session than a quote's profile describes:
- Time. A bill charges
TIMEover the transaction's charging time andPARKING_TIMEover its idle time (see Charging and idle time). Restrictions, time-of-day prices and step sizes apply to each interval as they do to a quote. When idle time cannot be detected, the whole transaction is charging time. - Energy and power. A bill knows the transaction's total kWh, not its
curve. It spreads the energy evenly over the charging time, so
min_kwhandmax_kwhsplit at the time the average rate would cross them, andmin_powerandmax_powercompare the session's average power (total kWh over the charging hours). Current is unknown, so an element that restricts current never applies on a bill. - Start time. A transaction without a start time cannot be priced by a
tariff that depends on time: one with a time, date, day or duration
restriction, or a
TIMEorPARKING_TIMEcomponent. Finalizing such a bill returns409 transaction_start_unknown, and its live estimate (estimated_minor) isnull. Other complex tariffs price it normally.
bill.finalized events carry the final bill, so they include its
billing_version and cost_breakdown.
Charging and idle time
A bill on a complex tariff tells charging from idle time from what the charger reports during the transaction:
- OCPP 2.0.1: the
chargingStateof eachTransactionEvent.Chargingis charging time;EVConnected,SuspendedEV,SuspendedEVSEandIdleare idle time. - OCPP 1.6: the connector's
StatusNotification.Chargingis charging time;SuspendedEV,SuspendedEVSEandFinishingare idle time. Some chargers stay inChargingwhile the vehicle draws nothing, so the meter values are read too: power below 0.1 kW for at least 2 minutes is idle time. - Neither: when a charger reports no charging state, the meter rule alone.
The meter rule reads Power.Active.Import when the charger sends it, otherwise
the energy register. A register only shows idle time when, across the whole
stretch, its change plus one step of its resolution still averages below
0.1 kW, so a register that counts in 0.1 kWh steps does not make a steadily
charging vehicle look idle between its steps. A low power reading is not idle
when the register readings either side of it show 0.1 kW or more flowing.
Then, whatever the source:
- Idle shorter than 2 minutes is charging time. Chargers often report
SuspendedEVorFinishingfor a second at the start or end of a transaction; that is not idle time. - Everything before the first charging is waiting (
statewaiting): plugged in before the vehicle starts to draw, for example on a scheduled charge. Waiting is neitherTIMEnorPARKING_TIME;PARKING_TIMEis the idle fee after charging. Charging only starts with at least 2 minutes of it (or charging that lasts to the end), so a second ofPreparing, or of an OCPP 2.0.1Startedevent without achargingState, before a long wait does not turn that wait into idle time. When no charging lasts 2 minutes, the firstCharging(status orchargingState) or metered draw of any length starts it, so a vehicle that charges briefly and then stays plugged in still pays the idle fee. Other OCPP 1.6 statuses (Preparing,Faulted,Available,Reserved,Unavailable) and time the charger reports nothing about never start charging: a session without aChargingstate or metered draw is all waiting and bills no time. - OCPP 1.6, before the first status: where the meter shows 0.1 kW or more, that time is charging time.
- No energy, no time. A session that draws less than 1 Wh is all waiting
(
reasonno_energy) and bills neitherTIMEnorPARKING_TIME, whatever the charger's states say, including when there is no data at all (detectedis thentruewithmethodnone). Its energy is priced at 0 (the bill keeps the metered kWh); exactly 1 Wh (0.001 kWh) and more bill as usual. Simple per-kWh tariffs are unchanged: kWh × rate, rounded half up. A quote prices its profile as given.
After the first charging, time the charger reports nothing about (and those
other statuses) counts as charging time, so a session with too little data bills as it did before idle detection: all of it as
charging time. The final bill's cost_breakdown.activity records what was
used: method (ocpp201_charging_state, ocpp16_status,
ocpp16_status_and_power, meter_power, or none with a reason; reason
no_energy for a session under 1 Wh),
charging_seconds, idle_seconds, waiting_seconds (whole seconds that add
up to the transaction's) and the intervals
(start, end, state: charging, idle or waiting). Live estimates (usage.estimated_minor) and charger running-cost
displays price the session so far the same way.
Bills on simple tariffs price kWh only and are unaffected. Existing final bills are never recalculated.
API versions
From API version 2026-10-15, every tariff response carries elements,
tariff_alt_text, rules and time_zone; a simple tariff shows its one
ENERGY element. Every version carries is_complex and pricing_summary (the
English description).
Version 2026-10-14 clients keep the simple fields. They see a complex
tariff as its base rate_minor_per_kwh (the last unrestricted element's
ENERGY price, which charger screens also show) plus is_complex: true and
pricing_summary. They can still send elements when they create or edit a
tariff.
Editing a complex tariff's rate_minor_per_kwh alone returns 409 tariff_complex: send elements instead. If you supplied tariff_alt_text,
send it again (or null for the generated text) whenever you change prices.
tariff.updated and tariff.archived events carry the tariff in the simple
shape; read the tariff for its elements.
See the changelog for the version policy.