Webhooks
When an async job reaches a terminal state, we POST a signed event to every active webhook on your account. No polling required.
Events
| Event | When |
|---|---|
job.succeeded | Job done, results ready |
job.failed | Upstream/chain failure |
job.timed_out | Upstream exceeded timeout |
job.aborted | Manually cancelled |
Payload
{ "event": "job.succeeded", "delivered_at": "2026-05-01T12:34:56.789Z", "data": { "job": { "id": "8c2a...", "type": "batch_detail", "status": "succeeded", "result_count": 213, "created_at": "...", "started_at": "...", "completed_at": "..." } }}To fetch the actual rows, follow up with GET /v1/jobs/{id}/results.
Headers
POST /your/webhook HTTP/1.1Content-Type: application/jsonX-Zillow-Signature: t=1714560000,v1=8a91…X-Zillow-Event: job.succeededUser-Agent: zillow-api-platform/1.0X-Zillow-Signature carries:
t— unix timestamp the signature was generated.v1— hex HMAC-SHA256 over<t>.<raw_body>using your webhook secret.
Verify the signature
import crypto from "node:crypto";
function verify(secret, header, rawBody) { const m = /^t=(\d+),v1=([a-f0-9]+)$/.exec(header); if (!m) return false; const [, t, sig] = m; if (Math.abs(Date.now()/1000 - Number(t)) > 300) return false; // 5-min skew const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));}import hmac, hashlib, time, re
def verify(secret: str, header: str, raw_body: bytes) -> bool: m = re.match(r"^t=(\d+),v1=([a-f0-9]+)$", header or "") if not m: return False t, sig = m.group(1), m.group(2) if abs(time.time() - int(t)) > 300: return False payload = f"{t}.{raw_body.decode('utf-8')}".encode() expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() return hmac.compare_digest(sig, expected)func verify(secret, header string, rawBody []byte) bool { parts := strings.Split(header, ",") if len(parts) != 2 { return false } t := strings.TrimPrefix(parts[0], "t=") sig := strings.TrimPrefix(parts[1], "v1=") ts, _ := strconv.ParseInt(t, 10, 64) if math.Abs(float64(time.Now().Unix() - ts)) > 300 { return false } h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(t + "." + string(rawBody))) expected := hex.EncodeToString(h.Sum(nil)) return hmac.Equal([]byte(sig), []byte(expected))}Delivery + retries
- 8s connect timeout, 8s body timeout.
- We retry up to 3 times total (initial + 2 retries) with quadratic backoff (250ms × n²).
- Every attempt is logged — see
GET /v1/webhooks/{id}/deliveries. - 2xx response = success. Anything else = retry until exhausted.
Best practices
- Verify the signature on every request. Reject unsigned payloads.
- Reject events older than 5 minutes (replay protection).
- Be idempotent — process the same
job.idtwice without side effects. - Return fast (< 5s). Queue the actual work and 200 immediately.