SightRadardocs

Batch indexing and webhooks

Index thousands of photos at the batch rate, receive one signed webhook per photo, verify signatures, and recover from failures.

Last updated

Batch is how you load a gallery. One POST /v1/batches call takes up to 1,000 URL-only photos, processes them asynchronously at the batch tier (62 credits per photo, versus 93 real-time), and delivers one webhook per photo as results land. Inline images are rejected in batch; use the real-time endpoints for those.

Register a webhook endpoint

An HTTPS URL on your side. Public hosts only; private and non-HTTPS URLs are rejected with 400.

curl -X POST "$SR_BASE/v1/webhooks" \
  -H "Authorization: Bearer $SR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://api.example.com/sightradar/webhook"}'
200 response
{
  "webhook_endpoint_id": "whe_…",
  "url": "https://api.example.com/sightradar/webhook",
  "status": "active",
  "secret": "whsec_…",
  "note": "Store the secret now; it is not returned again."
}

If you omit secret, one is generated and returned once. Store it: every delivery is signed with it.

Submit the batch

curl -X POST "$SR_BASE/v1/batches" \
  -H "Authorization: Bearer $SR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "collection_id": "event-2026",
    "op": "index",
    "webhook_endpoint_id": "whe_…",
    "photos": [
      { "url": "https://cdn.example.com/1.jpg", "external_id": "1" },
      { "url": "https://cdn.example.com/2.jpg", "external_id": "2" }
    ]
  }'
202 response
{ "batch_id": "bb237a15-…", "total_photos": 2, "status": "pending" }

external_id is echoed back on every result, so use your own photo key.

Receive results

One POST per photo to your endpoint:

BatchWebhookEvent
{
  "batch_id": "bb237a15-…",
  "external_id": "1",
  "photo_index": 0,
  "status": "succeeded",
  "charged": true,
  "processed_at": "2026-07-12T09:14:03Z",
  "result": {
    "face_count": 2,
    "detected_face_count": 3,
    "faces": [{ "face_index": 0, "point_id": "…", "det_score": 0.91, "min_px": 140, "bbox": { "x": 10, "y": 8, "w": 60, "h": 60 } }],
    "model_version": "sr-recog-1"
  }
}

A failed photo has status: "failed", charged: false, and an error.code such as fetch_failed or decode_failed. face_count: 0 with status: "succeeded" is a valid image with no indexable face.

Verify the signature

Three headers accompany every delivery:

HeaderValue
X-SightRadar-TimestampUnix seconds when the delivery was signed.
X-SightRadar-Signaturehex(HMAC-SHA256(secret, "<timestamp>.<raw body>"))
X-SightRadar-Idempotency-Keywh:<job-id>, stable per result. Dedupe on it.

Compute the HMAC over the raw request body, compare in constant time, and reject timestamps older than a few minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, headers: Headers, secret: string): boolean {
  const ts = headers.get("x-sightradar-timestamp") ?? "";
  const sig = headers.get("x-sightradar-signature") ?? "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  return sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Return a 2xx once you have persisted the result. Delivery is at least once, so the same X-SightRadar-Idempotency-Key can arrive twice; make your handler idempotent on it.

Polling instead of, or as well as, webhooks

GET /v1/batches/{id} returns progress counts. GET /v1/batches/{id}/photos is the authoritative per-photo record, keyset-paginated on photo_index: follow next_after_index while has_more is true. Use it to reconcile after a webhook outage, or skip webhooks entirely for small batches.

When deliveries fail

After the retry budget is exhausted a delivery is dead-lettered. GET /v1/webhooks/dead-letters lists them (metadata only, never payloads or secrets), and POST /v1/webhooks/dead-letters/replay requeues selected or all deliveries with their original idempotency keys.

When photos fail

Photos that failed with insufficient_credits, internal_error or retry_exhausted can be retried as one bounded wave with POST /v1/batches/failed/requeue: call once for a ten-minute preview (count, estimated credits, current balance), then again with the preview_id and execute: true. Charged photos and photos already in flight are excluded automatically.

Deletion events

The same endpoint also receives collection.deletion.completed events after a collection's vectors are verified gone. See deleting data.

On this page