Smart charging
Control how much power your chargers deliver: cap a whole station, set the
default limit every new session gets, cap one running session, and schedule
time-of-use charging windows. The API translates each request into OCPP smart
charging (SetChargingProfile, ClearChargingProfile and
GetCompositeSchedule) on OCPP 1.6, 2.0.1 and 2.1. No request carries a raw
OCPP message.
Reads need stations:read. Writes need stations:control and an
Idempotency-Key, and answer 202 with an operation whose
result.charger_status carries the charger's own answer once it arrives. The
composite schedule changes nothing on the charger, so it
needs only stations:read (viewers can ask for it), though it still takes an
Idempotency-Key and answers with an operation.
Writes that set or clear profiles or session caps are refused with
402 payment_method_required, like station commands other than stop and
unlock, when the project's billing team has no usable payment method for
Ivora's usage fee. Reads and the composite
schedule never are.
| Route | Purpose |
|---|---|
GET …/stations/{station_id}/smart-charging | What smart charging the charger reported |
GET …/stations/{station_id}/charging-profiles | The station's profiles and their state |
GET …/stations/{station_id}/charging-profiles/{profile_id} | One profile |
POST …/stations/{station_id}/charging-profiles | Set a profile |
DELETE …/stations/{station_id}/charging-profiles/{profile_id} | Clear one profile |
POST …/stations/{station_id}/charging-profiles/clear | Clear every profile of a purpose |
POST …/stations/{station_id}/composite-schedule | Ask the charger what it will actually apply |
PUT …/transactions/{transaction_id}/charging-limit | Cap a running session |
DELETE …/transactions/{transaction_id}/charging-limit | Remove a session's cap |
… is /v1/tenants/{tenant_id}.
Purposes and units
purpose | Applies to | OCPP 1.6 | OCPP 2.0.1/2.1 |
|---|---|---|---|
charger_max | The whole station, always | ChargePointMaxProfile, connector 0 | ChargingStationMaxProfile, EVSE 0 |
tx_default | Every new session on the station, or on one connector_id | TxDefaultProfile | TxDefaultProfile (the connector's EVSE) |
session | One active transaction_id on the station | TxProfile | TxProfile |
unit is always explicit: W (power) or A (current per phase). Limits are
0 to 1,000,000 W or 2,000 A, with at most one decimal, as OCPP requires. A
limit of 0 pauses charging while it applies: in a period, a time-of-use
window, default_limit or a session cap. The session
stays open and resumes when a higher limit applies. min_charging_rate must be
above 0. The charger combines the profiles it holds: within a purpose the
highest valid stack_level wins, and the lowest of the purposes' limits applies.
Safety and persistence
- The charger enforces the limits. It applies what it accepted and never delivers more than its hardware rating, whatever a profile says. The API refuses only what the charger reported it cannot take (a unit, stack level or period count) and limits above 1,000,000 W or 2,000 A; it does not check a limit against the charger's or the site's rating.
- Profiles persist on the charger, across reboots and reconnects, until they
are cleared, replaced or reach
valid_to. Clear acharger_maxortx_defaultprofile explicitly when it should no longer apply. - Session profiles end with their transaction: the charger drops a
sessionprofile, and every session cap, when the session ends, and the next session on the connector starts without it. - A limit of 0 pauses, it does not stop. The transaction stays open and the connector stays occupied until a higher limit applies, the cap is removed, or the session is stopped.
- Billing is unaffected. Profiles and session caps change only how fast a vehicle charges. Meter values and their units, tariffs and session amounts are computed exactly as without them.
Set a profile
POST /v1/tenants/7/stations/12/charging-profiles with an Idempotency-Key:
{
"purpose": "tx_default",
"unit": "W",
"start_at": "2026-10-14T00:00:00-07:00",
"periods": [
{"start_offset_seconds": 0, "limit": 11000},
{"start_offset_seconds": 21600, "limit": 7400, "number_phases": 3}
]
}
| Field | Rules |
|---|---|
purpose, unit | Required |
connector_id | tx_default only; omit for the whole station |
transaction_id | Required for session, and only there; the transaction must be active on this station |
stack_level | 0 to 100, default 0 |
kind | absolute (from start_at, now when omitted), recurring (needs recurrency daily or weekly and start_at) or relative (from the start of the session; never charger_max). Default: relative for session, recurring with windows, else absolute |
periods | 1 to 48; start_offset_seconds from the schedule start, the first at 0, each later than the one before; optional number_phases 1–3 |
duration_seconds | Optional length of the schedule |
valid_from, valid_to | Optional ISO 8601 times with an offset. On OCPP 2.x valid_from cannot be in the future: use start_at |
min_charging_rate | Optional, in unit |
Times are ISO 8601 with an offset; the API stores and sends them in UTC.
The result names the new profile:
{
"id": "op_…",
"status": "succeeded",
"result": {
"station_id": 12, "action": "set_charging_profile", "profile_id": "chp_…",
"ocpp_profile_id": 700042, "purpose": "tx_default", "transaction_id": null,
"stack_level": 0, "dispatch": "sent",
"charger_status": "Accepted", "charger_replied_at": "2026-10-13T10:00:01Z"
}
}
ocpp_profile_id is the chargingProfileId the charger knows the profile by.
Ivora allocates it (from 700001), unique across stations.
Time-of-use windows
Instead of periods, give windows with time_zone and default_limit. The
API builds a daily recurring schedule from local midnight: default_limit
outside the windows (0 pauses charging), each window's own limit inside. A
window whose end is not after its start runs over midnight; windows may not
overlap. Windows are for charger_max and tx_default.
{
"purpose": "charger_max",
"unit": "W",
"time_zone": "America/Los_Angeles",
"default_limit": 22000,
"windows": [{"start": "16:00", "end": "21:00", "limit": 7000}]
}
OCPP repeats a daily profile every 24 hours from its UTC start, so across a
daylight saving change the windows move by an hour until you set the profile
again. The profile records utc_offset, the offset on the day it was set. Ivora
tariffs are per kWh only, so windows are not derived from tariffs: choose them in
your application.
The charger's answer and the profile's state
The operation succeeds when the charging core sent the message. The charger's answer arrives within seconds: poll the operation, list the profiles, or subscribe to the webhooks. Nothing is ever resent.
status | Meaning |
|---|---|
pending | Sent; the charger has not answered |
accepted | The charger answered Accepted |
rejected | The charger answered Rejected, NotSupported (OCPP 1.6) or a CallError (charger_status, error.code charger_rejected), or the charging core refused it before sending (error.code charging_profile_refused with its reason), or the station could not be reached (station_offline and the like) |
unknown | The dispatch outcome is unknown; list the profiles or ask for a composite schedule before trying again with a new key |
no_reply | No answer 10 minutes after sending |
cleared | Cleared through this API |
replaced | OCPP 1.6: a later profile with the same purpose, connector and stack level took its place |
ended | A session profile whose transaction is over; the charger drops it with the transaction |
The list shows profiles set through this API, oldest first. Filter with
purpose, status, transaction_id or active=true (pending and accepted);
paginate with after=next_cursor. Profiles the charger received from elsewhere
are not listed: the composite schedule shows what it
actually applies.
Refusals before anything is sent
These create no operation and leave the Idempotency-Key unused:
| Status and code | When |
|---|---|
409 smart_charging_unsupported | The charger reports smart charging unsupported or disabled |
409 charging_limit_stack_full | OCPP 2.x: the session's caps already fill the charger's reported stack levels (details.stack_level, details.max_stack_level); see Cap a running session |
422 charging_profile_unsupported | details.field is unit, stack_level or periods and the charger reported it cannot take it (details.supported), or valid_from is in the future on OCPP 2.x |
402 payment_method_required | The project's billing team has no usable payment method for the usage fee (details.billing_profile); checked after the refusals above. A retry with an Idempotency-Key that already has an operation replays it |
422 invalid_request | The body breaks the rules above |
The OCPP 2.x core's own checks
On OCPP 2.0.1 and 2.1 the charging core checks a profile before sending it. It
refuses, among others, a second profile with the same purpose, EVSE and stack
level while the first is valid. The operation is then rejected with
422 charging_profile_refused and the core's reason. To replace a profile there,
clear it first or use another stack level. The session cap
does this for you.
Capability
GET …/smart-charging reads what the charger reported, from the configuration
the charging core recorded (not a live read):
{
"station_id": 12, "protocol": "ocpp1.6", "smart_charging": true,
"units": ["A", "W"], "max_stack_level": 8, "max_periods": 24, "max_profiles": 10,
"purposes": ["charger_max", "tx_default", "session"],
"source": "configuration", "reported": true
}
OCPP 1.6 reads SupportedFeatureProfiles, ChargingScheduleAllowedChargingRateUnit,
ChargeProfileMaxStackLevel, ChargingScheduleMaxPeriods and
MaxChargingProfilesInstalled; OCPP 2.x reads SmartChargingCtrlr Enabled,
Available, RateUnit, ProfileStackLevel, PeriodsPerSchedule and Entries.
A value the charger has not reported is null and is left to the charger's own
answer. Refresh them with
POST …/configuration/refresh. purposes is empty when the
charger reports smart charging disabled.
Clear profiles
DELETE …/charging-profiles/{profile_id} clears one profile by its
chargingProfileId. A profile that is no longer pending or accepted answers
409 charging_profile_inactive.
POST …/charging-profiles/clear with {"purpose": "tx_default"} and optional
connector_id and stack_level clears every matching profile on the station,
including profiles set outside this API. result.profile_ids lists the API's
profiles it covers.
A profile reads cleared once the charger answers Accepted, or Unknown (it
held no such profile). A Rejected answer leaves it as it was.
Composite schedule
POST …/composite-schedule with {"duration_seconds": 3600} and optional
connector_id and unit asks the charger for the limits it will apply from now,
every profile combined. It needs only stations:read and is never refused for
the usage fee. Once the charger answers, result.schedule holds them:
{
"ocpp_target": 2, "start_at": "2026-10-13T10:00:00Z", "duration_seconds": 3600,
"unit": "W", "min_charging_rate": null,
"periods": [{"start_offset_seconds": 0, "limit": 11000, "number_phases": null}]
}
schedule is null when the charger answers Rejected.
Cap a running session
PUT /v1/tenants/{tenant_id}/transactions/{transaction_id}/charging-limit with
{"limit": 16, "unit": "A"} and optional number_phases caps one active
transaction from its start. It suits prepaid sessions: lower the cap as the
balance runs down. {"limit": 0, "unit": "A"} pauses the session until you set
a higher cap or remove it; the transaction stays open.
Setting it again changes the cap. OCPP 1.6 replaces the earlier TxProfile at
the same stack level; on OCPP 2.x the new cap goes one stack level above the
session's earlier ones, which the higher level overrides. DELETE on the same
path clears every session profile on the transaction's connector or EVSE; the
session continues under the station's tx_default and charger_max profiles.
On OCPP 2.x the levels run out at the charger's reported max_stack_level
(the core refuses a second cap at one level, even under the same
chargingProfileId). The cap after that answers 409 charging_limit_stack_full
before anything is sent, with details.stack_level (the level it would need)
and details.max_stack_level. DELETE the cap, then set it again: it starts
at level 0. A charger that has not reported its maximum is left to answer
itself.
An unknown transaction answers 404 transaction_not_found; an ended one, a
rejected operation with transaction_ended.
Webhooks
| Type | When | data |
|---|---|---|
charging_profile.accepted | The charger accepted a profile | profile (full record), station_id, operation_id, charger_status |
charging_profile.rejected | The charger rejected it, or the charging core refused it | as above; charger_status is null for a core refusal, and profile.error gives the reason |
charging_profile.cleared | A clear removed it | as above |
They are charger events: resource_id is the profile ID, and they
carry station_id, station_sequence and occurred_at, and honour an
endpoint's stations filter. Read scope stations:read. Ivora reads answers
nobody polled for every 15 seconds.
Retries
Writes follow the retry rules: replay an uncertain
request with the same Idempotency-Key. An unknown operation may or may not
have reached the charger; list the profiles or read the composite schedule before
choosing a new key.