Re-index automatically when someone pushes to a repository. Every git source gets its own webhook URL and its own secret.
POST http://<your-host>/api/webhooks/git/<source-id>
Setting one up
- Open the git source in the dashboard (Edit on its row).
- The Push webhook box shows the URL and the secret, with copy buttons.
- In the repository settings, add a push webhook with that URL and that secret.
- Push something. The project’s status moves to
queuedwithin a second or two.
The server must be reachable from the git host. For a self-hosted Gitea on the same network that is usually automatic; for GitHub.com you need a public address or a tunnel.
Per provider
GitHub
Settings → Webhooks → Add webhook
| Field | Value |
|---|---|
| Payload URL | the webhook URL |
| Content type | application/json |
| Secret | the secret from the dialog |
| Events | Just the push event |
GitHub signs with X-Hub-Signature-256.
GitLab
Settings → Webhooks
| Field | Value |
|---|---|
| URL | the webhook URL |
| Secret token | the secret from the dialog |
| Trigger | Push events |
GitLab sends the secret itself in X-Gitlab-Token.
Gitea / Forgejo / Codeberg
Settings → Webhooks → Gitea
| Field | Value |
|---|---|
| Target URL | the webhook URL |
| HTTP method | POST |
| POST content type | application/json |
| Secret | the secret from the dialog |
| Trigger | Push events |
Signed with X-Gitea-Signature (newer versions also send X-Hub-Signature-256).
Bitbucket Cloud
Repository settings → Webhooks → Add webhook, triggering on Repository push, with the secret from
the dialog. Signed with X-Hub-Signature.
Confluence Data Center
Not a git host, but the same idea for a Confluence source on Data Center, with its own URL (ADR-0085):
POST http://<your-host>/api/webhooks/confluence/<source-id>
A Confluence source has no webhook until you turn it on; until then every delivery is refused with
401 not_enabled and nothing is stored.
- Edit the Confluence source and choose Turn on in the Confluence webhook box. A secret is generated; Copy URL and Copy secret.
- In Confluence, Administration → Webhooks → Create a webhook: paste the URL and the secret and choose the page events (created, updated, removed, restored, moved).
Confluence signs each delivery with X-Hub-Signature: sha256=<HMAC-SHA256 of the body>; one that does
not match is refused with 401 invalid_signature. Comments, labels, attachments, likes, user and group
changes and blog posts cannot change what is indexed — they are answered 200 and ignored. Anything
else, including permission changes and an event this build does not know, queues a run.
A burst of edits becomes one run, and runs are at least WEBHOOK_MIN_INTERVAL_MINUTES apart (5 by
default): a webhook makes the source look soon, not within seconds. Every answer that is not a
refusal is 200, because Confluence counts anything else as a failure and stops delivering after a run
of them. Once on, the button reads New secret and replaces the secret; Turn off removes it
(DELETE …/webhook-secret), and deliveries are refused again.
It does not see everything — a delivery Confluence gave up on, a page that became unreadable to the account without an event, deliveries Confluence skips for hours after repeated failures — so keep the sync interval on; the next scheduled sync catches what the webhook missed. Confluence Cloud cannot send these without a Forge or Connect app — see Confluence.
What happens on delivery
- The signature is verified against that source’s secret — before anything else happens.
- The branches in the payload are compared with the source’s branch. A push to another branch is ignored.
- A re-index of the project is queued, which syncs every source of that project (not only this one).
Responses: 202 queued · 200 ignored (other branch) · 401 invalid_signature · 404 unknown source.
Rotating the secret
Regenerate in the source dialog issues a new secret and invalidates the old one immediately. Paste
the new one into the repository settings; until you do, deliveries return 401.
Security notes
- This endpoint is the one part of
/api/*that takes neither an account norADMIN_TOKEN— the git host can hold neither. Its entire defence is the signature, which is verified against the raw body with a constant-time comparison before any work is queued. - Each source has its own secret, so a leak affects one source and is rotated on its own.
- The worst a leaked secret allows is triggering re-index runs for that source’s project. No data is exposed by the endpoint.
Troubleshooting
| Symptom | Cause |
|---|---|
401 invalid_signature |
The secret in the repository settings is not the one currently stored. Copy it again, or regenerate and paste the new one |
| Delivery succeeds but nothing indexes | The push was to a different branch than the source’s. Check the branch in the source dialog |
| The git host cannot reach the URL | The server is not reachable from the internet. Use a tunnel, or fall back to a scheduled POST /api/projects/:id/reindex |
404 |
The source was deleted, or it is not a git source |
Confluence: 401 not_enabled |
The source’s webhook is off. Turn on in the Confluence webhook box and paste the new secret into Confluence |
Without webhooks
Any scheduled job can do the same thing through the Admin API:
curl -X POST http://localhost:3444/api/projects/$PROJECT_ID/reindex \
-H "Authorization: Bearer $ADMIN_TOKEN"
ADMIN_TOKEN is the credential for a job like this — it is meant for scripts, and a session cookie is
not something a cron entry can hold. See Admin API.
Because indexing is incremental, running this every few minutes costs almost nothing when nothing changed — and Notion is not exempt so much as different: once verified, it delivers its own push webhook, with the secret running the other direction from the pattern above. See Notion for how it is verified and what that means for sync.