The official Node.js client for the EveryPage API - upload PDFs, mint trackable share links, read readership events, and verify signed webhooks, from TypeScript or JavaScript.
- Zero runtime dependencies - built on the
fetch,FormData, andBlobglobals Node 18 ships. - Types written by hand against the published OpenAPI document - the API's quirks are encoded, not papered over.
- ESM and CJS, fully typed, with a standalone
@everypage/node/webhookssubpath when all you need is signature verification.
Requires Node 18 or newer (for global fetch).
npm install @everypage/nodeimport { readFile } from "node:fs/promises";
import { EveryPage } from "@everypage/node";
const everypage = new EveryPage(process.env.EVERYPAGE_API_KEY!);
const { shortId } = await everypage.files.upload(await readFile("proposal.pdf"), {
filename: "proposal.pdf",
});
console.log(`Share it: https://everypage.co/${shortId}`);Constructor options - new EveryPage(token, options):
| Option | Default | What it does |
|---|---|---|
baseUrl |
https://everypage.co |
API origin. |
fetch |
global fetch |
Injectable fetch implementation - tests and proxies plug in here. |
maxRetries |
2 |
How many times a 429 is retried (§ 08). |
Two bearer credentials, one header. The SDK sends whichever token you construct it with.
- API keys (
ep_live_...) - create one at everypage.co/account under API keys. Keys hold every scope. - OAuth access tokens (
ep_at_...) - issued to integrations via authorization code + PKCE, limited to their granted scopes. The SDK does not run the OAuth flow itself (§ 09) - hand it the access token you obtained.
| Scope | Grants |
|---|---|
files:read |
See your files and share links |
files:write |
Upload, configure, and delete files |
readership:read |
See who read your documents |
webhooks:manage |
Create, list, test, and delete webhook subscriptions |
profile |
See your account email and plan |
// Upload: Blob, File, Buffer, Uint8Array, ArrayBuffer, or ReadableStream.
const { uuid, shortId } = await everypage.files.upload(buffer, { filename: "deck.pdf" });
// Every file on the account, newest first. No pagination exists upstream -
// this is the whole list, and the SDK does not fake pages.
const files = await everypage.files.list();
const file = await everypage.files.get(uuid);
// Omitted fields keep their current value. Plan-gated fields answer a
// plain-text 403 below the required plan; dead links (expired/burned) 410.
await everypage.files.updateSettings(uuid, {
viewerMode: "flipbook",
password: "hunter2", // Basic+
requireEmail: true, // Pro
neverExpire: true, // Pro
pageRange: { from: 1, to: 5 }, // setting requires Pro; clearing with {from:0,to:0} never does
});
// Server-side import from an allowlisted export URL (e.g. a Canva export).
// The advisory `palette` may be absent - that is normal, never an error.
const imported = await everypage.files.import({ url: exportUrl, filename: "design.pdf" });
// Replace the bytes behind a live link in place (Pro). UUID, short id,
// settings, and readership history all survive.
const { contentVersion } = await everypage.files.replaceContent(uuid, newPdf, {
clearAnchors: false, // keep page-anchored hotspots/notes; default clears them
});
// Readership report - the response shape follows the OWNER'S PLAN. Read the
// `tier` discriminator first; plan-specific sections are simply absent below
// their tier. On Pro, `variants` breaks readership down per link variant.
const report = await everypage.files.readership(uuid);
if (report.tier === "pro") console.log(report.variants);
// PNG QR code for the share link.
const png: ArrayBuffer = await everypage.files.qrCode(uuid);
await everypage.files.delete(uuid);Per-recipient child links off one file: each gets its own short id and URL, an optional recipient label, and optional overrides (allowDownload, pageRange). Viewers arriving through a variant are attributed to it in readership and in webhook payloads. Up to 200 per file. Passwords and viewing gates stay per-FILE by design.
const variant = await everypage.variants.create(uuid, {
label: "Jane at Acme",
overrides: { allowDownload: false },
});
console.log(variant.url); // Jane's personal link
const variants = await everypage.variants.list(uuid); // SDK unwraps {variants:[...]}
// `overrides`, when present, REPLACES the stored set as a whole object -
// send every override you want to keep.
await everypage.variants.update(uuid, variant.uuid, { revoked: true });
// GDPR redaction: erases the label (the only personal field) everywhere,
// while the variant keeps working so analytics stay consistent.
await everypage.variants.delete(uuid, variant.uuid, { redact: true });Three per-account streams selected by type: view (the default), download (explicit save-to-disk fetches - INCLUDING your own, unlike the file.downloaded webhook), and gate (completed viewing-gate forms; Pro plan). GET /api/v1/gate-responses upstream is a thin alias of the gate stream - use type: "gate" here.
Cursors are per-type and never portable across streams - each stream has its own id sequence. And the two since modes sort in opposite directions:
sinceomitted (or 0) - a newest-first DESCENDING peek at the stream.since > 0- events with id greater than the cursor, ASCENDING.
// Peek at recent activity (newest first).
const recent = await everypage.events.list({ type: "view", limit: 20 });
// Poll-from-now: take a cursor, then iterate everything after it.
let cursor = await everypage.events.latestCursor({ type: "view" });
for await (const event of everypage.events.iterate({ type: "view", since: cursor })) {
console.log(event.fileName, event.pagesViewed, event.timeMs);
cursor = event.id; // persist per-type; a view cursor is meaningless for downloads
}iterate() requires an explicit since > 0 and throws otherwise - since=0 is the descending peek, which cannot be paged forward. list() is the raw single page when you want the peek itself.
Filter either call to one file with file (UUID or short id): unknown or foreign identifiers return an empty list, not an error.
Webhooks are available on every plan (up to 10 per account); only gate.completed deliveries additionally require Pro at event time. The signing secret is returned exactly once, at creation - store it immediately, it is never retrievable again.
const { webhook, secret } = await everypage.webhooks.create({
url: "https://example.com/hooks/everypage",
events: ["file.viewed", "file.downloaded"],
fileUuid: uuid, // optional: scope delivery to one file
});
const webhooks = await everypage.webhooks.list(); // SDK unwraps {webhooks:[...]}
await everypage.webhooks.test(webhook.uuid); // real delivery path; 10/min per token
await everypage.webhooks.delete(webhook.uuid);Verifying deliveries. Every json-format delivery carries X-Everypage-Signature: t=<unix>,v1=<hex> - HMAC-SHA256 over <t>.<raw body>, keyed with the full secret string including the whsec_ prefix (stripping the prefix is the classic mistake). Verify against the RAW body bytes, before any JSON middleware touches them:
import express from "express";
import { constructEvent, WebhookVerificationError } from "@everypage/node/webhooks";
const app = express();
app.post("/hooks/everypage", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = constructEvent(req.body, req.header("X-Everypage-Signature") ?? "", process.env.WHSEC!);
} catch (err) {
if (err instanceof WebhookVerificationError) return res.status(400).end();
throw err;
}
console.log(event.event, event.data.fileUuid);
res.status(200).end();
});Both are pinned against the same committed test vectors the standalone
verifiers use - see webhook-examples
for fixtures.json and reference implementations in Python, PHP, and Go.
verifySignature(payload, header, secret) is the boolean form. Both enforce a 300-second replay window on the header timestamp (deliveries and retries are always signed at send time, so a tight window is safe) and compare in constant time. Delivery behavior upstream: 2xx counts as success, 5 attempts (1m/10m/1h backoff), 10s timeout, HTTPS-only, auto-disable after 20 consecutive failures. Deliveries carry no delivery id - if you need dedupe, build a best-effort key from (event, timestamp, data.fileUuid). Slack-format webhooks are unsigned (Slack authenticates by URL).
Every non-2xx response throws EveryPageError { status, message }.
The API returns errors as plain text (even on endpoints that answer JSON on success), so message is the raw response text - the SDK never JSON-parses an error body, and neither should you:
import { EveryPage, EveryPageError } from "@everypage/node";
try {
await everypage.events.list({ type: "gate" });
} catch (err) {
if (err instanceof EveryPageError) {
console.error(err.status, err.message); // 403 "This feature requires the pro plan or higher"
}
}Rate limits: 120 requests/minute per token across the API; the webhook test endpoint is tighter at 10/minute per token. Responses carry no rate-limit headers and no Retry-After - there is nothing to read - so on a 429 the SDK backs off blind: exponential delay with jitter, up to maxRetries times (default 2).
Retries apply to GET and DELETE only by default. The API has no idempotency keys, so a retried POST/PUT could be applied twice - the SDK will not pretend otherwise. Opt a specific write in when a duplicate is acceptable:
await everypage.files.import({ url }, { retryable: true });Honest list of what this SDK does not do:
- No OAuth flow helpers. Authorization code + PKCE, token exchange, and refresh are yours to run; the SDK just sends the bearer token you give it.
- No API-key management. Keys are created and revoked in the account dashboard only.
- No custom domains, folders, contact lists, billing, or trash. These are session-only surfaces of the product, not part of the public API.
- No oEmbed wrapper.
GET /oembedis public and unauthenticated - call it directly. - No browser bundle. Node 18+ only. An API key in a browser is exposed to every visitor; keep it server-side.
files.list()has no pagination because the endpoint has none. Documented, not faked.- No Canva import lane.
POST /api/v1/canva/importauthenticates with a Canva user JWT, not an EveryPage credential - it is not callable with the tokens this SDK holds. - No idempotency keys and no webhook delivery ids - upstream facts, which is why write retries are opt-in (§ 08) and webhook dedupe is best-effort (§ 07).
- 0.x: the SDK surface may change in minor versions until 1.0. From 1.0, SemVer.
- The
/eventsrow shape is append-only-stable upstream: existing fields keep their names and types; new fields are only ever appended. Response types carry index signatures so appended fields surface without an SDK update. - Verified against pinned fixtures: the signature test vectors in
test/fixtures/are reproducible (npm run fixtures) and cross-checked against an independent HMAC implementation.
MIT - see LICENSE.
EveryPage - secure PDF sharing with reader analytics.
Website - Developers - Docs - Status - support@everypage.co - LinkedIn