Coding Projects

Vercel Deployment Notifications on Your iPhone

Vercel already fires a webhook on every deployment event. Point it at one URL and a failed production deploy reaches your phone instead of a dashboard.

Red and black halftone illustration: a sharp triangular concrete monument at the edge of a dark empty plaza, lit hard from below (TheNotificationApp)

You push to main, Vercel starts building, and you close the laptop. The build fails on a type error that did not show up locally because your editor was using a different TypeScript version.

Production is still on the previous deployment, so nothing is broken. It is just that the fix you shipped is not live, and will not be until you happen to look at the dashboard. That gap is usually hours.

Vercel already fires a webhook for exactly this. It takes about three minutes to point it somewhere useful.

Step 1: The webhook URL

Create an application in TheNotificationApp, call it Vercel, and copy the app_key. Build the static webhook URL:

https://thenotification.app/api/webhook/static/?app_key=YOUR_API_KEY&title=Vercel

The static webhook takes whatever JSON arrives and formats it into a readable notification, which suits Vercel because its payload already names the project, the target and the state.

URL-encode any spaces in the title. Vercel%20Deploy, not Vercel Deploy. Test before going near the dashboard:

curl -X POST "https://thenotification.app/api/webhook/static/?app_key=YOUR_API_KEY&title=Vercel" \
  -H "Content-Type: application/json" \
  -d '{"type":"deployment.error","project":"marketing-site","target":"production"}'

Details are in the static webhook docs.

Step 2: Create the webhook in Vercel

Go to your project, then Settings, then Webhooks, then Create Webhook.

  • Endpoint: the URL from step 1
  • Events: deployment.error and deployment.succeeded
  • Projects: the one you are configuring, or all of them

Save, then push something to trigger a build.

Step 3: Subscribe to the right events

Vercel offers several deployment events and this is where the setup succeeds or fails as a daily-use thing.

  • deployment.error: take it. This is the whole reason you are here.
  • deployment.succeeded: take it while you are getting a feel for it, and be ready to turn it off. On a project where you push ten times a day, ten "it worked" notifications a day is noise.
  • deployment.created: skip. You just pushed. You know.
  • deployment.canceled: skip unless cancellations surprise you.

If you have a preview deployment for every pull request, be aware that these events fire for previews too. On a busy repository that is a lot of notifications about builds nobody is waiting on. The next section deals with that.

Filtering previews out

Vercel's webhook UI does not filter by deployment target, so if preview noise is the problem you need something in between. A tiny route handler in the project itself works, and it can also make the notification much better:

// app/api/vercel-webhook/route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const event = await request.json();
  const { type, payload } = event;
  const target = payload?.target ?? "preview";

  // Only production deployments are worth a phone buzz.
  if (target !== "production") {
    return NextResponse.json({ skipped: true });
  }

  const failed = type === "deployment.error";
  const project = payload?.name ?? "project";
  const url = payload?.url ? `https://${payload.url}` : undefined;

  await fetch("https://thenotification.app/api/sendNotification", {
    method: "POST",
    headers: {
      app_key: process.env.TNA_APP_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      title: failed ? `Deploy FAILED: ${project}` : `Deployed: ${project}`,
      body: failed
        ? "Production build did not ship. Check the build logs."
        : "Production is live on the new build.",
      link: url,
    }),
  });

  return NextResponse.json({ ok: true });
}

Point the Vercel webhook at this route instead of the static webhook URL, and put TNA_APP_KEY in your project's environment variables. You now get production-only notifications with a title that reads differently for success and failure.

One caveat that catches people: if this route lives in the same project that is deploying, a failed deployment means the currently live version handles the webhook, which is fine, but a project that has never deployed successfully has no route to receive it. Put it in a separate small project if that worries you.

Verifying the signature

Vercel signs webhook requests. The route above does not check that, which is acceptable for a notification endpoint that only forwards to your own phone, and not acceptable if the handler does anything else.

The worst case here is that someone who guesses your route URL can make your phone buzz. Worth knowing, and worth verifying the signature if you extend this route to do real work.

Multiple projects

The project name comes through in the payload, so one webhook URL across every project works fine, as long as the notification says which one. That is why the title above interpolates project: with six projects, "Deploy FAILED" on its own sends you to the dashboard to find out which.

If you use the static webhook URL rather than the route handler, put the project in the title query parameter and create one webhook per project:

https://thenotification.app/api/webhook/static/?app_key=YOUR_API_KEY&title=Vercel%3A%20marketing-site

Catching the deploy that broke production

A failed build is the easy case: nothing shipped, so nothing is worse than before. The expensive case is the deployment that succeeded and broke the site, because Vercel considers that a success and says so.

A smoke check right after the deploy event covers it. Extend the route handler to actually hit the site before declaring victory:

if (type === "deployment.succeeded" && target === "production") {
  const siteUrl = "https://yourdomain.example.com";
  let healthy = false;
  let detail = "";

  try {
    const check = await fetch(`${siteUrl}/api/health`, {
      cache: "no-store",
      signal: AbortSignal.timeout(8000),
    });
    healthy = check.ok;
    detail = `HTTP ${check.status}`;
  } catch (error) {
    detail = error instanceof Error ? error.message : "unreachable";
  }

  await notify({
    title: healthy ? `Deployed: ${project}` : `Deployed but UNHEALTHY: ${project}`,
    body: healthy ? "Production is live and answering." : `Health check failed: ${detail}`,
    link: siteUrl,
  });
  return NextResponse.json({ ok: true, healthy });
}

Now the notification distinguishes three states rather than two: the build failed, the build shipped and the site is fine, or the build shipped and the site is not. The third one is the one that matters and the one Vercel's own events cannot tell you about.

Worth noting the limit: this checks once, immediately. A site that breaks under load ten minutes later still needs real monitoring, which is a different job covered in getting a server down phone notification.

The honest part

The free tier is 100 notifications for the lifetime of the account, not per month. On failures only, a stable project sends a couple a month and that lasts a year. Subscribed to every event on a repository with preview deployments, it lasts about a week.

Start with deployment.error alone. Add success notifications only if you find you want them. Pro is $2.99 a month for 1,000, and this reaches iPhone only since it rides Apple's push service.

Where this fits

Any platform that fires webhooks works the same way. The self-hosted equivalent is in Coolify deployment notifications, and the general pattern is in webhook push notifications.

Grab a free key at thenotification.app and stop discovering failed deploys the next morning.