Skip to main content
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

1

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.
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):
{
  "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>"
}
The secret is returned only once in the creation response. Store it securely (e.g. as an environment variable) — you cannot retrieve it again.
2

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.
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")
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.
3

Handle the event payload

Every delivery is a POST with Content-Type: application/json. The payload contains the event object:
{
  "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.

Event types

TypeWhen it fires
document.completedAll fields extracted; confidence thresholds passed.
document.partially_completedExtraction finished but one or more fields triggered needs_review.
document.failedAn 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

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:
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:
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.