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.
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)| Endpoint | Purpose | Scope |
|---|---|---|
POST /v1/tenants/{tenant_id}/webhooks | Register an endpoint; returns the signing secret once | webhooks: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 failed | webhooks:write |
POST …/webhooks/{id}/test | Queue a signed webhook.test delivery | webhooks:write |
GET …/webhooks/{id}/deliveries | Attempts, HTTP results and next retry | webhooks:write |
GET /v1/tenants/{tenant_id}/events | Cursor-ordered event log; ?type= and ?station_id= filter | stations:read; billing events also need billing:read |
Event types
| Type | When | data |
|---|---|---|
operation.completed | A 202 operation reaches succeeded, rejected or unknown | operation (full record with result), scope such as station.command or external.start |
charging_session.status_changed | An external session's derived status changes | session_id, status, previous_status, session (full record) |
bill.finalized | A managed or external bill becomes final | bill, session_id when externally settled |
display.delivery_changed | A charger display is created or its delivery.state, delivery.shown or deleted changes | display (full record); read scope stations:read |
webhook.test | Requested through the test endpoint | webhook_id, message |
station.connected, station.disconnected, station.booted, connector.status_changed, station.fault_opened, station.fault_cleared, meter.sampled | See charger events | Carry 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.