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

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

```sh
cafe webhook create acme/project --url https://hooks.example.com/gitcafe \
  --event issue.created --event pull.merged
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](https://git.cafe/docs/ci/checks-and-logs.md) run's state 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.

## The request

Each delivery is an HTTPS `POST` with a JSON body. The body has an event `id`,
a `type`, a timestamp, the repository and organization IDs, 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](https://git.cafe/api/openapi).

## 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
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, timestamp, body, signature) {
  const expected = `sha256=${createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(body)
    .digest("hex")}`;
  return (
    expected.length === signature.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
  );
}
```

## 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
cafe webhook deliveries acme/project <webhook-id> --state failed
cafe webhook redeliver acme/project <webhook-id> <delivery-id> \
  --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.

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