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.
- Choose Add webhook.
- 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. - Save, then copy the signing secret into your receiver. It is shown only once.
- Choose Manage, then Send signed ping, and check Delivery history to confirm your receiver accepted it.
- 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.
If you publish an app, see App webhooks for what your receiver gets.
From the terminal, pass an organization slug or owner/repo as the target:
| 1 | cafe webhook create acme/project --url https://hooks.example.com/gitcafe \ |
| 2 | --event issue.created --event pull.merged |
| 3 | cafe 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
| Events | Sent when |
|---|---|
git.push | Branches or tags change by push, merge or sync |
git.ref_created, git.ref_deleted | Branches or tags are created or deleted |
repo.default_branch_changed | The default branch changes |
issue.created, issue.updated, issue.closed, issue.reopened | An issue is created, edited, closed or reopened |
issue.locked, issue.unlocked, issue.pinned, issue.unpinned | An issue is locked, unlocked, pinned or unpinned |
issue.assignees_updated, issue.labels_updated, issue.milestone_updated | An issue's assignees, labels or milestone change |
issue.linked, issue.unlinked | A pull request is linked to or unlinked from it |
issue.comment_created, issue.comment_updated, issue.comment_deleted | An issue comment changes |
issue.reaction_added, issue.reaction_removed | A reaction is added or removed |
pull.created, pull.updated, pull.closed, pull.reopened | A pull request is opened, edited, closed or reopened |
pull.synchronized | A pull request's source branch gets new commits |
pull.merged | A pull request merges in GitCafe |
pull.drafted, pull.ready, pull.locked, pull.unlocked | Draft or lock state changes |
pull.reviewers_updated, pull.assignees_updated | Reviewers or assignees change |
pull.labels_updated, pull.milestone_updated | Labels or milestone change |
pull.linked, pull.unlinked | An issue is linked or unlinked |
pull.review_submitted, pull.review_dismissed | A review is submitted or dismissed |
pull.comment_created, pull.comment_updated, pull.comment_deleted | A pull request comment changes |
pull.comment_hidden, pull.comment_unhidden | A comment is hidden or shown |
pull.comment_resolved, pull.comment_unresolved | A review thread is resolved or reopened |
pull.reaction_added, pull.reaction_removed | A reaction is added or removed |
label.created, label.updated, label.deleted | A repository label changes |
milestone.created, milestone.updated, milestone.deleted | A milestone changes |
ci.run_updated | A CI run's state changes |
actions.workflow_job | An Actions job on your organization's pool changes |
webhook.ping | You 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. It
is sent for a job that runs on one of your organization's pools, with an
action of queued, in_progress (a runner took the job) or completed:
queuedis sent once the job has a pool. A job that matches no pool yet sends it later, when a pool is bound.in_progressandcompletedfollow. A queued job that is cancelled or times out before a runner takes it sendscompletedwith no runner.- Jobs on the instance's shared pool send nothing.
Each action is sent once per job, in order. workflowJob holds the job, run,
pool and runner IDs, the run attempt, the job name, its labels, 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 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.
| Header | Contains |
|---|---|
X-GitCafe-Event | The event type, same as the body's type |
X-GitCafe-Delivery | The delivery ID, the same across retries |
X-GitCafe-Timestamp | When this attempt was signed, in Unix seconds |
X-GitCafe-Key-Version | Which signing secret was used |
X-GitCafe-Signature | sha256= followed by the hex HMAC |
The full payload schemas are in the webhooks section of the
OpenAPI description.
Verify the signature
Before you trust a request:
- Read
X-GitCafe-Timestampand reject it if it is too old for your liking. - Compute HMAC-SHA256 with your signing secret over the timestamp, a literal
., and the raw request body bytes. - Compare
sha256=<lowercase hex digest>withX-GitCafe-Signatureusing a constant-time comparison.
| 1 | import { createHmac, timingSafeEqual } from "node:crypto"; |
| 2 | |
| 3 | function 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.
| 1 | cafe webhook deliveries acme/project <webhook-id> --state failed |
| 2 | cafe 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.
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": ... }.
| Events | Sent when | Needs read access to |
|---|---|---|
issue.bot_assigned, issue.bot_unassigned | Someone assigns or unassigns the bot on an issue | Issues |
pull.bot_assigned, pull.bot_unassigned | Someone assigns or unassigns it on a pull request | Pull requests |
pull.bot_review_requested | Someone requests the bot's review | Pull requests |
issue.bot_mentioned, pull.bot_mentioned | A new comment @mentions the bot | The subject and comments |
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).
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.