# 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.

```javascript
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:

```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_filter` per prediction, and read `data.status` for 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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Replicate webhooks reliably in minutes.