Skip to content

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

EventWhen
job.succeededJob done, results ready
job.failedUpstream/chain failure
job.timed_outUpstream exceeded timeout
job.abortedManually 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.1
Content-Type: application/json
X-Zillow-Signature: t=1714560000,v1=8a91…
X-Zillow-Event: job.succeeded
User-Agent: zillow-api-platform/1.0

X-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));
}

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.id twice without side effects.
  • Return fast (< 5s). Queue the actual work and 200 immediately.