Vercel Integration
Receive and route Vercel webhooks for deployments, domains, projects, and other platform events.
Setup
1. Create a Source in Hookbase
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Vercel Production",
"slug": "vercel",
"provider": "vercel",
"signingSecret": "your-vercel-webhook-secret-key"
}'The signingSecret is the secret key Vercel generates when you create the webhook in step 2 — you
paste it into Hookbase, you don't choose it. If you create the source first, leave signingSecret out
and update the source once you have the key.
Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/vercel2. Configure Vercel Webhook
- Go to the Vercel Dashboard and choose your team scope
- Navigate to Settings → Webhooks (account webhooks require a Pro or Enterprise plan)
- Select the events you want to receive (e.g.,
deployment.created,deployment.succeeded) - Choose the target projects
- Enter your Hookbase URL as the Endpoint URL
- Click Create Webhook
- Copy the secret key from the Webhook Created dialog and paste it into your Hookbase source
as
signingSecret— Vercel shows it only once
Webhooks created by a Vercel Integration are signed with the integration's Client Secret
(Integration Console) instead; use that value as signingSecret for those.
3. Create Routes
# Create destination for your deployment handler
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-d '{"name": "Deploy Service", "slug": "deploy-service", "url": "https://api.myapp.com/webhooks/vercel"}'
# Create route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-d '{"name": "Vercel to Deploy Service", "sourceId": "src_...", "destinationId": "dst_..."}'Signature Verification
Vercel signs webhooks using HMAC-SHA1 with the secret key it generated when you created the webhook (or, for integration webhooks, the integration's Client Secret). The signature is sent in the x-vercel-signature header as a hex-encoded digest.
x-vercel-signature: a3f7b9c2e1d4f6a8b0c3e5d7f9a1b3c5d7e9f0a2Hookbase verifies this automatically once the source has both fields set:
{
"provider": "vercel",
"signingSecret": "your-vercel-webhook-secret-key"
}Once events are arriving with signature_valid: true, set rejectInvalidSignatures: true on the
source to have unverified requests refused with 401 instead of stored.
How Verification Works
- Hookbase receives the raw request body
- Computes
HMAC-SHA1(secret, rawBody)and hex-encodes the result - Compares the computed value against the
x-vercel-signatureheader - Marks the event
signature_valid: falseif the signatures do not match — or refuses it with401whenrejectInvalidSignaturesis on
Common Events
deployment.created
Triggered when a new deployment is initiated.
{
"id": "evt_abc123def456",
"type": "deployment.created",
"createdAt": 1709827200000,
"payload": {
"deployment": {
"id": "dpl_7Hx9qPzR4mN2vL5w",
"url": "my-app-abc123-team.vercel.app",
"name": "my-app",
"meta": {
"githubCommitSha": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
"githubCommitMessage": "feat: add user dashboard",
"githubCommitAuthorName": "Jane Smith",
"githubCommitRef": "main",
"githubOrg": "my-org",
"githubRepo": "my-app"
},
"projectId": "prj_9kL3mN5pQ7rS1tU3",
"target": "production",
"inspectorUrl": "https://vercel.com/my-team/my-app/dpl_7Hx9qPzR4mN2vL5w"
},
"project": {
"id": "prj_9kL3mN5pQ7rS1tU3",
"name": "my-app"
},
"team": {
"id": "team_4vW6xY8zA0bC2dE4",
"slug": "my-team"
},
"user": {
"id": "usr_fG6hI8jK0lM2nO4p"
}
}
}deployment.succeeded
Triggered when a deployment completes successfully.
{
"id": "evt_ghi789jkl012",
"type": "deployment.succeeded",
"createdAt": 1709827500000,
"payload": {
"deployment": {
"id": "dpl_7Hx9qPzR4mN2vL5w",
"url": "my-app-abc123-team.vercel.app",
"name": "my-app",
"meta": {
"githubCommitSha": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
"githubCommitMessage": "feat: add user dashboard",
"githubCommitAuthorName": "Jane Smith",
"githubCommitRef": "main",
"githubOrg": "my-org",
"githubRepo": "my-app"
},
"projectId": "prj_9kL3mN5pQ7rS1tU3",
"target": "production",
"inspectorUrl": "https://vercel.com/my-team/my-app/dpl_7Hx9qPzR4mN2vL5w",
"readyState": "READY"
},
"project": {
"id": "prj_9kL3mN5pQ7rS1tU3",
"name": "my-app"
},
"team": {
"id": "team_4vW6xY8zA0bC2dE4",
"slug": "my-team"
}
}
}deployment.error
Triggered when a deployment fails.
{
"id": "evt_mno345pqr678",
"type": "deployment.error",
"createdAt": 1709827800000,
"payload": {
"deployment": {
"id": "dpl_3bD5eF7gH9iJ1kL3",
"url": "my-app-def456-team.vercel.app",
"name": "my-app",
"meta": {
"githubCommitSha": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1",
"githubCommitMessage": "fix: resolve build error",
"githubCommitAuthorName": "John Doe",
"githubCommitRef": "feature/auth",
"githubOrg": "my-org",
"githubRepo": "my-app"
},
"projectId": "prj_9kL3mN5pQ7rS1tU3",
"target": "preview",
"readyState": "ERROR",
"errorMessage": "Command \"npm run build\" exited with 1"
},
"project": {
"id": "prj_9kL3mN5pQ7rS1tU3",
"name": "my-app"
},
"team": {
"id": "team_4vW6xY8zA0bC2dE4",
"slug": "my-team"
}
}
}deployment.canceled
Triggered when a deployment is canceled.
{
"id": "evt_stu901vwx234",
"type": "deployment.canceled",
"createdAt": 1709828100000,
"payload": {
"deployment": {
"id": "dpl_5nM7oP9qR1sT3uV5",
"url": "my-app-ghi789-team.vercel.app",
"name": "my-app",
"meta": {
"githubCommitSha": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2",
"githubCommitMessage": "chore: update dependencies",
"githubCommitAuthorName": "Jane Smith",
"githubCommitRef": "main",
"githubOrg": "my-org",
"githubRepo": "my-app"
},
"projectId": "prj_9kL3mN5pQ7rS1tU3",
"target": "production",
"readyState": "CANCELED"
},
"project": {
"id": "prj_9kL3mN5pQ7rS1tU3",
"name": "my-app"
}
}
}domain.created
Triggered when a new domain is added to a project.
{
"id": "evt_yza567bcd890",
"type": "domain.created",
"createdAt": 1709828400000,
"payload": {
"domain": {
"name": "app.example.com",
"projectId": "prj_9kL3mN5pQ7rS1tU3",
"redirect": null,
"configuredBy": "CNAME",
"verified": true
},
"project": {
"id": "prj_9kL3mN5pQ7rS1tU3",
"name": "my-app"
},
"team": {
"id": "team_4vW6xY8zA0bC2dE4",
"slug": "my-team"
}
}
}project.created
Triggered when a new project is created.
{
"id": "evt_efg123hij456",
"type": "project.created",
"createdAt": 1709828700000,
"payload": {
"project": {
"id": "prj_2wX4yZ6aB8cD0eF2",
"name": "new-service",
"framework": "nextjs",
"gitRepository": {
"type": "github",
"repo": "my-org/new-service"
}
},
"team": {
"id": "team_4vW6xY8zA0bC2dE4",
"slug": "my-team"
},
"user": {
"id": "usr_fG6hI8jK0lM2nO4p"
}
}
}project.removed
Triggered when a project is deleted.
{
"id": "evt_klm789nop012",
"type": "project.removed",
"createdAt": 1709829000000,
"payload": {
"project": {
"id": "prj_2wX4yZ6aB8cD0eF2",
"name": "old-service"
},
"team": {
"id": "team_4vW6xY8zA0bC2dE4",
"slug": "my-team"
},
"user": {
"id": "usr_fG6hI8jK0lM2nO4p"
}
}
}Transform Examples
Slack Alert for Deployment Status
function transform(payload) {
const deployment = payload.payload.deployment;
const project = payload.payload.project;
const meta = deployment.meta || {};
const eventType = payload.type;
const statusMap = {
"deployment.created": { emoji: ":rocket:", color: "#3498db", text: "Deployment Started" },
"deployment.succeeded": { emoji: ":white_check_mark:", color: "#2ecc71", text: "Deployment Succeeded" },
"deployment.error": { emoji: ":x:", color: "#e74c3c", text: "Deployment Failed" },
"deployment.canceled": { emoji: ":no_entry_sign:", color: "#95a5a6", text: "Deployment Canceled" }
};
const status = statusMap[eventType] || { emoji: ":bell:", color: "#7f8c8d", text: eventType };
return {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: `${status.emoji} ${status.text}`
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Project:*\n${project.name}`
},
{
type: "mrkdwn",
text: `*Target:*\n${deployment.target || "preview"}`
},
{
type: "mrkdwn",
text: `*Branch:*\n${meta.githubCommitRef || "N/A"}`
},
{
type: "mrkdwn",
text: `*Author:*\n${meta.githubCommitAuthorName || "N/A"}`
}
]
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Commit:* ${meta.githubCommitMessage || "N/A"}`
}
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `<${deployment.inspectorUrl || "#"}|View Deployment> | ${deployment.url}`
}
]
}
]
};
}Database Format
function transform(payload) {
const deployment = payload.payload.deployment;
const project = payload.payload.project;
const team = payload.payload.team;
const meta = deployment.meta || {};
return {
vercel_event_id: payload.id,
event_type: payload.type,
deployment_id: deployment.id,
deployment_url: deployment.url,
project_id: project.id,
project_name: project.name,
team_slug: team ? team.slug : null,
target: deployment.target || "preview",
ready_state: deployment.readyState || null,
error_message: deployment.errorMessage || null,
git_sha: meta.githubCommitSha || null,
git_branch: meta.githubCommitRef || null,
git_message: meta.githubCommitMessage || null,
git_author: meta.githubCommitAuthorName || null,
git_repo: meta.githubRepo ? `${meta.githubOrg}/${meta.githubRepo}` : null,
created_at: new Date(payload.createdAt).toISOString()
};
}Alert for Failed Deployments
function transform(payload) {
const deployment = payload.payload.deployment;
const project = payload.payload.project;
const meta = deployment.meta || {};
return {
channel: "#deploy-alerts",
username: "Vercel Bot",
icon_emoji: ":warning:",
attachments: [{
color: "danger",
title: `Deployment Failed: ${project.name}`,
fields: [
{ title: "Branch", value: meta.githubCommitRef || "N/A", short: true },
{ title: "Target", value: deployment.target || "preview", short: true },
{ title: "Author", value: meta.githubCommitAuthorName || "N/A", short: true },
{ title: "Commit", value: meta.githubCommitMessage || "N/A", short: false },
{ title: "Error", value: deployment.errorMessage || "Unknown error", short: false }
],
footer: `Deployment ${deployment.id}`,
ts: Math.floor(payload.createdAt / 1000)
}]
};
}Filter Examples
Production Deployments Only
{
"name": "Production Only",
"logic": "AND",
"conditions": [
{
"field": "payload.deployment.target",
"operator": "equals",
"value": "production"
}
]
}Deployment Events
{
"name": "Deployment Events",
"logic": "OR",
"conditions": [
{
"field": "type",
"operator": "starts_with",
"value": "deployment"
}
]
}Failed Deployments
{
"name": "Failed Deployments",
"logic": "AND",
"conditions": [
{
"field": "type",
"operator": "equals",
"value": "deployment.error"
}
]
}Specific Project
{
"name": "My App Deployments",
"logic": "AND",
"conditions": [
{
"field": "payload.project.name",
"operator": "equals",
"value": "my-app"
},
{
"field": "type",
"operator": "starts_with",
"value": "deployment"
}
]
}Headers
Vercel sends the following headers with each webhook request:
| Header | Description | Example |
|---|---|---|
x-vercel-signature | HMAC-SHA1 hex digest of the request body | a3f7b9c2e1d4f6... |
content-type | Always application/json | application/json |
user-agent | Vercel webhook user agent | Vercel-Webhook/1.0 |
Hookbase does not forward these to your destination. A delivery is a fresh request with
Hookbase's own X-Delivery-ID and X-Event-ID plus the headers and auth you configured on the
destination — nothing Vercel sent reaches your handler as a header. See
what headers a delivery carries.
Testing
Send a Test Event
From the Vercel Dashboard webhook settings, you can send a test event to verify your integration is working.
Create a Test Source
Create a separate source for development and staging environments:
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Vercel Staging",
"slug": "vercel-staging",
"provider": "vercel",
"signingSecret": "your-staging-webhook-secret-key"
}'Verify with cURL
Test signature verification manually:
SECRET="your-vercel-webhook-secret-key"
BODY='{"type":"deployment.created","payload":{"deployment":{"id":"dpl_test"}}}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha1 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://api.hookbase.app/ingest/{orgSlug}/vercel \
-H "Content-Type: application/json" \
-H "x-vercel-signature: $SIGNATURE" \
-d "$BODY"Best Practices
-
Separate environments: Use different webhook endpoints and sources for production and preview deployments
-
Filter by target: Use filters to route production and preview deployment events to different destinations
-
Handle idempotency: Use
payload.id(the event ID) to prevent duplicate processing -
Monitor build errors: Set up alerts for
deployment.errorevents to catch build failures quickly -
Track git metadata: Store commit SHA and branch information for deployment traceability
-
Scope to projects: If you have many Vercel projects, use filters to route events per project to the appropriate handlers
Troubleshooting
Signature Verification Failed
- Confirm the source's
providerisvercel. A source left oncustomreadsX-Signature,X-Webhook-SignatureandX-Hub-Signature-256and expects SHA-256, so Vercel'sx-vercel-signatureis never read - Confirm
signingSecretis the secret key from that webhook's Webhook Created dialog (or the integration's Client Secret for integration webhooks). The key is shown only once — if you no longer have it, create a new webhook and paste its key - Check that the secret has no leading or trailing whitespace
Missing Events
- Check which events are selected in Vercel webhook settings
- Verify filters are not blocking expected events
- Check the Vercel Dashboard webhook logs for delivery attempts and response codes
- Ensure your Hookbase source slug matches the URL path
Webhook Not Triggering
- Confirm the webhook is enabled in Vercel Dashboard
- Verify the endpoint URL is correct and publicly accessible
- Check that the selected events match the actions you are performing (e.g., deploying to production vs. preview)
Duplicate Events
- Vercel may send the same event more than once in rare cases
- On a source with
dedupEnabled: true, the defaultautostrategy dedups on the payload's eventid, and falls back to hashing the payload when it is missing - Implement idempotency checks in your handler using the payload's event
idfield - Check for multiple webhook endpoints configured for the same events