$npx -y skills add hookdeck/webhook-skills --skill replicate-webhooksReceive and verify Replicate webhooks. Use when setting up Replicate webhook handlers, debugging signature verification, or handling prediction events like start, output, logs, or completed.
| 1 | # Replicate Webhooks |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | - Setting up Replicate webhook handlers |
| 6 | - Debugging signature verification failures |
| 7 | - Understanding Replicate event types and payloads |
| 8 | - Handling prediction lifecycle events (start, output, logs, completed) |
| 9 | |
| 10 | ## Essential Code (USE THIS) |
| 11 | |
| 12 | ### Express Webhook Handler |
| 13 | |
| 14 | ```javascript |
| 15 | const express = require('express'); |
| 16 | const crypto = require('crypto'); |
| 17 | |
| 18 | const app = express(); |
| 19 | |
| 20 | // CRITICAL: Use express.raw() for webhook endpoint - Replicate needs raw body |
| 21 | app.post('/webhooks/replicate', |
| 22 | express.raw({ type: 'application/json' }), |
| 23 | async (req, res) => { |
| 24 | // Get webhook headers |
| 25 | const webhookId = req.headers['webhook-id']; |
| 26 | const webhookTimestamp = req.headers['webhook-timestamp']; |
| 27 | const webhookSignature = req.headers['webhook-signature']; |
| 28 | |
| 29 | // Verify we have required headers |
| 30 | if (!webhookId || !webhookTimestamp || !webhookSignature) { |
| 31 | return res.status(400).json({ error: 'Missing required webhook headers' }); |
| 32 | } |
| 33 | |
| 34 | // Manual signature verification (recommended approach) |
| 35 | const secret = process.env.REPLICATE_WEBHOOK_SECRET; // whsec_xxxxx from Replicate |
| 36 | const signedContent = `${webhookId}.${webhookTimestamp}.${req.body}`; |
| 37 | |
| 38 | try { |
| 39 | // Extract base64 secret after 'whsec_' prefix |
| 40 | const secretBytes = Buffer.from(secret.split('_')[1], 'base64'); |
| 41 | const expectedSignature = crypto |
| 42 | .createHmac('sha256', secretBytes) |
| 43 | .update(signedContent) |
| 44 | .digest('base64'); |
| 45 | |
| 46 | // Replicate can send multiple signatures, check each one |
| 47 | const signatures = webhookSignature.split(' ').map(sig => { |
| 48 | const parts = sig.split(','); |
| 49 | return parts.length > 1 ? parts[1] : sig; |
| 50 | }); |
| 51 | |
| 52 | const isValid = signatures.some(sig => { |
| 53 | try { |
| 54 | return crypto.timingSafeEqual( |
| 55 | Buffer.from(sig), |
| 56 | Buffer.from(expectedSignature) |
| 57 | ); |
| 58 | } catch { |
| 59 | return false; // Different lengths = invalid |
| 60 | } |
| 61 | }); |
| 62 | |
| 63 | if (!isValid) { |
| 64 | return res.status(400).json({ error: 'Invalid signature' }); |
| 65 | } |
| 66 | |
| 67 | // Check timestamp to prevent replay attacks (5-minute window) |
| 68 | const timestamp = parseInt(webhookTimestamp, 10); |
| 69 | const currentTime = Math.floor(Date.now() / 1000); |
| 70 | if (currentTime - timestamp > 300) { |
| 71 | return res.status(400).json({ error: 'Timestamp too old' }); |
| 72 | } |
| 73 | } catch (err) { |
| 74 | console.error('Signature verification error:', err); |
| 75 | return res.status(400).json({ error: 'Invalid signature' }); |
| 76 | } |
| 77 | |
| 78 | // Parse the verified webhook body |
| 79 | const prediction = JSON.parse(req.body.toString()); |
| 80 | |
| 81 | // Handle the prediction based on its status |
| 82 | console.log('Prediction webhook received:', { |
| 83 | id: prediction.id, |
| 84 | status: prediction.status, |
| 85 | version: prediction.version |
| 86 | }); |
| 87 | |
| 88 | switch (prediction.status) { |
| 89 | case 'starting': |
| 90 | console.log('Prediction starting:', prediction.id); |
| 91 | break; |
| 92 | case 'processing': |
| 93 | console.log('Prediction processing:', prediction.id); |
| 94 | if (prediction.logs) { |
| 95 | console.log('Logs:', prediction.logs); |
| 96 | } |
| 97 | break; |
| 98 | case 'succeeded': |
| 99 | console.log('Prediction completed successfully:', prediction.id); |
| 100 | console.log('Output:', prediction.output); |
| 101 | break; |
| 102 | case 'failed': |
| 103 | console.log('Prediction failed:', prediction.id); |
| 104 | console.log('Error:', prediction.error); |
| 105 | break; |
| 106 | case 'canceled': |
| 107 | console.log('Prediction canceled:', prediction.id); |
| 108 | break; |
| 109 | default: |
| 110 | console.log('Unknown status:', prediction.status); |
| 111 | } |
| 112 | |
| 113 | res.status(200).json({ received: true }); |
| 114 | } |
| 115 | ); |
| 116 | ``` |
| 117 | |
| 118 | ## Common Prediction Statuses |
| 119 | |
| 120 | | Status | Description | Common Use Cases | |
| 121 | |--------|-------------|------------------| |
| 122 | | `starting` | Prediction is initializing | Show loading state in UI | |
| 123 | | `processing` | Model is running | Display progress, show logs if available | |
| 124 | | `succeeded` | Prediction completed successfully | Process final output, update UI | |
| 125 | | `failed` | Prediction encountered an error | Show error message to user | |
| 126 | | `canceled` | Prediction was canceled | Clean up resources, notify user | |
| 127 | |
| 128 | ## Environment Variables |
| 129 | |
| 130 | ```bash |
| 131 | # Your webhook signing secret from Replicate |
| 132 | REPLICATE_WEBHOOK_SECRET=whsec_your_secret_here |
| 133 | ``` |
| 134 | |
| 135 | ## Local Development |
| 136 | |
| 137 | For local webhook testing, install the Hookdeck CLI: |
| 138 | |
| 139 | ```bash |
| 140 | ``` |
| 141 | |
| 142 | Then start the tunnel: |
| 143 | |
| 144 | ```bash |
| 145 | npx hookdeck-cli listen 3000 replicate --path /webhooks/replicate |
| 146 | ``` |
| 147 | |
| 148 | No account required. Provides local tunnel + web UI for inspecting requests. |
| 149 | |
| 150 | # |