Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Webhooks: every event, to your own addresses, signed and retried | 1 | --- |
| 2 | title: Webhooks | |
| 3 | description: Have g1t send a repository's or a workspace's events to your own address as they happen, signed and retried. | |
| 4 | --- | |
| 5 | ||
| 6 | A webhook sends events to an HTTPS address you choose, as they happen: a | |
| 7 | push, an issue opened, a pull request merged, checks finished, the merge | |
| 8 | queue moving. Use them to build on g1t: post to chat, start a deploy, keep | |
| 9 | another system in step. | |
| 10 | ||
| 11 | A webhook belongs to one of two things: | |
| 12 | ||
| 13 | | | Sent the events of | Managed by | Where | | |
| 14 | | --- | --- | --- | --- | | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 15 | | A repository's | That repository | People with the Admin [role](/guides/access-and-roles/) on it | The project's **Settings → Webhooks** | |
| Webhooks: every event, to your own addresses, signed and retried | 16 | | A workspace's | Every repository in the workspace | Owners | The workspace's **Settings → Webhooks** | |
| 17 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 18 | Seeing a repository's webhooks, and their deliveries, needs Admin too: the |
| 19 | page is not shown to anyone else, since a webhook's address and secret are | |
| 20 | the workspace's own. | |
| 21 | ||
| Webhooks: every event, to your own addresses, signed and retried | 22 | ## Add one |
| 23 | ||
| 24 | 1. Open **Settings → Webhooks** for the repository or the workspace. | |
| 25 | 2. Give the **payload URL**: an HTTPS address on the public internet. | |
| 26 | 3. Choose the events, or leave **Everything**, which also includes event | |
| 27 | types added later. | |
| 28 | 4. Give a **secret**, or leave it empty and g1t makes one. A secret g1t | |
| 29 | makes is shown once. | |
| 30 | 5. **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 | ||
| 35 | Each 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", | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 43 | "repository": { "id": "rep_cf985171afeee00a62a1c0acb0", "full_name": "acme/web" }, |
| Webhooks: every event, to your own addresses, signed and retried | 44 | "actor": { "id": "usr_b51a1a09fc53e9471cbe1426b7", "username": "ada" }, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 45 | "data": { "comment_id": "cmt_01m401te9eekb92kccgwhympr4", "number": 3, "repo_id": "rep_cf985171afeee00a62a1c0acb0" } |
| Webhooks: every event, to your own addresses, signed and retried | 46 | } |
| 47 | ``` | |
| 48 | ||
| 49 | `data` holds what the event is about: ids and numbers to fetch the rest with | |
| 50 | the [API](/reference/api/). `actor` is null for something g1t did by | |
| 51 | itself. | |
| 52 | ||
| 53 | With these headers: | |
| 54 | ||
| 55 | | Header | | | |
| 56 | | --- | --- | | |
| 57 | | `X-G1t-Event` | The event type, such as `pull.merged`, or `ping`. | | |
| 58 | | `X-G1t-Delivery` | The delivery's id. A redelivery has a new one. | | |
| 59 | | `X-G1t-Hook` | The webhook's id. | | |
| 60 | | `X-G1t-Signature-256` | `sha256=` and the HMAC-SHA256 of the body, keyed with the secret. | | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 61 | | `User-Agent` | `g1t-webhooks/1`, followed by a link to this page | |
| Webhooks: every event, to your own addresses, signed and retried | 62 | |
| 63 | ## Events | |
| 64 | ||
| 65 | | Event | When | | |
| 66 | | --- | --- | | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 67 | | `git.push` | A branch moved. `data.ref`, `data.after`, `data.default_branch`. | |
| Webhooks: every event, to your own addresses, signed and retried | 68 | | `repo.created`, `repo.forked` | A repository was made, or forked for a pull request. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 69 | | `repo.updated` | Its description, website, topics, protection or visibility changed. | |
| 70 | | `repo.visibility_changed` | It was made public or private. | | |
| 71 | | `repo.renamed` | It was given a new name. Its old address redirects. | | |
| 72 | | `repo.transferred` | It moved to another workspace. | | |
| 73 | | `repo.default_branch_changed` | Its default branch changed, or the default branch was renamed. | | |
| 74 | | `branch.renamed` | A branch was renamed. | | |
| 75 | | `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/). | | |
| 76 | | `repo.archived`, `repo.unarchived` | It was made read-only, or writable again. | | |
| 77 | | `repo.deleted`, `repo.restored`, `repo.purged` | It was deleted, restored within its 30 days, or removed for good. | | |
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 78 | | `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`. For an issue g1t's agent filed while at work, `data.author` is g1t and `data.requested_by` is the person it was working for. | |
| Webhooks: every event, to your own addresses, signed and retried | 79 | | `comment.created` | A comment or review on an issue or pull request. | |
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 80 | | `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`. For a change g1t made, `data.author` is g1t and `data.requested_by` is the person who asked for it; `actor` is still whoever caused the event. On a change by g1t, once g1t has worked it out, `data.confidence`: `level` (`high`, `medium` or `low`), `reasons`, `self_reported`, `uncertain_about`, `run_id` and `assessed_at`. See [how sure the agent is](/guides/working-with-g1t/#how-sure-the-agent-is). | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 81 | | `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`. | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 82 | | `review.completed` | g1t reviewed a pull request. `data.verdict`. | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 83 | | `workflow.completed` | A [workflow](/guides/actions/) run finished. `data.workflow`, `data.conclusion`, `data.run_id`, `data.sha`, `data.pull`. | |
| Webhooks: every event, to your own addresses, signed and retried | 84 | | `queue.changed` | The merge queue gained, lost or settled an entry. | |
| 85 | | `session.appended` | An agent's session grew. Busy: choose it only if you need it. | | |
| Agents asked while not at work are woken to answer | 86 | | `agent.asked` | An agent asked the agent on another pull request a question, or handed it work, while that one was not at work; g1t wakes it to answer. | |
| Webhooks: every event, to your own addresses, signed and retried | 87 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 88 | [Managing a repository](/guides/managing-repositories/) says what each of |
| 89 | the `repo.` and `branch.` changes does. | |
| 90 | ||
| Webhooks: every event, to your own addresses, signed and retried | 91 | ## Check the signature |
| 92 | ||
| 93 | Compute the HMAC-SHA256 of the raw body with your secret and compare it to | |
| 94 | `X-G1t-Signature-256` in constant time, before you parse the body. | |
| 95 | ||
| 96 | ```js | |
| 97 | // Node.js | |
| 98 | import { createHmac, timingSafeEqual } from "node:crypto"; | |
| 99 | ||
| 100 | function fromG1t(rawBody, signature, secret) { | |
| 101 | const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex"); | |
| 102 | return signature?.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); | |
| 103 | } | |
| 104 | ``` | |
| 105 | ||
| 106 | ```python | |
| 107 | # Python | |
| 108 | import hashlib, hmac | |
| 109 | ||
| 110 | def from_g1t(raw_body: bytes, signature: str, secret: str) -> bool: | |
| 111 | expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() | |
| 112 | return hmac.compare_digest(signature or "", expected) | |
| 113 | ``` | |
| 114 | ||
| 115 | ## Answering, and retries | |
| 116 | ||
| 117 | Answer with any 2xx within 10 seconds. Do slow work after answering. | |
| 118 | ||
| 119 | A delivery that gets anything else, or no answer, is tried again after 1 | |
| 120 | minute, 5 minutes, 30 minutes, 2 hours and 5 hours: six attempts over | |
| 121 | about seven and a half hours. Then it is marked failed. Pausing or removing | |
| 122 | a webhook stops its retries. | |
| 123 | ||
| 124 | An event is delivered to a webhook once. Use `X-G1t-Delivery`, or the | |
| 125 | event's `id`, to ignore one you have already handled. | |
| 126 | ||
| 127 | ## The delivery log | |
| 128 | ||
| 129 | Each webhook keeps its deliveries for 14 days. Open **Deliveries** to see, | |
| 130 | for each one, the request g1t sent, the status and body your receiver | |
| 131 | answered with, how long it took, and when it will be tried again. | |
| 132 | **Redeliver** sends the same payload again, as a new delivery. | |
| 133 | ||
| 134 | The status dot beside each webhook shows how its latest delivery went: | |
| 135 | green delivered, amber waiting to try again, red failed. | |
| 136 | ||
| 137 | ## Addresses | |
| 138 | ||
| 139 | Webhooks are sent only over HTTPS, to public addresses. Private and local | |
| 140 | addresses (`localhost`, `10.0.0.0/8`, `192.168.0.0/16` and the like) are | |
| 141 | refused. To receive webhooks on your own machine while you build, use a | |
| 142 | tunnel such as Cloudflare Tunnel. | |
| 143 | ||
| 144 | ## From the API | |
| 145 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 146 | The same routes, and the MCP server's `webhook` tool, work for a |
| 147 | repository's webhooks (give `repo`) and a workspace's (give `workspace` | |
| 148 | instead). | |
| Webhooks: every event, to your own addresses, signed and retried | 149 | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 150 | | `webhook` action | Route | |
| Webhooks: every event, to your own addresses, signed and retried | 151 | | --- | --- | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 152 | | `list` | `GET /repos/{owner}/{name}/hooks`, `GET /workspaces/{workspace}/hooks` | |
| 153 | | `create` | `POST …/hooks` with `url`, `events`, `secret` | | |
| 154 | | `update` | `PATCH …/hooks/{id}` with `url`, `events`, `active` | | |
| 155 | | `delete` | `DELETE …/hooks/{id}` | | |
| 156 | | `ping` | `POST …/hooks/{id}/pings` | | |
| 157 | | `list_deliveries` | `GET …/hooks/{id}/deliveries` | | |
| 158 | | `redeliver` | `POST …/hooks/{id}/deliveries/{delivery}/redeliver` | | |
| Webhooks: every event, to your own addresses, signed and retried | 159 | |
| 160 | ```sh | |
| 161 | curl -X POST https://api.g1t.sh/repos/acme/web/hooks \ | |
| 162 | -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \ | |
| 163 | -d '{"url": "https://example.com/g1t", "events": ["pull.merged", "checks.completed"]}' | |
| 164 | ``` |