Pular para o conteúdo principal
Developer docs

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:read and webhooks:write scopes.
  • A publicly reachable HTTPS endpoint that responds with a 2xx status quickly.
  1. 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_submitted and session_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 always human. Automated visits still appear in the audit log and in the audit-log, access-audit and interaction-event CSV exports, with traffic_class and traffic_reason columns. This is additive; existing fields are unchanged.

    2026-10-28 — Access events now include access_method

    Webhooks, CSV exports and the audit log now include access_method on access events: magic_link when the reader opened the flipbook from an emailed access link, reader_session when they were already signed in, and null otherwise (for example on access_requested and on events recorded before this change). This is additive; existing fields are unchanged. access_granted is now recorded once per flipbook per reader sign-in, not on every visit. Since 2026-10-09, access_requested fires only when a reader actually requests an access link. To act on every visit, use flipbook_opened.

  2. 2

    Create the subscription

    Set a signing_secret at 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 specific book_ids, tracked_link_ids or recipient_emails. Every filtered flipbook must belong to your workspace.

    POST /webhook-subscriptions
    curl -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 zapier
    Use "generic" for your own endpoint. "zapier" is reserved for Zapier's own subscription handshake.
  3. 3

    Verify the signature on every delivery

    Each delivery carries X-Webhook-Signature, X-Webhook-Timestamp and X-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 example
    import 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. 4

    Send yourself a test delivery

    Trigger a real delivery against the subscription to confirm your endpoint accepts it. A 202 means 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}/test
    curl -X POST https://app.flipbooker.com/api/v1/webhook-subscriptions/$SUBSCRIPTION_ID/test \
      -H "Authorization: Bearer $FLIPBOOKER_API_KEY"
  5. 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}/deliveries
    curl https://app.flipbooker.com/api/v1/webhook-subscriptions/$SUBSCRIPTION_ID/deliveries \
      -H "Authorization: Bearer $FLIPBOOKER_API_KEY"

Where to next