Billing and settlement

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.

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

API 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.

StepRequestBody or result
Snapshot tariffPOST /v1/tenants/{tenant_id}/billsstation_id, connector_id, tariff_id, source: csms
AuthorizePOST /payment/stripe/v1/tenants/{tenant_id}/paymentsbill_id, optional return_surface; returns next_action
Verify authorizationGET /payment/stripe/v1/tenants/{tenant_id}/payments/{payment_id}Wait for authorized
StartPOST /v1/tenants/{tenant_id}/bills/{bill_id}/startRequires billing and station-control scopes
Observe and stopGET /v1/tenants/{tenant_id}/bills/{bill_id} plus station commandsusage.transaction_id identifies the physical transaction
FinalizePOST /v1/tenants/{tenant_id}/bills/{bill_id}/finalizetransaction_id for completed physical usage
CapturePOST /payment/stripe/v1/tenants/{tenant_id}/payments/{payment_id}/captureNo 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.

stateDiagram-v2 diagram; its source follows
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
RequestAccessResult
GET /payment/stripe/v1/tenants/{tenant_id}/accountbilling:readstatus, 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-KeyA single-use Stripe onboarding url
POST …/account/disconnectSigned-in tenant-admin or Ivora staff; API keys are refused; Idempotency-KeyThe account view with status: disconnected

status is one of:

  • none: no payment account. New Stripe payments of physical bills return 409 payment_account_required.
  • pending_review: onboarded, awaiting Ivora's approval. It takes no payment yet: new payments return 409 payment_account_required.
  • active: new payments are charged on this account. While Stripe reports charges_enabled: false (for example, requirements are still due), new payments return 409 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}/release cancels an unused authorization.
  • POST .../payments/{payment_id}/refund requests one full refund after capture. It requires the billing:refund scope (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 canceled with captured_minor 0.

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_hold alert 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:write on 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_hold with total_minor and hold_minor, and records an over_hold alert. 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.