$npx -y skills add hookdeck/webhook-skills --skill vercel-webhooksReceive and verify Vercel webhooks. Use when setting up Vercel webhook handlers, debugging signature verification, or handling deployment events like deployment.created, deployment.succeeded, or project.created.
| 1 | # Vercel Webhooks |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | - Setting up Vercel webhook handlers |
| 6 | - Debugging signature verification failures |
| 7 | - Understanding Vercel event types and payloads |
| 8 | - Handling deployment, project, domain, or integration events |
| 9 | - Monitoring deployment status changes |
| 10 | |
| 11 | ## Essential Code (USE THIS) |
| 12 | |
| 13 | ### Express Webhook Handler with Manual Verification |
| 14 | |
| 15 | ```javascript |
| 16 | const express = require('express'); |
| 17 | const crypto = require('crypto'); |
| 18 | |
| 19 | const app = express(); |
| 20 | |
| 21 | // CRITICAL: Use express.raw() for webhook endpoint - Vercel needs raw body |
| 22 | app.post('/webhooks/vercel', |
| 23 | express.raw({ type: 'application/json' }), |
| 24 | async (req, res) => { |
| 25 | const signature = req.headers['x-vercel-signature']; |
| 26 | |
| 27 | if (!signature) { |
| 28 | return res.status(400).send('Missing x-vercel-signature header'); |
| 29 | } |
| 30 | |
| 31 | // Verify signature using SHA1 HMAC |
| 32 | const expectedSignature = crypto |
| 33 | .createHmac('sha1', process.env.VERCEL_WEBHOOK_SECRET) |
| 34 | .update(req.body) |
| 35 | .digest('hex'); |
| 36 | |
| 37 | // Use timing-safe comparison |
| 38 | let signaturesMatch; |
| 39 | try { |
| 40 | signaturesMatch = crypto.timingSafeEqual( |
| 41 | Buffer.from(signature), |
| 42 | Buffer.from(expectedSignature) |
| 43 | ); |
| 44 | } catch (err) { |
| 45 | // Buffer length mismatch = invalid signature |
| 46 | signaturesMatch = false; |
| 47 | } |
| 48 | |
| 49 | if (!signaturesMatch) { |
| 50 | console.error('Invalid Vercel webhook signature'); |
| 51 | return res.status(400).send('Invalid signature'); |
| 52 | } |
| 53 | |
| 54 | // Parse the verified payload |
| 55 | const event = JSON.parse(req.body.toString()); |
| 56 | |
| 57 | // Handle the event |
| 58 | switch (event.type) { |
| 59 | case 'deployment.created': |
| 60 | console.log('Deployment created:', event.payload.deployment.id); |
| 61 | break; |
| 62 | case 'deployment.succeeded': |
| 63 | console.log('Deployment succeeded:', event.payload.deployment.id); |
| 64 | break; |
| 65 | case 'deployment.error': |
| 66 | console.log('Deployment failed:', event.payload.deployment.id); |
| 67 | break; |
| 68 | case 'project.created': |
| 69 | console.log('Project created:', event.payload.project.name); |
| 70 | break; |
| 71 | default: |
| 72 | console.log('Unhandled event:', event.type); |
| 73 | } |
| 74 | |
| 75 | res.json({ received: true }); |
| 76 | } |
| 77 | ); |
| 78 | ``` |
| 79 | |
| 80 | ### Python (FastAPI) Webhook Handler |
| 81 | |
| 82 | ```python |
| 83 | import os |
| 84 | import hmac |
| 85 | import hashlib |
| 86 | from fastapi import FastAPI, Request, HTTPException, Header |
| 87 | |
| 88 | app = FastAPI() |
| 89 | webhook_secret = os.environ.get("VERCEL_WEBHOOK_SECRET") |
| 90 | |
| 91 | @app.post("/webhooks/vercel") |
| 92 | async def vercel_webhook( |
| 93 | request: Request, |
| 94 | x_vercel_signature: str = Header(None) |
| 95 | ): |
| 96 | if not x_vercel_signature: |
| 97 | raise HTTPException(status_code=400, detail="Missing x-vercel-signature header") |
| 98 | |
| 99 | # Get raw body |
| 100 | body = await request.body() |
| 101 | |
| 102 | # Compute expected signature |
| 103 | expected_signature = hmac.new( |
| 104 | webhook_secret.encode(), |
| 105 | body, |
| 106 | hashlib.sha1 |
| 107 | ).hexdigest() |
| 108 | |
| 109 | # Timing-safe comparison |
| 110 | if not hmac.compare_digest(x_vercel_signature, expected_signature): |
| 111 | raise HTTPException(status_code=400, detail="Invalid signature") |
| 112 | |
| 113 | # Parse verified payload |
| 114 | event = await request.json() |
| 115 | |
| 116 | # Handle event |
| 117 | if event["type"] == "deployment.created": |
| 118 | print(f"Deployment created: {event['payload']['deployment']['id']}") |
| 119 | elif event["type"] == "deployment.succeeded": |
| 120 | print(f"Deployment succeeded: {event['payload']['deployment']['id']}") |
| 121 | # ... handle other events |
| 122 | |
| 123 | return {"received": True} |
| 124 | ``` |
| 125 | |
| 126 | > **For complete working examples with tests**, see: |
| 127 | > - [examples/express/](examples/express/) - Full Express implementation |
| 128 | > - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation |
| 129 | > - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation |
| 130 | |
| 131 | ## Common Event Types |
| 132 | |
| 133 | | Event | Triggered When | Common Use Cases | |
| 134 | |-------|----------------|------------------| |
| 135 | | `deployment.created` | A new deployment starts | Start deployment monitoring, notify team | |
| 136 | | `deployment.succeeded` | Deployment completes successfully | Update status, trigger post-deploy tasks | |
| 137 | | `deployment.error` | Deployment fails | Alert team, rollback actions | |
| 138 | | `deployment.canceled` | Deployment is canceled | Clean up resources | |
| 139 | | `project.created` | New project is created | Set up monitoring, configure resources | |
| 140 | | `project.removed` | Project is deleted | Clean up external resources | |
| 141 | | `domain.created` | Domain is added | Update DNS, SSL configuration | |
| 142 | |
| 143 | See [references/overview.md](references/overview.md) for the complete event list. |
| 144 | |
| 145 | ## Environment Variables |
| 146 | |
| 147 | ```bash |
| 148 | # Required |
| 149 | VERCEL_WEBHOOK_SECRET=your_webhook_secret_from_dashboard |
| 150 | |
| 151 | # Optional (for API calls) |
| 152 | VERCEL_T |