Moving your payment service to Ivora
This guide is for a charging network moving its chargers from a previous platform to Ivora, one site at a time, that keeps its own payment service. Your checkout pages, app, payment terminal backend, QR codes and merchant accounts stay as they are. What changes is where your server sends its charging calls: for each site, from the previous platform's API to the Ivora API, at the moment that site's chargers move.
Important
Ivora does not move money or see card data. Your backend keeps authorizing, capturing and refunding with your own processor, as it does today. Ivora starts and stops the charger, meters the session and calculates an immutable bill. See application-managed payments for the full contract.
How a site moves
Ivora's migration team moves a site's chargers together, only while none of
them is charging (an operator can override this in an emergency; sessions then
end on the previous platform), so no session is split between two platforms. Each move is
recorded as a site flip and delivered to your webhook endpoint as
site.flipped. Your service switches that site's calls to Ivora when it
receives it. A rollback is delivered as site.flip_rolled_back, and your
service switches the site back.
Diagram source
sequenceDiagram
participant Ops as Ivora migration team
participant API as Ivora API
participant Pay as Your payment service
participant Old as Previous platform
Ops->>Ops: No charger charging, chargers moved to Ivora
Ops->>API: Record the site flip
API->>Pay: site.flipped (location, stations, EVSE IDs)
Pay->>Pay: Route this site's calls to Ivora
Pay->>API: New sessions at this site
Note over Pay,Old: Other sites keep using the previous platform
Ops->>API: Rollback, if needed
API->>Pay: site.flip_rolled_back
Pay->>Old: Route this site's calls backA session that started on the previous platform finishes there, because a site only moves while none of its chargers is charging (an operator can override this in an emergency; sessions then end on the previous platform). Keep the previous platform's integration running until every site has moved and the rollback window has ended.
The site flip events
Both events are delivered like every other webhook event,
signed and at least once, and are readable from the event log. They need
stations:read. resource_id is the location ID.
{
"id": "evt_…",
"type": "site.flipped",
"tenant_id": 7,
"resource_id": "4",
"created_at": "2026-10-19T16:30:02+00:00",
"data": {
"flip_id": "sfl_…",
"tenant_id": 7,
"direction": "to_ivora",
"previous": "previous_platform",
"current": "ivora",
"flipped_at": "2026-10-19T16:30:00+00:00",
"location": {"id": 4, "name": "Harbor garage", "external_reference": "site-88"},
"stations": [
{"id": 12, "name": "HG-01", "charger_id": null, "external_reference": "charger-1001",
"evses": [{"evse_number": 1, "evse_id": "US*ABC*E1001*1", "physical_reference": "A", "connector_ids": [14]}]}
],
"all_stations": true,
"flip_reference": "…",
"reason": "wave 1",
"actor": "user:…"
}
}
| Field | Meaning |
|---|---|
direction | to_ivora on site.flipped, to_previous_platform on site.flip_rolled_back |
previous, current | Where the site's chargers connected before and connect now: previous_platform or ivora |
flipped_at | When the chargers' routing changed |
location | The site, with your external_reference if you set one |
stations | The stations that moved, with their EVSEs. evse_id is the EVSE ID imported from the previous platform, else null. connector_ids are the connector resource IDs charging sessions take |
all_stations | true when every station at the location moved. When false, switch only the listed stations |
flip_id, flip_reference | Identifiers of the flip, for your logs and support requests |
Treat the event as state, not as a command: "this site is now on Ivora".
Deduplicate by event id. The same site can be reported more than once (a
retried flip), and events can arrive out of order: compare flipped_at and
keep the latest per location. If you missed deliveries, read
GET /v1/tenants/{tenant_id}/events?type=site.flipped and
?type=site.flip_rolled_back from your last cursor.
Mapping your calls
The table maps the calls a payment service usually makes to a charging
platform. Paths start with /v1/tenants/{tenant_id}. Every POST takes an
Idempotency-Key.
| Your service needs to… | Ivora | Scope |
|---|---|---|
| Find the charger behind a QR code or a printed EVSE ID | The site.flipped event's evses, or GET /stations (?charger_id=, ?external_reference=) and GET /locations?external_reference= | stations:read |
| Show whether a connector is free | connectors[].status on GET /stations/{station_id}; connector.status_changed events | stations:read |
| Show the price | GET /tariffs/{tariff_id} (pricing_summary); the connector's tariff_id names it | billing:read |
| Quote a session before payment | POST /tariffs/{tariff_id}/quote (quotes) | billing:read |
| Start charging after your processor authorized the payment | POST /charging-sessions, then POST /charging-sessions/{session_id}/start with {"funding_confirmed": true} | billing:write, settlement:write, stations:control |
| Cancel before the charger started | POST /charging-sessions/{session_id}/cancel | billing:write |
| Stop charging | POST /charging-sessions/{session_id}/stop | billing:write, stations:control |
| Follow the session | GET /charging-sessions/{session_id}; charging_session.status_changed events | billing:read |
| Show the running cost and energy | usage.estimated_minor, usage.energy_wh, usage.power_w on GET /charging-sessions/{session_id}; meter.sampled events for meter readings | billing:read; stations:read |
| Get the final amount to capture | POST /charging-sessions/{session_id}/finalize with the observed transaction_id, then bill.total_minor; bill.finalized event | billing:write |
| Record a capture, refund or release | POST /charging-sessions/{session_id}/settlement-reports; charging_session.settlement_reported events | settlement:write |
| Find a session again after a timeout | GET /charging-sessions?application_reference= | billing:read |
Amounts are integer minor units (_minor) of the bill's currency: cents for
USD and CAD, the two supported currencies. Energy is energy_wh (integer Wh)
or energy_kwh (a decimal kWh string). If the previous platform reported
energy in another unit, convert once, at your boundary.
A few differences from platforms that bill for you:
- Your backend asserts funding.
funding_confirmed: truetells Ivora your processor authorized the payment. Ivora does not check it. Never pass a browser's payment-success claim through. - Ivora does not stop at the authorized amount. If your policy needs a
cap, stop the session from your backend when
usage.estimated_minorapproaches it. - The bill is immutable. Capture
bill.total_minor. Ivora never changes a final bill to match a processor result. - Reports are optional. Settlement reports are your assertions, labeled
application_reported. They help when several of your systems (an app and a terminal backend, for example) need to see each other's captures.
Events to subscribe to
Register one endpoint with POST /v1/tenants/{tenant_id}/webhooks before the
first flip. A key with webhooks:write, stations:read and billing:read
can subscribe to all of these:
{
"url": "https://payments.example.com/ivora/webhooks",
"events": ["site.flipped", "site.flip_rolled_back", "charging_session.status_changed",
"bill.finalized", "charging_session.settlement_reported", "connector.status_changed"],
"description": "Payment service"
}
Add meter.sampled only if you show live meter readings; a session read
already carries the running cost. Verify Ivora-Signature on every delivery
(delivery contract), answer
2xx before slow work and deduplicate by event id.
Idempotency and retries
- Store your own booking or payment reference as
application_referencewhen you create a session. One reference identifies one session in your tenant, so a retried create can never charge twice. Recover a lost response with the sameIdempotency-Keyand body, or withGET /charging-sessions?application_reference=. - Persist each
Idempotency-Keybefore you send the request and reuse it with the identical body after a timeout. A new key is a new request. See retries. - Start and stop are dispatched at most once per session, whatever the key. If a start's outcome is unknown, read the session; do not create another one.
- Use your processor's operation reference as
operation_referencein settlement reports. The same reference and outcome deduplicate.
Switching per site
Keep a routing table in your service, keyed by location (or by station when a
flip lists only some stations): previous_platform or ivora. Every site
starts on previous_platform.
- Before the flip, map your QR codes, terminals and printed EVSE IDs to Ivora station and connector IDs. Imported EVSE IDs and external references make this a lookup; the event repeats them for each site.
- On
site.flipped, set the listed stations toivoraand send new sessions for them to the Ivora API. - Sessions already open on the previous platform finish there. Route their stop, status and final amount calls by where the session started, not by the current table.
- On
site.flip_rolled_back, set the listed stations back toprevious_platform. Sessions you started on Ivora finish on Ivora: keep following, finalizing and capturing them through the Ivora API.
If your service cannot accept the event (it is down, or rejects it), Ivora retries eight times over about a day and a half. You can also ask the migration team to hold a site until you confirm. A site that flips while your service still routes it to the previous platform cannot start new sessions there, because its chargers now connect to Ivora.
Rollback
A rollback is a site flip back to the previous platform, done the same way:
only while none of its chargers is charging (an operator can override this in
an emergency; sessions then end on the previous platform), all its chargers
together, and announced with site.flip_rolled_back. Nothing is deleted on either side. Bills and sessions
created on Ivora stay readable and capturable, and a later flip back to Ivora
sends site.flipped again. Agree on a rollback window with the migration team
and keep your previous platform integration until it ends.
Test checklist
Run these before the first production site, then once per site after its flip.
- Your webhook endpoint verifies
Ivora-Signatureand rejects a wrong secret;POST /webhooks/{webhook_id}/testdelivers a signedwebhook.test. - A replayed delivery (same event
id) changes nothing. -
site.flippedfor a test site moves only that site's stations in your routing table;site.flip_rolled_backmoves them back; an older event arriving late does not undo a newer one. - Every QR code, terminal and printed EVSE ID of the site maps to a station and connector ID.
- Each connector has the intended tariff, and a quote matches the price your service shows.
- Create, replay with the same key, and cancel a session before start; a
second create with the same
application_referenceand a newIdempotency-Keyis refused (409 conflict); find the first one withGET /charging-sessions?application_reference=. - One test payment per site: authorize, start, observe
charging, stop, finalize, capturebill.total_minor, and optionally report the capture. - A declined authorization never starts a charger.
- After a rollback, a session started on Ivora before it can still be finalized and captured.
Try the session contract without hardware first, on the free testing paths, then with one charger at a pilot site.



