ChargeKit JavaScript SDK

Your first paywall can be a few lines.

Paste the SDK, add your product public key, and let ChargeKit handle login, Stripe checkout, and access checks.

Your First Paywall

Start here. This is the whole flow: the user enters an email, gets a login link, pays through your Stripe Payment Link, and loads protected content after access is confirmed.

1. Create a product

Add your Stripe Payment Link in ChargeKit.

2. Copy the public key

It starts with your product public key and is safe in the browser.

3. Paste this snippet

Replace the key, adjust the HTML, and ship the first paid version.

<div id="loading">Checking access...</div>

<form id="login">
  <input id="email" type="email" placeholder="you@example.com" required />
  <button id="login-button">Send me a login link</button>
</form>

<div id="paywall">
  <p>This part is paid.</p>
  <button id="pay-button">Unlock with Stripe</button>
</div>

<main id="premium">
  <h1>Welcome in</h1>
  <div id="paid-content">Loading paid content...</div>
</main>

<p id="error"></p>

<script type="module">
  import { ChargeKit } from "/vendor/chargekit.js";

  const ck = ChargeKit("pk_live_your_product_public_key");

  ck.onAccessGranted(async (user) => {
    const response = await fetch("/api/paid-content", {
      headers: { Authorization: `Bearer ${user.accessToken}` },
    });

    document.querySelector("#paid-content").innerHTML =
      await response.text();
  });

  await ck.gateContent({
    content: "#premium",
    loadingIndicator: "#loading",
    loginPrompt: "#login",
    paymentPrompt: "#paywall",
    errorDisplay: "#error",
    emailInput: "#email",
    loginTrigger: "#login-button",
    paymentTrigger: "#pay-button",
    loginAction: "magicLink",
  });
</script>

You can make it prettier later

The protected content is fetched after access is granted, so it is not sitting in the initial HTML. For browser-safe content, you can render directly inside the premium area. For sensitive content, serve it from your backend after verifying the access token.

// Example: your app's /api/paid-content route.
export async function GET(request) {
  const accessToken = request.headers
    .get("authorization")
    ?.replace("Bearer ", "");

  const check = await fetch("https://api.your-chargekit-domain.com/api/verify-access", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      product_id: "pk_live_your_product_public_key",
      access_token: accessToken,
    }),
  });

  const access = await check.json();

  if (!access.valid) {
    return new Response("Payment required", { status: 402 });
  }

  return new Response("<h2>Your paid lesson</h2><p>Protected content...</p>", {
    headers: { "Content-Type": "text/html; charset=utf-8" },
  });
}

Install The SDK

The SDK is a browser ES module. For the simplest implementation, copy/sdk/chargekit.jsinto your app's public assets, for example/vendor/chargekit.js. You can download it from the ChargeKit dashboard. If you publish the SDK to your own CDN, replace that import path with the CDN URL.

<script type="module">
  import { ChargeKit } from "/vendor/chargekit.js";

  const chargekit = ChargeKit("pk_live_your_product_public_key");

  await chargekit.ready();
  const user = await chargekit.getUser("customer@example.com");

  if (user.verified && user.hasAccess) {
    // Show paid content.
  }
</script>

The first argument is the product public key from the ChargeKit dashboard. It is safe to expose in frontend code. Never expose Stripe API keys or webhook signing secrets in the browser.

Stripe And ChargeKit Setup

The required Stripe setup is intentionally small. You own the Stripe account and manage products directly in Stripe. ChargeKit only needs the Payment Link URL and the webhook signing secret Stripe creates for your ChargeKit endpoint. That secret only verifies webhook messages; it cannot charge cards or manage your Stripe account.

Stripe Dashboard:
1. Create Product and Price.
2. Go to Products > Payments > Payment Links.
3. Create a Payment Link for that Price and copy the buy.stripe.com URL.
4. Set the Payment Link confirmation page to redirect back to your app.
5. Create a webhook endpoint pointing to the ChargeKit dashboard domain, not your product website:
   https://your-chargekit-domain.com/api/webhook
6. Listen for these events:
   checkout.session.completed
   customer.subscription.created
   customer.subscription.updated
   customer.subscription.deleted
7. Copy the webhook signing secret that starts with whsec_.

ChargeKit Dashboard:
1. Create a product.
2. Add allowed origins, for example https://yourapp.com.
3. Paste the Stripe Payment Link URL into each plan.
4. Paste the Stripe webhook signing secret.
5. Copy the product public key for the SDK.

Important checkout rule

Do not send end users directly to the raw Stripe Payment Link from your app. Start checkout withopenCheckoutso ChargeKit can attach the reference that the webhook uses to grant access.

How It Works

ChargeKit sits between your app and your Stripe Payment Links. The SDK checks whether an email has access, starts login links, opens checkout, and updates your UI when access changes. Stripe remains the source of truth for billing.

  1. Create a Stripe Payment Link in your own Stripe Dashboard.
  2. Register that Payment Link and webhook signing secret in ChargeKit.
  3. Your app loads the SDK with the ChargeKit product public key.
  4. The SDK sends users through login, checkout, and access checks.
  5. Stripe webhooks update ChargeKit access state after payment or subscription changes.

Best fit

Use ChargeKit when the real question is "will people pay?" and you do not want to build billing first.

Full UI State Example

gateContent is the fastest path once you know which elements you want to control. You provide normal HTML for loading, login, payment, errors, logout, and the premium container. The SDK shows the right element as the user moves through the flow. Keep sensitive paid content on your server and fetch it after access is granted.

<div id="loading">Checking access...</div>

<form id="login">
  <input id="email" type="email" placeholder="you@example.com" required />
  <button id="login-button">Send login link</button>
</form>

<div id="paywall">
  <p>Your account is verified, but does not have access yet.</p>
  <button id="pay-button">Choose a plan</button>
</div>

<main id="premium">
  <h1>Premium content</h1>
  <div id="paid-content">Loading paid content...</div>
  <button id="logout-button">Log out</button>
</main>

<p id="error"></p>

<script type="module">
  import { ChargeKit } from "/vendor/chargekit.js";

  const ck = ChargeKit("pk_live_your_product_public_key");

  ck.onAccessGranted(async (user) => {
    const response = await fetch("/api/paid-content", {
      headers: { Authorization: `Bearer ${user.accessToken}` },
    });

    document.querySelector("#paid-content").innerHTML =
      await response.text();
  });

  await ck.gateContent({
    content: "#premium",
    loadingIndicator: "#loading",
    loginPrompt: "#login",
    paymentPrompt: "#paywall",
    errorDisplay: "#error",
    emailInput: "#email",
    loginTrigger: "#login-button",
    paymentTrigger: "#pay-button",
    // Optional. Omit this to use the first active plan.
    // planId: "your_chargekit_plan_id_or_stripe_price_id",
    logoutTrigger: "#logout-button",
    loginAction: "magicLink",
  });
</script>

The returned controls includecheckAccess(email),logout(), andcleanup() for single-page apps that mount and unmount screens.

Open Checkout

Use getPlans to list active plans configured for the product, then callopenCheckout with the plan ID and email.

import { ChargeKit } from "/vendor/chargekit.js";

const ck = ChargeKit("pk_live_your_product_public_key");

const email = document.querySelector("#email").value;
const plans = await ck.getPlans();

// Always start checkout through ChargeKit, not by linking directly to Stripe.
// ChargeKit appends the reference Stripe needs to send webhook access updates.
await ck.openCheckout(plans[0].id, email);

In Bring Your Own Stripe mode, checkout redirects to your Stripe Payment Link. ChargeKit appends client_reference_idand prefilled_email before redirecting.

Credit Packs

Credit packs use the same Stripe Payment Link flow. In the dashboard, set a plan toGrants: Credits, choose a one-time Payment Link, and enter how many credits that purchase adds.

import { ChargeKit } from "/vendor/chargekit.js";

const ck = ChargeKit("pk_live_your_product_public_key");

const email = "customer@example.com";
const plans = await ck.getPlans();
const creditPack = plans.find((plan) => plan.entitlement_type === "credits");

await ck.openCheckout(creditPack.id, email);

// After Stripe confirms payment, ChargeKit adds the credits.
const user = await ck.getUser(email);
console.log(user.creditsBalance);

For expensive work, consume credits from your backend before running the action. ChargeKit checks the signed access token, updates the balance, and writes a credit ledger event.

// Example: your app's /api/generate route.
export async function POST(request) {
  const accessToken = request.headers
    .get("authorization")
    ?.replace("Bearer ", "");

  const consume = await fetch("https://api.your-chargekit-domain.com/api/consume-credits", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      product_id: "pk_live_your_product_public_key",
      email: "customer@example.com",
      access_token: accessToken,
      amount: 1,
      action: "generate",
      idempotency_key: "generate_request_123"
    }),
  });

  if (!consume.ok) {
    return new Response("Not enough credits", { status: 402 });
  }

  // Run the expensive AI call here.
  return Response.json({ result: "Generated output" });
}

Credit checks belong near the expensive action

Browser UI can show the balance, but the real consume call should happen in the route that performs the AI call, export, generation, or other paid work.

Account Button

For a compact implementation, attach ChargeKit to one existing button. It will send login links, reflect checking state, and show the logged-in email when a session exists.

<button id="account-button">Log in</button>

<script type="module">
  import { ChargeKit } from "/vendor/chargekit.js";

  const ck = ChargeKit("pk_live_your_product_public_key");

  const controls = ck.createAuthButton("#account-button", {
    textLoggedOut: "Log in",
    textLoggedIn: "{{EMAIL}}",
  });

  window.addEventListener("beforeunload", () => controls.cleanup());
</script>

Manual State And Events

Use the low-level methods when you already have your own modal, router, or account page. The SDK emits state changes and access events so your UI can stay framework-native.

const ck = ChargeKit("pk_live_your_product_public_key");

ck.onStateChange((state) => {
  console.log("ChargeKit state:", state.status, state.email);
});

ck.onAccessGranted((user) => {
  enablePremiumFeatures(user);
});

ck.onAccessDenied(({ email }) => {
  showUpgradePrompt(email);
});

const user = await ck.getUser("customer@example.com");

if (user.verified && user.hasAccess) {
  enablePremiumFeatures(user);
}

Common state values arechecking,has_access,no_access,logged_out, anderror.

React And Next.js Example

In React or Next.js, load the SDK from a client component because it reads browser storage and attaches browser event listeners. Keep the product public key in frontend config, not in a secret environment variable.

"use client";

import { useEffect, useState } from "react";

export default function PremiumArea() {
  const [status, setStatus] = useState("checking");

  useEffect(() => {
    let cleanup = () => {};

    async function start() {
      const { ChargeKit } = await import("/vendor/chargekit.js");
      const ck = ChargeKit("pk_live_your_product_public_key");

      await ck.ready();
      cleanup = ck.onStateChange((state) => setStatus(state.status));
      await ck.checkCurrentUserAccess();
    }

    start().catch(() => setStatus("error"));

    return () => cleanup();
  }, []);

  if (status === "checking") return <p>Checking access...</p>;
  if (status !== "has_access") return <Paywall />;

  return <PremiumContent />;
}

Free Trials

If trials are enabled for a product, callopenTrial(email). The user still verifies ownership of the email through a magic link before trial access is activated.

const ck = ChargeKit("pk_live_your_product_public_key");

await ck.openTrial("customer@example.com");

// The user receives a login link. Access is granted after verification
// if the product still has trial capacity and the account is eligible.

Security Model

SDK gating is useful for product UI, premium screens, extensions, and content that can safely live in the browser. It is not a DRM layer. If your app returns sensitive data from your own server, enforce access on that server before returning the data. Send the SDK access token to your backend and verify it with ChargeKit before serving protected API responses.

POST https://api.your-chargekit-domain.com/api/verify-access
Content-Type: application/json

{
  "product_id": "pk_live_your_product_public_key",
  "access_token": "ACCESS_TOKEN_FROM_SDK",
  "email": "customer@example.com"
}
  • The product public key can be public.
  • End users must verify email ownership through a signed magic-link session before access is returned.
  • Use /api/verify-access from your backend for protected server data.
  • Stripe API keys must never be sent to ChargeKit or exposed to the browser.
  • Webhook signing secrets are stored server-side in ChargeKit product settings.
  • A webhook signing secret only verifies Stripe webhook messages; it cannot charge cards, issue refunds, or manage your Stripe account.
  • Allowed origins should include every production and development origin that can call the SDK API.
  • Payment and subscription changes should come from Stripe webhooks, not client-side trust.

Troubleshooting

Checkout opens, but access is not granted.

Confirm checkout was started throughopenCheckout, then confirm the Stripe webhook endpoint is enabled and its signing secret is saved on the ChargeKit product.

The browser request is blocked.

Add your app origin to the product's allowed origins. Include protocol and port, for examplehttp://localhost:3000during development andhttps://yourapp.comin production.

Magic link email is not received.

Check the email provider configuration for the ChargeKit deployment and inspect spam filtering. The SDK can only report that the server accepted the login-link request.

Usage reporting returns an error.

Usage reporting is not supported in the current Bring Your Own Stripe mode. Use fixed recurring or one-time Stripe Payment Links.

The free tier stops new access.

The default business model allows the first 20 paying users free across your products. You need an active ChargeKit subscription after that limit.