Webhooks
Receive episode lifecycle events at your endpoint instead of polling.
Webhooks push episode lifecycle events to an HTTPS endpoint you register, replacing polls of GET /v1/episodes.
Subscribe
POST /v1/webhooks
GET /v1/webhooks
DELETE /v1/webhooks/{id}Requires the admin scope.
curl -X POST https://api-prod.avearobotics.com/v1/webhooks \
-H "Authorization: Bearer ak_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://ingest.example.com/sentinel",
"events": ["episode.finalized", "episode.verified"]
}'The response includes the signing secret (whsec_...) once — it cannot be retrieved later, only rotated. URLs must be HTTPS and publicly resolvable; redirects are not followed. Up to 5 active subscriptions per org.
GET /v1/webhooks lists subscriptions with pending_deliveries, last_delivered_at, and last_delivery_error.
Events
| Event | Fires when | Scope |
|---|---|---|
episode.finalized | An episode's report is first indexed | per episode |
episode.synced | All files reached a destination | per episode × destination |
episode.verified | Uploaded checksums matched the manifest | per episode × destination |
episode.failed | A transfer attempt failed — may recover on retry | per episode × destination |
episode.pruned | The local copy was deleted by retention policy | per episode |
Payloads
Payloads carry identifiers plus the state change; fetch detail from the API with episode_id.
{
"schema_version": 1,
"episode_id": "3e1b...",
"robot_id": "5f3a...",
"recording_session_id": "session_20260810_141133",
"episode_seq": 3,
"destination_id": "dst_...",
"grant_id": "g_01H...",
"object_key": "sentinel/pick-place/unit-04/session_20260810_141133/",
"state": "verified"
}All events share this shape. Fields not relevant to an event are omitted: episode.finalized and episode.pruned carry identity only (no destination_id); error appears only on episode.failed.
Verify signatures
The secret from subscribe time (whsec_...) is a shared HMAC key: Avea signs every delivery with it, and your endpoint recomputes the signature to confirm the delivery came from Avea and wasn't altered in transit. Store it in your secret manager and verify every delivery before acting on it — the URL is publicly reachable, and the signature is a delivery's only authentication.
Deliveries carry no Authorization header. Your API key (Bearer ak_...) authenticates calls to the management endpoints above; it never appears in traffic to your endpoint.
| Header | Contents |
|---|---|
X-Avea-Event | Event type, e.g. episode.verified |
X-Avea-Event-Id | Unique event id — dedupe on this |
X-Avea-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>"> |
Compute the HMAC over the raw request body with your whsec_... secret; compare in constant time; reject stale timestamps (5 minutes is a reasonable window). During a rotation the header carries two v1= entries — accept if any matches.
import crypto from "node:crypto";
function verify(header, rawBody, secret, toleranceSec = 300) {
const parts = header.split(",").map((kv) => kv.split("="));
const t = parts.find(([k]) => k === "t")?.[1];
const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return sigs.some((sig) => {
const a = Buffer.from(sig, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}Delivery semantics
- At-least-once — dedupe on
X-Avea-Event-Id. - Ordered per subscription — a failing delivery holds the events behind it until it succeeds or dead-letters.
- 10-second timeout — return a 2xx fast and do the work async.
- Retries: network errors and
408,425,429,5xxretry at 1 m, 5 m, 30 m, 2 h, 12 h, 24 h — 7 attempts, then the delivery dead-letters. Other responses dead-letter immediately. The subscription stays active either way. - Subscriptions are auto-disabled only for unusable configuration (an unresolvable or invalid URL), never for HTTP failures. Re-activate with
POST /v1/webhooks/{id}/enable.
GET /v1/webhooks/{id}/dead-letters
POST /v1/webhooks/{id}/dead-letters/{delivery_id}/redeliverRotate the secret
POST /v1/webhooks/{id}/rotate-secretReturns a new secret. The old one keeps signing (as a second v1= entry) for 24 hours.
Test your endpoint
POST /v1/webhooks/{id}/testSends a synthetic episode.finalized (payload carries "test": true) through the real delivery pipeline, regardless of the subscription's event filter.