Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mesa.dev/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks allow you to receive notifications of changes to your organization’s repositories in real time. A webhook target represents an https:// URL to which Mesa will send these notifications, in the form of HTTP POST requests. Each target is scoped to your organization, can subscribe to multiple event types, and can optionally be restricted to a subset of repositories.

Create a webhook target

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

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

const target = await mesa.webhookTargets.create({
  org: "acme",
  name: "My App - Prod", // Optional human-readable label.
  url: "https://acme.dev/webhooks/mesa",
  events: ["push", "change.created", "change.evolved"],
  // Omit `repo_ids` for an org-wide target.
  repo_ids: ["repo_abc123"],
});

// The secret is returned once on create. Save it to verify signatures later.
console.log(target.secret);
Store target.secret in your secret manager. The SDK and REST API only return it once, here on create. The dashboard always shows it on the target’s detail page, and you can also rotate to mint a new one.

Supported events

Subscribe to any combination of the following on a target:
EventFires whendata shape
repo.createdA repository is created.{ repo }
repo.updatedA repository’s name, default bookmark, or tags change.{ repo, before, after }
repo.deletedA repository is deleted.{ repo }
bookmark.createdA bookmark is created (including the default bookmark on repo.created).{ bookmark }
bookmark.deletedA bookmark is deleted.{ bookmark }
bookmark.movedA bookmark is moved to a different change.{ bookmark, from }
bookmark.mergedA bookmark is advanced via a merge.{ bookmark }
change.createdA new change is created.{ change }
change.evolvedAn existing change advances to a new commit (rebase, content edit, message edit, etc.).{ change, previous_current_commit_oid }
pushA git push lands one or more bookmark updates via git-receive-pack.{ source, updates, reconciliation }
If events is omitted on create, it defaults to ["push"].
Managing webhook targets requires the admin scope. Repo-scoped tokens can only manage targets whose repo_ids is a subset of the repos the token has access to. They cannot create org-wide targets.

List webhook targets

Paginated. limit is at most 100 and defaults to 100. Pass the previous response’s next_cursor to fetch the next page; has_more indicates whether another page is available.
const { webhook_targets, next_cursor, has_more } = await mesa.webhookTargets.list({
  org: "acme",
  limit: 50,
  // cursor: previousResponse.next_cursor,
});

Update a webhook target

PATCH uses full-replace semantics on each field you include. Omitted fields are left unchanged.
  • events: providing an array replaces the current list of subscribed events in full. There is no incremental add/remove.
  • repo_ids: providing an array replaces the current repo filter. Pass null to clear the filter and make the target org-wide. Empty arrays are rejected.
await mesa.webhookTargets.update({
  org: "acme",
  webhookTargetId: "wh_123",
  url: "https://acme.dev/webhooks/mesa-v2",
  events: ["push"], // replaces all previously subscribed events
  repo_ids: null,    // clear the filter → org-wide
});
The secret is not returned by update. See Rotate the signing secret below.

Delete a webhook target

await mesa.webhookTargets.delete({
  org: "acme",
  webhookTargetId: "wh_123",
});

Payload shape

Every delivery shares the same envelope:
{
  "id": "01HXXXXXXXXXXXXXXXXXXXXXX",
  "type": "push",
  "occurred_at": "2026-01-28T19:01:05.000Z",
  "organization": {
    "id": "org_abc123",
    "slug": "acme",
    "name": "Acme"
  },
  "repository": {
    "id": "repo_abc123",
    "name": "vibecode-dashboards",
    "url": "https://api.mesa.dev/acme/vibecode-dashboards.git"
  },
  "data": { ... }
}

push event data

{
  "source": "git_receive_pack",
  "updates": [
    {
      "ref": "refs/heads/main",
      "bookmark": "main",
      "before": "abc123...",
      "after": "def456...",
      "action": "updated"
    }
  ],
  "reconciliation": {
    "reconciled_commit_count": 1,
    "touched_change_count": 1,
    "changes_created_count": 0,
    "evolog_inserted_count": 0,
    "invalid_change_id_header_count": 0,
    "dirty_change_skip_count": 0
  }
}
updates[].action is created, updated, or deleted. before is null when the action is created; after is null when the action is deleted.

repo.* event data

{
  "repo": {
    "id": "repo_abc123",
    "name": "vibecode-dashboards",
    "default_bookmark": "main",
    "head_change_id": "...",
    "created_at": "2026-01-28T19:01:05.000Z",
    "tags": { "team": "ui" }
  },
  "before": { "name": "...", "default_bookmark": "...", "tags": { ... } },
  "after":  { "name": "...", "default_bookmark": "...", "tags": { ... } }
}
before and after are only included on repo.updated. repo.created and repo.deleted carry just repo.

bookmark.* event data

{
  "bookmark": {
    "name": "main",
    "is_default": true,
    "change_id": "...",
    "commit_oid": "..."
  },
  "from": { "change_id": "...", "commit_oid": "..." }
}
from is only included on bookmark.moved. bookmark.created, bookmark.deleted, and bookmark.merged carry just bookmark.

change.* event data

{
  "change": {
    "id": "...",
    "current_commit_oid": "def456...",
    "is_conflicted": false,
    "message": "feat: add foo",
    "author":    { "name": "...", "email": "...", "date": "..." },
    "committer": { "name": "...", "email": "...", "date": "..." },
    "parents": ["abc123..."],
    "created_at": "2026-01-28T19:01:05.000Z",
    "updated_at": "2026-01-28T19:01:05.000Z"
  },
  "previous_current_commit_oid": "abc123..."
}
previous_current_commit_oid is only included on change.evolved. change.created carries just change.

Webhook headers

  • Content-Type: application/json
  • User-Agent: Depot-Webhook/1.0
  • X-Depot-Event: <event_type>
  • 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");
In your receiver, also reject deliveries whose t is more than a few minutes old (5 minutes is a common choice). This stops an attacker from replaying a captured request later. The signature stays valid until you rotate the secret. Time alone doesn’t invalidate it.

Rotate the signing secret

To rotate, open the webhook target’s detail page in the dashboard (Webhooks → click the target) and click Regenerate in the Signing secret section. Confirm the dialog.
Rotation takes effect immediately:
  • Mesa starts signing every subsequent delivery with the new secret.
  • Deliveries will fail signature verification at your receiver until you update it with the new value.
To minimize the failure window, copy the new secret and update your receiver as soon as you rotate. Rotation requires the admin scope, same as create, update, and delete. See the note at the top of this page.

Delivery behavior

Mesa fires webhooks asynchronously, with a 10-second timeout per request. Receivers should respond fast and defer long-running work to a background queue. Anything longer than 10s is treated as a failure. There is no automatic retry. A failed delivery is logged but not redelivered. If you need at-least-once semantics, queue the work yourself the moment you receive the request. The X-Depot-Delivery header is unique per delivery. Use it as an idempotency key on the receiver.

Local development

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