Reliability and testing

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.

flowchart diagram; its source follows
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]
ContractRetry behavior
Location, station and tariff creation, location update, station deletionSynchronous 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 moves8–128 printable non-space ASCII characters; unknown dispatch is not blindly repeated
Charger display set and deleteSynchronous 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 settingSynchronous 200; the same key and body replays the first result. Ivora itself resends running and final cost messages (see below)
Tenant bills/paymentsSame 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.

CodeMeaning
invalid_request, unauthenticated, forbidden, not_found, conflict, rate_limitedGeneric conditions without a more specific code
idempotency_key_invalid, idempotency_key_reused, request_in_progress, outcome_unknownRetry identity problems; see the table above
external_reference_takenAnother 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_configuredAbsent or outside your tenant
station_name_taken, station_offline, station_online, station_has_history, protocol_unsupported, transaction_ended, dispatch_unconfirmedCharger 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_releasedSession 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_conflictBilling and settlement invariants
template_render_failed, display_action_invalidCharger display delivery errors, reported in delivery.last_error rather than as a response
transaction_id_invalid, charger_rejected, final_cost_rejectedRunning 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_disabledIvora 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_builtinIvora's display adapter administration only; tenant routes do not return them
payment_account_required, payment_account_not_readyThe 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_pinOnboarding refused because Ivora's records already name a Stripe account for the tenant; Ivora staff pin that account instead
bill_exceeds_holdThe 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_refusedA simulated bill cannot be paid with live Stripe; use the simulator service
stripe_rejectedStripe refused the request before creating anything; correct the input and retry with a new idempotency key
checkout_expired_before_createWhy a payment ended canceled when a retried authorization came too late to create its hosted checkout; nothing was collected
stripe_not_configuredThe payment provider has no Stripe key in this environment (503)
csms_unavailable, csms_request_failed, provider_error, temporarily_unavailableDependencies; 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.
BoundaryUnit
Raw OCPP 1.6 energy registersWh
Core transaction energykWh
Explicit simulated bill inputInteger Wh
API energy fieldsInteger energy_wh for arithmetic beside a decimal energy_kwh string for display
New bill/payment/tariff amountsInteger 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.