Guide to Vercel Webhooks: Features and Best Practices
Vercel webhooks notify your application about deployment and project activity: a deployment succeeds or errors, a project is created or removed, a domain is added. If you're building on Vercel, webhooks are how you react to these events without polling.
This guide covers how Vercel webhooks work, the events you'll handle, how to verify the x-vercel-signature (it's SHA-1, not SHA-256), the three places the secret can come from, and the best practices for production.
What are Vercel webhooks?
Vercel webhooks are JSON POSTs delivered to a URL you configure. Each is signed with an x-vercel-signature header: an HMAC-SHA1 (not SHA-256) over the raw request body, hex-encoded. Assuming SHA-256 is the single most likely mistake here. The signed input is the raw body only, with no timestamp concatenated.
Vercel webhook features
| Feature | Details |
|---|---|
| Configuration | Account/team settings > Webhooks (limit of 20 per team) |
| Signature header | x-vercel-signature (lowercase) |
| Signature scheme | HMAC-SHA1 (hex) over the raw body |
| Secret sources | Team webhook secret, Integration client secret, or Log Drain secret (all in the same header) |
| Event field | Top-level type (dotted) |
| Retries | Not documented |
| SDK | npm @vercel/sdk (REST client, no verification helper); no official pip |
Common events
Vercel reports the event type in the top-level type field (dotted):
| Event | Fires when |
|---|---|
deployment.created / deployment.succeeded / deployment.error | A deployment changes state |
deployment.canceled / deployment.promoted | A deployment is canceled or promoted |
project.created / project.removed | A project is created or removed |
domain.created | A domain is added |
integration-configuration.removed | An integration configuration is removed |
The envelope carries id, type, createdAt, region, and payload.
See Vercel webhook payloads in action. Inspect and replay sample Vercel webhook payloads in the Hookdeck Console — no account or setup required.
Setting up Vercel webhooks
Where the secret comes from depends on the webhook type, and all three arrive in the same x-vercel-signature header. Team/account webhooks (Account or team settings > Webhooks) show their secret exactly once at creation. Integration webhooks use the Integration's Client Secret. Log Drains, a distinct mechanism documented separately, use their own Drain signature secret. Store the relevant one as VERCEL_WEBHOOK_SECRET.
Securing Vercel webhooks
Compute an HMAC-SHA1 (not SHA-256) over the raw body, hex-encode it, and compare against x-vercel-signature in constant time. Verify against the raw body before parsing, on Next.js this needs a little care (see below).
const crypto = require("crypto");
const SECRET = process.env.VERCEL_WEBHOOK_SECRET;
function verify(rawBody, signature) {
if (!signature) return false;
const expected = crypto.createHmac("sha1", SECRET).update(rawBody).digest("hex"); // sha1, NOT sha256
try {
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
} catch {
return false;
}
}
app.post("/webhooks/vercel", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.headers["x-vercel-signature"])) return res.sendStatus(401);
res.sendStatus(200); // acknowledge fast
processQueue.add(JSON.parse(req.body.toString())); // branch on type, async
});
The same check in Python:
import hashlib
import hmac
import os
SECRET = os.environ["VERCEL_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, signature: str) -> bool:
if not signature:
return False
expected = hmac.new(SECRET, raw_body, hashlib.sha1).hexdigest() # SHA-1
return hmac.compare_digest(signature, expected)
Make Vercel webhooks production-ready. Hookdeck Event Gateway verifies the
x-vercel-signature, deduplicates, and durably queues every event.
Vercel webhook limitations and pain points
It's SHA-1, not SHA-256
The Problem: x-vercel-signature is HMAC-SHA1. Reaching for SHA-256 (as Stripe and GitHub use) never matches, and it's the easiest mistake to make.
Why It Happens: Vercel signs with SHA-1.
Workarounds:
- Use
sha1in the HMAC, hex, over the raw body.
How Hookdeck Can Help: Hookdeck verifies the signature at the edge with the right algorithm, so your app doesn't hard-code it.
The secret comes from three places
The Problem: Team webhooks, Integration webhooks, and Log Drains each use a different secret, all delivered in the same x-vercel-signature header. Using the wrong one fails verification.
Why It Happens: Vercel scopes the signing secret by webhook type.
Workarounds:
- Use the team webhook secret (shown once at creation), the Integration client secret, or the Log Drain secret, matching the webhook's type.
How Hookdeck Can Help: Hookdeck can verify each source with its own secret, so you don't juggle three in one handler.
Raw-body handling on Next.js
The Problem: Frameworks that parse JSON before you verify break the signature. On Vercel's own Next.js this bites: the Pages Router parses the body by default.
Why It Happens: Automatic body parsing re-serializes the payload.
Workarounds:
- Next.js Pages Router: set
export const config = { api: { bodyParser: false } }and read the raw body (for example with theraw-bodypackage). App Router:await request.text().
How Hookdeck Can Help: Hookdeck verifies before forwarding, so your framework's body parsing can't invalidate the check.
Retries aren't documented, and Log Drains are separate
The Problem: Vercel doesn't document a retry schedule, so you can't rely on redelivery. And Log Drains are a distinct mechanism, not platform webhooks, so conflating them leads to wrong assumptions.
Why It Happens: Retry behavior is unpublished; Log Drains are a separate product (with their own Hookdeck source type).
Workarounds:
- Don't depend on redelivery, make handlers idempotent, and treat Log Drains separately.
How Hookdeck Can Help: Hookdeck durably queues and retries on its own schedule, so delivery doesn't depend on Vercel's unpublished behavior.
Best practices
Verify HMAC-SHA1 over the raw body
Compute the hex HMAC-SHA1 over the raw body and compare against x-vercel-signature in constant time.
Use the right secret for the webhook type
Team webhook secret, Integration client secret, or Log Drain secret, matched to the source.
Preserve the raw body on Next.js
Disable body parsing (Pages Router) or use request.text() (App Router) so verification sees the exact bytes.
Acknowledge fast, process asynchronously
Return 200 quickly and defer work to a queue; make handlers idempotent since retries aren't documented. See why to process webhooks asynchronously.
Conclusion
Vercel webhooks are verified with an x-vercel-signature HMAC-SHA1 (not SHA-256) over the raw body, hex-encoded. The secret comes from one of three places depending on webhook type, all in the same header. Use SHA-1, preserve the raw body (especially on Next.js), make handlers idempotent since retries aren't documented, and treat Log Drains as a separate mechanism.
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 Vercel webhooks reliably in minutes.