Batch indexing and webhooks
Index thousands of photos at the batch rate, receive one signed webhook per photo, verify signatures, and recover from failures.
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"}'{
"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" }
]
}'{ "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:
{
"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:
| Header | Value |
|---|---|
X-SightRadar-Timestamp | Unix seconds when the delivery was signed. |
X-SightRadar-Signature | hex(HMAC-SHA256(secret, "<timestamp>.<raw body>")) |
X-SightRadar-Idempotency-Key | wh:<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.