Browser CDN — Getting Started
Add a paywall to any webpage without a build step.
Drop in a single <script> tag and the
Ledewire global is ready to use.
Add the script tag
Include the CDN bundle on every page that needs paywall logic. Pin to a major version so you receive non-breaking updates automatically.
<!-- Pin to a major version in production -->
<script src="https://cdn.jsdelivr.net/npm/@ledewire/browser@0/dist/ledewire.min.js"></script>
Initialise the client
Call Ledewire.init() with your API key (available in
the LedeWire merchant dashboard).
const lw = Ledewire.init({
apiKey: 'your_api_key',
// Optional: called when the buyer's session expires
onAuthExpired: () => showLoginModal(),
})
Check checkout state
Call lw.checkout.state(contentId) to determine what
the current visitor needs to do next.
The next_required_action field drives your UI.
const state = await lw.checkout.state('content-id')
switch (state.checkout_state.next_required_action) {
case 'authenticate':
showLoginPrompt()
break
case 'fund_wallet':
showFundWalletPrompt(state.price_cents)
break
case 'purchase':
showPurchaseButton()
break
}
Authenticate the buyer
Use email/password or Google OAuth. On success, tokens are stored
in memory (or localStorage if you prefer persistence).
// Email + password
await lw.auth.loginWithEmail({ email, password })
// Google OAuth (receives the id_token from Google Sign-In)
await lw.auth.loginWithGoogle({ id_token: googleIdToken })
Fund the wallet (Stripe)
LedeWire uses a wallet model. Buyers deposit funds first, then
purchase content. Use createPaymentSession to get a
Stripe client secret, then present the Stripe Elements UI.
const session = await lw.wallet.createPaymentSession({ amount_cents: 1000 })
// session.public_key → Stripe publishable key
// session.client_secret → pass to Stripe Elements / PaymentElement
Purchase and reveal content
Purchases are single-use: once the wallet has sufficient funds,
buying delivers the content directly in the
purchases.create() response — the only place
content_body / content_uri ever appear
for a purchase. There is no route that serves the same delivery
again, so persist it immediately.
// price_cents is the price the buyer was shown; a stale one is refused (422)
const { price_cents } = await lw.checkout.state('content-id')
const purchase = await lw.purchases.create({ content_id: 'content-id', price_cents })
await saveDeliveredContent(purchase) // your own persistence, e.g. IndexedDB
if (purchase.content_body) {
// content_body is always plain UTF-8 text on the wire — never
// base64-encoded — so no decoding step is needed. What it contains
// depends on purchase.content.content_type.
if (purchase.content.content_type === 'markdown') {
renderMarkdown(purchase.content_body)
} else if (purchase.content.content_type === 'html') {
// Inline HTML content is seller-supplied and must be sanitised
// before insertion — never assign it to innerHTML directly.
document.getElementById('article').innerHTML = DOMPurify.sanitize(purchase.content_body)
}
} else if (purchase.content_uri) {
// content_uri points at a remote resource (video, PDF, image, or
// remote HTML). Check the scheme before navigating or linking to it —
// the URI is seller-supplied and a non-http(s) scheme should not be
// followed automatically.
const uri = new URL(purchase.content_uri)
if (['https:', 'http:'].includes(uri.protocol)) {
window.location.href = purchase.content_uri
}
}
lw.purchases.create()
rejects with a SpendCapReachedError (HTTP 402).
Funding the wallet does not clear it — the cap resets at
err.resetsAt, or the buyer can raise it via
lw.user.spendCap.update().
lw.wallet.balance() returns
balance_cents: null, with remaining_cents
and company_name instead, and
next_required_action is never
'fund_wallet'. Their cap is set by a Company admin
(lw.company.members.update()).
license (type, permissions,
document_url, or null when none was
recorded), content.publication and
content.filing (an SEC filing, else null).
lw.purchases.licenseDocument(id) downloads the licence
document as { body: ArrayBuffer, contentType } —
usually a PDF. Offer it as a download, building the Blob with
type: 'application/octet-stream' unless
contentType is application/pdf or
text/markdown, so a server-sent
text/html can never run in your origin; call
URL.revokeObjectURL() after the click. Never render it
inline.
Full example
See examples/browser-cdn/index.html for a complete working page including wallet funding and the full paywall state machine.
Persistent sessions
By default, tokens are stored in memory and lost on page reload.
To keep the buyer logged in across reloads, pass the built-in
localStorageAdapter (imported via ES module, not CDN):
import { init, localStorageAdapter } from '@ledewire/browser'
const lw = init({
apiKey: 'your_api_key',
storage: localStorageAdapter(), // persists to localStorage
})