Guide to Replicate Webhooks: Features and Best Practices
Replicate webhooks notify your application as a prediction runs: it starts, produces output, emits logs, and completes. If you're running AI models on Replicate, webhooks are how these async jobs report progress and results without polling.
This guide covers how Replicate webhooks work, the prediction events you'll handle, how to verify the Standard Webhooks signature, and the best practices for production.
What are Replicate webhooks?
Replicate webhooks are JSON POSTs configured per prediction. They follow the Standard Webhooks format, with three headers: webhook-id, webhook-timestamp, and webhook-signature. The signing secret starts with whsec_ and its remainder is base64. The signature is an HMAC-SHA256 (base64) over <webhook-id>.<webhook-timestamp>.<raw_body>, and the webhook-signature header can carry multiple space-separated values (each prefixed v1,) during secret rotation; any one matching is valid. Reject deliveries older than 5 minutes.
Replicate webhook features
| Feature | Details |
|---|---|
| Configuration | Per prediction via the API: webhook URL + webhook_events_filter |
| Signature headers | webhook-id, webhook-timestamp, webhook-signature |
| Signature scheme | Base64 HMAC-SHA256 over id.timestamp.body, whsec_ base64 secret |
| Replay window | 5 minutes; multiple signatures during rotation, any match is valid |
| Throttling | Events throttled to max once per 500ms (except start and completed) |
| SDK | Manual verification (the Replicate SDK registers webhooks but doesn't verify) |
Common events
You choose which prediction lifecycle events to receive via webhook_events_filter when you create the prediction:
| Event | Fires when |
|---|---|
start | The prediction begins processing |
output | The prediction generates output |
logs | Log output is generated |
completed | The prediction reaches a terminal state (succeeded, failed, or canceled) |
The payload envelope is { type, data }; the data.status field carries the prediction status (starting, processing, succeeded, failed, canceled).
Setting up Replicate webhooks
Replicate webhooks are configured per prediction, not in a dashboard. Pass a webhook URL and a webhook_events_filter array when you create the prediction (for example ["start", "completed"]). Replicate provides a signing secret in the format whsec_<base64>, store it as REPLICATE_WEBHOOK_SECRET. You can attach custom metadata via query parameters on the webhook URL (?userId=123).
Securing Replicate webhooks
Reconstruct <webhook-id>.<webhook-timestamp>.<raw_body>, strip the whsec_ prefix and base64-decode the secret, compute a base64 HMAC-SHA256, and compare against each signature in the header (any match passes). Then confirm the timestamp is recent. Verify against the raw body before parsing.
const crypto = require("crypto");
const SECRET = process.env.REPLICATE_WEBHOOK_SECRET;
function verify(rawBody, headers) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const header = headers["webhook-signature"];
if (!id || !timestamp || !header) return false;
const key = Buffer.from(SECRET.split("_")[1], "base64"); // strip whsec_, base64-decode
const signed = `${id}.${timestamp}.${rawBody.toString()}`;
const expected = crypto.createHmac("sha256", key).update(signed).digest("base64");
// The header may carry multiple space-separated "v1,<sig>" values (rotation)
const provided = header.split(" ").map((s) => (s.includes(",") ? s.split(",")[1] : s));
const match = provided.some((sig) => {
const a = Buffer.from(sig);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
if (Math.floor(Date.now() / 1000) - parseInt(timestamp, 10) > 300) return false; // 5 min
return match;
}
app.post("/webhooks/replicate", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.headers)) return res.sendStatus(401);
res.sendStatus(200); // acknowledge fast
processQueue.add(JSON.parse(req.body.toString())); // dedupe on webhook-id, async
});
The same verification in Python:
import base64
import hashlib
import hmac
import os
import time
SECRET = os.environ["REPLICATE_WEBHOOK_SECRET"]
def verify(raw_body: bytes, webhook_id: str, webhook_timestamp: str, webhook_signature: str) -> bool:
key = base64.b64decode(SECRET.split("_")[1]) # strip whsec_, base64-decode
signed = f"{webhook_id}.{webhook_timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
provided = [p.split(",")[1] if "," in p else p for p in webhook_signature.split()]
if not any(hmac.compare_digest(sig, expected) for sig in provided):
return False
return int(time.time()) - int(webhook_timestamp) <= 300 # 5-minute window
Replicate webhook limitations and pain points
Multiple signatures during rotation
The Problem: During a secret rotation, webhook-signature carries more than one space-separated signature. Checking only the first can fail intermittently right after rotating.
Why It Happens: Standard Webhooks sends one signature per active key during the overlap.
Workarounds:
- Split on spaces and accept if any signature matches.
How Hookdeck Can Help: Hookdeck verifies the Standard Webhooks signature at the edge, handling rotation for you.
The secret is base64 after the prefix
The Problem: The whsec_ secret must have its prefix stripped and the remainder base64-decoded before use as the HMAC key. Using it whole never matches.
Why It Happens: Standard Webhooks secrets are whsec_ + base64 key material.
Workarounds:
- Strip
whsec_, base64-decode, then HMAC.
How Hookdeck Can Help: Hookdeck handles the secret format at the edge.
Per-prediction configuration, and two vocabularies
The Problem: Webhooks are set per prediction (not a global endpoint), and the lifecycle events (start/output/logs/completed) differ from the prediction statuses (starting/processing/succeeded/failed/canceled). Conflating them leads to handlers that never fire.
Why It Happens: Replicate configures webhooks at prediction creation and reports both an event type and a status.
Workarounds:
- Set
webhook_events_filterper prediction, and readdata.statusfor the outcome.
How Hookdeck Can Help: Hookdeck's filters can route on the event type and status you actually receive.
Throttling and duplicates
The Problem: Intermediate events are throttled to once per 500ms (except start and completed), and retries can duplicate.
Why It Happens: Replicate throttles high-frequency events and delivers at-least-once.
Workarounds:
- Don't assume every intermediate event arrives; dedupe on
webhook-id.
How Hookdeck Can Help: Hookdeck deduplicates and durably queues events at the edge. See our guide to webhook idempotency.
Best practices
Verify base64 HMAC-SHA256 over id.timestamp.body
Reconstruct the signed content, decode the whsec_ secret, and compare against each signature in constant time.
Handle rotation and the replay window
Accept any matching signature, and reject deliveries older than 5 minutes.
Read data.status, not just the event type
Branch on the prediction status for the outcome, and dedupe on webhook-id.
Acknowledge fast, process asynchronously
Return 200 quickly and defer work to a queue. See why to process webhooks asynchronously.
Make Replicate webhooks production-ready
Hookdeck verifies the Standard Webhooks signature, deduplicates, and durably queues every prediction event
Conclusion
Replicate webhooks follow the Standard Webhooks format: webhook-id / webhook-timestamp / webhook-signature headers, a whsec_ base64 secret, base64 HMAC-SHA256 over id.timestamp.body, a 5-minute window, and multiple signatures during rotation. Configure events per prediction with webhook_events_filter, verify over the raw body, read data.status, and dedupe on webhook-id.
Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.
Get started with Hookdeck for free and handle Replicate webhooks reliably in minutes.