Webhooks
Instead of polling, ask the API to POST to your server when a background job finishes.
Asking for a callback
Add callbackUrl and callbackSecret as form fields on POST /v1/tools/:slug/jobs or POST /v1/pipeline. There is nothing to register in advance. Callbacks need an API key.
curl -X POST "https://api.sadafiles.com/v1/tools/compress-video/jobs" \
-H "X-API-Key: $SADAFILES_API_KEY" \
-F "files[]=@clip.mp4" \
-F "callbackUrl=https://example.com/hooks/sada" \
-F "callbackSecret=$WEBHOOK_SECRET"Use these exact field names
callbackUrl and callbackSecret; snake_case form fields are ignored. Both SDKs send the right fields for you: callbackUrl in TypeScript and callback_url in Python.What you receive
One POST with JSON metadata for job.completed or job.failed. It never contains file contents; download the result with the job endpoints.
| Header | Value |
|---|---|
X-Sada-Event | job.completed or job.failed |
X-Sada-Timestamp | Unix seconds when this attempt was sent |
X-Sada-Signature | sha256=<hex>, present when you set a secret |
{
"event": "job.completed",
"job": {
"id": "de01bc4a-0686-400b-a594-20f031191629",
"status": "done",
"steps": [
"compress-video"
],
"expiresAt": "2026-11-02T16:31:37.708Z"
},
"sentAt": "2026-11-01T16:32:02.114Z"
}The job object is the same summary GET /v1/jobs/:id returns; this example is shortened.
Verifying the signature
The signature is HMAC-SHA256 of timestamp.rawBody with your secret. Check it against the raw bytes before parsing the JSON, and reject old timestamps to stop replays.
# The signature covers "<X-Sada-Timestamp>.<raw body>"
printf '%s.%s' "$TIMESTAMP" "$BODY" | openssl dgst -sha256 -hmac "$SECRET"Retries
Answer with any 2xx within 10 seconds. Otherwise the delivery is retried, up to 6 attempts in all, after 30 s, 2 min, 10 min, 30 min, 1 h. Retries are checked once a minute, so these delays are minimums. Because a delivery can repeat, make your handler safe to run twice, using the job id. The callback settings expire with the job, so nothing is sent after that.

