Reference

Version 5 · Updated

Webhooks

Events sent to your systems as they happen: what's sent, verifying signatures, retries and the delivery log.

Webhooks tell another system, the moment it happens, that a push was decided, a change was opened or merged, a review finished, a graph was built, a release was published, something was deployed, or an agent acted. CI, chat notifications and agent platforms can react without asking Branchscope over and over.

Adding one#

  • Repository webhooks: in a repository's Settings → Webhooks, for repository administrators.
  • Organization webhooks: on the organization's page, for its owners. They get events from every repository in the organization.

Give it a URL and choose its events (all by default). Its secret (whsec_…) is shown once, when you add the webhook; keep it where your receiver can read it. New secret replaces it at once, and also shows the new one once.

The same from the CLI and the API:

bscope webhook create -R acme/web --url https://ci.example.com/branchscope --event change.opened,review.completed
bscope webhook create --org acme --url https://chat.example.com/hooks/branchscope

Only people can manage webhooks, with a session or a personal write token, or OAuth apps given the webhooks:admin scope. Agents can't, because a webhook sends a repository's events out of Branchscope. Read-only tokens can see webhooks but not change them.

Events#

EventWhen
push.checkedthe push guard decided a push or a merge: allowed, denied or unavailable
change.openeda change was opened (an import opens them too)
change.closed, change.reopeneda change was closed without merging, or reopened
change.mergeda change was merged
review.completeda change's graph review finished, with counts of rule violations, connections needing review, and files against the plan
review.faileda review failed
graph.builta graph build finished, done or failed
comment.createdsomeone commented on a change or replied, or an agent posted its status
check.completeda check on a commit finished: passed, failed, cancelled or timed out (Branchscope's own workflow jobs, and checks apps report)
run.completeda workflow run finished, once all its jobs did: its status is passed, failed (a job failed or timed out) or cancelled, and its jobs say which did what. Once per run; each job's check still sends check.completed
release.publisheda release was published: a draft, or a new release made published (pre-releases too)
deployment.createda deployment was recorded: by a workflow job with environment:, a token or an app
deployment.statusa deployment's status changed: its job moved, it was set through the API, or a newer deployment made it inactive
deployment.review_requesteda job deploying to an environment with required reviewers is waiting for one of them
agent.taska task was handed to an agent app, or cancelled (its status says which); see Agents
agent.actionan agent made a write over MCP on the repository: opened or merged a change, re-ran a review; refused writes too
pingsent when you press Send a ping

A webhook only gets events its creator can still see. If they lose access to a repository, their webhooks stop getting its events.

What a delivery looks like#

A POST with a JSON body:

{
  "id": "evt_5f0c2a9e1b7d4c3a8e6f0b12",
  "event": "change.merged",
  "occurredAt": "2026-10-20T12:00:00.000Z",
  "repo": { "name": "acme/web", "url": "https://branchscope.example.com/acme/web" },
  "actor": { "username": "alice", "agent": null },
  "data": { "number": 12, "title": "Add invoice export", "status": "merged", "author": "release-bot" },
  "links": { "api": "https://api.branchscope.example.com/v1/repos/acme/web/changes/12", "web": "https://branchscope.example.com/acme/web/changes/12" }
}
  • actor: who did it, and the agent if an agent credential was used. It's null when nobody did, as when a review finishes.
  • data: the object. For changes it's the change as the API shows it; each event's exact shape is in the API reference and the OpenAPI document (webhooks).

Verifying deliveries#

Deliveries are signed per Standard Webhooks, so existing libraries in many languages can check them. Each delivery has three headers:

  • webhook-id: the event's ID;
  • webhook-timestamp: when it was sent, in seconds;
  • webhook-signature: v1, and the base64 HMAC-SHA256 of id.timestamp.body, keyed with the secret (the part after whsec_, base64-decoded).

With the TypeScript SDK:

import { verifyWebhook } from "@branchscope/sdk";

// body: the raw request body, exactly as received
const event = await verifyWebhook(process.env.BRANCHSCOPE_WEBHOOK_SECRET!, request.headers, body);
if (event.event === "review.completed" && event.data.counts.violations > 0) {
  // …
}

It also refuses deliveries more than 5 minutes old. Check the raw body: parsing it and writing it out again changes the bytes, and the signature with them.

Retries and the delivery log#

Any 2xx answer means received. Anything else, no answer within 10 seconds, or a redirect counts as a failure.

  • Retries: about 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 6 hours after a failure: a day in all.
  • Then: the delivery has failed, and Branchscope pauses the webhook and says why on its page. Turn it back on once the receiver works.
  • At least once: a delivery can arrive more than once. De-duplicate on webhook-id, which stays the same on every retry and redelivery.

Each webhook's page shows its recent deliveries: status, attempts, response code, duration, the error, the request body and the start of the response. Bodies are kept for 7 days, deliveries for 30. Redeliver sends an event again, with the same ID.

Integrations#

Integrations post a repository's news where a team already talks, and tell Sentry what shipped. Each is a webhook made for its service: its events become messages in that service's format, sent, retried, paused and logged as above. Add them in a repository's Settings → Integrations, for its administrators, or on the organization's Integrations tab, for its owners, where they get events from every repository in the organization. Only people can manage them, with a session or a personal write token: agents and apps can't, and there are no MCP tools for them.

Choose the events each one posts:

EventWhen
Change opened, Change mergedchange.opened, change.merged
Review completedreview.completed, with counts of rule violations, connections needing review, and files against the plan
Run failed, Run succeededa workflow run finished: failed (one message per run, naming the jobs that failed or timed out), or passed (off by default: it would drown the channel). From run.completed
Release publishedrelease.published
Deployment succeeded or faileda deployment finished, from a job or recorded through the API
Deployment waiting for reviewdeployment.review_requested: a job waits for an environment's reviewers

A message says what happened, names the change, run, release or environment, and links back to its page in Branchscope. It never holds a secret, a log line, a check's summary or a deployment's description.

From the CLI: integration list / add / remove / test, and linear status for the Linear connection, read-only. The webhook URL (--url -) or Sentry's token is read from standard input, so it stays out of your shell history:

bscope integration add -R acme/web --service slack --url - --channel '#deploys' < hook-url.txt
printf %s "$SENTRY_TOKEN" | bscope integration add --org acme --service sentry --sentry-url https://de.sentry.io --sentry-org acme --sentry-projects web,api
bscope integration list -R acme/web
bscope integration test 7
bscope linear status --org acme

Chat services#

Each takes the incoming webhook URL its service gives you; the webhook posts to the channel it was made for, so the channel name you enter is for display only.

  • Slack: make a Slack app with incoming webhooks turned on, add a webhook to the channel, and copy its URL (https://hooks.slack.com/services/…). Messages use Block Kit, with plain text as the notification's fallback.
  • Discord: in the channel's settings, Integrations → Webhooks → New Webhook (Discord's guide). Messages are embeds, and never ping anyone: an @everyone in a change's title stays text.
  • Microsoft Teams: add the Workflows template Send webhook alerts to a channel (Microsoft's guide) and copy the URL it gives you. Messages are Adaptive Cards (version 1.4) with an Open in Branchscope button. Office 365 connectors are retired; use Workflows.
  • Mattermost: Integrations → Incoming Webhooks → Add Incoming Webhook (Mattermost's guide). Messages are Markdown text, as Mattermost's Slack-compatible webhooks take them.

Sentry#

Sentry (and services compatible with its API) hears of two events:

  • Release published: Branchscope makes the release in Sentry, with the tag as its version, in each project you named; sets its commits, those since the previous release (or the latest 100 for a first release), sent as a list, so Sentry needs no repository integration of its own; then finalizes it. Errors in Sentry then link back to the commits that shipped them.
  • Deployment succeeded: a deploy of that release to the environment, with the deployment's URL. The release is the one the deployment's tag names, or else the newest published at its commit; a deployment no release is at isn't sent.

Give it Sentry's address (https://sentry.io, https://de.sentry.io for Sentry's EU region, or your own Sentry), the organization's slug, the projects' slugs and an auth token that can manage releases, such as an organization token. If one of Sentry's calls fails, the rest wait and the whole delivery is retried.

Linear#

An organization's owners connect its Linear workspace at the top of its Integrations tab. Changes in all its repositories are then linked to the workspace's issues, and the issues hear about the changes. Linear's own automations only know GitHub and GitLab; these are Branchscope's.

Connecting with Linear. Where the server has its own Linear app (on the hosted service it does; a self-hosted server registers one, see running Branchscope), Connect with Linear takes you to Linear to approve it. Branchscope then acts in Linear as its own app: attachments, moves and comments show as Branchscope, and nothing breaks when the person who connected it leaves. Linear only lets a workspace admin install an app. It asks to read and write (attachments and moving issues need write), and Branchscope keeps its token, and the token that renews it, encrypted with SECRETS_KEY. The token is renewed shortly before it expires; if Linear withdraws access (the app was removed in Linear), the connection pauses with the reason Linear access was withdrawn; connect again, and its updates wait until it's connected again.

Connecting with an API key. Under Use an API key instead, or on a server without a Linear app, paste a Linear API key (in Linear, Settings → Security & access → Personal API keys), best made by a member set aside for Branchscope: what it does in Linear shows under that member's name. Branchscope checks it by reading the workspace and its teams, keeps it encrypted, and never shows it again, only its last 4 characters. Connecting the same workspace the other way replaces how it's connected and keeps the mapping, links and logs. One workspace per organization.

Disconnecting. For a connection made with the Linear app, Disconnect revokes Branchscope's access in Linear first. If Linear refuses or can't be reached, it's disconnected here anyway, and the page and the audit log say revoking failed, with a link to remove the app in the workspace's settings in Linear. An API key can't be revoked from here: revoke it in Linear too. Either way, the token or key, any webhook Branchscope made in Linear, the mapping, the logs, the stored issues and changes' links are deleted.

Linking. A change is linked to an issue whose key (ENG-123) is in its branch name, title, description or its commits' subjects, in any case (so Linear's own branch names, such as ada/eng-123-export-invoices, count), as long as the key is one of the workspace's teams'; up to 10 per change. While the change is open, a key that's gone from all of those (a typo fixed, ENG-12 for ENG-123) is unlinked again and Branchscope's attachment taken off its issue; its comment stays, and moves aren't undone. Once merged or closed, its links stay as they are. People who can write to the repository link another issue by its key under the change's title, or unlink one, which then stays unlinked; links made by hand are never removed by detection. Members of the organization see each issue's title, status and assignee there; anyone else who can read the change sees only the keys.

What Linear gets. Each linked issue gets the change as an attachment, with its status: Open, In review (its review finished), Merged or Closed. Two automations are off until an owner turns them on:

  • Moving issues: opening a change moves its issues to the team's In Progress, its review finishing to In Review, and merging to Done. Each team maps these steps onto its own workflow states, filled in to start with; Don't move turns a step off. Issues only move forward: one someone has moved further on by hand, or that's done or cancelled, stays where it is.
  • Review comments: a finished review posts one comment on each issue, with the number of rule violations, connections needing review, renamed or moved elements, and whether the change stays within its plan, and links to its findings, change map and API diagrams. Running the review again updates that comment instead of adding another.

Nothing sent is written by an LLM or holds code: titles, statuses, counts and links, whose pages still need access in Branchscope. Updates come from Branchscope's own events through the connection's delivery log, with its retries, so a push, review or merge never waits for Linear. When Linear's rate limit is reached, an update waits until Linear says it may try again, without counting as a failure. While an update fails, its issue shows update failed on the change, with why; after a day of failures the connection is paused. Agents read a change's issues, descriptions included, with the MCP tool get_linked_issues.

What comes back from Linear. Linear tells Branchscope when a linked issue's state, title or assignee changes, when it moves to another team, when it's deleted, and when someone removes Branchscope's attachment from it. Through the Linear app, that's the app's own webhook. For an API key, Branchscope makes a webhook in the workspace with the key (issues and attachments, all public teams); Linear only lets an admin do that, so for another member's key the From Linear part of the page shows a URL: make a webhook in Linear (Settings → API → Webhooks) sending Issues and Attachments there, and paste its signing secret. With that, the change page shows issues as Branchscope last heard of them, without asking Linear each time:

  • an issue moved to another team shows its new key, as OPS-45, was ENG-123; the link keeps the key it was made with, and the old key in the branch name doesn't link it twice;
  • an issue deleted in Linear shows deleted in Linear, and nothing more is sent to it; people who can write can unlink it;
  • an issue whose Branchscope attachment someone removed in Linear is unlinked, as if by hand, so the next update doesn't put it back.

Linear only ever changes how a change's issues are shown and linked: an issue done or cancelled in Linear never closes or merges a change, and nothing is sent to Linear in answer. Branchscope's own doings that come back are ignored, as are new issues, comments and issues not linked to a change. Without events coming in, issues are read from Linear when a change is shown, and kept for a minute. Only linked issues' titles, states, assignees' names and links are kept in Branchscope, never their descriptions; they're deleted with their last link and on disconnecting. The page shows when the last event came, the last 7 days' events and what was done with each, and a hint when nothing has come for 7 days (a quiet workspace sends nothing either).

Each event is checked before it's used: its Linear-Signature must be the HMAC-SHA256 of the body under the webhook's signing secret, its webhookTimestamp within 60 seconds of now, and its Linear-Delivery ID one not seen before, so a replayed event does nothing. Events arriving out of order can't undo each other: one older than what Branchscope has for an issue is ignored. Events with a bad signature are refused and counted per hour on the page, not kept. After a disconnect, events for the workspace are answered and dropped, so Linear doesn't retry them.

Testing and the delivery log#

Send test sends a message at once, and once, and says how it went: in a chat service, a message saying the channel is connected; in Sentry, a look at each project's releases, which checks the token, the organization and every project. A failed test isn't retried and doesn't pause the integration. Each integration's Deliveries page is a webhook's delivery log: what was sent, each attempt and the start of each answer, and Redeliver.

Keeping the URLs and tokens secret#

A chat service's webhook URL is a credential: whoever has it can post to the channel. Webhook URLs and Sentry's settings and token are encrypted with the server's SECRETS_KEY, as secrets for runs are, and never shown again after you save them: an integration shows only the host and the last 4 characters. To change one, enter it again. Delivery logs and the audit log never show them either, even when a service's answer repeats the URL. Without SECRETS_KEY, integrations are unavailable. They only go to public HTTPS addresses, checked as webhooks' are, Sentry's own address included.

Safety#

  • Webhooks only go to public HTTPS addresses. A URL whose host resolves to a private, loopback, link-local or other internal address is refused when you add it, and again when each delivery connects, so DNS can't be changed afterwards to point a delivery inward.
  • Webhooks don't follow redirects.
  • Secrets are never shown again after they're made, and never logged.