Skip to main content
Webhooks let you receive real-time HTTP POST notifications when branches move. They are scoped to a repo and can filter on branches and file globs.

Create a webhook

import { Mesa } from "@mesadev/sdk";

const mesa = new Mesa({ apiKey: process.env.MESA_API_KEY });

const webhook = await mesa.webhooks.create({
  org: "acme",
  repo: "vibecode-dashboards",
  body: {
    url: "https://example.com/webhooks/depot",
    events: ["push"],
    branches: ["main"],
    globs: ["**/*.json"],
    secret: "" // Optional secret to be used when generating webhook signature. If not provided, a random secret will be generated and returned.
  },
});

// The secret is returned once on create. Save it to verify webhook signatures later.
console.log(webhook.secret);
Creating webhooks requires the webhook:write scope. Listing webhooks requires webhook:read.

List webhooks

const { webhooks } = await mesa.webhooks.list({
  org: "acme",
  repo: "vibecode-dashboards",
});

Delete a webhook

await mesa.webhooks.delete({
  org: "acme",
  repo: "vibecode-dashboards",
  webhookId: "wh_123",
});

Payload shape

{
  "event": "push",
  "repository": {
    "id": "repo_123",
    "org": "acme",
    "name": "vibecode-dashboards",
    "url": "https://depot.mesa.dev/acme/vibecode-dashboards.git"
  },
  "ref": "refs/heads/main",
  "branch": "main",
  "before": "abc123...",
  "after": "def456...",
  "commits": [
    {
      "id": "def456...",
      "sha": "def456...",
      "message": "Add forecast widget",
      "author": {
        "name": "UI Agent",
        "email": "[email protected]",
        "date": "2026-01-28T19:01:00.000Z"
      }
    }
  ],
  "pushed_at": "2026-01-28T19:01:05.000Z"
}
author can be null when the commit does not include author metadata.

Webhook headers

  • Content-Type: application/json
  • User-Agent: Depot-Webhook/1.0
  • X-Depot-Event: push
  • X-Depot-Delivery: <delivery_id>
  • X-Depot-Signature: t=<unix>,sha256=<hex>

Verify signatures

The signature is computed as:
HMAC_SHA256(secret, `${timestamp}.${rawBody}`)
import { createHmac, timingSafeEqual } from "crypto";

const signatureHeader = request.headers.get("x-depot-signature");
if (!signatureHeader) throw new Error("Missing signature header");

const parts = Object.fromEntries(
  signatureHeader.split(",").map((part) => part.trim().split("="))
);

const timestamp = Number(parts.t);
const signature = parts.sha256;
const body = await request.text();

const expected = createHmac("sha256", process.env.WEBHOOK_SECRET)
  .update(`${timestamp}.${body}`)
  .digest("hex");

const valid = timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) throw new Error("Invalid signature");
Consider enforcing a max age (for example 5 minutes) when validating the t timestamp to prevent replay attacks.

Filtering

  • branches filters by exact branch name (example: main).
  • globs filters by changed file paths (example: **/*.json).
  • If both are set, both must match.

Delivery behavior

Mesa fires webhooks asynchronously and does not retry automatically. Each attempt is logged so you can audit or retry later.

Local development

Expose your local webhook listener using a tunnel like Tailscale or Cloudflare Tunnel, then use the public URL when creating the webhook.