BYO Wall (SDK)

Build a custom paywall interface using the Paperwall SDK.

Install the SDK, subscribe to its state, and render your own paywall. The prebuilt embeds use this same SDK, so nothing in them is unavailable to you.

Use this integration when the paywall must match an existing design system, or when it needs to appear alongside an offer Paperwall does not manage, such as your own subscription.

Install

npm install paperwall

Create the client

Create the client at module scope so that every component shares one instance.

// lib/paperwall.ts
import { initPaperwall } from 'paperwall';

export const pw = initPaperwall({
  mode: 'live',
  siteToken: 'YOUR_SITE_TOKEN',
  articleFinder: {
    selector: 'blog-post',
    postUrls: [/\/posts\/.*/],
  },
});

Do not set wallType. That option tells the loader script which prebuilt bundle to fetch, and the SDK integration does not load one.

Subscribe, then initialise

pw.wallState is a store. Calling sub(callback) registers a listener and returns an unsubscribe function. Subscribe before calling pw.initApp(), or the first state transitions will be missed.

// Svelte
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { pw } from '$lib/paperwall';

export const wallState = writable('@paperwall/loading');

onMount(() => {
  const unsub = pw.wallState.sub((s) => wallState.set(s));
  pw.initApp();
  return unsub;
});
// React
import { useEffect, useState } from 'react';
import { pw } from './lib/paperwall';

export function useWallState() {
  const [wallState, setWallState] = useState('@paperwall/loading');
  useEffect(() => {
    const unsub = pw.wallState.sub(setWallState);
    pw.initApp();
    return unsub;
  }, []);
  return wallState;
}

Wall states

Three states require a rendering decision. The others are intermediate steps and can be handled as "still loading".

State Meaning
@paperwall/loading Initial state, before anything has been resolved.
@paperwall/app_pending Establishing the site session.
@paperwall/authenticating Resolving the reader's identity.
@paperwall/session_pending Creating or fetching the article session.
@paperwall/no_wall Show the article as normal.
@paperwall/show_wall Show your paywall. The reader has not paid for this article.
@paperwall/show_article Reveal the article. The reader has paid.

no_wall is returned whenever the page is not an article, the selector matches no element, the article is below its threshold, or the site is in preview mode and the reader is not a team member. Incomplete configuration therefore results in readers seeing your content rather than an empty wall.

Available methods

initPaperwall returns the following alongside wallState.

  • entities — a store containing article, report, articleSession, balance, flags, currency, and platform. Uses the same sub() interface.
  • getCta() — returns the URL that sends the reader to Paperwall to pay. It includes the article, the session, and a redirect back to the current page.
  • isFree() — returns true when the article currently costs nothing.
  • isPreviewMode() — returns true when the site or article is not yet public.
  • thresholds.whyUnder() and thresholds.whyOver() — return readable explanations of why an article is free or has started charging, for use in wall copy such as "free until it reaches 20 reads".
  • getReadingTime() — returns the article's reading time, falling back to a word count of the element articleFinder located.
  • rateArticle(articleId, sessionId, rating) — submits a rating. Accepted only from a reader who has purchased the article.
  • articleFinder.getEl(), setEl(), and reset() — access the resolved article element directly.
  • resetOnNav() — re-runs detection on client-side navigation.
  • initArticle() — refetches the article session, for use after a reader returns from paying.
  • config — the resolved configuration, including the API and portal URLs.

A minimal wall

if (state === '@paperwall/show_wall') {
  render(`
    <p>${pw.thresholds.whyOver()}</p>
    <a href="${pw.getCta()}">Unlock this article</a>
  `);
}

Single-page applications

Two additional requirements apply.

  1. Call pw.resetOnNav() once at start-up, so that a route change clears the previous article's state. Without it, the first page's result persists.
  2. Ensure articleFinder.selector matches an element that exists when initApp() runs. If the article body mounts later, call pw.articleFinder.setEl() once it does, followed by pw.initArticle().