Agent Skill · Hookdeck

replicate-webhooks

Receive and verify Replicate webhooks. Use when setting up Replicate webhook handlers, debugging signature verification, or handling prediction events like start, output, logs, or completed.

Provider: Hookdeck Path in repo: skills/replicate-webhooks/SKILL.md

Skill body

Replicate Webhooks

When to Use This Skill

Essential Code (USE THIS)

Express Webhook Handler

const express = require('express');
const crypto = require('crypto');

const app = express();

// CRITICAL: Use express.raw() for webhook endpoint - Replicate needs raw body
app.post('/webhooks/replicate',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    // Get webhook headers
    const webhookId = req.headers['webhook-id'];
    const webhookTimestamp = req.headers['webhook-timestamp'];
    const webhookSignature = req.headers['webhook-signature'];

    // Verify we have required headers
    if (!webhookId || !webhookTimestamp || !webhookSignature) {
      return res.status(400).json({ error: 'Missing required webhook headers' });
    }

    // Manual signature verification (recommended approach)
    const secret = process.env.REPLICATE_WEBHOOK_SECRET; // whsec_xxxxx from Replicate
    const signedContent = `${webhookId}.${webhookTimestamp}.${req.body}`;

    try {
      // Extract base64 secret after 'whsec_' prefix
      const secretBytes = Buffer.from(secret.split('_')[1], 'base64');
      const expectedSignature = crypto
        .createHmac('sha256', secretBytes)
        .update(signedContent)
        .digest('base64');

      // Replicate can send multiple signatures, check each one
      const signatures = webhookSignature.split(' ').map(sig => {
        const parts = sig.split(',');
        return parts.length > 1 ? parts[1] : sig;
      });

      const isValid = signatures.some(sig => {
        try {
          return crypto.timingSafeEqual(
            Buffer.from(sig),
            Buffer.from(expectedSignature)
          );
        } catch {
          return false; // Different lengths = invalid
        }
      });

      if (!isValid) {
        return res.status(400).json({ error: 'Invalid signature' });
      }

      // Check timestamp to prevent replay attacks (5-minute window)
      const timestamp = parseInt(webhookTimestamp, 10);
      const currentTime = Math.floor(Date.now() / 1000);
      if (currentTime - timestamp > 300) {
        return res.status(400).json({ error: 'Timestamp too old' });
      }
    } catch (err) {
      console.error('Signature verification error:', err);
      return res.status(400).json({ error: 'Invalid signature' });
    }

    // Parse the verified webhook body
    const prediction = JSON.parse(req.body.toString());

    // Handle the prediction based on its status
    console.log('Prediction webhook received:', {
      id: prediction.id,
      status: prediction.status,
      version: prediction.version
    });

    switch (prediction.status) {
      case 'starting':
        console.log('Prediction starting:', prediction.id);
        break;
      case 'processing':
        console.log('Prediction processing:', prediction.id);
        if (prediction.logs) {
          console.log('Logs:', prediction.logs);
        }
        break;
      case 'succeeded':
        console.log('Prediction completed successfully:', prediction.id);
        console.log('Output:', prediction.output);
        break;
      case 'failed':
        console.log('Prediction failed:', prediction.id);
        console.log('Error:', prediction.error);
        break;
      case 'canceled':
        console.log('Prediction canceled:', prediction.id);
        break;
      default:
        console.log('Unknown status:', prediction.status);
    }

    res.status(200).json({ received: true });
  }
);

Common Prediction Statuses

Status Description Common Use Cases
starting Prediction is initializing Show loading state in UI
processing Model is running Display progress, show logs if available
succeeded Prediction completed successfully Process final output, update UI
failed Prediction encountered an error Show error message to user
canceled Prediction was canceled Clean up resources, notify user

Environment Variables

# Your webhook signing secret from Replicate
REPLICATE_WEBHOOK_SECRET=whsec_your_secret_here

Local Development

For local webhook testing, install the Hookdeck CLI:

Then start the tunnel:

npx hookdeck-cli listen 3000 replicate --path /webhooks/replicate

No account required. Provides local tunnel + web UI for inspecting requests.

Reference Materials

Resources for Implementation

Framework Examples

Documentation

Enhance your webhook implementation with these patterns:

Skill frontmatter

license: MIT metadata: {"author"=>"hookdeck", "version"=>"0.1.0", "repository"=>"https://github.com/hookdeck/webhook-skills"}