Skip to content
204 linesCodeBlameRaw
1---
2title: Webhooks
3description: Have g1t send a repository's or a workspace's events to your own address as they happen, signed and retried.
4---
5
6A webhook sends events to an HTTPS address you choose, as they happen: a
7push, an issue opened, a pull request merged, checks finished, the merge
8queue moving. Use them to build on g1t: post to chat, start a deploy, keep
9another system in step.
10
11A webhook belongs to one of two things:
12
13| | Sent the events of | Managed by | Where |
14| --- | --- | --- | --- |
15| A repository's | That repository | People with the Admin [role](/guides/access-and-roles/) on it | The project's **Settings → Webhooks** |
16| A workspace's | Every repository in the workspace, the workspace's own [packages](/guides/packages/), and its [teams](/guides/teams/) | Owners | The workspace's **Settings → Webhooks** |
17
18Seeing a repository's webhooks, and their deliveries, needs Admin too: the
19page is not shown to anyone else, since a webhook's address and secret are
20the workspace's own.
21
22## Add one
23
241. Open **Settings → Webhooks** for the repository or the workspace.
252. Give the **payload URL**: an HTTPS address on the public internet.
263. Choose the events, or leave **Everything**, which also includes event
27 types added later.
284. Give a **secret**, or leave it empty and g1t makes one. A secret g1t
29 makes is shown once.
305. **Add webhook**. g1t sends it a `ping` at once, so you can see straight
31 away whether your receiver answers.
32
33## What is sent
34
35Each delivery is a `POST` with a JSON body:
36
37```json
38{
39 "id": "evt_01m401tecce9h8wt7k3ayvs72n",
40 "type": "comment.created",
41 "time": "2026-10-03T04:54:37.708Z",
42 "workspace": "acme",
43 "repository": { "id": "rep_cf985171afeee00a62a1c0acb0", "full_name": "acme/web" },
44 "actor": { "id": "usr_b51a1a09fc53e9471cbe1426b7", "username": "ada" },
45 "data": { "comment_id": "cmt_01m401te9eekb92kccgwhympr4", "number": 3, "repo_id": "rep_cf985171afeee00a62a1c0acb0" }
46}
47```
48
49`data` holds what the event is about: ids and numbers to fetch the rest with
50the [API](/reference/api/). `actor` is null for something g1t did by
51itself. An event whose `data` would be larger than about 96 KB has its
52long text, such as a comment's or release's body, shortened, and
53`data.truncated` is `true`: fetch the whole text from the API.
54
55With these headers:
56
57| Header | |
58| --- | --- |
59| `X-G1t-Event` | The event type, such as `pull.merged`, or `ping`. |
60| `X-G1t-Delivery` | The delivery's id. A redelivery has a new one. |
61| `X-G1t-Hook` | The webhook's id. |
62| `X-G1t-Signature-256` | `sha256=` and the HMAC-SHA256 of the body, keyed with the secret. |
63| `User-Agent` | `g1t-webhooks/1`, followed by a link to this page |
64
65## Events
66
67| Event | When |
68| --- | --- |
69| `git.push` | A branch moved. `data.ref`, `data.after`, `data.default_branch`, and `data.commits` (how many commits the push brought to the branch, at most 50; absent for a tag, a deleted branch or a merge). A push of more than 3 tags, or more than 1,000 branches, sends fewer; see [pushes of many branches or tags](/guides/git/#pushes-of-many-branches-or-tags). |
70| `repo.created`, `repo.forked` | A repository was made, or forked for a pull request. |
71| `repo.updated` | Its description, website, topics, protection or visibility changed. |
72| `repo.visibility_changed` | It was made public or private. |
73| `repo.renamed` | It was given a new name. Its old address redirects. |
74| `repo.transferred` | It moved to another workspace. |
75| `repo.default_branch_changed` | Its default branch changed, or the default branch was renamed. |
76| `git.deleted` | A push deleted a branch or tag. `data.ref` is the full ref, and `data.before` the commit it pointed to. A push that deletes more than 3 tags sends none for them. |
77| `branch.renamed` | A branch was renamed. |
78| `repo.collaborator_added`, `repo.collaborator_role_changed`, `repo.collaborator_removed` | Someone was given a role on it, had their role changed, or lost it. `data.username`, `data.role`, `data.previous_role`. See [access and roles](/guides/access-and-roles/). |
79| `team.created`, `team.edited`, `team.deleted` | A [team](/guides/teams/) was created, changed or deleted. Sent to the workspace's webhooks. `data.workspace`, `data.team` (its slug), `data.team_id`, `data.name`, `data.visibility`, `data.parent`; on `team.edited`, `data.changes`. |
80| `team.member_added`, `team.member_role_changed`, `team.member_removed` | Someone joined or left a team, or became its maintainer or a member. Sent to the workspace's webhooks. `data.username`, `data.role` (`member` or `maintainer`), `data.previous_role`. |
81| `team.repo_added`, `team.repo_role_changed`, `team.repo_removed` | A team was given a role on the repository, had it changed, or lost it. `data.repo`, `data.repo_id`, `data.repo_role`, `data.previous_repo_role`. |
82| `workspace.member_joined` | A person became a member of the [workspace](/guides/workspaces/#when-someone-joins). Sent to the workspace's webhooks. `data.workspace_id`, `data.workspace` (its slug), `data.user_id`, `data.username`, `data.role` (`owner` or `member`) and `data.how`: `invitation` (they accepted an invitation), `joined` (they used an invite code that names the workspace) or `created` (they made the workspace and are its first owner). |
83| `repo.archived`, `repo.unarchived` | It was made read-only, or writable again. |
84| `repo.deleted`, `repo.restored`, `repo.purged` | It was deleted, restored within its 30 days, or removed for good. |
85| `issue.labeled`, `issue.unlabeled`, `pull.labeled`, `pull.unlabeled` | A [label](/guides/labels/) was put on an issue or a pull request, or taken off: one event for each label. `data.number` and `data.label` (`name` and `color`). |
86| `issue.milestoned`, `issue.demilestoned`, `pull.milestoned`, `pull.demilestoned` | An issue or a pull request was put in a [milestone](/guides/milestones/), or taken out of one. `data.milestone` (`number` and `title`). Moving it from one to another is both. |
87| `pull.base_changed` | The branch a pull request merges into changed. `data.base` names it. See [pull requests into other branches](/guides/base-branches/). |
88| `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number` and `data.author` (`id` and `username`); on close, `data.reason` and `data.resolved_by`; on assignment, `data.assignees` and the newly assigned `data.added`. |
89| `comment.created` | A comment or review on an issue or pull request. |
90| `comment.edited` | A comment's text was [edited](/guides/pull-requests/#editing-and-deleting-comments). `data.comment_id`, `data.number`, `data.pull_id` on a pull request, and `data.changes.body.from`, what it said before. |
91| `comment.deleted` | A comment was [deleted](/guides/pull-requests/#editing-and-deleting-comments). `data.comment_id`, `data.number`, `data.pull_id` on a pull request, and `data.comment` as it was: `id`, `body`, `author` (`id` and `username`), `created_at`, `path` and `line`. |
92| `pull.opened`, `pull.ready`, `pull.updated`, `pull.merge_requested`, `pull.merged`, `pull.closed` | A pull request changed. `data.number`, `data.issue` and `data.author` (`id` and `username`); on merge, `data.commit`. `actor` is whoever caused the event. |
93| `pull.reopened`, `pull.converted_to_draft` | A closed pull request was [reopened](/guides/pull-requests/#reopen-a-pull-request), or one ready for review was [turned back into a draft](/guides/pull-requests/#convert-to-a-draft). The same data as `pull.closed`; on reopen, `data.commit` is its head. |
94| `pull.assigned` | People were assigned to a pull request. `data.assignees` is everyone assigned now, `data.added` those newly assigned. |
95| `pull.review_requested`, `pull.review_request_removed` | Reviewers were asked for a pull request, or no longer are. `data.reviewers` names the people. `data.teams` lists the [teams](/guides/teams/#review-requests) asked, or no longer asked, each with `team` (`workspace/team`), `notified` (who in it was told) and `assigned` (who review assignment picked). `data.code_owners` is `true` when its [CODEOWNERS file](/guides/codeowners/) asked them. |
96| `secret_scanning_alert.created`, `.fixed`, `.dismissed`, `.reopened` | A [secret alert](/guides/security/secret-protection/) opened (found in history, or blocked at a push: `data.pusher`), was revoked, dismissed or reopened. `data.alert_id`, `data.title`, `data.path`, `data.line`, `data.severity`, `data.state`, `data.reason`, `data.link`. Never the secret. The first 200 of a scan's alerts get one each; the scan's `.summary` carries the counts. |
97| `secret_scanning_alert.summary`, `code_scanning_alert.summary`, `vulnerability_alert.summary` | What one scan did to a repository's alerts of one type, published once per scan: `data.scan` (`push`, `history`, `dependencies`, `analysis`, or `change` for a dismissal or reopening), `data.opened` (up to 200 ids) and `data.opened_count`, `data.fixed` and `data.fixed_count`, `data.dismissed_count`, `data.new_by_severity` and `data.open` (counts by severity), `data.worst`, `data.latest` (the worst new alert's title), `data.link`, and for a refused push `data.pusher` and `data.blocked`. What [notifications](/guides/notifications/#security) are built from. |
98| `secret_scanning.bypass_requested`, `secret_scanning.bypass_reviewed` | Someone asked to bypass push protection, or the request was approved or denied (`data.state`). `data.request_id`, `data.alert_id`, `data.reason`. |
99| `code_scanning_alert.created`, `.fixed`, `.dismissed`, `.reopened` | A [code scanning alert](/guides/security/code-scanning/) changed. `data.alert_id`, `data.alert_number`, `data.title`, `data.severity`, `data.path`, `data.line`, `data.state`, `data.link`. |
100| `vulnerability_alert.created`, `.fixed`, `.dismissed`, `.reopened` | A [vulnerability alert](/guides/security/supply-chain/) changed. `data.alert_id`, `data.title` (package, version, lockfile and advisory), `data.severity`, `data.state`, `data.link`. |
101| `security_advisory.published`, `security_advisory.withdrawn` | The repository [published or withdrew a security advisory](/guides/security/advisories/). `data.id`, `data.g1tsa_id`, `data.title`, `data.severity`, `data.state`, `data.link`. |
102| `security_advisory.reported` | Someone [reported a vulnerability privately](/guides/security/advisories/#private-vulnerability-reporting) to the repository's maintainers. `data.id`, `data.title`, `data.severity`, `data.link`. Never the reporter or the description: it says to look. |
103| `checks.completed` | A pull request's checks finished: every status on its head has reported and none is still pending, or the merge queue took it out. `data.number`, `data.commit`, and `data.status`, `passed` or `failed`. |
104| `status.created` | A status was set on a commit: through the API, by a workflow run, by a deploy, or by another part of g1t. `data.sha`, `data.context`, `data.state`, `data.description`, `data.target_url`. See [Checks](/guides/checks/). |
105| `check_run.created`, `check_run.completed` | A check run was reported on a commit, or completed. `data.check_run`: `id`, `name`, `head_sha`, `status`, `conclusion`, `details_url`, `external_id`, `html_url`, `output`, `actions`, `check_suite` and `app`. |
106| `check_run.rerequested`, `check_run.requested_action` | Someone asked for a check run to run again, or pressed one of its buttons: `data.requested_action` is the button's `identifier`. Report a new run, or do what the button says. |
107| `check_suite.completed`, `check_suite.rerequested` | Every latest check run of a reporter's suite on a commit completed, or someone asked for it to run again. `data.check_suite`: `id`, `head_sha`, `status`, `conclusion`, `app`. |
108| `workflow.completed` | A [workflow](/guides/actions/) run finished. `data.workflow`, `data.conclusion`, `data.run_id`, `data.sha`, `data.pull`. |
109| `deployment.succeeded`, `deployment.failed` | A g1t.page build of a [project](/guides/deployments/) finished, for production or a pull request's preview. `data.deployment_id`, `data.project`, `data.kind` (`production` or `preview`), `data.number` for a preview, `data.commit`, `data.path`, `data.error` on failure, and `data.recovered` when a success follows a failure. |
110| `deployment.created` | A deployment was made, from any source: reported through the [API](/guides/deployments-api/), made by a g1t Actions job with an `environment:`, or a g1t.page build. `data.repo_id` and `data.deployment`, without its `payload`. |
111| `deployment_status.created` | A deployment got a status, from any source. `data.repo_id`, `data.deployment` without its `payload`, and `data.deployment_status`. |
112| `release.created`, `.published`, `.released`, `.prereleased`, `.edited`, `.unpublished`, `.deleted` | A [release](/guides/releases/) changed; one change can send several, as the [release activity types](/guides/actions/#releases) say. `data.release_id`, `data.tag_name`, and `data.release` (`tag_name`, `target`, `name`, `body`, `draft`, `prerelease`, `author`, `created_at`, `published_at`), as it was for `release.deleted`. On `release.edited`, `data.changes` holds the old `name` and `body`. |
113| `queue.changed` | The merge queue gained, lost or settled an entry. |
114| `integration.failed` | A pull request failed in the [merge queue](/guides/merge-queue/#when-an-entry-fails) combined with the ones ahead of it. `pull` (also `number`) is the one that answers for it, `issue` its issue, `combination` the pull requests tested together, `check` the check that failed (`name`, `conclusion`, `url`), `suspects` the pull requests ahead it fails with, `bisected` whether smaller combinations were built to find them, and `other_pulls` the rest of the combination. |
115| `integration.passed` | A pull request's combined state passed in the merge queue: `pull` and `combination`. |
116| `decision.recorded` | A [decision record](/guides/why-blame/#decision-records) was kept on an issue or pull request: `decision`, with `number` and `issue`. |
117| `session.appended` | A pull request's [session](/guides/why-blame/#sessions) grew: a coding agent recorded entries with `record_session`, or g1t noted a merge queue failure. Busy: choose it only if you need it. |
118
119[Managing a repository](/guides/managing-repositories/) says what each of
120the `repo.` and `branch.` changes does.
121
122## Check the signature
123
124Compute the HMAC-SHA256 of the raw body with your secret and compare it to
125`X-G1t-Signature-256` in constant time, before you parse the body.
126
127```js
128// Node.js
129import { createHmac, timingSafeEqual } from "node:crypto";
130
131function fromG1t(rawBody, signature, secret) {
132 const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
133 return signature?.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
134}
135```
136
137```python
138# Python
139import hashlib, hmac
140
141def from_g1t(raw_body: bytes, signature: str, secret: str) -> bool:
142 expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
143 return hmac.compare_digest(signature or "", expected)
144```
145
146## Answering, and retries
147
148Answer with any 2xx within 10 seconds. Do slow work after answering.
149
150A delivery that gets anything else, or no answer, is tried again after 1
151minute, 5 minutes, 30 minutes, 2 hours and 5 hours: six attempts over
152about seven and a half hours. Then it is marked failed. Pausing or removing
153a webhook stops its retries.
154
155An event is delivered to a webhook once. Use `X-G1t-Delivery`, or the
156event's `id`, to ignore one you have already handled.
157
158## The delivery log
159
160Each webhook keeps its deliveries for 14 days. Open **Deliveries** to see,
161for each one, the request g1t sent, the status and body your receiver
162answered with, how long it took, and when it will be tried again.
163**Redeliver** sends the same payload again, as a new delivery.
164
165The status dot beside each webhook shows how its latest delivery went:
166green delivered, amber waiting to try again, red failed.
167
168## Addresses
169
170Webhooks are sent only over HTTPS, to public addresses. Private and local
171addresses (`localhost`, `10.0.0.0/8`, `192.168.0.0/16` and the like) are
172refused. To receive webhooks on your own machine while you build, use a
173tunnel such as Cloudflare Tunnel.
174
175## Without a public address
176
177An agent on a laptop, or anything else that cannot take requests from the
178internet, can follow a repository's events instead:
179[`list_events`](/reference/api/repositories/list-events/) with `after` and
180`wait` holds each request until an event is written, so one request at a
181time hears about events within a second or so. `g1t work events --follow`
182does this for you ([The g1t CLI](/guides/cli/#following-events)).
183
184## From the API
185
186The same routes, and the MCP server's `webhook` tool, work for a
187repository's webhooks (give `repo`) and a workspace's (give `workspace`
188instead).
189
190| `webhook` action | Route |
191| --- | --- |
192| `list` | `GET /repos/{owner}/{name}/hooks`, `GET /workspaces/{workspace}/hooks` |
193| `create` | `POST …/hooks` with `url`, `events`, `secret` |
194| `update` | `PATCH …/hooks/{id}` with `url`, `events`, `active` |
195| `delete` | `DELETE …/hooks/{id}` |
196| `ping` | `POST …/hooks/{id}/pings` |
197| `list_deliveries` | `GET …/hooks/{id}/deliveries` |
198| `redeliver` | `POST …/hooks/{id}/deliveries/{delivery}/redeliver` |
199
200```sh
201curl -X POST https://api.g1t.sh/repos/acme/web/hooks \
202 -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \
203 -d '{"url": "https://example.com/g1t", "events": ["pull.merged", "checks.completed"]}'
204```