Billing and settlement

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 typePrice perstep_size unit
ENERGYkWhWh
TIMEhour while chargingseconds
PARKING_TIMEhour idle: connected but not chargingseconds
FLATsession (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.

RestrictionMeaning
start_time, end_timeLocal time of day, HH:MM
start_date, end_dateLocal date, YYYY-MM-DD
day_of_weekLocal days, such as ["MONDAY", "FRIDAY"]
min_kwh, max_kwhEnergy already charged in the session, kWh
min_power, max_powerCharging power, kW
min_current, max_currentCharging current, A. An element that restricts current never applies when the current is unknown
min_duration, max_durationSession duration so far, seconds
reservationRESERVATION 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 ENERGY or PARKING_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.

RuleEffect
group_discountsPercent off for sessions whose pricing_group or token matches. The largest matching group discount applies; group discounts never stack with each other
merchant_validationpercent_off or amount_off_minor when the session carries a validation code in codes (any code when codes is empty)
max_session_cost_minorCaps the session's total
prepaid_limitOn 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:

  1. the group discount;
  2. 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_minor validation comes off the already-discounted amount);
  3. 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 TIME over the transaction's charging time and PARKING_TIME over 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_kwh and max_kwh split at the time the average rate would cross them, and min_power and max_power compare 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 TIME or PARKING_TIME component. Finalizing such a bill returns 409 transaction_start_unknown, and its live estimate (estimated_minor) is null. 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 chargingState of each TransactionEvent. Charging is charging time; EVConnected, SuspendedEV, SuspendedEVSE and Idle are idle time.
  • OCPP 1.6: the connector's StatusNotification. Charging is charging time; SuspendedEV, SuspendedEVSE and Finishing are idle time. Some chargers stay in Charging while 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 SuspendedEV or Finishing for a second at the start or end of a transaction; that is not idle time.
  • Everything before the first charging is waiting (state waiting): plugged in before the vehicle starts to draw, for example on a scheduled charge. Waiting is neither TIME nor PARKING_TIME; PARKING_TIME is 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 of Preparing, or of an OCPP 2.0.1 Started event without a chargingState, before a long wait does not turn that wait into idle time. When no charging lasts 2 minutes, the first Charging (status or chargingState) 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 a Charging state 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 (reason no_energy) and bills neither TIME nor PARKING_TIME, whatever the charger's states say, including when there is no data at all (detected is then true with method none). 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.