01Concepts
A webhook subscription is a tuple of (URL, event types, secret) tied to a workspace. When a matching event fires, Hypedata POSTs the event payload to your URL, signs the request with the secret, and considers it delivered when your endpoint returns 2xx within 10 seconds.
Configure subscriptions from app.hypedata.io/webhooks or via the Webhooks API (POST /v1/webhooks).
02Event types
| Event | When it fires |
|---|---|
scrape.completed | An async scrape (one with webhook set) finished successfully. |
scrape.failed | An async scrape exhausted retries. |
job.completed | A batch job finished. The payload includes a signed download URL. |
job.failed | A batch job hit a hard error. |
stream.completed | A Stream API session ended (either normally or because the consumer disconnected and the timer elapsed). |
credits.low | Workspace balance fell below the threshold set in billing settings. |
credits.exhausted | Workspace balance hit zero. New requests will return 402. |
key.created | An API key was created — for audit pipelines. |
key.revoked | An API key was revoked. Includes the reason. |
03Your endpoint
The request shape:
POST /your/webhook/path HTTP/1.1
Host: your-app.com
Content-Type: application/json
X-Hypedata-Event: job.completed
X-Hypedata-Delivery: dlv_8K2nB7q4XwY1pVzR
X-Hypedata-Timestamp: 1747038290
X-Hypedata-Signature: sha256=a1b2c3…
User-Agent: Hypedata-Webhooks/2.4
{
"event": "job.completed",
"id": "evt_3F2D1A77B0E1",
"created_at": "2026-05-12T14:18:10Z",
"data": {
"job_id": "job_8K2nB7",
"urls_total": 12480,
"urls_ok": 12410,
"urls_failed": 70,
"download_url": "https://exports.hypedata.io/…?signature=…",
"download_url_expires_at": "2026-05-13T14:18:10Z"
}
}Return 2xx as soon as you've enqueued the work — don't process the payload synchronously. Anything over 10 seconds will be retried, which produces duplicate deliveries.
04Signature verification
The signature is HMAC-SHA256 over "{timestamp}.{raw_body}" using your subscription's secret. Verify on every request — never trust the payload alone.
import crypto from "node:crypto"; function verify(req, secret) { const ts = req.headers["x-hypedata-timestamp"]; const sig = req.headers["x-hypedata-signature"]; const payload = `${ts}.${req.rawBody}`; const expected = "sha256=" + crypto .createHmac("sha256", secret) .update(payload) .digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) throw new Error("bad sig"); if (Math.abs(Date.now()/1000 - Number(ts)) > 300) throw new Error("stale"); }
import hmac, hashlib, time def verify(headers, raw_body, secret): ts = headers["x-hypedata-timestamp"] sig = headers["x-hypedata-signature"] expected = "sha256=" + hmac.new( secret.encode(), f"{ts}.{raw_body.decode()}".encode(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(sig, expected): raise ValueError("bad sig") if abs(time.time() - int(ts)) > 300: raise ValueError("stale")
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" ) func verify(headers http.Header, body []byte, secret string) error { ts := headers.Get("X-Hypedata-Timestamp") sig := headers.Get("X-Hypedata-Signature") mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts + "." + string(body))) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(sig), []byte(expected)) { return errors.New("bad sig") } return nil }
require "openssl" def verify(req, secret) ts = req.headers["X-Hypedata-Timestamp"] sig = req.headers["X-Hypedata-Signature"] expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "#{ts}.#{req.raw_post}") raise "bad sig" unless Rack::Utils.secure_compare(sig, expected) end
Always reject requests where the timestamp is more than 5 minutes stale — this prevents replay attacks even if a signature leaks.
05Retries
If your endpoint doesn't return 2xx within 10 seconds, Hypedata retries with exponential backoff:
- Attempt 1 — immediately
- Attempt 2 — 30 seconds later
- Attempt 3 — 2 minutes later
- Attempt 4 — 10 minutes later
- Attempt 5 — 1 hour later
- Attempt 6 — 4 hours later
- Attempt 7 — 12 hours later
- Attempt 8 — 24 hours later
After 8 failures over 24 hours, the delivery is marked failed and the subscription is paused if 5 consecutive deliveries failed. You'll be emailed and Slack-pinged if your dashboard is connected.
06Replay
Every delivery is stored for 30 days. From app.hypedata.io/webhooks you can:
- Inspect the full request body, headers, response, and timing.
- Replay a single delivery against the live URL.
- Bulk-replay everything in a time range (after fixing an outage on your side).
Replays are sent with the original timestamp and a new X-Hypedata-Replay: true header, so your endpoint can decide whether to skip idempotently or process again.
07Local testing
Two options:
- Hypedata CLI tunnel.
hypedata listen --forward http://localhost:3000/webhookopens a public tunnel and signs requests with your test-mode secret. Works without an account-level tunnel domain. - Manual fixtures. The dashboard's Send test event button posts a real fixture payload against any URL — useful for validating your signature check before going live.