Events and webhooks
Every accepted action appends one or more events in the same transaction
as the change it describes. The log is append-only, ordered by a monotonic
seq, and is the only source every downstream surface reads: webhooks,
rollups, the live boards, and your integration. There is no change that is
not an event.
Reading the log
Section titled “Reading the log”GET /v1/events returns a page of events, oldest first. Scope:
events:read.
| Parameter | Use |
|---|---|
since | Events with seq greater than this number. The simplest way to keep your own position. |
cursor | The next_cursor from the previous page. Opaque; wins over since if both are set. |
correlation_id | Only the events one action invocation appended. |
object_type | One type, or several comma-separated. Matches object events of that type and link events touching it. |
Each event carries its seq, its kind (object_created, object_modified,
a link established or removed, and the action’s own domain events such as
reservation.checked_in.decided), the objects involved, the principal who
caused it, and the correlation_id of the invocation.
Polling is the right shape for a batch consumer: durable, replayable, and
indifferent to your downtime. Store the last seq you processed; start
from it next time.
Tailing it live
Section titled “Tailing it live”GET /v1/events/stream holds the connection open and frames each new
event as it lands, as Server-Sent Events:
id: 12345data: {"seq": 12345, "kind": "object_modified", …}The id of each frame is the event’s seq. A browser EventSource or any
SSE client reconnects with Last-Event-ID set to the last one it saw and
the server resumes from there; ?since=12345 does the same for clients
that do not send the header. The stream opens with a comment frame so a
proxy forwards the headers at once, and sends a keepalive comment while
nothing is happening. Scope: events:read.
Use the stream for anything that should react within a beat: a board, an agent, a screen.
Webhooks
Section titled “Webhooks”A webhook subscription asks the kernel to deliver matching events to a URL
of yours. Manage subscriptions with webhooks:write and read them with
webhooks:read:
| Route | What it does |
|---|---|
POST /v1/webhook-subscriptions | Create a subscription: the receiver URL, a filter, and a signing key you are shown once. |
GET /v1/webhook-subscriptions · GET …/{id} | List, or read one, with its status and last_delivered_seq. |
POST …/{id}/rotate-key | Add a secondary signing key. Both keys are valid during the rotation window. |
POST …/{id}/promote-key | Make the secondary key primary and retire the old one. |
GET …/{id}/deliveries | The delivery attempts, with their outcomes. |
What arrives
Section titled “What arrives”One POST per batch, a CloudEvents 1.0 envelope carrying the events past
the subscription’s last_delivered_seq that match its filter. The exact
payload is in the API reference under webhooks, as
aerandil_event_delivery.
Verifying a delivery
Section titled “Verifying a delivery”Every delivery carries an Aerandil-Signature header:
Aerandil-Signature: t=1730000000,v1=5f3a…[,v1=9c1b…]Compute HMAC-SHA256 over the string "{t}.{raw_body}" with your signing
key and compare it to a v1 value with a constant-time comparison. Reject a
delivery whose t is more than five minutes old. During a key rotation
there are two v1 entries, one per key; a match on either is valid. Ignore
any other chunk you do not recognise.
The receiver contract
Section titled “The receiver contract”- Return a
2xxwithin ten seconds. Do the work after you have answered. - Anything else, or no answer, marks the batch failed and schedules a retry
at
60 · 2ⁿseconds plus up to thirty seconds of jitter, capped at 24 hours, for up to twelve consecutive failures. - After the twelfth failure the subscription moves to
dead_letterand deliveries stop until an operator resets it, usually after advancinglast_delivered_seqpast the batch that could not be processed. - Delivery is at least once. De-duplicate on the event
id, which is the log’sseq. The same event may arrive twice during a retry.
A receiver that cannot keep up should not slow its answer; it should
acknowledge and queue. The log is replayable, so nothing is lost by
reading it again from a seq you trust.
Which to use
Section titled “Which to use”| You are building | Use |
|---|---|
| A nightly sync or a data warehouse load | Poll GET /v1/events from your last seq. |
| A board, an agent, a screen that reacts now | GET /v1/events/stream. |
| A system of yours that should be told, without polling | A webhook subscription, with the receiver contract above. |
- Usage guidelines: idempotency, versions and errors.
- API reference: the event and delivery schemas.