Billing and settlement

Application-managed payments

Available in production: externally funded charging sessions. Integrate a payment processor in your own backend. Ivora provides the charging session, token, usage and immutable bill. Adding a processor to your application requires no Ivora adapter deployment or CSMS core change.

Important

Ivora does not move money or independently verify external settlement. Your backend verifies processor results and controls funding, capture and refunds. Optional reports are labeled application_reported, with the reporting actor and receipt time. Keep the authorized API key on your server.

sequence diagram; its source follows
Diagram source
sequenceDiagram
  participant App as Application backend
  participant Pay as Chosen processor
  participant API as Ivora API
  participant C as CSMS and charger
  App->>Pay: Authorize funding
  Pay-->>App: Verified authorization and payment ID
  App->>API: Create charging session with stable references
  API-->>App: Session ID and tariff snapshot
  App->>API: Start with funding_confirmed true
  API->>C: Register short-lived token and dispatch once
  App->>API: Poll session and request stop
  API->>C: Stop only the matching transaction
  C-->>API: Completed usage in kWh
  App->>API: Finalize using observed transaction ID
  API-->>App: Immutable final amount in cents
  App->>Pay: Capture final amount
  Pay-->>App: Verified result
  App->>API: Optionally report external outcome
  Note over App,API: Application-reported, no payment execution by Ivora

What works today

All routes below start with /v1/tenants/{tenant_id}/charging-sessions. Open the External charging sessions group in the API reference for exact schemas and samples. Every POST requires Idempotency-Key.

Method and suffixPurposeRequired scopes
POST /Create session and tariff snapshot atomicallybilling:write, settlement:write
GET /Find stored sessions, including by application_referencebilling:read
GET /{session_id}Observe live matched usage, bill, operations and reported settlementbilling:read
POST /{session_id}/startAttest funding and dispatch oncebilling:write, settlement:write, stations:control
POST /{session_id}/stopStop the session's matching active transactionbilling:write, stations:control
POST /{session_id}/finalizeFinalize trusted completed usagebilling:write
POST /{session_id}/cancelCancel before any start reservationbilling:write
POST /{session_id}/settlement-reportsAppend an external outcomesettlement:write
GET /{session_id}/settlement-reportsPaginate the audit trailbilling:read

The new settlement:write scope is an explicit opt-in by a tenant writer. Existing keys do not gain it automatically. Keys remain tenant-bound and the issuer's current access is checked on every request. Keys with sufficient permissions share access to that tenant's sessions; application_reference is an identifier, not an application-level ACL. Your backend enforces host/guest and booking access.

Create and charge

Authorize funding in your backend first, then create a session. The IDs below are examples; read your tenant's station, connector and tariff IDs first.

{
  "station_id": 12,
  "connector_id": 14,
  "tariff_id": 5,
  "source": "csms",
  "application_reference": "booking-2026-001",
  "processor": "my-processor",
  "merchant_reference": "merchant-1",
  "payment_reference": "payment-1"
}

The response contains an ecs_ session ID and a bill_ tariff snapshot. Store both with your booking. References must contain only non-secret identifiers. A booking reference and processor/merchant/payment combination each identify one session in the tenant. Recover a lost response using the same creation key or GET with application_reference; do not generate another payment.

Start with {"funding_confirmed": true}. This asserts that your backend verified funding; Ivora does not inspect your processor. The API registers a 15-minute charging token and reserves one start operation. Read the session until it exposes usage.transaction_id, then stop when your application policy requires. Read again until the usage is inactive and finalize with {"transaction_id": 21} using the observed resource ID.

Capture bill.total_minor in your backend. At $0.35/kWh, 6.941 kWh produces 243 cents under energy-v1. The tariff snapshot and final amount cannot change. No caller-supplied cost or physical meter reading is accepted. An external bill cannot be passed to the managed Stripe/simulator payment endpoints.

stateDiagram-v2 diagram; its source follows
Diagram source
stateDiagram-v2
  [*] --> prepared
  prepared --> canceled: Cancel before start reservation
  prepared --> starting: Funding attested and command dispatched
  starting --> charging: Matching active transaction observed
  starting --> reconciliation_required: Dispatch uncertain or stalled
  charging --> stopping: Stop dispatched
  charging --> awaiting_bill: Charger ends transaction
  stopping --> awaiting_bill: Completed transaction observed
  reconciliation_required --> awaiting_bill: Completed matching usage found
  awaiting_bill --> completed: Immutable bill finalized
  completed --> [*]
  canceled --> [*]

GET never finalizes, dispatches or captures. List responses are stored snapshots; individual session reads query live usage for an open, started physical session. Completed bills remain readable during a CSMS outage, and a completed session keeps a final usage snapshot: transaction_id matches bill.transaction_id, energy_wh/energy_kwh match the bill, and estimated_minor equals bill.total_minor. usage is null only before a transaction is observed and on canceled sessions. While charging, usage also carries the charger's newest meter sample: power_w (watts, 0 once the transaction ended, null when the charger reports no power), soc_percent when the vehicle reports state of charge, and metered_at. Managed bills expose the same usage on GET /bills/{id}.

Every status change is published as a charging_session.status_changed webhook event and finalization as bill.finalized. Tenants with a subscription have their started sessions observed by the API about every 15 seconds, so charging and awaiting_bill arrive without a client read. Errors use stable codes such as start_already_reserved, no_active_transaction, transactions_ambiguous and funding_released.

Optional settlement reports

After your backend verifies a processor result, record it if Ivora-side reporting helps your operations. Charging and bill finalization do not depend on submitting these reports.

{
  "kind": "capture",
  "operation_reference": "capture-1",
  "outcome": "succeeded",
  "amount_minor": 243,
  "currency": "USD"
}

Kinds are authorization, capture, refund and release. Outcomes are pending, unknown, succeeded and failed. Release uses zero; other kinds require positive integer cents. Use the same processor operation reference for an outcome update; a new reference means a distinct financial effect.

  • Pending/unknown reports can resolve to succeeded/failed. Terminal outcomes, amounts and currencies cannot change. Corrections require operator review.
  • Same operation and outcome deduplicate across different request keys. Delayed duplicates return the historical report without regressing the current state.
  • Captures require a final bill. Successful capture totals cannot exceed it. Partial captures are allowed. Refund totals cannot exceed successful captures; partial refunds are allowed. A refund does not reopen capture capacity.
  • Report successful captures before refunds. Out-of-order prerequisites return 409; reconcile in your backend and retry after recording the prerequisite.
  • Release requires a canceled or completed session. Reporting release does not call the processor or stop a charger.
  • captured_minor, refunded_minor and net_minor count successful reports only. uncollected_minor is final bill minus captures; voluntary refunds are not treated as newly collectible debt. The immutable bill never changes.

Your backend owns webhook signature checks, merchant verification, event inbox/deduplication and processor reads. Do not forward unsigned public webhook payloads or accept a browser's payment-success claim as evidence.

Recovery and current limits

One open physical external session is allowed per connector. This is an API session claim, not a global hardware reservation: manual commands and other CSMS interfaces still exist. Close the session through cancellation before start, or through finalization after actual completion, before creating another.

Start/stop reservations prevent duplicate dispatch even with different keys or callers. Reads include the original operations, so a rotated authorized key can inspect them. An offline preflight can reject before reserving start; inspect start_operation before deciding whether a new attempt is appropriate. Once reserved, a missing transaction alone is not proof of zero usage. The charging token lives 15 minutes; 20 minutes after the start was dispatched, cancel succeeds if the API finds no transaction matching the session's token, because none can start anymore. Before that, or once a transaction matched, cancel returns start_reserved: finalize actual usage instead. Do not create a new session to bypass uncertainty. Crashed dispatching operations, unmatched sessions and ambiguous transactions need operator reconciliation; there is no self-service force-close endpoint.

There is no automatic hold/energy cutoff, authorization-expiry handling, processor callback endpoint or background reconciliation in this release. Outbound webhooks announce state changes; your backend still owns those policies. A single-process SQLite repository is used internally; a dedicated PostgreSQL repository and worker coordination are required before multiple API replicas. Pricing is USD, energy-only. Marketplace payouts, taxes, discounts, subscriptions and disputes stay with the application. Physical end-to-end acceptance with external funding has not yet been performed.

Test without hardware or money movement

Create with source: "simulated", finalize with {"energy_wh": 6941}, and report fictional processor outcomes using synthetic references. Simulated sessions reject physical start/stop and cannot accept a real transaction ID. The reporting API itself never calls any payment processor. Run your processor's sandbox tests separately before connecting a real charger.

Compare managed adapters and the payment integration guide.