How to Send Vercel Production Deployment Alerts to Your Team's Phones
Verify Vercel's signed deployment.promoted webhook, keep deployment details out of the alert, and send one fixed Ping to a shared release Room.

To send one Vercel production deployment alert to a shared group of phones, subscribe a Vercel account webhook to Deployment Promoted, verify its signature in a small relay, and forward fixed copy to a PingRoom incoming webhook. Use Vercel's delivery ID as the retry identity. Keep commit messages, authors, URLs, logs, and deployment metadata in Vercel.
This is a webhook recipe, not a native Vercel integration. It requires a Vercel Pro or Enterprise team for account webhooks and PingRoom Pro for the destination Room's incoming webhook.
Check Vercel's own notifications first
Vercel already offers configurable dashboard and email notifications on all plans. Its notification guide also documents opt-in push notifications on desktop and mobile web. Those preferences belong to each person; one team member cannot configure another member's channels.
Start with the smaller path:
| Need | Better starting point |
|---|---|
| One account holder needs Vercel updates | Vercel's own notification settings |
| A team already follows deployment status in Slack | Vercel's Slack integration |
| One selected production promotion should reach a bounded shared group | Vercel account webhook → relay → PingRoom Room |
| People need logs, checks, aliases, commit details, or rollback controls | Open the deployment in Vercel |
| A release needs paging, escalation, or an immutable operational record | Keep the established release or incident system |
Do not forward every preview build. This guide uses Deployment Promoted because Vercel defines that event as a production deployment beginning to serve production traffic. It is a more useful human signal than “a build exists.”
Create the destination Room
Create or choose a private PingRoom Room for the people who need this production update. The Room owner needs PingRoom Pro for incoming webhooks.
Open Settings → Connections → From another app, add a webhook named Vercel production, choose Custom message, and copy the full URL. The incoming-webhook setup guide explains the Room-side steps, while the webhook reference lists request limits, idempotency behavior, and error responses.
Store the URL as PINGROOM_WEBHOOK_URL in protected server configuration. It is a credential. Do not put it in a Vercel environment exposed to the browser, source control, screenshots, or logs.
The relay also needs:
VERCEL_WEBHOOK_SECRET: the one-time secret Vercel shows after the account webhook is created;VERCEL_PROJECT_ID: the selected project'sprj_…identifier;PINGROOM_PROJECT_LABEL: a short approved label for the lock screen, such asWebsite.
Verify the raw body before parsing
Vercel's webhook reference says x-vercel-signature is an HMAC-SHA1 digest of the raw request body using the webhook secret. Read the exact bytes once, calculate the expected digest, and compare it in constant time before parsing JSON.
The following Web-standard handler works in a Node runtime. Adapt only the route wrapper for your framework.
import { createHash, createHmac, timingSafeEqual } from "node:crypto"; const MAX_BODY_BYTES = 256_000; function reply(status, body = null) { if (body === null) return new Response(null, { status }); return Response.json(body, { status }); } function signaturesMatch(received, expected) { if (!/^[a-f0-9]{40}$/i.test(received ?? "")) return false; const actual = Buffer.from(received, "hex"); const wanted = Buffer.from(expected, "hex"); return actual.length === wanted.length && timingSafeEqual(actual, wanted); } export async function handleVercelPromotion(request, env = process.env) { if (request.method !== "POST") return reply(405, { error: "method_not_allowed" }); const { VERCEL_WEBHOOK_SECRET, VERCEL_PROJECT_ID, PINGROOM_WEBHOOK_URL, PINGROOM_PROJECT_LABEL, } = env; if ( !VERCEL_WEBHOOK_SECRET || !VERCEL_PROJECT_ID?.startsWith("prj_") || !PINGROOM_WEBHOOK_URL?.startsWith("https://") || !PINGROOM_PROJECT_LABEL || PINGROOM_PROJECT_LABEL.length > 50 ) { return reply(500, { error: "server_configuration_error" }); } const declaredLength = Number(request.headers.get("content-length") ?? 0); if (declaredLength > MAX_BODY_BYTES) return reply(413, { error: "body_too_large" }); const rawBody = await request.text(); if (Buffer.byteLength(rawBody) > MAX_BODY_BYTES) { return reply(413, { error: "body_too_large" }); } const expected = createHmac("sha1", VERCEL_WEBHOOK_SECRET) .update(rawBody) .digest("hex"); if (!signaturesMatch(request.headers.get("x-vercel-signature"), expected)) { return reply(401, { error: "invalid_signature" }); } let event; try { event = JSON.parse(rawBody); } catch { return reply(400, { error: "invalid_json" }); } if (event.type !== "deployment.promoted") return reply(204); const projectId = event.payload?.project?.id; const deploymentId = event.payload?.deployment?.id; const deliveryId = event.id; if (!projectId || !deploymentId || !deliveryId) { return reply(400, { error: "missing_event_identity" }); } if (projectId !== VERCEL_PROJECT_ID) return reply(204); const eventKey = `vercel-promoted-${createHash("sha256") .update(String(deliveryId)) .digest("hex") .slice(0, 32)}`; const payload = { title: "Vercel production deployed", message: `${PINGROOM_PROJECT_LABEL} is serving production. Check Vercel before acting.`, }; try { const response = await fetch(PINGROOM_WEBHOOK_URL, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", "Idempotency-Key": eventKey, }, body: JSON.stringify(payload), signal: AbortSignal.timeout(8000), }); if (!response.ok) return reply(502, { error: "pingroom_rejected" }); const accepted = await response.json().catch(() => null); if (accepted?.success !== true) { return reply(502, { error: "pingroom_invalid_response" }); } return reply(200, { ok: true }); } catch { return reply(502, { error: "pingroom_unavailable" }); } }
The handler forwards no field from Vercel's payload. PINGROOM_PROJECT_LABEL is a human-reviewed server setting, not source metadata. If even the project label is sensitive, use a generic value such as Selected project.
Vercel's event ID becomes an opaque, bounded PingRoom idempotency key. If Vercel retries the same delivery after losing a response, PingRoom can return the original accepted result instead of creating another Ping.
Add the Vercel account webhook
Vercel's account-webhook setup is available to Pro and Enterprise teams:
- Select the team scope, then open Settings → Webhooks.
- Choose Deployment Promoted as the only event for this endpoint.
- Scope the webhook to the intended project instead of every team project.
- Enter the public HTTPS URL for your relay.
- Create the webhook and save its secret immediately as
VERCEL_WEBHOOK_SECRET; Vercel does not show it again. - Copy the project's ID from its settings into
VERCEL_PROJECT_ID.
The relay checks the project again even though Vercel scopes the webhook. That second boundary keeps a later dashboard change from silently broadening the destination.
deployment.promoted means Vercel has started serving that production deployment. It does not prove that every external dependency is healthy, every cache is warm, every client can reach the site, or every phone displayed the Ping. Keep Vercel as the deployment record and keep any required release verification in the release process.
Give retries one safe outcome
Vercel retries non-2xx webhook deliveries with exponential backoff for up to 24 hours. The relay therefore returns a non-2xx response when PingRoom rejects or cannot accept the handoff.
| Relay outcome | HTTP result | What Vercel does |
|---|---|---|
| Valid promotion accepted by PingRoom | 200 | Delivery is complete |
| Valid but unrelated event or project | 204 | Delivery is complete; no Ping is created |
| Invalid signature or malformed event | 400/401 | Delivery remains visibly unsuccessful |
| PingRoom rejects or times out | 502 | Vercel can retry |
If configuration stays broken, disable the Vercel webhook or repair the relay before the retry window ends. Do not let a retired Room or expired PingRoom Pro plan produce a day of pointless attempts.

These AI-generated editorial illustrations feature approved PingRoom avatars and Pingo the Pigeon, PingRoom's fictional Chief Marketing Officer. They are not Vercel or PingRoom screenshots and do not depict customers.
Pingo the Pigeon is giving the stopped duplicate his usual look. He cannot access Vercel, inspect a private deployment, enter a Room, hold credentials, or send a Ping. The team chooses the project, event, destination, lock-screen copy, and retry policy.
Test the boundary before a real release
Use locally signed fixtures first, then a harmless production promotion for a non-sensitive test project:
- Sign a
deployment.promotedfixture and confirm one fixed Ping is accepted. - Replay the exact event ID and confirm the Room still has one accepted Ping for that delivery.
- Change the project ID and confirm the relay returns
204without calling PingRoom. - Alter the body after signing and confirm the relay returns
401. - Disable the PingRoom webhook and confirm the relay returns non-2xx so the failure stays visible.
- Check the Ping on a locked test phone. Its title and body should reveal no source, commit, author, URL, alias, team, region, or deployment metadata.
For this article, we imported the exact handler above and ran it against locally signed Vercel-style fixtures plus a stateful mock of PingRoom's documented idempotency contract. No live Vercel team, deployment, PingRoom Room, phone, or network request was used.
If one Vercel production promotion deserves a shared signal, create the destination Room and webhook, deploy the relay, and run the six checks with non-sensitive data. Use the broader CI/CD guide to decide which other release states genuinely need attention.
Vercel is a trademark of Vercel Inc. PingRoom is not affiliated with or endorsed by Vercel.
PingRoom
Short-form communication.


