Integrations

Webhooks

Webhooks are in beta: event coverage and payload fields may still change. A webhook sends a signed HTTPS request to your server when something happens in a repository or organization, such as a push, a new issue or a merged pull request.

Add a webhook

Repository admins open Settings → Webhooks in the repository; organization owners and admins open Webhooks in the organization's settings.

  1. Choose Add webhook.
  2. Enter your receiver's HTTPS URL and select events. All events subscribes to every supported event. To test a new receiver first, select only webhook.ping.
  3. Save, then copy the signing secret into your receiver. It is shown only once.
  4. Choose Manage, then Send signed ping, and check Delivery history to confirm your receiver accepted it.
  5. Edit the webhook to add the events you need.

A repository or organization can have up to ten webhooks.

Through the API, use a personal access token. Apps and clients you connected with OAuth, such as MCP clients and apps acting as you, can list and delete webhooks but cannot create, edit or re-key one: a webhook keeps sending new data after you disconnect them.

Webhooks of installed apps

An app installed in your organization can receive events for the repositories you selected for it, if its manifest asks for them and you approved the installation. These webhooks belong to the installation: they do not appear under Webhooks, do not count toward the limit of ten, and you cannot edit, test or delete them there. Pausing the app stops its deliveries, resuming starts them again for new events, removing a repository from the app stops deliveries for that repository, and uninstalling the app removes its webhook. Deliveries still waiting when you do any of this are not sent, even if you resume the app or add the repository back. A delivery already being sent finishes and is recorded with the receiver's answer, but is not retried. The app is still told that it was paused, resumed, uninstalled or given different repositories (installation events), so it can stop work and drop credentials; these events name no repository it can no longer read.

If you publish an app, see App webhooks for what your receiver gets, and Your app's deliveries for its delivery history.

From the terminal, pass an organization slug or owner/repo as the target:

sh
1cafe webhook create acme/project --url https://hooks.example.com/gitcafe \
2 --event issue.created --event pull.merged
3cafe webhook ping acme/project <webhook-id> --request-id first-ping --expected-version <version>

cafe webhook capabilities lists the supported events, and cafe webhook readiness checks whether a webhook is ready to send.

Events

EventsSent when
git.pushBranches or tags change by push, merge or sync
git.ref_created, git.ref_deletedBranches or tags are created or deleted
repo.default_branch_changedThe default branch changes
issue.created, issue.updated, issue.closed, issue.reopenedAn issue is created, edited, closed or reopened
issue.locked, issue.unlocked, issue.pinned, issue.unpinnedAn issue is locked, unlocked, pinned or unpinned
issue.assignees_updated, issue.labels_updated, issue.milestone_updatedAn issue's assignees, labels or milestone change
issue.linked, issue.unlinkedA pull request is linked to or unlinked from it
issue.comment_created, issue.comment_updated, issue.comment_deletedAn issue comment changes
issue.reaction_added, issue.reaction_removedA reaction is added or removed
pull.created, pull.updated, pull.closed, pull.reopenedA pull request is opened, edited, closed or reopened
pull.synchronizedA pull request's source branch gets new commits
pull.mergedA pull request merges in GitCafe
pull.drafted, pull.ready, pull.locked, pull.unlockedDraft or lock state changes
pull.reviewers_updated, pull.assignees_updatedReviewers or assignees change
pull.labels_updated, pull.milestone_updatedLabels or milestone change
pull.linked, pull.unlinkedAn issue is linked or unlinked
pull.review_submitted, pull.review_dismissedA review is submitted or dismissed
pull.comment_created, pull.comment_updated, pull.comment_deletedA pull request comment changes
pull.comment_hidden, pull.comment_unhiddenA comment is hidden or shown
pull.comment_resolved, pull.comment_unresolvedA review thread is resolved or reopened
pull.reaction_added, pull.reaction_removedA reaction is added or removed
label.created, label.updated, label.deletedA repository label changes
milestone.created, milestone.updated, milestone.deletedA milestone changes
ci.run_updatedA CI run's state changes
actions.workflow_jobAn Actions job on your organization's pool changes
webhook.pingYou send a test ping

Events describe changes made in GitCafe. Issues and pull requests imported from GitHub do not send issue.* or pull.* events, and past activity is not sent when you add a webhook.

Actions job events

actions.workflow_job tells an autoscaler when to start and stop runners, as GitHub's workflow_job does. It is sent for a job that your organization's runner groups serve, with an action of queued, in_progress (a runner took the job) or completed:

  • queued is sent when the job is queued and at least one enabled runner group of the repository's organization may run it. The job has no pool yet (poolId is null); runnerGroupId names the group when only one serves the job and is null when several do. Decide from the job's labels whether to start a runner.
  • in_progress follows when a runner of one of your pools takes it, with that pool and runner, and completed when it ends. Every job announced queued gets a completed: a job cancelled or timed out before any runner takes it, or taken by GitCafe's hosted runners after it was announced, sends completed with its conclusion and no pool or runner (a hosted runner taking it sends no in_progress).
  • A job no runner group of your organization serves sends nothing, and so does a job on, or served by, GitCafe's hosted runners.

Each action is captured once per job, in order, and a receiver is never sent in_progress or completed for a job it was not sent queued for: when a runner of your pool takes a job that was not announced to you (GitCafe's hosted runners also served it, or your group was created, bound or enabled after it was queued), queued is captured first. Deliveries may still arrive out of order, because a retried delivery waits for its next attempt while later events go out: a retried queued can arrive after its in_progress. Order a job's events by status and the queuedAt, startedAt and completedAt timestamps, as on GitHub. A job whose repository is transferred to another organization while it runs sends no completed to the old organization; reconcile it with the demand read. workflowJob holds the job and run IDs, the pool and runner IDs once a runner took the job, a runner group number (runnerGroupId, the runner_group_id of generate-jitconfig), the run attempt, the job name, its labels (as in runs-on, lower-cased), its status (the same as action), its conclusion once completed, and when it was queued, started and completed. It never contains scripts, environment variables, secrets, tokens, clone URLs or logs, and has no actor. The shape follows GitHub's workflow_job event but is not compatible with it. A receiver that missed deliveries reconciles with the pool's provider demand read.

The body changed when queued was added: workflowJob gained runnerGroupId, and poolId is now null on queued and on a completed no runner of your pools took. A decoder that rejects unknown fields or a null poolId must accept both.

An installed app receives these events when it has the Runners permission (org:runners, read or write), only for jobs on its installing organization's own pools.

The request

Each delivery is an HTTPS POST with a JSON body. The body has an event id, a type, a timestamp, the repository (its ID, its organization's ID, and its owner and name when the event happened, as in /api/repos/<owner>/<name>), the actor who made the change, and a snapshot of the resource after the change. Edit events include the fields that changed. Text bodies longer than 65,536 characters are cut and flagged as truncated.

HeaderContains
X-GitCafe-EventThe event type, same as the body's type
X-GitCafe-DeliveryThe delivery ID, the same across retries
X-GitCafe-TimestampWhen this attempt was signed, in Unix seconds
X-GitCafe-Key-VersionWhich signing secret was used
X-GitCafe-Signaturesha256= followed by the hex HMAC

The full payload schemas are in the webhooks section of the OpenAPI description.

Git events

git.push, git.ref_created and git.ref_deleted carry a git object:

  • refUpdates: each branch or tag that changed, with its ref, beforeOid, afterOid and whether it was created, deleted or forced. At most 100 are listed; refUpdatesCount is the total and refUpdatesTruncated is true when some were left out.
  • source: what changed the refs. push is a Git push to GitCafe, merge and merge_cleanup a pull request merge and its branch cleanup, restack a stack update, mirror a pull mirror fetching from its upstream, and github_sync a repository synced with GitHub importing GitHub's refs.

A CI service should not start builds for github_sync (or mirror) pushes: for a synced repository, GitHub remains where CI runs on those changes, and GitCafe Actions does not run for them either.

Verify the signature

Before you trust a request:

  1. Read X-GitCafe-Timestamp and reject it if it is too old for your liking.
  2. Compute HMAC-SHA256 with your signing secret over the timestamp, a literal ., and the raw request body bytes.
  3. Compare sha256=<lowercase hex digest> with X-GitCafe-Signature using a constant-time comparison.
js
1import { createHmac, timingSafeEqual } from "node:crypto";
2
3function verify(secret, timestamp, body, signature) {
4 const expected = `sha256=${createHmac("sha256", secret)
5 .update(`${timestamp}.`)
6 .update(body)
7 .digest("hex")}`;
8 return (
9 expected.length === signature.length &&
10 timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
11 );
12}

Respond and handle retries

Return any 2xx status once you have stored the event. GitCafe retries on network errors, timeouts (10 seconds), 408, 425, 429 and 5xx, honouring Retry-After up to an hour, for up to eight attempts within 24 hours. Other statuses and redirects end the delivery.

Deliveries can arrive more than once and in any order. Deduplicate by the body's event id.

Inspect and redeliver

Under Manage, Delivery history shows each delivery's state, attempts and HTTP status; View attempts and View payload show the details. For a delivery that finally failed, fix your receiver, then choose Request redelivery to send the same event again. Each event can be redelivered twice, within 30 days.

sh
1cafe webhook deliveries acme/project <webhook-id> --state failed
2cafe webhook redeliver acme/project <webhook-id> <delivery-id> \
3 --request-id retry-1 --expected-version <version> --event-created-at <timestamp>

Delivery history is kept for 30 days.

Rotate the signing secret

Choose Rotate signing secret under More actions, or run cafe webhook rotate-secret <target> <webhook-id> --yes. The new secret is shown once. From then on every attempt, including retries of earlier events, is signed with the new secret, so update your receiver straight away. X-GitCafe-Key-Version tells you which secret signed a request.

App webhooks

An app that lists capabilities.webhooks (an HTTPS url and the events it needs) in its manifest receives those events for every repository each installation selected. Each event needs read access to its payload in the manifest's permissions.autonomous; registration refuses one that lacks it. Label and milestone events need repository Metadata (repo:metadata), and reaction events need comments too, since a reaction can be on a comment. actions.workflow_job needs the organization Runners permission (org:runners) and is sent only for jobs on the installing organization's own pools; a runner service uses it with generate-jitconfig. Apps cannot subscribe to all events with * or to webhook.ping.

The signing secret belongs to the app registration: it is shown once when you register the app, and POST /api/orgs/<publisher>/apps/<app>/webhook-secret/rotate replaces it for every installation at once and shows the new one once. Requests are signed and verified as above, and X-GitCafe-Key-Version names the secret version. The body also contains installation: { "id": ... }; deduplicate by the installation and event id together.

An app receives what its bot could read: the body of a hidden or deleted pull request comment, and of a deleted issue comment, is null. Events your app's own bot causes are not sent to that installation.

Apps also have events about their own bot, sent only to the installation whose bot is targeted and never to other webhooks. Each names the bot as bot: { "id": ... }.

EventsSent whenNeeds read access to
issue.bot_assigned, issue.bot_unassignedSomeone assigns or unassigns the bot on an issueIssues
pull.bot_assigned, pull.bot_unassignedSomeone assigns or unassigns it on a pull requestPull requests
pull.bot_review_requestedSomeone requests the bot's reviewPull requests
issue.bot_mentioned, pull.bot_mentionedA new comment @mentions the botThe subject and comments
check.rerequestedSomeone asks to re-run a check the app reportedChecks

check.rerequested names the app's bot and carries checkRun: the check's id, key, headOid, name, externalId, detailsUrl and the conclusion being re-run. See Report checks from your CI.

Mentions count in new comments, inline comments and replies, outside code, up to 20 handles a comment; edits, review summaries and descriptions send nothing. The bot must be mentionable (capabilities.bot.mentionable).

Installation events

An app with capabilities.webhooks also hears when an organization admin installs or changes its installation, whether or not events lists these. They go only to that installation's webhook, need no permission, and name the installation and its organization as installation: { "id": ... } and organization: { "id": ... }, plus the admin as actor.

EventSent when
installation.createdThe app is installed and the installation active
installation.pausedThe installation is paused
installation.resumedIt is resumed
installation.uninstalledThe app is uninstalled
installation.permissions_acceptedA newer revision of the app is approved
installation.repositories_updatedThe repositories the installation reaches change

installation.created is sent once per installation, when an admin completes the install. It has repositorySelection and, for selected repositories, repositories: each one's id, owner and name, up to 50 (the most an installation selects; repositoriesTruncated would say more were selected). For all repositories it names none. Like repositoriesAdded, it names only the repositories the installation may still read when it is sent: one removed before then, or every one if the installation was paused, is left out, and the event is still sent. Read the installation (GET /api/apps/installations/<id>) for its current repositories. Uninstalling and installing the app again is a new installation, with a new ID, and sends installation.created again.

installation.permissions_accepted is sent once when an admin approves a newer revision of the app (GitHub's new_permissions_accepted); reviewing an update without approving it, or approving the revision the installation already runs, sends nothing. It has the adopted revision and the permissions the installation now holds, as GET /api/apps/installations/<id> shows them: permissions for the bot, and user with permissions and audiences for people who connect the app. It names no repository. Approving stops every token the app holds, so this is the app's cue to request new ones; it is sent to the webhook the new revision names, and arrives even though the app's old tokens no longer work.

Pausing and uninstalling still drop every other delivery that was waiting (see Webhooks of installed apps), but not these events: installation.paused and installation.uninstalled are sent, and retried, after the app has lost access. They carry no repository. After installation.uninstalled the installation sends nothing more; drop its credentials and stop its work. After installation.resumed, events from while it was paused are not sent: read the installation (GET /api/apps/installations/<id>) to reconcile.

installation.repositories_updated has repositorySelection (all or selected) and previousRepositorySelection. Between two selections, repositoriesAdded names each repository the installation gained (id, owner and name) and repositoriesRemoved lists only the id of each one it lost; repositoriesRemovedTruncated is true if more than 100 were removed. A change to or from all repositories lists neither: read the installation for its selection. repositoriesAdded names only the repositories the installation may still read when the event is sent: one removed again before then, or every one if the installation was paused, is left out, and the event is still sent, unless that leaves it with nothing added, nothing removed and the same selection. So repositoriesRemoved can list, by ID only, a repository the app never received, such as one added and removed again while the installation was paused. A repository your bot creates joins the selection without an event.

Only an organization admin's changes send these events. GitCafe suspending your app sends none, and none is sent while it is suspended: the events waiting when it was suspended, and the changes admins make during it, are not sent after GitCafe lifts the suspension. Once it is lifted, read each installation (GET /api/apps/installations/<id>) to reconcile. Nor does a revision you publish send one: what it removes leaves every installation at once, and installation.permissions_accepted follows only when an admin approves it. Until then, installation events still go to the webhook the installation approved, even if the new revision lists none of its events or has no capabilities.webhooks. Once approved, a new webhook URL gets only events from then on (installation events waiting for the old one are not sent), and a revision without capabilities.webhooks leaves the installation no webhook, so nothing more is sent to it, not even the installation.permissions_accepted for that approval: the revision removed its URL, and the old one gets nothing.

Installation events are not ordered with each other or with other events: a retried event can arrive after a later one. Use createdAt, or read the installation, when the order matters.

Your app's deliveries

Owners and admins of the organization that publishes an app see its deliveries under Settings → Apps: edit the app and open Webhook deliveries. The list covers every installation, newest first, with the event, the installation and its organization, the state, the attempts, the last HTTP status and error, and when the event happened. View attempts shows each attempt's status and duration, and View payload the body your receiver was last sent, exactly as it was signed. Signing headers and your receiver's response bodies are never shown. Only deliveries GitCafe has tried to send are listed, and the redeliveries you request, so an event dropped before its first attempt (when the app was paused, or a repository removed) does not appear. History is kept for 30 days, also after an organization uninstalls your app: its organization is named by its handle while your app is installed there, and by its ID only once it is uninstalled.

Redeliver sends an event again, under the same limits as other webhooks: twice per event, one at a time, within 30 days. It goes only to the URL the event was first sent to, and only while the installation may still receive the event, so events from a repository the organization removed from your app are not sent again. The installation's own events (installation events) can be sent again while GitCafe has not suspended your app. Once an organization uninstalls your app, nothing of that installation can be redelivered, its own events included, though its history stays listed.

sh
1GET /api/orgs/<publisher>/apps/<app>/deliveries?state=failed&installationId=<id>
2GET /api/orgs/<publisher>/apps/<app>/deliveries/<delivery>/attempts?eventCreatedAt=<timestamp>
3GET /api/orgs/<publisher>/apps/<app>/deliveries/<delivery>/payload?eventCreatedAt=<timestamp>
4POST /api/orgs/<publisher>/apps/<app>/deliveries/<delivery>/redeliver
5 {"requestId": "retry-1", "eventCreatedAt": "<timestamp>"}

Organizations that install your app do not see these deliveries (Webhooks of installed apps).

Stop a webhook

Disable it to pause deliveries, or delete it under More actions. Both stop pending deliveries. Changing a repository's visibility or removing the person who created a webhook does not remove it; webhooks can carry private repository data, so review them when access changes.

Read as MarkdownGitCafe documentation
Documentation