Charger management

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.

RoutePurpose
GET …/stations/{station_id}/smart-chargingWhat smart charging the charger reported
GET …/stations/{station_id}/charging-profilesThe station's profiles and their state
GET …/stations/{station_id}/charging-profiles/{profile_id}One profile
POST …/stations/{station_id}/charging-profilesSet a profile
DELETE …/stations/{station_id}/charging-profiles/{profile_id}Clear one profile
POST …/stations/{station_id}/charging-profiles/clearClear every profile of a purpose
POST …/stations/{station_id}/composite-scheduleAsk the charger what it will actually apply
PUT …/transactions/{transaction_id}/charging-limitCap a running session
DELETE …/transactions/{transaction_id}/charging-limitRemove a session's cap

… is /v1/tenants/{tenant_id}.

Purposes and units

purposeApplies toOCPP 1.6OCPP 2.0.1/2.1
charger_maxThe whole station, alwaysChargePointMaxProfile, connector 0ChargingStationMaxProfile, EVSE 0
tx_defaultEvery new session on the station, or on one connector_idTxDefaultProfileTxDefaultProfile (the connector's EVSE)
sessionOne active transaction_id on the stationTxProfileTxProfile

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 a charger_max or tx_default profile explicitly when it should no longer apply.
  • Session profiles end with their transaction: the charger drops a session profile, 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}
  ]
}
FieldRules
purpose, unitRequired
connector_idtx_default only; omit for the whole station
transaction_idRequired for session, and only there; the transaction must be active on this station
stack_level0 to 100, default 0
kindabsolute (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
periods1 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_secondsOptional length of the schedule
valid_from, valid_toOptional ISO 8601 times with an offset. On OCPP 2.x valid_from cannot be in the future: use start_at
min_charging_rateOptional, 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.

statusMeaning
pendingSent; the charger has not answered
acceptedThe charger answered Accepted
rejectedThe 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)
unknownThe dispatch outcome is unknown; list the profiles or ask for a composite schedule before trying again with a new key
no_replyNo answer 10 minutes after sending
clearedCleared through this API
replacedOCPP 1.6: a later profile with the same purpose, connector and stack level took its place
endedA 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 codeWhen
409 smart_charging_unsupportedThe charger reports smart charging unsupported or disabled
409 charging_limit_stack_fullOCPP 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_unsupporteddetails.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_requiredThe 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_requestThe 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

TypeWhendata
charging_profile.acceptedThe charger accepted a profileprofile (full record), station_id, operation_id, charger_status
charging_profile.rejectedThe charger rejected it, or the charging core refused itas above; charger_status is null for a core refusal, and profile.error gives the reason
charging_profile.clearedA clear removed itas 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.