Log inTalk to us
Billing and settlement

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.

sequence diagram; its source follows
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 back

A 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:…"
  }
}
FieldMeaning
directionto_ivora on site.flipped, to_previous_platform on site.flip_rolled_back
previous, currentWhere the site's chargers connected before and connect now: previous_platform or ivora
flipped_atWhen the chargers' routing changed
locationThe site, with your external_reference if you set one
stationsThe 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_stationstrue when every station at the location moved. When false, switch only the listed stations
flip_id, flip_referenceIdentifiers 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…IvoraScope
Find the charger behind a QR code or a printed EVSE IDThe site.flipped event's evses, or GET /stations (?charger_id=, ?external_reference=) and GET /locations?external_reference=stations:read
Show whether a connector is freeconnectors[].status on GET /stations/{station_id}; connector.status_changed eventsstations:read
Show the priceGET /tariffs/{tariff_id} (pricing_summary); the connector's tariff_id names itbilling:read
Quote a session before paymentPOST /tariffs/{tariff_id}/quote (quotes)billing:read
Start charging after your processor authorized the paymentPOST /charging-sessions, then POST /charging-sessions/{session_id}/start with {"funding_confirmed": true}billing:write, settlement:write, stations:control
Cancel before the charger startedPOST /charging-sessions/{session_id}/cancelbilling:write
Stop chargingPOST /charging-sessions/{session_id}/stopbilling:write, stations:control
Follow the sessionGET /charging-sessions/{session_id}; charging_session.status_changed eventsbilling:read
Show the running cost and energyusage.estimated_minor, usage.energy_wh, usage.power_w on GET /charging-sessions/{session_id}; meter.sampled events for meter readingsbilling:read; stations:read
Get the final amount to capturePOST /charging-sessions/{session_id}/finalize with the observed transaction_id, then bill.total_minor; bill.finalized eventbilling:write
Record a capture, refund or releasePOST /charging-sessions/{session_id}/settlement-reports; charging_session.settlement_reported eventssettlement:write
Find a session again after a timeoutGET /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: true tells 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_minor approaches 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_reference when 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 same Idempotency-Key and body, or with GET /charging-sessions?application_reference=.
  • Persist each Idempotency-Key before 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_reference in 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.

  1. 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.
  2. On site.flipped, set the listed stations to ivora and send new sessions for them to the Ivora API.
  3. 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.
  4. On site.flip_rolled_back, set the listed stations back to previous_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-Signature and rejects a wrong secret; POST /webhooks/{webhook_id}/test delivers a signed webhook.test.
  • A replayed delivery (same event id) changes nothing.
  • site.flipped for a test site moves only that site's stations in your routing table; site.flip_rolled_back moves 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_reference and a new Idempotency-Key is refused (409 conflict); find the first one with GET /charging-sessions?application_reference=.
  • One test payment per site: authorize, start, observe charging, stop, finalize, capture bill.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.