> ## Documentation Index
> Fetch the complete documentation index at: https://docs.glialhealth.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time push notifications when documents finish processing.

Webhooks let Folio call your server the moment a document reaches a terminal state, eliminating polling. This guide covers registering an endpoint, verifying the signature, handling retries, and replaying events.

## Register a webhook endpoint

<Steps>
  <Step title="Create the endpoint">
    Send a `POST /v1/webhook-endpoints` with the URL you want Folio to call. Optionally restrict which event types are delivered; omitting `events` subscribes to all types.

    ```bash theme={null}
    curl -X POST https://api.glialhealth.com/v1/webhook-endpoints \
      -H "Authorization: Bearer sk_test_..." \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://yourapp.example.com/folio-webhook",
        "events": ["document.completed", "document.partially_completed", "document.failed"]
      }'
    ```

    Response (`201 Created`):

    ```json theme={null}
    {
      "id": "whe_01jaxyz...",
      "object": "webhook_endpoint",
      "url": "https://yourapp.example.com/folio-webhook",
      "events": ["document.completed", "document.partially_completed", "document.failed"],
      "active": true,
      "created_at": "2025-10-14T18:23:00Z",
      "secret_prefix": "whsec_...",
      "secret": "whsec_<shown_once_store_immediately>"
    }
    ```

    <Warning>
      The `secret` is returned **only once** in the creation response. Store it securely (e.g. as an environment variable) — you cannot retrieve it again.
    </Warning>
  </Step>

  <Step title="Verify the signature on every delivery">
    Folio signs each delivery with an `X-Signature` header. The header value has the format:

    ```
    t=<unix_timestamp>,v1=<hex_hmac>
    ```

    The HMAC-SHA256 is computed over `"<timestamp>." + raw_body` (the timestamp as a decimal string, a literal `.`, then the raw request bytes), keyed with your endpoint secret. Always verify this signature before acting on the payload. Use a constant-time comparison to prevent timing attacks.

    <CodeGroup>
      ```python Python theme={null}
      import hashlib
      import hmac
      import os

      WEBHOOK_SECRET = os.environ["FOLIO_WEBHOOK_SECRET"]  # whsec_...

      def verify_folio_signature(raw_body: bytes, signature_header: str) -> bool:
          # Parse t=<timestamp>,v1=<hex> from the header
          parts = dict(item.split("=", 1) for item in signature_header.split(",") if "=" in item)
          timestamp = parts.get("t", "")
          v1 = parts.get("v1", "")
          if not timestamp or not v1:
              return False
          # Recompute HMAC-SHA256 over "<timestamp>." + raw_body
          signed_payload = f"{timestamp}.".encode() + raw_body
          expected = hmac.new(
              WEBHOOK_SECRET.encode(),
              signed_payload,
              hashlib.sha256,
          ).hexdigest()
          return hmac.compare_digest(expected, v1)

      # In your request handler (e.g. Flask):
      # raw_body = request.get_data()
      # sig = request.headers.get("X-Signature", "")
      # if not verify_folio_signature(raw_body, sig):
      #     abort(403, "Invalid signature")
      ```

      ```typescript TypeScript theme={null}
      import * as crypto from "crypto";

      const WEBHOOK_SECRET = process.env.FOLIO_WEBHOOK_SECRET!; // whsec_...

      function verifyFolioSignature(rawBody: Buffer, signatureHeader: string): boolean {
        // Parse t=<timestamp>,v1=<hex> from the header
        const parts: Record<string, string> = {};
        for (const item of signatureHeader.split(",")) {
          const idx = item.indexOf("=");
          if (idx !== -1) parts[item.slice(0, idx)] = item.slice(idx + 1);
        }
        const timestamp = parts["t"] ?? "";
        const v1 = parts["v1"] ?? "";
        if (!timestamp || !v1) return false;
        // Recompute HMAC-SHA256 over "<timestamp>." + rawBody
        const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]);
        const expected = crypto
          .createHmac("sha256", WEBHOOK_SECRET)
          .update(signedPayload)
          .digest("hex");
        // constant-time comparison
        return crypto.timingSafeEqual(
          Buffer.from(expected, "utf8"),
          Buffer.from(v1, "utf8"),
        );
      }
      ```
    </CodeGroup>

    <Note>
      Compute the HMAC over the **raw bytes** of the body before any JSON parsing. Parsing and re-serialising can alter whitespace and break the signature.
    </Note>
  </Step>

  <Step title="Handle the event payload">
    Every delivery is a `POST` with `Content-Type: application/json`. The payload contains the event object:

    ```json theme={null}
    {
      "id": "evt_01jaxyz...",
      "object": "event",
      "type": "document.completed",
      "document_id": "doc_01j9xkqz...",
      "payload": { ... },
      "created_at": "2025-10-14T18:26:44Z",
      "deliveries": []
    }
    ```

    Respond with any `2xx` status as quickly as possible. Offload heavy work to a background queue — Folio considers a delivery successful on any `2xx`.
  </Step>
</Steps>

## Event types

| Type                           | When it fires                                                        |
| ------------------------------ | -------------------------------------------------------------------- |
| `document.completed`           | All fields extracted; confidence thresholds passed.                  |
| `document.partially_completed` | Extraction finished but one or more fields triggered `needs_review`. |
| `document.failed`              | An unrecoverable error occurred during processing.                   |

## Retries and backoff

If your server returns a non-`2xx` response (or times out), Folio retries the delivery with exponential backoff. Each delivery attempt is tracked with a `WebhookDeliverySummary` object containing `status` (`pending` | `succeeded` | `failed` | `dead`), `attempts`, `last_status_code`, and `next_attempt_at`. A delivery is marked `dead` after all retry attempts are exhausted.

## List your endpoints

```bash theme={null}
curl https://api.glialhealth.com/v1/webhook-endpoints \
  -H "Authorization: Bearer sk_test_..."
```

The list response omits the `secret` — only the `secret_prefix` is shown.

## Fetch an event

Use the event ID from any delivery payload to retrieve the full event object, including all delivery attempts:

```bash theme={null}
curl https://api.glialhealth.com/v1/events/evt_01jaxyz... \
  -H "Authorization: Bearer sk_test_..."
```

## Replay an event

Re-deliver an event to all active endpoints — useful for testing or recovering from a missed delivery:

```bash theme={null}
curl -X POST https://api.glialhealth.com/v1/events/evt_01jaxyz.../replay \
  -H "Authorization: Bearer sk_test_..."
```

The replay returns the updated event object with a fresh delivery entry.
