Webhooks

Webhook endpoints receive an HTTPS POST whenever a subscribed event fires. Event vocabulary (closed, v1): video.ready, video.failed, analysis.completed, analysis.failed, analysis.canceled, stream.ended.

Rule #1: the webhook is a notification, not the source of truth

Payloads over 256 KiB are truncated (data.truncated: true). Delivery is at-least-once with no ordering guarantee. The reliable pattern is always:

  1. Receive the event → verify the signature → dedupe on the webhook-id header
  2. GET the resource by id for the authoritative state
  3. Act on that

Never assume analysis.completed arrives before (or after) your poll sees completed.

Register an endpoint

curl -s -X POST https://api.primateintelligence.ai/v1/webhook_endpoints \
  -H "Authorization: Bearer $PRIMATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://yourapp.com/webhooks/primate", "events": ["analysis.completed", "analysis.failed"]}'

The response contains the signing secret (whsec_…) exactly once. Store it. Max 10 endpoints per account.

Delivery contract

  • Body: {"id": "evt_…", "type": "analysis.completed", "created_at": "…", "data": {…full resource DTO…}}
  • Timeout 10s; only 2xx counts as delivered (3xx = failure)
  • Retry schedule: 1m, 5m, 30m, 2h, 8h, 24h with jitter; then the event dead-letters (visible in the deliveries API for 30 days)
  • 72h of 100% failure → the endpoint auto-disables + you get an email. Re-enable with POST /v1/webhook_endpoints/{id}/enable

Verify signatures

Deliveries are signed per Standard Webhooks. Three headers: webhook-id, webhook-timestamp, webhook-signature (v1,<base64 HMAC-SHA256>; multiple space-separated during rotation). Signed content is `${id}.${timestamp}.${rawBody}`; the HMAC key is the base64-decoded part of your whsec_ secret. Reject skew > 5 minutes; compare constant-time.

Both SDKs ship verifiers:

import { verifyWebhookRequest } from '@primate-intelligence/sdk';

app.post('/webhooks/primate', (req, reply) => {
  verifyWebhookRequest({
    secret: process.env.PRIMATE_WEBHOOK_SECRET!,
    headers: req.headers,
    rawBody: req.rawBody,          // MUST be the raw bytes, not re-serialized JSON
  });                              // throws on failure
  const event = JSON.parse(req.rawBody);
  if (alreadyProcessed(event.id)) return reply.code(204).send(); // dedupe on evt_ id
  // … fetch the resource, act …
  reply.code(204).send();
});
from primate_intelligence import verify_webhook_request

@app.post("/webhooks/primate")
async def receive(request: Request):
    raw = await request.body()
    verify_webhook_request(
        secret=os.environ["PRIMATE_WEBHOOK_SECRET"],
        headers=dict(request.headers),
        raw_body=raw,              # raw bytes, not re-serialized JSON
    )                              # raises ValueError on failure
    event = json.loads(raw)
    ...
    return Response(status_code=204)

Per-request webhook overrides on POST /v1/analyses / POST /v1/streams have no secret exchange and are delivered unsigned — register an endpoint when you need verification.

Rotate secrets

curl -s -X POST https://api.primateintelligence.ai/v1/webhook_endpoints/$ID/rotate_secret \
  -H "Authorization: Bearer $PRIMATE_API_KEY"

Both old and new secrets sign deliveries for 24h (the signature header carries two entries); verification passes when any signature matches your secret. Full key-compromise procedure: security model.

Debug deliveries

# last 100 deliveries with response codes
curl -s https://api.primateintelligence.ai/v1/webhook_endpoints/$ID/deliveries \
  -H "Authorization: Bearer $PRIMATE_API_KEY"

# redeliver a failed/dead one now
curl -s -X POST https://api.primateintelligence.ai/v1/webhook_endpoints/$ID/deliveries/$DELIVERY_ID/redeliver \
  -H "Authorization: Bearer $PRIMATE_API_KEY"

Local development

Use a tunnel (ngrok, cloudflared) so the API can reach your dev machine, or skip webhooks locally and use Prefer: wait — the two strategies are drop-in replacements for the completion signal.

Prefer raw text? This page is available as markdown.