Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

222 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, a work item 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, and the [work items](/guides/work-items/) linked to it | 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, all its work items, 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
55A work item belongs to the workspace, not to one repository, so a
56`work_item.` event's `data` has `workspace`, `number`, `item_id` and
57`repo_ids`, every repository linked to it, its origin first. Each of
58those repositories' webhooks gets it, with `repository` naming that
59repository, and so do the workspace's, with `repository` naming the
60origin, or null for a work item filed in the workspace alone.
61
62With these headers:
63
64| Header | |
65| --- | --- |
66| `X-G1t-Event` | The event type, such as `pull.merged`, or `ping`. |
67| `X-G1t-Delivery` | The delivery's id. A redelivery has a new one. |
68| `X-G1t-Hook` | The webhook's id. |
69| `X-G1t-Signature-256` | `sha256=` and the HMAC-SHA256 of the body, keyed with the secret. |
70| `User-Agent` | `g1t-webhooks/1`, followed by a link to this page |
71
72## Events
73
74| Event | When |
75| --- | --- |
76| `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). |
77| `repo.created` | A repository was made. A [fork](/concepts/forks/) has `data.parent_id`, the repository it was forked from. |
78| `repo.forked` | Someone forked this repository, or a pull request's working copy was made of it. `data.repo_id` is the new repository, `data.source_repo_id` this one, and `data.pull_id` the pull request, for a working copy only. |
79| `repo.detached` | A fork left its parent: an admin detached it, or its parent was made public or private. `data.repo_id` and `data.parent_id`. Its workflows stay off unless someone had turned them on. |
80| `repo.updated` | Its website, topics, protection or visibility changed. |
81| `repo.visibility_changed` | It was made public or private. |
82| `repo.renamed` | It was given a new name. Its old address redirects. |
83| `repo.transferred` | It moved to another workspace. |
84| `repo.default_branch_changed` | Its default branch changed, or the default branch was renamed. |
85| `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. |
86| `branch.renamed` | A branch was renamed. |
87| `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/). |
88| `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`. |
89| `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`. |
90| `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`. |
91| `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). |
92| `repo.archived`, `repo.unarchived` | It was made read-only, or writable again. |
93| `repo.deleted`, `repo.restored`, `repo.purged` | It was deleted, restored within its 30 days, or removed for good. |
94| `work_item.labeled`, `work_item.unlabeled`, `pull.labeled`, `pull.unlabeled` | A [label](/guides/labels/) was put on a work item or a pull request, or taken off: one event for each label. `data.number` and `data.label` (`name` and `color`). |
95| `work_item.milestoned`, `work_item.demilestoned`, `pull.milestoned`, `pull.demilestoned` | A work item 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. |
96| `pull.base_changed` | The branch a pull request merges into changed. `data.base` names it. See [pull requests into other branches](/guides/base-branches/). |
97| `work_item.opened`, `work_item.updated`, `work_item.assigned`, `work_item.closed`, `work_item.reopened` | A [work item](/guides/work-items/) changed. `data.workspace`, `data.number`, `data.item_id`, `data.repo_ids` and `data.author` (`id` and `username`); on close, `data.reason`, `data.resolution` when a landing closed it (the `repo` and the `pull` request or default-branch `commit` that landed it), and `data.session` when it was closed in a session; on assignment, `data.assignees` and the newly assigned `data.added`. |
98| `work_item.linked`, `work_item.unlinked` | A repository was linked to a work item in a role, or unlinked. `data.workspace`, `data.number`, `data.item_id`, `data.repo_ids`, `data.repo` (`owner/name`) and `data.role`. |
99| `work_item.landed` | A pull request or a default-branch commit landed a work item in one of its repositories. `data.resolution` (the `repo`, and the `pull` or `commit`) and `data.missing`, the targets it has still to land in. When nothing is left, `work_item.closed` follows. |
100| `comment.created` | A comment or review on a work item or pull request. |
101| `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. |
102| `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`. |
103| `commit_comment.created`, `commit_comment.edited`, `commit_comment.deleted` | A [comment on a commit](/guides/pull-requests/#commit-comments) was made, edited or deleted. `data.comment_id`, `data.commit` and `data.comment` (`id`, `commit`, `path`, `line`, `author`, `body`, `created_at`, `updated_at`); on edit, `data.changes.body.from`, what it said before. |
104| `pull.opened`, `pull.ready`, `pull.updated`, `pull.merge_requested`, `pull.merged`, `pull.closed` | A pull request changed. `data.number`, `data.item` (its work item's number in the workspace) and `data.author` (`id` and `username`); on merge, `data.commit`. `actor` is whoever caused the event. |
105| `pull.auto_merge_enabled`, `pull.auto_merge_disabled` | [Auto-merge](/guides/pull-requests/#auto-merge) was turned on for a pull request, or off: by a person, or by g1t when it merged, closed or someone else pushed. `data.number`; on enable, `data.detail` is the merge method; on disable, `data.detail` says why g1t turned it off, `data.auto_merge_by` names who had turned it on, and `data.queued` is `true` when g1t turned it off because the pull request joined the merge queue, which lands it. |
106| `pull.head_branch_deleted`, `pull.head_branch_restored` | A merged or closed pull request's [branch](/guides/pull-requests/#deleting-the-branch) was deleted, on merge or by hand, or made again. `data.number` and `data.detail`, the branch. |
107| `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. |
108| `pull.assigned` | People were assigned to a pull request. `data.assignees` is everyone assigned now, `data.added` those newly assigned. |
109| `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. |
110| `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. |
111| `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. |
112| `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`. |
113| `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`. |
114| `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`. |
115| `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`. |
116| `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. |
117| `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`. |
118| `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/). |
119| `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`. |
120| `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. |
121| `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`. |
122| `workflow.completed` | A [workflow](/guides/actions/) run finished. `data.workflow`, `data.conclusion`, `data.run_id`, `data.sha`, `data.pull`. |
123| `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. |
124| `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`. |
125| `deployment_status.created` | A deployment got a status, from any source. `data.repo_id`, `data.deployment` without its `payload`, and `data.deployment_status`. |
126| `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`. |
127| `queue.changed` | The merge queue gained, lost or settled an entry. |
128| `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, `item` its work item, `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, `other_pulls` the rest of the combination, and `landed`, when nothing was ahead of it though it passes its checks on its own, the pull requests merged into the default branch since it branched, newest first and at most ten (empty otherwise). |
129| `integration.passed` | A pull request's combined state passed in the merge queue: `pull` and `combination`. |
130| `decision.recorded` | A [decision record](/guides/why-blame/#decision-records) was kept on a work item or pull request: `decision`, with `number` and `item`. |
131| `session.appended` | A pull request's [session](/guides/why-blame/#sessions) grew: a coding agent recorded entries with `record_session`, a session on it recorded some, or g1t noted a merge queue failure. Busy: choose it only if you need it. |
132| `session.started`, `session.ended` | A [session](/guides/sessions/#sessions) of someone's harness started (not on a repeated start), or ended or timed out. `data.session`, and `data.number` (its pull request, else its work item) and `data.item`. |
133| `work.claimed`, `work.released`, `work.claim_expired`, `work.handoff_requested` | A [claim](/guides/sessions/#claims) on a work item was made, released, expired or handed off. `data.number` and `data.item` (the work item), `data.claim`; on `work.claimed`, `data.others` (the other active claims) and `data.took_over`; on `work.released`, `data.reason`; on a hand-off, `data.from`, `data.to` and `data.note`. |
134| `dependency.changed` | What a work item is [blocked by](/guides/sessions/#dependencies) changed, or a blocker closed or had its pull request merged. `data.number` (the blocked work item), `data.blocked_by`, `data.added`, `data.removed`, `data.cleared`, `data.remaining` (blockers still open) and `data.merged_pull`. |
135| `overlap.detected` | A push made a pull request [change files another open one changes](/guides/sessions/#overlaps). Sent once for each of the two: `data.pull`, `data.other_pull`, `data.files`, `data.item` and `data.other_item`. |
136
137[Managing a repository](/guides/managing-repositories/) says what each of
138the `repo.` and `branch.` changes does.
139
140## Check the signature
141
142Compute the HMAC-SHA256 of the raw body with your secret and compare it to
143`X-G1t-Signature-256` in constant time, before you parse the body.
144
145```js
146// Node.js
147import { createHmac, timingSafeEqual } from "node:crypto";
148
149function fromG1t(rawBody, signature, secret) {
150 const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
151 return signature?.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
152}
153```
154
155```python
156# Python
157import hashlib, hmac
158
159def from_g1t(raw_body: bytes, signature: str, secret: str) -> bool:
160 expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
161 return hmac.compare_digest(signature or "", expected)
162```
163
164## Answering, and retries
165
166Answer with any 2xx within 10 seconds. Do slow work after answering.
167
168A delivery that gets anything else, or no answer, is tried again after 1
169minute, 5 minutes, 30 minutes, 2 hours and 5 hours: six attempts over
170about seven and a half hours. Then it is marked failed. Pausing or removing
171a webhook stops its retries.
172
173An event is delivered to a webhook once. Use `X-G1t-Delivery`, or the
174event's `id`, to ignore one you have already handled.
175
176## The delivery log
177
178Each webhook keeps its deliveries for 14 days. Open **Deliveries** to see,
179for each one, the request g1t sent, the status and body your receiver
180answered with, how long it took, and when it will be tried again.
181**Redeliver** sends the same payload again, as a new delivery.
182
183The status dot beside each webhook shows how its latest delivery went:
184green delivered, amber waiting to try again, red failed.
185
186## Addresses
187
188Webhooks are sent only over HTTPS, to public addresses. Private and local
189addresses (`localhost`, `10.0.0.0/8`, `192.168.0.0/16` and the like) are
190refused. To receive webhooks on your own machine while you build, use a
191tunnel such as Cloudflare Tunnel.
192
193## Without a public address
194
195An agent on a laptop, or anything else that cannot take requests from the
196internet, can follow a repository's events instead:
197[`list_events`](/reference/api/repositories/list-events/) with `after` and
198`wait` holds each request until an event is written, so one request at a
199time hears about events within a second or so. `g1t work events --follow`
200does this for you ([The g1t CLI](/guides/cli/#following-events)).
201
202## From the API
203
204The same routes, and the MCP server's `webhook` tool, work for a
205repository's webhooks (give `repo`) and a workspace's (give `workspace`
206instead).
207
208| `webhook` action | Route |
209| --- | --- |
210| `list` | `GET /repos/{owner}/{name}/hooks`, `GET /workspaces/{workspace}/hooks` |
211| `create` | `POST …/hooks` with `url`, `events`, `secret` |
212| `update` | `PATCH …/hooks/{id}` with `url`, `events`, `active` |
213| `delete` | `DELETE …/hooks/{id}` |
214| `ping` | `POST …/hooks/{id}/pings` |
215| `list_deliveries` | `GET …/hooks/{id}/deliveries` |
216| `redeliver` | `POST …/hooks/{id}/deliveries/{delivery}/redeliver` |
217
218```sh
219curl -X POST https://api.g1t.sh/repos/acme/web/hooks \
220 -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \
221 -d '{"url": "https://example.com/g1t", "events": ["pull.merged", "checks.completed"]}'
222```