Skip to content

Create a webhook subscription

POST
/v1/webhook-subscriptions
curl --request POST \
--url https://api.aerandil.com/v1/webhook-subscriptions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "filter": { "object_types": [ "example" ] }, "start_position": "beginning", "url": "https://example.com" }'

Register a receiver URL plus an optional object_types filter and a starting position. Returns the freshly generated signing_key exactly once — record it in your secret vault; subsequent reads expose only the fingerprint.

The start_position defaults to now: only events past the current head are delivered. Pick beginning to replay the full event log (useful for backfill / historical projection rebuilds).

Accepts Idempotency-Key (Stripe shape — see [draft-ietf-httpapi-idempotency-key-header]). A retry with the same key returns the same response, including the original signing_key.

Receiver URL must use HTTPS (loopback HTTP allowed in dev only — see validate_url in webhooks/handlers.rs).

Idempotency-Key
string
>= 1 characters

Opaque idempotency token (Stripe-shape). Repeated POSTs with the same key replay the original response within the cache TTL rather than creating a duplicate object.

Media typeapplication/json
object
filter

Filter narrowing what the worker delivers. Empty / absent means “every event in the tenant”.

object
object_types

Narrow to events whose object_type matches one of the listed api_names. For link events, either endpoint type matching counts — a filter of ["hospitality.reservation"] includes reservation_for_guest events.

Array<string>
start_position

Where the cursor starts. beginning replays from seq = 0 (full backfill); now / head skips historic events and delivers only what arrives after the subscription is created. Default now.

Allowed values: beginning now head
url
required

Receiver URL. HTTPS only (HTTP allowed only against loopback hosts for dev/test). Max 2048 chars.

string format: uri

Subscription created. The signing_key is returned only here and on rotate-key — record it now or rotate to get a new one.

Media typeapplication/json

Subscription state plus the freshly minted signing key. Returned ONLY by create (POST .../webhook-subscriptions) and rotate (POST .../{id}/rotate-key); every other read surfaces fingerprints only. Customer captures the secret into their vault at this moment — there is no recovery path if it’s lost (the rotation flow is the recovery).

object
consecutive_failures
required

Resets to 0 on the first 2xx after any failure. Transitioning to dead_letter happens at 12 consecutive failures (~3-day retry window).

integer
created_at
required
string format: date-time
filter
required

Filter narrowing what the worker delivers. Empty / absent means “every event in the tenant”.

object
object_types

Narrow to events whose object_type matches one of the listed api_names. For link events, either endpoint type matching counts — a filter of ["hospitality.reservation"] includes reservation_for_guest events.

Array<string>
id
required
string format: uuid
last_delivered_seq
required

Cursor into the event log. Advances to the highest seq read on every successful delivery — filtered-out events are still cursored past so the worker stays bounded under a narrow filter.

integer
next_attempt_at

Earliest moment the worker will retry. Unset when the subscription is caught up; set in the future when an exponential-backoff retry is scheduled.

string format: date-time
primary_key_fingerprint
required

First 8 hex chars of sha256(primary_signing_key) — 16 chars in total. Exposed instead of the secret on every read.

string
/^[0-9a-f]{16}$/
secondary_key_fingerprint

Present during a key-rotation window. Cleared on promote-key.

string
/^[0-9a-f]{16}$/
status
required

Lifecycle of a webhook subscription. dead_letter is sticky — the worker stops attempting further deliveries until the customer PATCHes status back to active (and typically advances last_delivered_seq past the poison batch).

Allowed values: active paused dead_letter
tenant_id
required
string format: uuid
updated_at
required
string format: date-time
url
required

Receiver URL. HTTPS-only (except loopback in dev).

string format: uri
idempotent_replay

Set to true on a retry that the idempotency helper served from cache. Treat as advisory — the response body is the canonical state regardless.

boolean
signing_key
required

32-byte HMAC key, hex-encoded (64 chars). Used to verify the Aerandil-Signature header on every inbound delivery to the receiver.

string
/^[0-9a-f]{64}$/
signing_key_slot
required

primary on create (the only key); secondary on rotate (added alongside the existing primary during the rotation window).

Allowed values: primary secondary
Example
{
"status": "active",
"signing_key_slot": "primary"
}
Cache-Control
string
Allowed value: no-store

Always no-store.

Location
string

/v1/webhook-subscriptions/{id} for the created resource.

Validation error — invalid_url, malformed body, unknown fields, or invalid Idempotency-Key.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}