Skip to main content
Webhooks let your backend react to Aurous Labs events without polling. Register an endpoint, subscribe to one or more event types, and we POST a signed payload as soon as the event fires.

Quick start

1

Register an endpoint

Send a POST /v1/webhook_endpoints with your HTTPS receiver URL. The response carries a one-time secret — store it; you cannot read it again.
Response (the only time secret is non-null):
2

Verify the signature on every delivery

Every delivery carries an Aurous-Webhook-Signature header of the form t=<unix_sec>,v1=<hex>. Reconstruct the canonical signed payload (${t}.${raw_body}) and HMAC-SHA256 it with your stored secret. See the verifier examples.Aurous Labs uses Unix seconds (a 10-digit timestamp) for the t= parameter, matching the Stripe webhook convention.
3

Return 2xx to acknowledge

Acknowledge within 10 seconds. Any non-2xx, timeout, connection refused, or TLS error counts as a failed delivery and triggers a retry on the exponential-backoff schedule.

Event taxonomy

Subscribe to a subset via the events array, or pass ["*"] to subscribe to every type at create time (the wildcard is expanded server-side at create time — events added in a later API version do NOT auto-subscribe). image.expired, video.expired, image.moderation_rejected, and video.moderation_rejected are reserved enum values that are not emitted today — see the Changelog for when each lights up.

Routing by API key

By default every active endpoint subscribed to an event receives it. You can instead bind an endpoint to a single API key so it receives only the events generated with that key — ideal for keeping environments separate (your staging key delivers to your staging receiver, your production key to your production receiver). Set api_key_id to a key’s public id (key_…) on create or update:
Delivery is most-specific:
  • An event generated with key K is delivered to the endpoint(s) bound to K.
  • If no endpoint is bound to K, the event falls back to your unscoped endpoints (api_key_id: null — the catch-all).
  • Events with no API key (dashboard activity, usage.balance_low) go to unscoped endpoints only.
Binding the first endpoint to a key removes that key’s events from your catch-all endpoint(s) — from then on they are delivered only to the bound endpoint. Bind one endpoint per key, or keep one unscoped as a deliberate fallback.
Pass api_key_id: null on PATCH to unbind (the endpoint becomes a catch-all again); omit the field to leave the binding unchanged. If the bound key is deleted, the endpoint automatically reverts to a catch-all. Finding a key id. Use the key_… public id — it is case-sensitive. Read your key ids from the API keys page in the dashboard, or programmatically from GET /v1/usage?group_by=api_key_id (each bucket is keyed by its api_key_id).

Payload shape

Every delivery body is a single JSON object — the AurousEvent envelope:
synthetic: true appears on the envelope (never inside data) when the delivery was triggered via POST /v1/webhook_endpoints/:id/test. Receivers can filter test fires from production traffic with a single field check.

Headers

Every delivery carries the following request headers:

Signature format

Aurous-Webhook-Signature: t=<unix_sec>,v1=<hex> Where:
  • <unix_sec> is the moment we minted the signature (a 10-digit Unix timestamp in seconds — the Stripe convention).
  • <hex> is the lowercase hex of HMAC-SHA256(secret, "${t}.${raw_body}").
The t value goes INSIDE the HMAC payload, so an attacker cannot replay a captured body with a tweaked timestamp. We recommend rejecting any delivery whose t is older than 5 minutes — adjust the tolerance if your receiver uses a lossy queue.

Retries and dead-letter

Failed deliveries (non-2xx, timeout, connect refused, or TLS error) are retried on this schedule: After attempt 5, the delivery is marked is_terminal: true and the row is flagged with is_dead_letter: true. We stop retrying. If consecutive_failures >= 20 AND there has been no successful delivery in the last 24 hours, we auto-disable the endpoint:
  • is_active flips to false.
  • We email the team owner.
  • Other active endpoints on the team receive a webhook.endpoint_disabled event so peer integrations can react.
Re-enable via PATCH /v1/webhook_endpoints/:id with { "is_active": true } once you’ve fixed the receiver. The counter resets to 0 on re-enable.

Receiver cookbook

The contract is symmetric: we sign with the active secret only; you verify against the active secret first, then the previous secret as a 24h fallback. This receiver-side fallback is what makes secret rotation zero-downtime.

Node.js (Express)

Python (Flask)

Manual curl verification

Useful for one-off debugging: capture a delivery body + signature, then verify locally.

Rotating the secret

Hit POST /v1/webhook_endpoints/:id/rotate_secret to mint a new plaintext. The response carries the new secret exactly once. The contract during rotation:
  • Sender (Aurous Labs): signs every new delivery with the active secret only.
  • Receiver (you): for the next 24 hours, store the previous secret as PREV and verify against ACTIVE first, then fall back to PREV (as shown in the cookbook).
Why? A delivery in flight at the moment of rotation may arrive seconds later, signed with the old secret. The 24h dual-validate window guarantees zero downtime for receivers under realistic clock skew + retry windows. After 24 hours, drop PREV. We never sign with it again.

Test firing

Hit POST /v1/webhook_endpoints/:id/test with an event_type to enqueue a real, signed synthetic delivery. The envelope carries synthetic: true so receivers can filter test fires from production traffic.
Test fires use an isolated rate-limit bucket (webhooks_test, 30 / minute) so they never contend with normal webhook traffic.

Inspecting deliveries

Walk the per-attempt log via GET /v1/webhook_endpoints/:id/deliveries:
Each row carries the attempt number, response status (or null for transport errors), error class (http_4xx / http_5xx / timeout / connect_refused / tls_error / connect_error), and the first 1KB of the response body if any. Cursor-paged via ?starting_after=<dlv_id>.