Retries, uncertain outcomes and units
Persist one idempotency key before each intended write. Reuse the same key and identical body after a lost response. A changed key can describe a new operation; it is not a recovery technique.
Diagram source
flowchart TD
Write[Persist identity and send write] --> Response{Response known?}
Response -->|Yes| State[Inspect returned state]
Response -->|Timeout or disconnect| Read[Read original operation or payment]
Read --> Known{Outcome resolved?}
Known -->|Yes| State
Known -->|No| Reconcile[Keep original identity and reconcile]
State --> Physical[Observe physical and financial completion separately]| Contract | Retry behavior |
|---|---|
| Location, station and tariff creation, location update, station deletion | Synchronous 201 or 200; the same key and body replays the same resource, 409 request_in_progress while the first attempt runs, 503 outcome_unknown needs an inventory read before a new key |
| Tenant commands, credentials and moves | 8–128 printable non-space ASCII characters; unknown dispatch is not blindly repeated |
| Charger display set and delete | Synchronous 200; the same key and body replays the first result. Sending the same display content under a new key keeps its revision. Ivora itself resends the screen push (see below) |
| Running cost setting | Synchronous 200; the same key and body replays the first result. Ivora itself resends running and final cost messages (see below) |
| Tenant bills/payments | Same key and body; one payment per bill across keys/providers; money actions retain durable identity |
For commands, succeeded means the adapter replied. dispatching and unknown
require observation/reconciliation. A restart does not prove an upstream action
failed. Read operations with the originating key.
Charger displays are the one deliberate exception to at-most-once dispatch. A display is idempotent screen content, so Ivora's sweep resends it after failures and reboots. Running cost messages fall under the same exception: they only replace the cost a screen shows, so a failed send is retried with backoff. A charger that refuses them is not retried until it reboots. The exception never extends to commands or to anything that moves money.
Managed Stripe's current unknown-operation retry bound is 23 hours. It is a provider-specific recovery rule, not a universal deadline for future processors. Incomplete holds and mismatched provider receipts require reconciliation; they cannot be treated as successful payments. Refunds may be pending or failed.
Error codes
Every failure is {"error": {"code", "message", "details"}, "request_id"}.
code is stable and safe to branch on; message is for people; details
carries identifiers such as operation_id or resource_id when they exist.
Rejected operations repeat the same shape in result.error.
| Code | Meaning |
|---|---|
invalid_request, unauthenticated, forbidden, not_found, conflict, rate_limited | Generic conditions without a more specific code |
idempotency_key_invalid, idempotency_key_reused, request_in_progress, outcome_unknown | Retry identity problems; see the table above |
external_reference_taken | Another resource already carries the reference; details.resource_id names it |
location_not_found, station_not_found, tariff_not_found, connector_not_found, transaction_not_found, session_not_found, bill_not_found, webhook_not_found, evse_not_found, display_not_configured | Absent or outside your tenant |
station_name_taken, station_offline, station_online, station_has_history, protocol_unsupported, transaction_ended, dispatch_unconfirmed | Charger preconditions, deletion rules and dispatch results |
session_closed, session_not_physical, start_already_reserved, stop_already_reserved, start_reserved, no_active_transaction, transactions_ambiguous, funding_released | Session state machine; start_reserved also guards payment release until the charging token expired without a transaction |
bill_final, bill_open, transaction_mismatch, transaction_active, usage_invalid, settlement_conflict | Billing and settlement invariants |
template_render_failed, display_action_invalid | Charger display delivery errors, reported in delivery.last_error rather than as a response |
transaction_id_invalid, charger_rejected, final_cost_rejected | Running cost delivery errors, reported in a session's last_error rather than as a response. charger_rejected: the charger refused the California Pricing RunningCost; nothing is sent until it reboots or the setting is written. final_cost_rejected: the charger refused that session's FinalCost; nothing is retried and later sessions are unaffected |
displays_disabled | Ivora has switched charger displays off in this environment: display PUT and DELETE return 409, reads still work |
template_not_found, template_invalid, template_name_reserved, template_not_draft, template_not_published, template_builtin | Ivora's display adapter administration only; tenant routes do not return them |
payment_account_required, payment_account_not_ready | The tenant has no active payment account (none, pending_review or disconnected), or Stripe does not yet let it take charges. No payment was created; the bill stays payable |
payment_account_requires_staff_pin | Onboarding refused because Ivora's records already name a Stripe account for the tenant; Ivora staff pin that account instead |
bill_exceeds_hold | The final bill is above the authorization hold; capture is refused and Ivora's operators settle it. details carries total_minor and hold_minor |
simulated_live_refused | A simulated bill cannot be paid with live Stripe; use the simulator service |
stripe_rejected | Stripe refused the request before creating anything; correct the input and retry with a new idempotency key |
checkout_expired_before_create | Why a payment ended canceled when a retried authorization came too late to create its hosted checkout; nothing was collected |
stripe_not_configured | The payment provider has no Stripe key in this environment (503) |
csms_unavailable, csms_request_failed, provider_error, temporarily_unavailable | Dependencies; treat writes as potentially uncertain |
Financial invariants
- A bill snapshots its tariff before charging and fixes its final amount once.
- A physical transaction can belong to one bill. Legacy-settled usage is rejected.
- Capture uses the finalized bill, never a browser-supplied amount, and never exceeds the authorization hold.
- Stop charging and release payment are different operations.
- Unknown financial effects keep their original identities for investigation.
| Boundary | Unit |
|---|---|
| Raw OCPP 1.6 energy registers | Wh |
| Core transaction energy | kWh |
| Explicit simulated bill input | Integer Wh |
| API energy fields | Integer energy_wh for arithmetic beside a decimal energy_kwh string for display |
| New bill/payment/tariff amounts | Integer minor units; USD cents currently |
Do not divide already-normalized kWh by 1,000. New bills round the final amount half up; legacy settlement keeps its existing truncation and ledger. See the legacy example before comparing totals.