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 containingarticle,report,articleSession,balance,flags,currency, andplatform. Uses the samesub()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()— returnstruewhen the article currently costs nothing.isPreviewMode()— returnstruewhen the site or article is not yet public.thresholds.whyUnder()andthresholds.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 elementarticleFinderlocated.rateArticle(articleId, sessionId, rating)— submits a rating. Accepted only from a reader who has purchased the article.articleFinder.getEl(),setEl(), andreset()— 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.
- 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. - Ensure
articleFinder.selectormatches an element that exists wheninitApp()runs. If the article body mounts later, callpw.articleFinder.setEl()once it does, followed bypw.initArticle().