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.
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.