Developer API
EveryPage API
Upload PDFs and get trackable share links programmatically — the same engine behind our integrations. Authenticate with an API key from your account settings.
Prefer a machine-readable spec? Grab the OpenAPI definition (OpenAPI 3.0).
Authentication
Pass your key as a bearer token. Keys are scoped to your account and respect your plan's upload limits.
Authorization: Bearer ep_live_xxxxxxxx
Every endpoint is guarded by a scope. Personal API keys (ep_live_…) hold every scope, including ones we add later; OAuth access tokens (ep_at_…) are narrowed to what the user granted.
files:read— list files, file details, QR codes, list link variants.files:write— upload, import, settings, content replacement, delete, create/update/delete link variants.readership:read— the events feed, gate responses, per-file readership reports.webhooks:manage— create, list, test, and delete webhooks.profile— your account and plan (GET /api/v1/user).
Upload a PDF
POST /api/v1/files — multipart form, field file. Returns the file's UUID and its short ID; the share link is https://everypage.co/<shortId> (the UUID form works too).
curl -X POST https://everypage.co/api/v1/files \ -H "Authorization: Bearer ep_live_xxxxxxxx" \ -F "[email protected]" # {"uuid":"550e8400-…","shortId":"Ab3xY9kQ2mZ7","message":"PDF uploaded successfully"}
POST /api/v1/files/import — JSON body {"url", "filename"}. Creates a file by server-side download instead of an upload. The URL must be plain HTTPS on our import allowlist (design-tool export URLs, e.g. Canva exports — not arbitrary hosts), PDF only, capped at your plan's size limit. The response may include an advisory palette of hex swatches extracted from the document.
curl -X POST https://everypage.co/api/v1/files/import \
-H "Authorization: Bearer ep_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"url": "https://export.canva.com/…/design.pdf", "filename": "brochure.pdf"}'
# {"uuid":"550e8400-…","shortId":"Ab3xY9kQ2mZ7","shareUrl":"https://everypage.co/Ab3xY9kQ2mZ7"}Configure a file
PUT /api/v1/files/<uuid>/settings — JSON body. Set the viewer mode, a password, the download permission, the expiry, and everything below. Omitted fields keep their current value.
curl -X PUT https://everypage.co/api/v1/files/<uuid>/settings \
-H "Authorization: Bearer ep_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"viewerMode": "flipbook",
"password": "hunter2",
"allowDownload": false,
"neverExpire": true,
"viewLimit": 25,
"pageRange": {"from": 1, "to": 10},
"viewerSettings": {
"background": {"type": "gradient", "gradient": "dusk"},
"brand": {"accentColor": "#e63b2e"}
}
}'viewerMode:standard,flipbook,swipe, ormagazine.password: non-empty to protect (Basic+),""to clear;allowDownload:falseto disable downloads.requireEmail: gate viewing behind an email (Pro).notifyOnView: email you when someone opens it (Basic+).- Expiry:
"deleteAt": "<ISO 8601>"for a fixed expiry (capped by plan: free 7 days, Basic 365 days), or"neverExpire": truefor a permanent link (Pro). Omit both to keep the current expiry. viewLimit: self-destruct after this many views (Basic+);0clears the limit. A burned link keeps its row and readership; only the bytes are destroyed — see content replacement below for the un-burn path.pageRange:{"from": 1, "to": 10}limits non-owner viewers to that page window, trimmed server-side (Pro to set; files up to 100MB).{"from": 0, "to": 0}clears it on any plan. Enforcement deliberately survives a downgrade; owners always get the full document.gateFields: a lead-capture form definition shown instead of the plain email prompt (Pro;nullor[]clears on any plan; a non-empty form switchesrequireEmailon).gateDomains: restrict the email gate to listed domains (Pro).askReceipt: ask viewers to confirm receipt (Basic+).commentsEnabled(+commentsAllowPublic,notifyOnComment): reader feedback notes (Pro).marksEnabled/annotationsEnabled: proofing (Pro).watermark: stamp viewer pages (Pro). Watermarked downloads are limited to files up to 100MB.slug: a custom link name on your custom domain (Pro) — lowercase letters, digits, and hyphens;""to clear. Returns409if another of your files already uses it.- Writes to an already-dead link — expired, or burned by its view limit — answer
410. Extend the expiry before it lapses, not after.
Viewer appearance (viewerSettings)
viewerSettings is a JSON object of appearance and behavior blocks. Any deviation from the defaults requires Basic+; the fields marked Pro require Pro. Unknown fields are dropped, out-of-range numbers are clamped, and the gates are re-checked when pages are served — a downgraded plan's Pro styling stops rendering for viewers immediately.
background:typesolid(+color#RRGGBB),gradient(preset:dusk sand slate forest blush mono), orimage(+assetId,fitcover|contain,blur0–20,dim0–80).logo:assetId,positiontl|tr|bl|br,sizes|m|l|xl,linkUrl,hideBadge. Asset IDs come from the file's uploaded assets.page:shadow0–3,rounded0–3,edges,coverAlone.flip:speedMs200–1200 (default 450),sound,rtl,layoutadaptive|single|double.swipe:autoAdvancewithintervalMs3000–30000 (default 6000).protect:contextMenu,print,select(Basic+);blurOnLeave(Pro). Soft deterrents, not DRM.ga4Id: your Google Analytics 4 measurement ID,G-XXXXXXXXXX(Pro).brand:accentColor#RRGGBB,toolbarThemelight|dark|auto,badgePositiontl|tr|bl|br (Pro).cta: a call-to-action button —label(1–40 chars) and an httpsurlrequired,stylesolid|outline,color,positiontl|tr|bl|br|bar (Pro).watermarkStyle:opacity0.05–0.5 (default 0.12),densitysparse|normal|dense (Pro; thewatermarksetting above turns the watermark itself on).
Replace content (dynamic links)
POST /api/v1/files/<uuid>/content — multipart, field file (Pro). Swaps the PDF behind a share link in place: the UUID, short ID, slug, QR code, settings, and all readership history survive — only the bytes change. Each replacement increments contentVersion, and superseded versions appear as revisions on the file detail.
curl -X POST https://everypage.co/api/v1/files/<uuid>/content \ -H "Authorization: Bearer ep_live_xxxxxxxx" \ -F "[email protected]" # {"uuid":"550e8400-…","contentVersion":2,"originalName":"proposal-v2.pdf", # "size":88123,"anchorsCleared":true,"clearedHotspots":2,"clearedNotes":0, # "message":"Content replaced"}
- Page-anchored hotspots and notes are cleared by default (the new pages don't line up with the old ones); pass
?clearAnchors=falseto keep them. Document-level notes always survive. - Fires a
content.replacedwebhook event carrying the newcontentVersion. - Burned files (view limit reached) ARE replaceable — content replacement is the un-burn path, and the link comes back to life. Expired links answer
410.
Link variants (per-recipient links)
Mint child links off one file — one per recipient — each with its own short ID and URL (Pro; up to 200 per file). Viewers arriving through a variant are attributed to it in readership and in file.viewed/file.downloaded webhook payloads.
GET /api/v1/files/<uuid>/variants— list (revoked and redacted included)POST /api/v1/files/<uuid>/variants— createPUT /api/v1/files/<uuid>/variants/<variantUuid>— update label/overrides, revoke/un-revokeDELETE /api/v1/files/<uuid>/variants/<variantUuid>— delete; add?redact=1to GDPR-redact instead
curl -X POST https://everypage.co/api/v1/files/<uuid>/variants \
-H "Authorization: Bearer ep_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"label": "Jane at Acme", "overrides": {"allowDownload": false}}'
# 201 {"uuid":"7c9e6679-…","shortId":"Xy7Qm2aB9kZ3",
# "url":"https://everypage.co/Xy7Qm2aB9kZ3","label":"Jane at Acme",
# "overrides":{"allowDownload":false},"revoked":false,"createdAt":"…"}label(up to 200 chars) is the recipient tag — the only personal field on a variant.overridesrefine the file's settings for this variant's viewers:allowDownloadandpageRange({"from":0,"to":0}serves the full document even when the file has a range). On update,overridesREPLACES the stored set as a whole object — never a per-key merge — so send every override you want to keep;{}clears them all.- Revoking (
"revoked": true) makes the variant's link answer 404 to viewers, reversibly, without revealing that a canonical link exists. ?redact=1erases the label for good — from the variant, analytics, and stored webhook payloads — while the link and its analytics keep working. That's the compliance path for GDPR erasure requests; a redacted label can never be set again (409).- Passwords and viewing gates are per-file, not per-variant: a viewer who passed the gate through one link has passed it for all of the file's links.
Other endpoints
GET /api/v1/files— list your filesGET /api/v1/files/<uuid>— file details (includes the replacementrevisionshistory)GET /api/v1/files/<uuid>/qr-code— QR code (PNG) for the share linkDELETE /api/v1/files/<uuid>— delete a fileGET /api/v1/user— your account + plan
A file object (returned by list and detail) looks like:
{
"uuid": "550e8400-…",
"shortId": "Ab3xY9kQ2mZ7", // short share link; absent on older files
"slug": "2026-pricing", // custom link name; absent unless set
"originalName": "proposal.pdf",
"size": 91234,
"mimeType": "application/pdf",
"createdAt": "2026-06-15T14:00:00Z",
"deleteAt": "2026-06-16T14:00:00Z", // absent if it never expires
"viewCount": 12,
"viewerMode": "flipbook",
"protected": true,
"allowDownload": false,
"requireEmail": false,
"notifyOnView": true,
"contentVersion": 1 // increments on content replacement
}Readership report
GET /api/v1/files/<uuid>/readership — the same aggregates the readership dashboard shows (scope readership:read). The response is shaped by your plan: a tier field (free, basic, pro) says which sections are present; sections above your tier are omitted entirely.
- Every plan:
summary— total views, sessions, unique visitors, unique countries, average read time, downloads — plus thefileobject. - Basic adds: median read time, completion rate, and re-read share in the summary, plus
viewsOverTime, the read-throughfunnel, andreferrers/browsers/sources/viewerModes/peakTimes/devices/countriesbreakdowns. - Pro adds: live
activeReaders, new-vs-returning session counts, recentsessionswith reader identities and captured emails,hotspotTaps, CTA clicks (ctaTaps), the per-variant breakdown, andcontactscaptured by the lead-form gate. - When a page range is active the response carries
effectivePages— the shared window's size — so funnels over partial documents read honestly.
Read events (for automations)
GET /api/v1/events serves three polling streams selected by ?type= — view (the default: finished read sessions), download (explicit saves), and gate (completed lead-form gates; Pro). The basis for "new read" automations like the Zapier trigger.
curl "https://everypage.co/api/v1/events?type=view&since=0" \
-H "Authorization: Bearer ep_live_xxxxxxxx"
# [{"id":42,"fileUuid":"550e8400-…","fileName":"deck.pdf",
# "readAt":"2026-06-15T14:00:00Z","country":"United Kingdom",
# "pagesViewed":7,"timeMs":48000,"type":"view"}]- Each event has a stable
id. Cursors are per-stream: everytypehas its own id sequence, so asincevalue from one stream is meaningless in another — never mix them. - Without a cursor (
since=0) you get the newest events first — a peek window. Withsince=<id>results ascend from the cursor, so polling with the highest id you've seen walks the stream forward without gaps or duplicates. ?limit=up to 100 (default 50).?file=filters to one file (UUID or short ID; unknown or foreign identifiers return[], never an error).- The
downloadstream includes your own downloads — unlike thefile.downloadedwebhook, which only fires for non-owners. gateevents carry the submitted form infields.GET /api/v1/gate-responsesis a resource-path alias of?type=gate— same rows, cursor, and Pro gate.
Webhooks
Prefer push over polling? Register an HTTPS endpoint and we POST events to it as they happen. Webhooks are available on every plan — only gate.completed deliveries additionally require Pro at event time, because their payload carries captured lead data. Up to 25 webhooks per account; your own actions on your own files never fire them.
GET /api/v1/webhooks— list (never includes secrets)POST /api/v1/webhooks— createDELETE /api/v1/webhooks/<uuid>— deletePOST /api/v1/webhooks/<uuid>/test— synchronous test delivery (10/min per token)
curl -X POST https://everypage.co/api/v1/webhooks \
-H "Authorization: Bearer ep_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/everypage",
"events": ["file.viewed", "gate.completed"]}'
# 201 {"webhook":{"uuid":"…","url":"…","events":["file.viewed","gate.completed"],
# "format":"json","active":true,"createdAt":"…"},
# "secret":"whsec_9f2c…"} <- shown exactly once; store it nowformat:json(default, signed) orslack(Slack Block Kit for incoming webhooks — unsigned; Slack authenticates by URL).fileUuid(optional, UUID or short ID of a file you own) scopes delivery to that one file; omit it for account-wide delivery.
Event kinds
file.viewed— a non-owner read session ended. Extras:pagesViewed,timeMs.file.downloaded— an explicit save-to-disk by a non-owner. No extras.gate.completed— a viewer passed the email/lead-form gate. Extras:source,email,fields(Pro delivery).note.created— a viewer left a public feedback note. Extras:author,body,pageNumber(when page-anchored).receipt.confirmed— a fresh "mark as received". Extra:name.file.burned— the view limit destroyed the file's bytes. No extras.content.replaced— the document behind the link was swapped. Extra:contentVersion.invite.viewed— an email invitee opened the document for the first time. Extras:email,name.proofing.updated— a viewer's first page mark or annotation on a document. Extras:kind,pageNumber,viewer.
Every json delivery is an envelope whose data always carries fileUuid and fileName plus the kind's extras. Sessions and downloads that arrived through a link variant additionally carry variantUuid and variantLabel:
POST <your url>
Content-Type: application/json
X-Everypage-Signature: t=1750000000,v1=5257a869e7…
{
"event": "file.viewed",
"timestamp": "2026-06-15T14:03:22Z",
"data": {
"fileUuid": "550e8400-…",
"fileName": "deck.pdf",
"pagesViewed": 7,
"timeMs": 48000
}
}Verifying signatures
The X-Everypage-Signature header is t=<unix time>,v1=<hex> where v1 is HMAC-SHA256 over the string <t>.<raw body>. The HMAC key is the FULL secret string exactly as shown at creation — the whsec_ prefix is part of the key, not a wrapper to strip. Verify against the raw request bytes (before any JSON parsing), and reject stale timestamps (we suggest ±5 minutes) to prevent replays.
const crypto = require('crypto')
// secret is the full "whsec_…" string; rawBody is the unparsed request body
function verifyEverypage(secret, rawBody, header) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const expected = crypto.createHmac('sha256', secret)
.update(parts.t + '.' + rawBody).digest('hex')
return parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1, 'hex'), Buffer.from(expected, 'hex')) &&
Math.abs(Date.now() / 1000 - Number(parts.t)) < 300
}- Delivery: any 2xx from your endpoint counts as delivered; the request times out after 10 seconds. Failures retry up to 5 attempts with 1 minute / 10 minutes / 1 hour backoff.
- After 20 consecutive exhausted deliveries the webhook is switched off automatically and you get one email. Re-create it (or use
/test) once your endpoint is healthy. - Deliveries carry no delivery ID and retries send the same body, so dedupe best-effort on
(event, timestamp, data)if double-processing matters to you. - The signing secret is shown once, at creation, and is never retrievable again — delete and re-create the webhook if you lose it.
OAuth 2.0 (for apps)
Building an app that acts for other users? Use OAuth 2.0 (Authorization Code + PKCE) instead of asking them for an API key. Register your app with us to get a client_id (and a secret for server-side apps).
- Send users to
GET /oauth/authorizewithresponse_type=code,client_id,redirect_uri,scope,state, and a PKCEcode_challenge(S256). - After they approve, we redirect back with
?code=. Exchange it atPOST /oauth/token(withcode_verifier) for an access + refresh token. - Call
/api/v1withAuthorization: Bearer <access token>. Refresh with therefresh_tokengrant (tokens rotate); revoke atPOST /oauth/revoke. - Scopes:
files:read,files:write,readership:read,webhooks:manage,profile(see the scope table above). Users manage authorized apps under their account.
QR codes & embedding
GET /api/v1/files/<uuid>/qr-code— a PNG QR code for the share link (scopefiles:read). The encoded link is durable — the short ID or UUID, never the renameable slug — and carries?via=qrso scans show up in the readership source breakdown.GET /api/files/<uuid>/qr-code— the same PNG, public and unauthenticated (rate-limited to 60 requests/minute per IP), for embedding a QR without exposing a token.GET /oembed?url=<share link>— public oEmbed provider endpoint: turns any pasted share link (UUID, short ID, or slug form) into an embeddable rich object.
Client libraries & examples
- everypage-node — the official TypeScript/JavaScript client.
- webhook-examples — working signature verification in Node, Python, PHP, and Go.
Errors
Errors come back as the HTTP status with a short plain-text message:
400— bad request (e.g. the file isn't a PDF, or a malformed body)401— missing, malformed, invalid, or revoked API key403— not allowed: you don't own the file, or the setting is above your plan (e.g. password on Free)404— no such file (or not owned by you)409— conflict: the slug is already used by another of your files, or a redacted variant's label was re-set410— the link is dead: expired, or burned by its view limit (settings writes; content replacement 410s only on expiry — burned links replace fine)413— the file is larger than your plan allows429— rate limit exceeded (120 requests per minute per key; webhook tests 10 per minute)502— an import's export source couldn't be fetched or returned an error
Notes
- PDF first: Word (.docx) uploads are converted to PDF where conversion is enabled; everything else is rejected. Max file size follows your plan (20MB free, 200MB Basic, 2GB Pro).
- Rate limit: 120 requests per minute per key (per token, not per IP — integrations may share an egress IP).
- Revoke a key anytime from account settings — it stops working immediately.
- Readership analytics on API-uploaded files work exactly as they do in the app.
- Webhooks work on every plan; the plan only gates
gate.completeddeliveries.