Running cost on the charger screen
California's Type Evaluation Program (CTEP) requires a charger to show the driver the running cost while charging and the final cost afterwards. Ivora can push those amounts to chargers from the cost it already calculates for each bill. It is on by default for every station, existing and new. You can switch a station off, or back on, and that explicit choice always wins over the default.
Important
Status: not enabled in production yet. Running cost follows the charger display
switch: while displays are off, reads work and PUT returns
409 displays_disabled.
On by default
A station with no explicit choice reads "source": "default" and is enabled
with interval_seconds 60. PUT stores an explicit choice
("source": "manual", with updated_by and updated_at). It stays in place
across restarts and deploys until you change it, so a station you switch off
stays off. Only explicit choices are stored.
Ivora can switch the default off for a whole environment without touching
stations. Stations with an explicit choice keep it, and the capabilities
document shows the current default in running_cost.default (on or off).
Running cost also follows the charger display switch: while displays are off
in an environment, nothing is sent and PUT returns 409 displays_disabled.
What is priced
Only sessions started from an Ivora bill are priced: a managed bill started
with POST …/bills/{bill_id}/start, or an
external charging session started through its
session endpoint. The bill's charging token identifies its transaction, exactly
as bill finalization does. Sessions started any other way (RFID, operator
start, the legacy checkout) get no message from Ivora.
The cost is the bill's own calculation, so the screen and the final bill agree:
cost_minor= the transaction's energy in kWh × the bill's frozenrate_minor, rounded half up to a whole minor unit, as finalization rounds it.- The final cost is the final bill's
total_minorwhen the bill is final, or the same calculation from the ended transaction before it is.
Units are converted only on the charger wire. The core reports energy in kWh;
the API reports energy_wh. OCPP 1.6 meterValue is a Wh meter register
(the core's kWh meterStart plus session energy, times 1,000), and cost and
prices are major units (0.53 for 53 cents).
Messages
| Protocol | During the session | After it ends |
|---|---|---|
| OCPP 1.6 | DataTransfer vendorId org.openchargealliance.costmsg, messageId RunningCost | Same vendorId, messageId FinalCost, once |
| OCPP 2.0.1 / 2.1 | CostUpdated {totalCost, transactionId} where enabled | The core's TransactionEvent response totalCost |
The OCPP 1.6 messages follow the Open Charge Alliance California Pricing
Requirements extension. RunningCost data is
{transactionId, timestamp, meterValue, cost, state: "Charging", chargingPrice: {kWhPrice, hourPrice: 0, flatFee: 0}};
FinalCost data is {transactionId, cost, priceText}, for example
"Total $2.43 for 6.941 kWh at $0.35/kWh". The charger must support the
extension (configuration key CustomDisplayCostAndPrice); Ivora does not change
that key.
Chargers without the extension
A charger that does not support the extension answers the RunningCost
DataTransfer with UnknownVendorId, UnknownMessageId or Rejected. Ivora
then records
support: charger_rejected, with the status and the time in rejection, and
sends that station nothing more, with no retries, until either:
- the charger reboots (a new
BootNotification), or - someone writes the station's setting with
PUT(any write, including{"enabled": true}when the station is already on).
The next session then tries the charger again. Sessions that got no message read
last_error: charger_rejected. A charger answering Accepted only means it took
the message, not that the screen changed.
A refused FinalCost concerns only that session: it reads
state: final_rejected with last_error: final_cost_rejected, and the station
keeps its support, so the next session still gets RunningCost. Some
chargers, including those built on the open-source libocpp library, refuse a
FinalCost that arrives after the transaction has stopped.
Cadence: every sweep (about 15 seconds) Ivora reads the transactions of the
started bills on stations with running cost on. A RunningCost or CostUpdated goes out when the energy or cost
changed since the last accepted message and at least interval_seconds (default
60, 15–900) passed. FinalCost goes out once, retried for an hour after the
session ended.
OCPP 2.0.1 and 2.1
The charging core already sends its own CostUpdated every 60 seconds
(costUpdatedInterval), priced from the connector tariff, and computes the
TransactionEvent response totalCost the same way. Two writers would make the
screen alternate between two amounts, so Ivora sends 2.x CostUpdated only
where the environment enables it (support: ocpp2_cost_updated). Otherwise the
setting reads support: ocpp2_deferred and sessions read state: deferred.
Routes
Under /v1/tenants/{tenant_id}/stations/{station_id}, in the Charger
displays group:
| Route | Purpose | Scope |
|---|---|---|
GET /running-cost | The setting, where it comes from, how the charger's protocol receives cost, and recent sessions | stations:read; sessions needs billing:read |
PUT /running-cost | An explicit choice: {"enabled": false} or {"enabled": true, "interval_seconds": 60} | stations:write, Idempotency-Key |
PUT returns at once and never sends. Replays with the same key return the
first result. A read looks like this:
{
"tenant_id": 7, "station_id": 12,
"enabled": true, "interval_seconds": 60,
"source": "default", "updated_by": null, "updated_at": null,
"support": "charger_rejected",
"rejection": {"status": "UnknownVendorId", "at": "2026-10-02T20:15:04.120Z"},
"sessions": []
}
support | Meaning |
|---|---|
ocpp16_costmsg | OCPP 1.6 California Pricing DataTransfer |
ocpp2_cost_updated | OCPP 2.x CostUpdated during the session |
ocpp2_deferred | OCPP 2.x cost messages are switched off in this environment |
charger_rejected | The OCPP 1.6 charger refused the extension; see rejection |
not_booted | The charger has not reported its protocol yet |
protocol_unsupported | The charger's protocol cannot receive cost |
Each of the 20 most recent sessions carries bill_id, transaction_id,
state, cost_minor, energy_wh, sent_cost_minor, sent_at, sends,
final_minor, final_sent_at and last_error. state:
| State | Meaning |
|---|---|
running | Running cost messages are being sent |
final_pending | The session ended; the final cost is being sent (OCPP 1.6) |
final_sent | The charging core accepted the final cost message |
final_rejected | The charger refused the final cost (last_error: final_cost_rejected); later sessions are unaffected |
final_by_core | OCPP 2.x: the core's TransactionEvent response carries the final cost |
deferred | OCPP 2.x CostUpdated is switched off in this environment |
expired | The final cost could not be sent within an hour of the end |
failed | The bill could not be matched or priced (transactions_ambiguous, transaction_id_invalid, …) |
last_error also shows why a message was held back, for example
station_offline, charger_rejected or final_cost_rejected.
Like a display, "accepted" means the charging core dispatched the message, not that the screen changed.
Retries
Running cost messages are screen content: a resend replaces the cost shown and moves no money, so a failed send is retried with the display backoff (1, 5, 15, then 30 minutes). An offline charger is retried when it reconnects, without backoff. A charger that refused the extension is not retried until it reboots or its setting is written. An unreachable core ends the sweep's tick. These sends never change a bill, payment or capture.
Not supported yet
SetUserPrice(the driver-specific tariff text before a session starts). RCD/Renova chargers already show the station price through their display adapterDefaultPrice.- Idle fees, time-of-use
nextPeriodandtriggerMeterValue: Ivora tariffs price energy only. - OCPP 2.x final cost from the bill: it needs a change in the charging core's
TransactionEventhandling.