Ivora-managed payments
Available where Ivora has enabled live payments: Stripe live mode and
simulator. Your backend calls Ivora's common payment contract. The registered
adapter owns processor credentials and money operations. Discover configured
services using GET /v1/tenants/{tenant_id}/payment-services: stripe appears,
with payment_mode: live, only once Ivora has enabled live card payments in
production. Until then the simulator is the only managed service; integrate your
own processor through application-managed payments. A physical
bill's Stripe payment is a direct charge on the tenant's own Stripe account, so
the tenant needs an active payment account first.
Diagram source
sequenceDiagram
participant App as Application backend
participant API as Ivora billing API
participant Pay as Payment adapter
participant C as CSMS and charger
App->>API: Create bill with station, connector and tariff
API-->>App: Bill ID and frozen tariff
App->>API: Create payment for bill
API->>Pay: Prepare authorization on the tenant's pinned account
Pay-->>App: Checkout next action through API
Note over App,Pay: Driver completes hosted checkout
App->>API: Read payment status
API->>Pay: Verify provider state
API-->>App: Authorized
App->>API: Start bill
API->>C: Register entry token and dispatch start once
Note over App,C: Observe actual charging and stop when appropriate
App->>API: Finalize using completed transaction ID
API->>C: Read trusted usage
API-->>App: Immutable final amount
App->>API: Capture payment
API->>Pay: Capture final bill amount or release zero usageAPI sequence
All paths below are relative to https://api.ivoracharge.com. Writes require a
persisted Idempotency-Key. Examples use placeholder IDs; discover real IDs first.
| Step | Request | Body or result |
|---|---|---|
| Snapshot tariff | POST /v1/tenants/{tenant_id}/bills | station_id, connector_id, tariff_id, source: csms |
| Authorize | POST /payment/stripe/v1/tenants/{tenant_id}/payments | bill_id, optional return_surface; returns next_action |
| Verify authorization | GET /payment/stripe/v1/tenants/{tenant_id}/payments/{payment_id} | Wait for authorized |
| Start | POST /v1/tenants/{tenant_id}/bills/{bill_id}/start | Requires billing and station-control scopes |
| Observe and stop | GET /v1/tenants/{tenant_id}/bills/{bill_id} plus station commands | usage.transaction_id identifies the physical transaction |
| Finalize | POST /v1/tenants/{tenant_id}/bills/{bill_id}/finalize | transaction_id for completed physical usage |
| Capture | POST /payment/stripe/v1/tenants/{tenant_id}/payments/{payment_id}/capture | No client-selected amount |
return_surface chooses where hosted checkout returns: developer (default,
the sandbox page) or driver, for which the API selects the driver charging
page for the bill's station and connector; callers never supply URLs. A single
bill read reports usage (the matched transaction once start was dispatched,
with energy_wh, estimated_minor and timestamps; a final snapshot once the
bill is final) and start_operation. GET /v1/tenants/{tenant_id}/bills
lists stored bills by station_id and status without observing usage.
Finalization verifies tenant, station, connector, charging token, timestamps and transaction ownership. Caller-supplied energy is accepted only for explicitly simulated bills. The core token's entry window is 15 minutes; that does not define the length of an already-started charging session.
The hosted checkout expires 60 minutes after the payment is first requested,
and Stripe needs at least 30 minutes of that left when it creates the checkout.
If a retried authorization arrives too late for that and no checkout exists
yet, the payment ends canceled (reason checkout_expired_before_create) and
nothing is collected. A bill keeps its one payment, so start again with a new
bill and payment.
Payment records
Every payment carries payment_mode, which is the provider's own record of the
payment: live only for a Stripe payment taken in live mode on a connected
account, test otherwise. The simulator is always test. authorized_at is
the time Stripe authorized the hold (null before authorization and for the
simulator).
The tenant-scoped single read of one payment (GET …/payments/{payment_id})
also returns processor_reference (the Stripe PaymentIntent ID, for reconciliation
in the Stripe dashboard) and customer_email, the address the driver entered at
hosted checkout. No list, event or other response carries these two fields.
customer_email is personal data: use it for that driver's receipt, and show it
only to the tenant.
Payment account
A Stripe payment of a physical bill is a direct charge on the tenant's own Stripe Connect Standard account. The account Ivora charges is the one pinned in Ivora's payment provider; it is fixed on the payment when the payment is authorized, so a later change of account never moves an existing payment. Ivora takes no application fee, and the checkout uses the account's own statement descriptor. Funds settle to that account, and refunds are issued from it.
Diagram source
stateDiagram-v2
[*] --> none
none --> active: onboarding (test) or staff pin
none --> pending_review: onboarding (production)
pending_review --> active: Ivora staff approve
active --> disconnected: disconnect
pending_review --> disconnected: disconnect
disconnected --> active: new onboarding or staff re-pin| Request | Access | Result |
|---|---|---|
GET /payment/stripe/v1/tenants/{tenant_id}/account | billing:read | status, stripe_account_id, charges_enabled, details_submitted, requirements_due, source, livemode |
POST …/account/onboarding-link {portal} | Signed-in tenant-admin of the tenant, or Ivora staff; API keys are refused; Idempotency-Key | A single-use Stripe onboarding url |
POST …/account/disconnect | Signed-in tenant-admin or Ivora staff; API keys are refused; Idempotency-Key | The account view with status: disconnected |
status is one of:
none: no payment account. New Stripe payments of physical bills return409 payment_account_required.pending_review: onboarded, awaiting Ivora's approval. It takes no payment yet: new payments return409 payment_account_required.active: new payments are charged on this account. While Stripe reportscharges_enabled: false(for example, requirements are still due), new payments return409 payment_account_not_ready.disconnected: no payment account. Payments already authorized keep settling on the account they were authorized on. The Stripe account itself is not changed.
Onboarding reuses a pinned account: it returns a new link for the same account
and never creates a second one. A tenant with no pin gets a new Standard
account, and after a disconnect onboarding creates a new account. Each link is
single-use, so an identical retry returns a fresh link. The return page is
chosen by the server from portal; callers never supply a URL.
In production an onboarding-created account is pinned pending_review and takes
no live payment until an Ivora platform administrator approves it. Ivora
reviews the account and approves it through the staff pin route below. The only
portal is analytics, the Platform's Host page.
Existing Stripe accounts are pinned by Ivora staff. When Ivora's earlier
records already name a Stripe account for the tenant and nothing is pinned yet,
onboarding returns 409 payment_account_requires_staff_pin instead of creating
a second account. Ivora staff then pin the existing account with
PUT /v1/admin/tenants/{tenant_id}/payment-account (signed-in admin or
platform-admin only; API keys are refused). The same route approves a
pending_review pin (approve_onboarding), replaces an active pin (replace)
or pins an account whose earlier records disagree (force_unverified_source);
each flag needs a recorded reason. Before any pin, Ivora verifies the account
with Stripe: a Standard account, charges enabled, United States, USD, and not
pinned to another tenant. Tenant applications never call this route.
Release and refund
POST .../payments/{payment_id}/releasecancels an unused authorization.POST .../payments/{payment_id}/refundrequests one full refund after capture. It requires thebilling:refundscope (see authentication).- Neither action stops the charger. Resolve physical charging first.
- A zero final bill releases its hold. So does a Stripe bill below Stripe's
50-cent minimum charge, which Stripe cannot collect: capture waives it,
releases the hold and returns
canceledwithcaptured_minor0.
Bills near or above the hold
Capture never exceeds the authorization hold, and there is no automatic top-up or silent amount cap.
- When Ivora observes an open bill's estimate at 80% of its hold, it records a
near_holdalert for its operators. Nothing is stopped or charged. - Your application should stop charging before the hold is used up. Record
that you did with
POST /v1/billing/bills/{bill_id}/alerts{"kind": "hold_stop"}(billing:writeon the bill's tenant). It is recorded once per bill; repeating it returns the same alert. Alerts never move money. - A final bill above its hold makes capture return
409 bill_exceeds_holdwithtotal_minorandhold_minor, and records anover_holdalert. Ivora staff settle it by capturing the authorization limit; the remainder is not charged.
Limits
The initial path supports USD, a $0.50–$100 authorization, one capture and one full refund. It has no partial refunds, application fees, marketplace payouts, general external provider registration or signed settlement callbacks. Your application polls status and controls settlement timing. Ivora runs a nightly read-only reconciliation of payments against Stripe and against bills; it reports differences to Ivora's operators and never captures, releases or refunds.
Simulated bills never reach live Stripe: a simulated bill paid with service
stripe returns 409 simulated_live_refused. Use the simulator service for
simulated bills.
The API and provider currently use single-worker storage. Replicas require a storage/worker coordination migration. Physical charging-to-settlement acceptance is outstanding; test-mode Stripe with simulated usage does not prove hardware interoperability. See testing.
Releasing an abandoned start
A hold cannot be released while a dispatched start could still produce a
transaction. The charging token lives 15 minutes; 20 minutes after the start
was dispatched, release succeeds when no transaction matches the bill's
token. Before that, or when a transaction matched, release returns
start_reserved; finalize the actual usage and capture instead.