Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EveryPage webhook examples

Verify X-Everypage-Signature in Node, Python, PHP, and Go.

Every verifier in this repo passes the same committed test vectors (fixtures.json) - correctness is pinned, not promised. Copy the verify file for your language into your project and run its test against the fixtures before you trust it.

  • node/ - verify.js, an Express receiver, zero-dependency tests
  • python/ - verify.py, a Flask receiver, zero-dependency tests
  • php/ - verify.php, a plain-PHP receiver, CLI tests
  • go/ - everypage.Verify, an http.Handler, go test

Webhooks are available on every plan. Manage them at everypage.co/developers or via the API.

You may not need to verify by hand. The official Node client (everypage-node) ships constructEvent/verifySignature, and the n8n community node verifies every delivery before the workflow runs. Reach for the examples below when you are receiving webhooks in your own service, or porting to a language we don't ship a client for.

§ 01 · The signature

Every JSON-format delivery carries one header:

X-Everypage-Signature: t=<unix seconds>,v1=<hex>

To verify:

  1. Read the raw body bytes - before any JSON parsing.
  2. Parse the header into t and v1. Ignore pairs you don't recognize - future versions may add them.
  3. Reject if t is more than 300 seconds from your clock, either direction. Retries are re-signed with a fresh t at send time, so the window never rejects a genuine retry.
  4. Compute HMAC-SHA256 over <t> + . + raw body. The key is your signing secret as ASCII bytes. Hex-encode, lowercase.
  5. Compare against v1 in constant time - timingSafeEqual, compare_digest, hash_equals, hmac.Equal. Never ==.

The whole secret string is the key. Your secret looks like whsec_9f2c... - 70 characters, prefix included. The HMAC key is that entire string, whsec_ and all. Stripping the prefix is the most common porting mistake, so the fixtures include a vector (prefix_stripped_key_must_fail) that a prefix-stripping verifier wrongly accepts.

The body is an envelope:

{
  "event": "file.viewed",
  "timestamp": "2026-07-27T12:00:00Z",
  "data": {
    "fileUuid": "0b5e9a1c-2f47-4c8d-9e3a-6d1b8f0c4a72",
    "fileName": "quarterly-report.pdf",
    "pagesViewed": 7,
    "timeMs": 60000
  }
}

event is the kind, timestamp is RFC 3339 UTC, data always carries fileUuid and fileName plus kind-specific extras. When the viewer arrived through a link variant, file.viewed and file.downloaded additionally carry variantUuid and variantLabel - additive keys, absent on canonical-link events.

Slack-format webhooks are unsigned. They post Block Kit to a Slack incoming-webhook URL, which authenticates by URL secrecy. There is nothing to verify - this repo is about JSON-format webhooks only.

§ 02 · Verify one yourself

Don't take our verifiers' word for it. One fixture, plain openssl:

T=1785153600
SECRET='whsec_4f9d8a2b6c1e3f7a0b5d9c8e2f4a6b1d3c7e9f0a2b4d6c8e1f3a5b7d9c0e2f4a'
BODY='{"data":{"fileName":"quarterly-report.pdf","fileUuid":"0b5e9a1c-2f47-4c8d-9e3a-6d1b8f0c4a72","pagesViewed":7,"timeMs":60000},"event":"file.viewed","timestamp":"2026-07-27T12:00:00Z"}'
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET"

Output: e26e23667223756fc8d6fcadef6f75dcbc604da313c70270cf055ed566636b0f - the v1 of the valid case in fixtures.json.

§ 03 · The raw-body gotcha

The signature covers the exact bytes we sent. Parse-then-re-serialize changes key order or whitespace and the signature dies. Every server example here shows the correct pattern:

Stack Raw body
Express express.raw({ type: 'application/json' }) on the route - not express.json()
Flask request.get_data() - not request.json
PHP file_get_contents('php://input') - not $_POST
Go io.ReadAll(r.Body) before any decoding

§ 04 · Delivery behavior

What we promise, and what enforces it:

  • HTTPS only. Plain-HTTP endpoints are rejected at creation and at send.
  • Any 2xx is success. Everything else - other statuses, timeouts, connection failures - is a failed attempt. Respond fast and do slow work async; a delivery attempt times out after 10 seconds.
  • 5 attempts per delivery, backing off 1 minute, 10 minutes, then 1 hour between retries. Each retry is re-signed with a fresh t.
  • Auto-disable at 20 consecutive failures. After 20 exhausted deliveries in a row the endpoint is switched off and the account owner is emailed. Re-enable by fixing your endpoint and creating the webhook again.
  • The secret is shown exactly once, in the create response. There is no rotation endpoint - to rotate, delete the webhook and create a new one, then retire the old secret.

§ 05 · Deduplication - the honest version

There is no delivery id and no idempotency key in the payload. We won't pretend otherwise, and you should not build on a field that doesn't exist.

A retry re-sends the same envelope - same event, same timestamp, same data - with only the signature's t refreshed. So the best available dedupe key is:

(event, timestamp, data.fileUuid)

That collapses retries of one delivery. Two distinct events of the same kind on the same file in the same second would collide - rare, and for most consumers (notifications, counters, CRM sync) collapsing them is acceptable. If your handler must be strictly effect-once, make the effect itself idempotent.

§ 06 · Managing webhooks

Bearer-authenticated, base https://everypage.co. API keys hold every scope; OAuth tokens need webhooks:manage. Full spec: everypage.co/openapi.yaml.

Endpoint What it does
GET /api/v1/webhooks List yours. Never includes secrets.
POST /api/v1/webhooks Create. Returns the secret - once. Up to 10 per account.
DELETE /api/v1/webhooks/{uuid} Delete. Queued deliveries are discarded.
POST /api/v1/webhooks/{uuid}/test Synchronous webhook.test through the real delivery path - same body format, same signature. Rate-limited to 10/minute.

Create takes url, events, optional format (json default, slack), and optional fileUuid to scope delivery to one file.

§ 07 · Event kinds

Webhooks are available on every plan.

Kind Fires when
file.viewed A non-owner view session ended
file.downloaded A non-owner explicitly saved the file
gate.completed An email or lead-form gate was passed (delivery requires Pro - the payload carries captured lead data)
note.created A viewer left a public note
receipt.confirmed A viewer marked the document as received
file.burned The view limit destroyed the document
content.replaced The owner swapped the document's content in place
invite.viewed An email invitee opened the document for the first time
proofing.updated A viewer left their first page mark or annotation
webhook.test You called the test endpoint - not subscribable, test-only

Owner actions never fire events - your own views and downloads are not echoed back at you.

§ 08 · Quickstarts

Each folder is self-contained. The verify file is dependency-free in every language; the server examples name their one framework.

# Node - test needs nothing; the server example needs express
node node/test.js

# Python - test needs nothing; the server example needs flask
python3 python/test_verify.py

# PHP - nothing needed
php php/test.php

# Go
cd go && go test ./...

§ 09 · Fixtures and porting

fixtures.json holds 8 deterministic vectors - genuine, prefix-stripped key, tampered body, wrong secret, stale timestamp, future timestamp, malformed header, unknown extra pair. Each case carries an explicit now so your verifier tests the timestamp window with an injected clock and the fixtures never go stale.

Porting to another language? Write verify, loop over cases, assert verify(secret, body, header, now) == valid. All 8 green means your port handles every failure mode we ship. PRs with new language ports are welcome - they must pass the fixtures, and the verify function must stay dependency-free.

The vectors are generated by tools/genfixtures, a transcription of the production signer - regenerate with go run . from that directory.

License

MIT.


EveryPage - secure PDF sharing with reader analytics.

Website · Developers · Docs · Status · support@everypage.co · LinkedIn

About

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages