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.
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 IvoraWhat 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 suffix | Purpose | Required scopes |
|---|---|---|
POST / | Create session and tariff snapshot atomically | billing:write, settlement:write |
GET / | Find stored sessions, including by application_reference | billing:read |
GET /{session_id} | Observe live matched usage, bill, operations and reported settlement | billing:read |
POST /{session_id}/start | Attest funding and dispatch once | billing:write, settlement:write, stations:control |
POST /{session_id}/stop | Stop the session's matching active transaction | billing:write, stations:control |
POST /{session_id}/finalize | Finalize trusted completed usage | billing:write |
POST /{session_id}/cancel | Cancel before any start reservation | billing:write |
POST /{session_id}/settlement-reports | Append an external outcome | settlement:write |
GET /{session_id}/settlement-reports | Paginate the audit trail | billing: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.
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_minorandnet_minorcount successful reports only.uncollected_minoris 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.