Drivers want to know what they will pay before they charge and what they paid when they leave, and in many markets price transparency is a regulatory requirement (see NEVI and AFIR requirements). OCPP 1.6 had no standard way to put text on a station screen. 2.0.1 adds two functional blocks for it: O (Display Message) and I (Tariff and Cost).
Display messages (block O)
The CSMS can store messages on the station for display, each with a priority, an optional state in which it applies, an optional validity window and an optional transaction binding.
[2, "sdm-11", "SetDisplayMessage", {
"message": {
"id": 14,
"priority": "NormalCycle",
"state": "Idle",
"startDateTime": "2026-10-26T00:00:00.000Z",
"endDateTime": "2026-11-30T23:59:59.000Z",
"message": {
"format": "UTF8",
"language": "en",
"content": "0.45 EUR/kWh, idle fee 0.10 EUR/min after 30 min"
}
}
}]
[3, "sdm-11", {
"status": "Accepted"
}]| Field | Values |
|---|---|
priority | AlwaysFront (shown permanently, overrides others), InFront (shown before normal-cycle messages), NormalCycle (rotates with others) |
state | Charging, Faulted, Idle, Unavailable; omit for any state |
message.format | ASCII, HTML, URI, UTF8 |
transactionId | Show only during that transaction (use case O02) |
display | A ComponentType to target a specific display on multi-display stations |
The response status tells you exactly what the station could not do: Accepted, NotSupportedMessageFormat, NotSupportedPriority, NotSupportedState, UnknownTransaction or Rejected. Read DisplayMessageCtrlr first (SupportedFormats, SupportedPriorities and DisplayMessages, the number of messages the station can store) so you only send what will be accepted.
Setting a message with an id that already exists replaces it (O06). GetDisplayMessages takes a requestId and optional filters (id, priority, state), returns Accepted or Unknown, and delivers the stored messages asynchronously in NotifyDisplayMessages. ClearDisplayMessage removes one by id.
Messages for one transaction
Setting transactionId binds a message to a running transaction (O02). It is shown only during that transaction and is no longer relevant once it ends, which makes it the right tool for per-session information such as "Charging limited to 11 kW by site load management" or a reminder about idle fees:
[2, "sdm-12", "SetDisplayMessage", {
"message": {
"id": 9001,
"priority": "InFront",
"state": "Charging",
"transactionId": "7f3c2a10-5e61-4b8e-9d2a-0c4b1e8f6a33",
"message": {
"format": "UTF8",
"language": "en",
"content": "Idle fee starts 30 min after charging completes."
}
}
}]
[3, "sdm-12", {
"status": "Accepted"
}]If the transaction has already ended or is unknown to the station, the response is UnknownTransaction. Allocate transaction-bound message ids from a separate range so they never overwrite your site-wide messages by accident.
Languages
IdTokenInfo carries language1 and optionally language2, the driver's preferred languages as language codes. When a message exists in several languages, the station can choose the one matching the driver. In practice this means sending one SetDisplayMessage per language (each with its own id) for general messages, and returning personalMessage in the driver's language for personal ones.
Tariff and cost (block I)
In 2.0.1 the CSMS calculates cost; the station only displays what it is told. (Local cost calculation on the station is an OCPP 2.1 feature, covered in part 11.) The pieces fit together like this:
| When | Mechanism | Use case |
|---|---|---|
| Driver authorizes | idTokenInfo.personalMessage in AuthorizeResponse or TransactionEventResponse carries the driver-specific tariff text | I01 |
| During charging | CostUpdated from CSMS, or totalCost in a TransactionEventResponse | I02 |
| After charging | totalCost in the response to the Ended TransactionEvent | I03 |
| CSMS unreachable | TariffCostCtrlr.TariffFallbackMessage and TotalCostFallbackMessage | I04, I05 |
| Tariff changes mid-session | updatedPersonalMessage in TransactionEventResponse | Block I |
Showing the tariff at authorization
[3, "au-0202", {
"idTokenInfo": {
"status": "Accepted",
"language1": "en",
"personalMessage": {
"format": "UTF8",
"language": "en",
"content": "Your rate: 0.39 EUR/kWh. Idle fee after 30 min."
}
}
}]Because the CSMS knows who the driver is, this is where contract-specific or roaming prices are shown. For roaming drivers, the text should reflect the tariff from your OCPI tariffs for that partner.
Running cost
As meter values arrive, the CSMS recomputes the cost and pushes it with CostUpdated, in the currency configured in TariffCostCtrlr.Currency:
[2, "cu-0451", "CostUpdated", {
"totalCost": 7.42,
"transactionId": "7f3c2a10-5e61-4b8e-9d2a-0c4b1e8f6a33"
}]
[3, "cu-0451", {}]Alternatively, return totalCost in the response to periodic TransactionEvent updates, which avoids an extra round trip. Either way, rate-limit it: once a minute or on meaningful change is plenty, and a cost push on every meter value just adds load and log noise.
Final cost
[3, "te-0009", {
"totalCost": 15.37,
"idTokenInfo": {
"status": "Accepted"
}
}]This is the response to the Ended event from part 4. It has a catch: you must answer quickly, so totalCost must be computable from data you already have. If billing involves an asynchronous step (a roaming partner's tariff, a payment capture), return your best estimate or leave totalCost out and let the station show TotalCostFallbackMessage. Never delay the response waiting for an external system.
Field gotchas
- Rounding.
totalCostis a decimal including taxes as you define them. Round the same way your invoice does, or drivers will see a different number on the screen than on their receipt. - Offline transactions. Events delivered late (
offline: true) produce cost responses the driver will never see. Do not rely on the screen as the only receipt; send it via app or email too. - Fallback text. Set
TariffFallbackMessageandTotalCostFallbackMessageon every station during provisioning. A blank screen is worse than "Price shown in app". - Feature flags. Tariff and cost support is optional and is enabled per station through
TariffCostCtrlr. Check before relying on it.
Next
Next: migrating from OCPP 1.6 to 2.0.1. For tariff design itself, see EV charging pricing models.

