Reliability and testing

Webhooks and events

Available: tenant fleet API. Register an HTTPS endpoint and Ivora pushes signed events for operation completion, charging-session status changes, bill finalization, charger display delivery and charger events (connectivity, boots, connector status, faults and meter samples). The same events are readable by cursor at GET /v1/tenants/{tenant_id}/events for recovery or polling.

sequence diagram; its source follows
Diagram source
sequenceDiagram
  participant App as Application backend
  participant API as Ivora API
  App->>API: POST webhooks {url, events}
  API-->>App: 201 endpoint with one-time secret
  App->>API: Command, session or finalize write
  API-->>App: Immediate response
  API->>App: POST event with Ivora-Signature
  App-->>API: 2xx acknowledges (retry with backoff otherwise)
  App->>API: GET events?after=cursor (recovery)
EndpointPurposeScope
POST /v1/tenants/{tenant_id}/webhooksRegister an endpoint; returns the signing secret oncewebhooks:write plus the read scope of each event
GET …/webhooks, GET …/webhooks/{id}Inspect endpoints (no secret)webhooks:write
DELETE …/webhooks/{id}Disable; pending deliveries become failedwebhooks:write
POST …/webhooks/{id}/testQueue a signed webhook.test deliverywebhooks:write
GET …/webhooks/{id}/deliveriesAttempts, HTTP results and next retrywebhooks:write
GET /v1/tenants/{tenant_id}/eventsCursor-ordered event log; ?type= and ?station_id= filterstations:read; billing events also need billing:read

Event types

TypeWhendata
operation.completedA 202 operation reaches succeeded, rejected or unknownoperation (full record with result), scope such as station.command or external.start
charging_session.status_changedAn external session's derived status changessession_id, status, previous_status, session (full record)
bill.finalizedA managed or external bill becomes finalbill, session_id when externally settled
display.delivery_changedA charger display is created or its delivery.state, delivery.shown or deleted changesdisplay (full record); read scope stations:read
webhook.testRequested through the test endpointwebhook_id, message
station.connected, station.disconnected, station.booted, connector.status_changed, station.fault_opened, station.fault_cleared, meter.sampledSee charger eventsCarry station_id, station_sequence and occurred_at as well; read scope stations:read

Endpoints may set stations (a list of station resource IDs) to receive the charger event types for those stations only, and meter.sample_interval_seconds (default 60, minimum 10) to space meter.sampled deliveries per transaction.

Session statuses that depend on the charger (charging, awaiting_bill, stopping) are discovered by reads. While a tenant has an active subscription to charging_session.status_changed, the API observes its started physical sessions about every 15 seconds and publishes the change itself. Every client read also publishes a change it observes, so a status is emitted once even under concurrent reads.

Delivery contract

The body is compact JSON with id, type, tenant_id, resource_id, created_at and data. Headers carry Ivora-Event-Id, Ivora-Event-Type and Ivora-Signature: t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256 of <t>.<raw body> with the endpoint secret. Verify with a constant-time compare and reject stale timestamps according to your own policy.

import hashlib, hmac
def verify(secret, header, body: bytes):
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Any 2xx acknowledges. Ivora reads only the status line and headers, up to 32 KiB, never the response body, and does not follow redirects. Each connect, write or read step is limited to 10 seconds and a whole attempt to 15 seconds, so acknowledge before doing slow work. Other responses and failed attempts, including those that exceed either limit, retry after 1 minute, 5 minutes, 30 minutes, 2 hours and then every 12 hours, eight attempts in total, before the delivery is marked failed. Delivery is at-least-once and may be out of order: deduplicate by event id, which is also the event's id in the event log, so one inbox can absorb both webhooks and cursor reads, and rely on status in the payload or a fresh read rather than on arrival order. A tenant may keep ten active endpoints. Subscriptions belong to the tenant and keep delivering after the creating key is revoked until they are disabled.

Endpoints must be public HTTPS hostnames with a valid port. IP literals, local names, Ivora's own domains and invalid ports are refused with 422. Ivora resolves the hostname at every attempt and sends only when each resolved address is public. An address that is private, loopback, link-local, multicast, reserved or unspecified fails the attempt with last_error DestinationRefused, and nothing is sent. Otherwise Ivora tries the addresses in the resolver's order until one connects.

In production, endpoints must also use port 443. Registering any other port returns 422.

The event log and delivery journal live in the same single-process store as operations. Replicated delivery workers and per-endpoint rate limits are not part of this release.