npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@everypage/node

v1.0.0

Published

Official Node.js client for the EveryPage API - upload PDFs, mint trackable share links, read readership events, and verify signed webhooks.

Readme

@everypage/node

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, and Blob globals 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/webhooks subpath when all you need is signature verification.

§ 01 - Install

Requires Node 18 or newer (for global fetch).

npm install @everypage/node

§ 02 - Quickstart

import { 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). |

§ 03 - Authentication

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 |

§ 04 - Files

// 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);

§ 05 - Link variants (Pro)

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 });

§ 06 - Events

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:

  • since omitted (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.

§ 07 - Webhooks

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).

§ 08 - Errors and rate limits

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 });

§ 09 - Limitations

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 /oembed is 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/import authenticates 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).

§ 10 - Stability

  • 0.x: the SDK surface may change in minor versions until 1.0. From 1.0, SemVer.
  • The /events row 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.

License

MIT - see LICENSE.


EveryPage - secure PDF sharing with reader analytics.

Website - Developers - Docs - Status - [email protected] - LinkedIn