Guide
Receive webhooks
Subscribe an endpoint to reader events, verify that each delivery genuinely came from Flipbooker, and debug the ones that never arrive.
Before you start
- An API key with the
webhooks:readandwebhooks:writescopes. - A publicly reachable HTTPS endpoint that responds with a 2xx status quickly.
- 1
Choose the events you care about
Nine event types are available. Subscribe only to what you will act on — every extra event is another delivery your endpoint has to absorb.
access_requested,access_granted,access_denied,flipbook_opened,page_dwelled,link_clicked,download_requested,cta_submittedandsession_ended.2026-10-28 — Automated visits are no longer delivered
Visits from email security scanners (for example Microsoft Defender Safe Links), verified bots and automated browsers are now recorded as automated and are not delivered to webhooks or Zapier. This covers every event type. Every delivered event now includes
traffic_class, currently alwayshuman. Automated visits still appear in the audit log and in the audit-log, access-audit and interaction-event CSV exports, withtraffic_classandtraffic_reasoncolumns. This is additive; existing fields are unchanged.2026-10-28 — Access events now include
access_methodWebhooks, CSV exports and the audit log now include
access_methodon access events:magic_linkwhen the reader opened the flipbook from an emailed access link,reader_sessionwhen they were already signed in, andnullotherwise (for example onaccess_requestedand on events recorded before this change). This is additive; existing fields are unchanged.access_grantedis now recorded once per flipbook per reader sign-in, not on every visit. Since 2026-10-09,access_requestedfires only when a reader actually requests an access link. To act on every visit, useflipbook_opened. - 2
Create the subscription
Set a
signing_secretat creation — without one you cannot verify that a delivery came from Flipbooker. Generate a long random string and store it alongside your API key.Narrow the firehose with
filters: restrict to specificbook_ids,tracked_link_idsorrecipient_emails. Every filtered flipbook must belong to your workspace.POST /webhook-subscriptionscurl -X POST https://app.flipbooker.com/api/v1/webhook-subscriptions \ -H "Authorization: Bearer $FLIPBOOKER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "CRM sync", "provider": "generic", "target_url": "https://example.com/hooks/flipbooker", "signing_secret": "whsec_...", "event_types": ["flipbook_opened", "cta_submitted", "session_ended"], "filters": { "book_ids": ["aB3x"] } }'provider must be generic or zapierUse "generic" for your own endpoint. "zapier" is reserved for Zapier's own subscription handshake. - 3
Verify the signature on every delivery
Each delivery carries
X-Webhook-Signature,X-Webhook-TimestampandX-Webhook-Event. The signature is an HMAC-SHA256 of"{timestamp}.{raw body}", keyed with your signing secret.Compute it over the raw request body — do not parse and re-serialise the JSON first, or the bytes will differ and the signature will never match. Compare with a constant-time function, and reject timestamps more than 5 minutes old to block replays.
Express exampleimport crypto from 'node:crypto' // Give this route the RAW body, not a parsed one. app.post('/hooks/flipbooker', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.get('X-Webhook-Signature') ?? '' const timestamp = Number(req.get('X-Webhook-Timestamp') ?? 0) // Reject anything outside Flipbooker's 5 minute tolerance. if (Math.abs(Date.now() / 1000 - timestamp) > 300) { return res.status(400).send('stale') } const expected = crypto .createHmac('sha256', process.env.FLIPBOOKER_WEBHOOK_SECRET) .update(`${timestamp}.${req.body.toString('utf8')}`) .digest('hex') // timingSafeEqual throws on a length mismatch, so check that first. if (signature.length !== expected.length) { return res.status(401).send('bad signature') } const ok = crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex') ) if (!ok) return res.status(401).send('bad signature') // Acknowledge fast, then do the slow work out of band. res.sendStatus(202) queue.push(JSON.parse(req.body)) } ) - 4
Send yourself a test delivery
Trigger a real delivery against the subscription to confirm your endpoint accepts it. A
202means Flipbooker queued it.You can also fire an ad-hoc sample without a subscription using
POST /webhooks, which posts a sample payload to any URL you name.POST /webhook-subscriptions/{subscription}/testcurl -X POST https://app.flipbooker.com/api/v1/webhook-subscriptions/$SUBSCRIPTION_ID/test \ -H "Authorization: Bearer $FLIPBOOKER_API_KEY" - 5
Debug what went wrong
When events stop arriving, read the delivery history before changing anything. It records each attempt with the status your endpoint returned, so you can tell a Flipbooker problem from a your-server problem.
A delivery that never got a response usually means your endpoint took too long — acknowledge within a few seconds and process asynchronously.
GET /webhook-subscriptions/{subscription}/deliveriescurl https://app.flipbooker.com/api/v1/webhook-subscriptions/$SUBSCRIPTION_ID/deliveries \ -H "Authorization: Bearer $FLIPBOOKER_API_KEY"