paddle-checkout-web
Add a Paddle Checkout to a Next.js web app — overlay or inline, with event handling, customer pre-fill, and dynamic line item updates.
- Installs
- 2.3K
- Last 24h
- +24
- Trending rank
- #5028
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://developer.paddle.com --skill paddle-checkout-web- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads paddle-checkout-web automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Build a Paddle Checkout in Next.js
When to use this skill
Use this skill when adding a hosted Paddle checkout to a Next.js (App Router) app, either as an overlay (modal-style) or inline (embedded within your page). It covers initialization, opening checkout with line items, listening to events, customer pre-fill, the post-checkout redirect, and the throttled updateItems pattern for cart-like UIs.
This skill is client-side only. Pair it with:
webhooksto receivetransaction.completed/subscription.createdevents on your server.subscription-syncto mirror Paddle subscription state into your database.sandbox-testingto test the integration end-to-end in the Paddle sandbox.
Prerequisites
You need:
- A Paddle account, with the sandbox environment active during development.
- Sign up for sandbox: https://sandbox-login.paddle.com/signup
- Sign up for live: https://vendors.paddle.com/
- At least one product and price set up. Use
catalog-setupif you haven't yet — note the price ID (looks likepri_01h...) for use below. - Your domain approved for checkout. In sandbox, checkout opens on any page, including
localhost, and domains you add are approved automatically. In live, the user submits domains under Paddle > My account > Settings > Website approval — there's no API to submit one. If a Paddle MCP server is available, check approval withclient.checkoutDomains.list()(GET /checkout-domains): each domain has astatus, and onlyapproveddomains can run checkout. - Your default payment link set. Use a page on your own domain, not
localhost. In live, its host must be a domain approved for the account, or that domain withwww.. Sandbox doesn't need an approved domain. If a sandbox update returnscheckout_url_domain_not_approved, add the domain under Paddle > My account > Settings > Website approval, where sandbox approves it straight away, and try again. If a Paddle MCP server is available, read it withclient.settings.checkoutUrl.get()(GET /settings/checkout-url) —default_urlisnulluntil set. After asking the user, set it withclient.settings.checkoutUrl.update({ default_url: "https://..." })(PATCH /settings/checkout-url, needs thecheckout_settings.writepermission). If that's unavailable or returnsforbidden, ask the user to set it under Paddle > Checkout > Checkout configuration > Default payment link. - Two environment variables. Create the client-side token under Paddle > My account > Settings > Authentication, on the Client-side tokens tab:
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=test_... # client-side token, safe to expose
NEXT_PUBLIC_PADDLE_ENV=sandbox # or "production"
If a Paddle MCP server is available to you, call client.clientTokens.create({ name: "Frontend dev token" }) inside an execute to provision the token programmatically rather than asking the user to copy it from the dashboard. Note clientTokens is camelCase. Creating a token needs the client_token.write permission.
The Paddle MCP exposes three tools per server (
search,execute,report_missing_tool). Workflow: callsearchto confirm the exact method name and parameter shapes, then callexecutewith an async function that callsclient.<resource>.<operation>(...). Method paths are camelCase (client.clientTokens.create,client.pricingPreview.preview). Body params and response fields are snake_case (tax_category,product_id,unit_price,currency_code). List methods return{ pagination: { hasMore, estimatedTotal }, <resource>: [...] }, notdata. The array's key is the resource's API path segment, so it's kebab-case where the method path is camelCase:(await client.products.list()).products,(await client.clientTokens.list())["client-tokens"]. Page with{ after: "<last_id>" }whilepagination.hasMoreis true, not.next(). Lists that aren't paginated, likeclient.currencies.list(), return a plain array. Single-entity methods return the entity. Chain multi-step workflows inside oneexecute; variables don't persist between calls. Hard caps: 50 API calls per execute, 30s timeout, and 32,000 characters of code.
Install the client library:
npm install @paddle/paddle-js
The inline example below throttles updateItems with lodash.throttle. If you use that pattern, install it and its types too:
npm install lodash.throttle
npm install -D @types/lodash.throttle
The Node SDK (@paddle/paddle-node-sdk) is for server-side work — you do not need it for checkout.
Choose your checkout style
| Style | When to use | What the user sees |
|---|---|---|
| Overlay | Fastest to integrate. Opens over the current page. | Modal-style window that covers your page |
| Inline | When you want full control over the surrounding layout and branding. | Checkout fields render inside a <div> you control |
Both use the same Paddle.Checkout.open() call — the only difference is the displayMode setting and where it renders. Default to overlay unless you need branded inline. Overlay works without any layout changes; inline requires a target element.
Choose your checkout variant
| Variant | Description |
|---|---|
| One-page | A single-page checkout experience with all fields (customer and payment details) on the same screen. |
| Multi-page | A two-page checkout: customer details are collected on the first page, payment details on the second page. |
| Express | An inline checkout for express wallets (Apple Pay, Google Pay). |
Multi-page is the default. One-page is recommended for most use cases.
Express (variant: "express") has extra rules:
- Inline only (
displayMode: "inline"). - Checkout won't open if you combine it with an authenticated customer (
customerAuthToken),upsell,allowedPaymentMethods, or a business on the transaction. showAddDiscounts,allowDiscountRemoval,showAddTaxId, andallowLogoutare ignored.- Other payment methods show alongside the wallets unless you set
showNonExpressPaymentMethods: false(defaults totrue). - The types need
@paddle/paddle-js1.6.4+ forexpress, and 1.6.5+ forshowNonExpressPaymentMethods.
Overlay checkout — the minimum viable integration
This is the fastest path: a button that opens checkout for one price. Recommended.
// app/buy/page.tsx
"use client";
import { initializePaddle, type Paddle } from "@paddle/paddle-js";
import { useEffect, useState } from "react";
export default function BuyPage() {
const [paddle, setPaddle] = useState<Paddle | null>(null);
useEffect(() => {
if (
!process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN ||
!process.env.NEXT_PUBLIC_PADDLE_ENV
) {
return;
}
initializePaddle({
token: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
environment: process.env.NEXT_PUBLIC_PADDLE_ENV as
| "sandbox"
| "production",
}).then((p) => p && setPaddle(p));
}, []);
function openCheckout() {
paddle?.Checkout.open({
items: [{ priceId: "pri_01h...", quantity: 1 }],
settings: {
variant: "one-page", // or "multi-page"
},
});
}
return (
<button onClick={openCheckout} disabled={!paddle}>
Buy now
</button>
);
}
That's it for overlay — no displayMode setting needed (overlay is the default). The user is shown the modal, completes payment, and Paddle handles the success page.
Inline checkout with full event handling
Inline checkout is what you want when you need to render checkout next to other UI (e.g. a price summary, branded layout, custom success state). The full pattern:
// app/checkout/[priceId]/checkout-contents.tsx
"use client";
import {
type Environments,
initializePaddle,
type Paddle,
} from "@paddle/paddle-js";
import type { CheckoutEventsData } from "@paddle/paddle-js/types/checkout/events";
import throttle from "lodash.throttle";
import { useParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
interface Props {
userEmail?: string;
}
export function CheckoutContents({ userEmail }: Props) {
const { priceId } = useParams<{ priceId: string }>();
const [quantity, setQuantity] = useState(1);
const [paddle, setPaddle] = useState<Paddle | null>(null);
const [checkoutData, setCheckoutData] = useState<CheckoutEventsData | null>(
null,
);
// Throttle updateItems to avoid hammering Paddle on rapid quantity changes.
const updateItems = useCallback(
throttle((paddle: Paddle, priceId: string, quantity: number) => {
paddle.Checkout.updateItems([{ priceId, quantity }]);
}, 1000),
[],
);
useEffect(() => {
if (paddle?.Initialized) return;
if (!process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN) return;
initializePaddle({
token: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
environment: process.env.NEXT_PUBLIC_PADDLE_ENV as Environments,
eventCallback: (event) => {
if (event.name && event.data) {
// Every data event, checkout.loaded included, carries the latest totals.
setCheckoutData(event.data);
} else if (event.type === "checkout.warning") {
// Warnings have no `name`: checkout opened, but some values were dropped.
console.warn("[Paddle]", event.detail);
} else if (event.name) {
// checkout.error / checkout.payment.error: flat { name, type, code, detail, documentation_url }.
console.error("[Paddle]", event.name, event.code, event.detail);
}
},
checkout: {
settings: {
variant: "one-page",
displayMode: "inline",
theme: "dark",
allowLogout: !userEmail,
frameTarget: "paddle-checkout-frame",
frameInitialHeight: 450,
frameStyle:
"width: 100%; background-color: transparent; border: none",
successUrl: `${window.location.origin}/checkout/success`, // must be absolute
},
},
}).then((p) => {
if (p && priceId) {
setPaddle(p);
p.Checkout.open({
...(userEmail && { customer: { email: userEmail } }),
items: [{ priceId, quantity }],
});
}
});
}, [paddle?.Initialized, priceId, userEmail]);
useEffect(() => {
if (paddle?.Initialized && priceId) {
updateItems(paddle, priceId, quantity);
}
}, [paddle, priceId, quantity, updateItems]);
return (
<div>
<PriceSection
checkoutData={checkoutData}
quantity={quantity}
onQuantityChange={setQuantity}
/>
{/* The class name here MUST match `frameTarget` above. */}
<div className="paddle-checkout-frame" />
</div>
);
}
Three things to notice:
frameTargetis a CSS class name (no leading dot). Whatever you set here, you must also render an element with that exact class. Paddle injects the iframe into it.eventCallbackfires for every event — use theevent.nameto discriminate (checkout.loaded,checkout.items.updated,checkout.completed,checkout.payment.failed, etc.). The example keeps the latest event data so a sibling component can show price totals. Taking it from every data event,checkout.loadedincluded, means the totals are right before the first update. Error and warning events have a different shape — see "Reading checkout state" below.successUrlis where Paddle redirects after checkout completes. It must be an absolutehttp(s)://URL, so build it fromwindow.location.origin.Checkout.open()throws on a relative URL, and a relative URL passed toinitializePaddleis dropped: checkout emitscheckout.warningand doesn't redirect. Make this a route that handles post-purchase UX (e.g. shows an order confirmation) — but do not rely on it for provisioning; the webhook is the source of truth.
Customer pre-fill
If your user is already authenticated, pass their email so they don't need to type it:
paddle.Checkout.open({
customer: { email: "jane@example.com" },
items: [{ priceId: "pri_01h...", quantity: 1 }],
});
To go further, pass an existing Paddle customer ID (created via the API or returned by a previous checkout):
paddle.Checkout.open({
customer: { id: "ctm_01h..." },
items: [{ priceId: "pri_01h...", quantity: 1 }],
});
Setting allowLogout: false in the checkout settings prevents the user from signing out of their pre-filled session — useful when you've already authenticated them.
Dynamic line items
To change quantity, swap a price, or add another item without closing the checkout, call Paddle.Checkout.updateItems(). It replaces the whole list — any item you leave out is removed:
paddle.Checkout.updateItems([
{ priceId: "pri_01h...", quantity: 3 },
{ priceId: "pri_02h...", quantity: 1 },
]);
- Recurring items must share the same billing cycle and trial period.
- An empty list, or any item with
quantitybelow 1, is ignored. To remove an item, send the list without it. - Check the list against these rules before you call it.
Always throttle these calls (1 second is a good default — see the lodash.throttle example above). Paddle re-renders the checkout on each call, and unthrottled updates produce a flicker and rate-limit risk.
Reading checkout state
Hook into the eventCallback to drive your own UI (running totals, line item breakdowns, applied discounts). The most useful events:
| Event | Fires when | Common use |
|---|---|---|
checkout.loaded | Checkout finishes initial render | Hide a loading spinner, set the initial totals |
checkout.items.updated | Line items change (incl. via updateItems) | Update a sibling price summary |
checkout.customer.created | New customer is created during checkout | Capture the new customer.id |
checkout.payment.selected | User picks a payment method | Conditionally show region-specific copy |
checkout.completed | Customer completes checkout — the payment may still be processing | Show a thank-you state (grant access from webhooks) |
checkout.error | Something went wrong opening checkout | Surface a fallback (mailto, support link) |
checkout.payment.failed | A payment attempt failed (card decline, 3DS, wallet) | Show retry guidance — never the raw error |
checkout.payment.error | No payment methods are available (code: "no_payment_methods_available") | Surface a fallback (mailto, support link) |
checkout.warning | Checkout opened, but some prefill or settings values were dropped | Log it in development to catch bad input |
Error events (checkout.error, checkout.payment.error) have no data. Their name, type, code, detail, and documentation_url are top-level fields on the event (since @paddle/paddle-js 1.6.1). checkout.warning has no name at all, so match it with event.type === "checkout.warning".
Full list at developer.paddle.com/paddle-js/events.
Post-checkout: redirect vs webhook
When checkout completes, two things happen in parallel:
- Checkout fires
checkout.completedand sends the user's browser to yoursuccessUrl(or your custom event handler). For some payment methods the payment is still processing at this point. - Paddle fires a
transaction.completed(and possiblysubscription.created) webhook to your server.
Provisioning belongs in the webhook, not the redirect or checkout.completed. The redirect is for UX (showing "Thanks for your order"); the webhook is the durable, retried, signed event you can trust. See webhooks for setup.
Common pitfalls
- Something went wrong message — typically means there's no default payment link, or, in live, the domain isn't approved for checkout. With a Paddle MCP server, check both:
client.settings.checkoutUrl.get()(GET /settings/checkout-url) should return adefault_url, and in liveclient.checkoutDomains.list()(GET /checkout-domains) should list that URL's domain asapproved. Without MCP, ask the user to check Paddle > Checkout > Checkout configuration and Paddle > My account > Settings > Website approval. Paddle is not defined— you forgot to awaitinitializePaddle()before callingPaddle.Checkout.open(). The promise resolves with thePaddleobject; only then can you open checkout.- Checkout doesn't render inline — your
frameTargetclass name doesn't match a rendered element, or the element isn't in the DOM yet whenCheckout.open()runs. Render the target first; open in auseEffect. - Sandbox vs production drift —
NEXT_PUBLIC_PADDLE_ENVcontrols which environment Paddle.js talks to. If your client token is for sandbox but you setNEXT_PUBLIC_PADDLE_ENV=production, checkout will fail to load. Tokens and prices are environment-scoped — sandboxpri_...IDs don't exist in production. - Pasting price IDs into client code — fine for a quick prototype, but for a real app load price IDs from the server (after fetching the catalog) so you can swap them without a redeploy.
- Using the redirect or
checkout.completedfor provisioning —checkout.completedcan fire while the payment is still processing, and users close tabs, lose connections, or block redirects. Webhooks are the source of truth. - Relative
successUrl—Checkout.open()throws, and one set throughinitializePaddleis dropped with only acheckout.warningto show for it. Use`${window.location.origin}/checkout/success`. - Throttling
updateItems— without throttling, rapid quantity changes flicker and may rate-limit. 1 second is a sensible default. - Calling
initializePaddletwice — the second call doesn't initialize again; it callsPaddle.Update()with the new options, so they replace the ones you set first. Guard withpaddle?.Initialized(as in the example) or use a singleton pattern, and pass per-checkout settings toPaddle.Checkout.open({ settings }).
Verify the integration
- Run
npm run devand navigate to your checkout page. - Confirm the checkout loads or the iframe renders and the price matches what you expect.
- Use a Paddle sandbox test card — e.g.
4242 4242 4242 4242with any future expiry and any 3-digit CVC. - Complete the purchase and confirm:
- The browser lands on your
successUrl. - The
checkout.completedevent fired (console.logfromeventCallback). - In the Paddle dashboard (sandbox), the transaction shows under Transactions.
- The browser lands on your
- To test the webhook side, see
sandbox-testingfor the simulator.
Related docs
- Paddle.js overview
- Build an overlay checkout
- Build a branded inline checkout
- Checkout events reference
Paddle.Checkout.openreference- Default payment link & domain approval
- Reference implementation: paddle-nextjs-starter-kit — see
src/components/checkout/checkout-contents.tsx.
FAQ
What does the paddle-checkout-web skill do?
Add a Paddle Checkout to a Next.js web app — overlay or inline, with event handling, customer pre-fill, and dynamic line item updates.
How do I install paddle-checkout-web?
Run npx skills add https://developer.paddle.com --skill paddle-checkout-web in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/paddle-checkout-web/ (personal) or .claude/skills/paddle-checkout-web/ (project).
Which AI agents can use paddle-checkout-web?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is paddle-checkout-web safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains paddle-checkout-web?
It is published by developer.paddle.com in developer.paddle.com. It has 2,259 recorded installs.
More from developer.paddle.com
paddle-sandbox-testing
developer.paddle.com
Test a Paddle integration end-to-end using the sandbox environment, test cards, the webhook simulator, and local tunnels — without taking real money.
paddle-webhooks
developer.paddle.com
Receive and verify Paddle webhooks in a Next.js Route Handler — signature verification, idempotency, retry semantics, and local testing.
paddle-catalog-setup
developer.paddle.com
Create the Paddle products and prices that other Paddle skills depend on — try MCP tools first, fall back to a Node SDK seed script, or dictate dashboard steps as a last resort.
paddle-subscription-sync
developer.paddle.com
Mirror Paddle subscription and customer state into your database via webhooks — schema, upsert pattern, status semantics, scheduled changes, and access gating.
paddle-billing-history
developer.paddle.com
Render the authenticated user's billing history in Next.js — listing transactions via the Paddle Node SDK, the mandatory customer-id filter, pagination via .next()/.hasMore, status filtering, on-demand invoice PDF links, and formatting raw transaction totals (lowest-unit conversion + Intl.NumberFormat, with the zero-decimal currency special case for JPY/KRW/VND/CLP).
paddle-pricing-pages
developer.paddle.com
Render country-localized prices on a Next.js pricing page using Paddle.js PricePreview — country detection, billing frequency toggle, and currency formatting.
Popular frontend & design skills
setup-matt-pocock-skills
mattpocock/skills
Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
frontend-design
anthropics/skills
Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.
hyperframes-animation
heygen-com/hyperframes
All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.
prototype
mattpocock/skills
Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
hyperframes-core
heygen-com/hyperframes
The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.
vercel-react-best-practices
vercel-labs/agent-skills
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.