# Guide to GitBook Webhooks: Features and Best Practices

GitBook hosts product documentation, API references and internal knowledge bases, edited in a block editor or synced from Git. Its Webhook integration sends events out of GitBook when content changes or readers interact with a published site, which lets you rebuild a search index after an edit, stream page views into your own analytics, or route reader feedback to the team that owns the page.

Here we look at where the GitBook integration is installed, the three events it sends, how to verify `X-GitBook-Signature`, and what GitBook's short retry schedule means for your receiver.

## What are GitBook webhooks?

Webhooks in GitBook are an integration rather than a workspace setting. You install the Webhook integration in your organisation, then into individual spaces or sites, and give each installation a webhook URL and a selection of event types.

Where you install it decides what you can subscribe to. A space installation reports content changes. A site installation reports reader activity: page views and page feedback. Every event carries the `installationId` it came from, so one receiver can serve several installations.

## GitBook webhook features

| Feature | Details |
| --- | --- |
| Signature | hex HMAC-SHA256 |
| Signature header | `x-gitbook-signature` |
| Body | JSON |
| Event routing | Body field `type` |

GitBook's [Webhook integration guide](https://gitbook.com/docs/developers/integrations/guides/webhook) also documents:

* A signature header of `t=<unix>,v1=<hex>`, with the HMAC taken over the timestamp, a dot and the body.
* Up to 3 retries with exponential backoff and jitter from a 1-second base, for network errors, `5xx` and `429`.
* No retry for any other `4xx`.

It does not state a response timeout, where the signing secret is shown, or any source IP addresses.

## Events and payloads

| Event | Meaning |
| --- | --- |
| `space_content_updated` | Triggered when content in a space is modified. |
| `site_view` | Triggered when a user visits a page on your GitBook site. |
| `page_feedback` | Triggered when users provide feedback on pages. |

All three bodies share `eventId`, `type` and `installationId`. A content update adds `spaceId` and `revisionId`, which you can pass to GitBook's API to fetch what changed. The two site events add `siteId`, a `visitor` object, the page `url` and the `referrer`, and `page_feedback` adds `pageId`, `spaceId` and a `feedback` object.

This is the `page_feedback` sample from the [webhook samples library](https://github.com/hookdeck/webhook-samples/blob/main/providers/gitbook/latest/page_feedback.json):

```json
{
  "eventId": "evt_3456789012cdefgh",
  "type": "page_feedback",
  "siteId": "site_abc123",
  "spaceId": "space_xyz789",
  "installationId": "inst_def456",
  "pageId": "page_feedback123",
  "feedback": {
    "rating": "good",
    "comment": "This page was very helpful!"
  },
  "visitor": {
    "anonymousId": "anon_789ghi",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "ip": "192.168.1.101",
    "cookies": {}
  },
  "url": "https://docs.example.com/api-reference",
  "referrer": "https://docs.example.com/getting-started"
}

```

`visitor` includes the reader's IP address, user agent and cookies, so `site_view` and `page_feedback` events contain personal data. Decide what you store before you start collecting them.

## Setting up GitBook webhooks

1. Install the **Webhook** integration in your GitBook organisation.
2. Install it into each space or site you want events from.
3. Set the **Webhook URL**, for example `https://example.com/webhooks/gitbook`.
4. Select the event types. Spaces offer content updates. Sites offer site views and page feedback.
5. Find the signing secret for the installation and store it in your server's configuration. GitBook's samples read it from `GITBOOK_SECRET`. The guide doesn't say where the secret appears, so check the installation's configuration screen.

For Hookdeck, create a source with the GitBook type, use the installation's secret as its verification secret, and enter the source URL as the webhook URL. The [Hookdeck CLI](/docs/cli) then forwards events to a local server:

```bash
npx hookdeck-cli listen 3000 gitbook --path /webhooks/gitbook

```

Editing a page in a space, or visiting the site, produces a test event.

## Securing GitBook webhooks

Split `X-GitBook-Signature` on commas to get `t` and `v1`. Concatenate the `t` string, a dot and the raw body, compute a hex HMAC-SHA256 with the installation's secret, and compare it with `v1` in constant time. GitBook's [verification example](https://gitbook.com/docs/developers/integrations/guides/webhook#hmac-signature-verification) uses the same `timestamp.payload` construction.

### Node.js verification

```javascript
const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-gitbook-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const parts = signature.split(',').map((part) => part.trim());
  const times = parts.filter((part) => part.startsWith('t=')).map((part) => part.slice(2));
  if (times.length !== 1 || !/^[0-9]+$/.test(times[0])) return false;
  const message = Buffer.concat([Buffer.from(`${times[0]}.`, 'utf8'), rawBody]);
  const candidates = parts.filter((part) => part.startsWith('v1=')).map((part) => part.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

The verifier sets no age limit on `t`. If you add one for a direct receiver, keep it generous enough to cover GitBook's retries.

The handler stores each event under its `eventId` and only then responds, so a retry finds the row already there:

```javascript
const express = require('express');

const app = express();

const HANDLED_TYPES = new Set(['space_content_updated', 'site_view', 'page_feedback']);

app.post('/webhooks/gitbook', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.GITBOOK_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!event || typeof event !== 'object' || Array.isArray(event)) return res.sendStatus(400);
  try {
    if (!HANDLED_TYPES.has(event.type)) return res.sendStatus(200);

    const isNew = await recordGitBookEvent(event.eventId, event.installationId, event);
    if (isNew && event.type === 'space_content_updated') {
      await queueReindex(event.spaceId, event.revisionId);
    }
  } catch {
    return res.sendStatus(503);
  }

  res.sendStatus(200);
});

```

`recordGitBookEvent` returns `false` when the `eventId` already exists. GitBook's own example keeps seen IDs in memory for 2 minutes, which only works on a single process. A database table survives restarts and scales across instances.

Return `401` for a bad signature, since GitBook does not retry it, but never return `400` for a temporary problem on your side. A `400` is final, while a `500` or `429` gets a retry.

Hookdeck's GitBook source verifies the same timestamped HMAC without an age check. It replies `200` to GitBook and records a mismatch as failed verification.

> **Make GitBook webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies each GitBook event, keeps it, and retries delivery to your indexer or analytics pipeline for far longer than GitBook's three attempts in about 7 seconds.

## GitBook webhook limitations and pain points

### Retries are over within seconds

**The Problem:** A restart or deploy of your receiver loses events.

**Why It Happens:** GitBook retries up to 3 times, with delays of roughly 1, 2 and 4 seconds. Any outage longer than that exhausts the schedule.

**Workarounds:**

* Run more than one instance of the receiver so a deploy never takes it fully offline.
* For content updates, periodically compare each space's current revision with the last one you processed.

**How Hookdeck Can Help:** Hookdeck accepts the event on GitBook's first attempt and retries your service on a schedule you choose, over minutes or hours.

### Client errors are final

**The Problem:** A transient fault that your framework reports as `400`, `404` or `413` drops the event permanently.

**Why It Happens:** GitBook treats every `4xx` other than `429` as a client error and doesn't retry.

**Workarounds:**

* Return `5xx` or `429` for anything that might succeed later.
* Make sure body size limits and routing are correct before going live.

**How Hookdeck Can Help:** Hookdeck receives GitBook's request regardless of your service's state, and its retry rules for your destination are independent of GitBook's.

### Undocumented secret location

**The Problem:** It isn't obvious where to find the key that verifies signatures.

**Why It Happens:** GitBook's guide shows the `X-GitBook-Signature` format but doesn't say where the integration's secret is set or displayed.

**Workarounds:**

* Look in the Webhook integration's configuration for the space or site.
* Treat each installation as having its own secret until you've confirmed otherwise.

**How Hookdeck Can Help:** With a Hookdeck source per installation, each secret lives on its own source and failed verifications show up in the request log, which makes a wrong secret easy to spot.

### Reader events carry personal data

**The Problem:** `site_view` and `page_feedback` include IP addresses, user agents and cookies.

**Why It Happens:** GitBook sends the visitor object as captured.

**Workarounds:**

* Drop or hash `visitor.ip` and `visitor.cookies` before storage.
* Subscribe to site views only if you have a use for them.

**How Hookdeck Can Help:** A Hookdeck transformation can strip the `visitor` fields before the event reaches your systems.

## Best practices

### Choose status codes deliberately

GitBook's retry decision depends entirely on your status code. Use `200` for success, `401` for a bad signature and `5xx` for anything you'd like to see again.

### Deduplicate durably on `eventId`

Every event has an `eventId`. Store it with a unique constraint, as described in the guide to [implementing webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Keep handlers fast

Reindexing a space or writing to a warehouse takes longer than a webhook should. Queue the work and respond, following the approach in [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Fetch content by revision

`space_content_updated` tells you which revision to look at, not what changed. Fetch the revision from the GitBook API in a worker and compare it with the previous one.

## Conclusion

GitBook's Webhook integration gives you content updates from spaces and view and feedback events from sites, each signed over a timestamp and the body in `X-GitBook-Signature`. Verify against the raw bytes, store by `eventId`, and treat your status codes carefully, because GitBook's three retries end within seconds and a `4xx` ends them at once.

[Hookdeck Event Gateway](https://hookdeck.com) holds every GitBook event until your services can accept it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle GitBook webhooks reliably in minutes.