Sentinel

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

EventFires whenScope
episode.finalizedAn episode's report is first indexedper episode
episode.syncedAll files reached a destinationper episode × destination
episode.verifiedUploaded checksums matched the manifestper episode × destination
episode.failedA transfer attempt failed — may recover on retryper episode × destination
episode.prunedThe local copy was deleted by retention policyper 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.

HeaderContents
X-Avea-EventEvent type, e.g. episode.verified
X-Avea-Event-IdUnique event id — dedupe on this
X-Avea-Signaturet=<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, 5xx retry 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}/redeliver

Rotate the secret

POST /v1/webhooks/{id}/rotate-secret

Returns a new secret. The old one keeps signing (as a second v1= entry) for 24 hours.

Test your endpoint

POST /v1/webhooks/{id}/test

Sends a synthetic episode.finalized (payload carries "test": true) through the real delivery pipeline, regardless of the subscription's event filter.