--- url: https://js-one.klappay.com/getting-started.md --- # Getting started `@klappay/one` is the embeddable payment button for Klappay One. It never proposes a transaction, never signs anything, and never sees a private key or a session token — it just opens a modal (an iframe, on desktop) or a popup (mobile) pointing at Klappay's own hosted identity/wallet flow, and relays the outcome back to your page via `postMessage`. Everything sensitive — OTP, wallet selection, WalletConnect, signing — happens entirely on Klappay's own origin. See [Protocol & security](/protocol) for exactly how that boundary is enforced. ## Install ::: code-group ```bash [npm] npm install @klappay/one ``` ```bash [pnpm] pnpm add @klappay/one ``` ```bash [yarn] yarn add @klappay/one ``` ::: Or via ` ``` Pin to a specific version instead of the `@1` major alias if you need a frozen build — an exact version is also required if you want to add `integrity`/`crossorigin` for Subresource Integrity, since `@1` is a moving target and can't carry a fixed hash: ```html ``` ## A charge, not an amount Every checkout this button opens is tied to a `chargeId` — a `Charge` your own backend already created against Klappay Core (with [`@klappay/node`](https://node-sdk.klappay.com) or [`@klappay/checkout-kit`](https://node-checkout-sdk.klappay.com)). The button never takes an `amount`/`recipient`/`token` directly — the popup/ iframe fetches the real charge data itself once it opens, so nothing about what's being paid ever needs to be trusted from client-side config. See [Examples](/examples) for a full create-charge-then-render-button flow. ```ts // on your own backend — never in the browser import { createClient } from '@klappay/node' const klap = createClient({ apiKey: process.env.KLAP_API_KEY! }) const charge = await klap.charges.create({ amount: 25, currency: 'USD', expiresIn: 3600, acceptedPayments: [{ token: 'USDC', network: 'base' }], }) // charge.id -> hand this to the button below ``` ## Three ways to render the button ### Drop-in Web Component ```html ``` ### Your own button ```html ``` Both are wired up automatically as soon as the script loads — no JavaScript required for either, and both pick up elements added to the DOM later too (a client-side router, an infinite-scroll list, a modal opened after the fact). See [The button](/button) for every attribute. ### Programmatic ```ts import { createKlappayOne } from '@klappay/one' const klappayOne = createKlappayOne({ chargeId: 'ch_123', origin: 'https://klap.one', onSuccess: (result) => { // UX signal only — confirm fulfillment via Klappay Core's webhook, // never from this callback alone. See /protocol. }, onError: (error) => console.error(error), onCancel: () => console.log('payer closed the checkout'), }) klappayOne.open() ``` See [Programmatic API](/programmatic) for the full `KlappayOneConfig` shape, and [React](/react) for `` / `useKlappayOne()`. ## `origin`, one way or another Every entry point needs to know which Klappay origin to open — there's no baked-in default, since sandbox and production point at different hosts. Pass it explicitly on every call, or set it once for the whole page: ```ts import { configure } from '@klappay/one' configure({ origin: 'https://klap.one', locale: 'en' }) ``` `configure()` only affects the two zero-JS entry points (`` and `data-klappay-one`) — `createKlappayOne()` itself still requires `origin` explicitly in its config; the programmatic API has no attribute to fall back to, so there's no reason to make it implicit. ## Where to go next * [`button.md`](/button) — every attribute `` and `data-klappay-one` support, and the `success`/`error`/`cancel` events they dispatch. * [`programmatic.md`](/programmatic) — the full `createKlappayOne()` API. * [`react.md`](/react) — `` and `useKlappayOne()`. * [`frameworks.md`](/frameworks) — Vue, Svelte, and anything else, since the core is a plain Web Component. * [`modes.md`](/modes) — when you get an iframe/modal vs. a popup, and the automatic fallback between them. * [`styling.md`](/styling) — `variant`/`size` and the CSS custom properties that cross the Shadow DOM boundary. * [`errors.md`](/errors) — every error code, where each one comes from. * [`protocol.md`](/protocol) — the `postMessage` wire format, the non-negotiable security invariants, and why `onSuccess` is never proof of payment. * [`examples.md`](/examples) — full create-charge-then-render-button integrations, one per stack. ## For LLMs and agents This site (built from these same files with VitePress) publishes [`llms.txt`](/llms.txt) — a link index of every doc page — and [`llms-full.txt`](/llms-full.txt) — the full content of every doc page concatenated into one plain-text file. Point an agent, RAG pipeline, or MCP server at either as a lightweight way to give it the whole package's documentation without scraping HTML. Both regenerate on every deploy, so they never drift from what's on this page. --- --- url: https://js-one.klappay.com/button.md --- # The button Two zero-JavaScript entry points, both wired up by `core/klappay-one.ts` under the hood — neither is a second implementation, both just build a `KlappayOneConfig` from attributes and call `createKlappayOne(config).open()`. ## `` A real Custom Element (`customElements.define('klappay-button', ...)`), rendered inside a Shadow DOM so neither the host page's CSS nor Klappay's own leaks across the boundary: ```html ``` | Attribute | Required | Description | | --- | --- | --- | | `charge-id` | Yes | The `Charge` this checkout is for. | | `origin` | Only if not [`configure()`'d](/getting-started#origin-one-way-or-another) | Which Klappay origin to open. | | `variant` | No | `white` | `yellow` | `black` — defaults to `black`. See [Styling](/styling). | | `size` | No | `sm` | `md` | `lg` — defaults to `md`. See [Styling](/styling). | | `locale` | No | Forwarded to the checkout — falls back to `configure()`'s `locale`. | | `mode` | No | `iframe` | `popup` — forces a mode instead of the [device default](/modes). | `variant`/`size` are reactive — changing either attribute after the element is already on the page (`el.setAttribute('variant', 'white')`) updates the rendered button immediately, via `attributeChangedCallback`. ### Events ```ts const button = document.querySelector('klappay-button') button.addEventListener('success', (event) => console.log(event.detail)) // PaymentResult button.addEventListener('error', (event) => console.log(event.detail)) // KlappayOneError button.addEventListener('cancel', () => console.log('payer closed the checkout')) ``` A second click before the first checkout settles is ignored — the button disables itself (`this.#button.disabled = true`) the moment it opens the popup/iframe, and re-enables on whichever of `success`/`error`/`cancel` fires first. That's what stops a fast double-click from opening two popups/iframes stacked on top of each other. Missing `charge-id` or `origin` (and no [`configure()`](/getting-started#origin-one-way-or-another) default) logs a `console.error` and does nothing on click — it never throws, so one misconfigured button on a page doesn't take the rest of the page down with it. ## Your own button: `data-klappay-one` For when you already have a button and don't want a second custom element in your markup — any clickable element works, not just ` ``` | Attribute | Required | Description | | --- | --- | --- | | `data-klappay-one` | Yes | The `chargeId` — also what marks the element for auto-wiring. | | `data-klappay-one-origin` | Only if not [`configure()`'d](/getting-started#origin-one-way-or-another) | Which Klappay origin to open. | | `data-klappay-one-locale` | No | Falls back to `configure()`'s `locale`. | | `data-klappay-one-mode` | No | `iframe` | `popup` — forces a mode instead of the [device default](/modes). | Same `success`/`error`/`cancel` `CustomEvent`s as `` above, dispatched on the element itself. There's no `variant`/`size` here — this path renders nothing, it only adds a click handler to markup you already control, so styling is entirely up to your own CSS. While a checkout is in flight the element carries a `data-klappay-one-busy` attribute (added on click, removed on `success`/`error`/`cancel`) — the same double-click guard as ``, just expressed as an attribute instead of the native `disabled` property, since the wired element isn't necessarily a form control. ```css [data-klappay-one][data-klappay-one-busy] { opacity: 0.6; pointer-events: none; } ``` ### Elements added after the script loads Both `wireExisting()` (run once on load) and a `MutationObserver` (`observeNewElements()`, watching `document.body` for the lifetime of the page) wire up `[data-klappay-one]` elements — so a button rendered by a client-side router, injected by a third-party script, or added inside a modal opened later all get wired automatically, with no manual `re-wire()` call needed anywhere in your code. ## Choosing between the two Reach for `` when you want Klappay's own button styling (pick a `variant`/`size` and move on) — it's the fastest path and the one [the button preview in `klap-app`](https://github.com/klappay/klap-one) matches exactly. Reach for `data-klappay-one` when the button already needs to match a design system you don't control from here — your own markup, your own CSS, this package only adds the click handler. --- --- url: https://js-one.klappay.com/programmatic.md --- # Programmatic API For anything the two zero-JS entry points can't express — opening the checkout from your own event handler, deferring `origin` resolution, or building a fully custom trigger element — call `createKlappayOne()` directly. `ui/klappay-button.ts` and `ui/auto-wire.ts` are themselves thin callers into this exact function; there's no separate logic path hiding behind the markup-based entry points. ```ts import { createKlappayOne } from '@klappay/one' const klappayOne = createKlappayOne({ chargeId: 'ch_123', origin: 'https://klap.one', locale: 'en', mode: 'iframe', onReady: () => console.log('checkout loaded'), onSuccess: (result) => { // result: PaymentResult — see below. UX signal only, see /protocol. }, onError: (error) => { // error: KlappayOneError — see /errors. }, onCancel: () => console.log('payer closed the checkout'), }) document.querySelector('#pay-button')!.addEventListener('click', () => { klappayOne.open() }) ``` ## `KlappayOneConfig` ```ts interface KlappayOneConfig { chargeId: string origin: string locale?: string mode?: 'iframe' | 'popup' onReady?: () => void onSuccess?: (result: PaymentResult) => void onError?: (error: KlappayOneError) => void onCancel?: () => void } ``` | Field | Description | | --- | --- | | `chargeId` | Required. The `Charge` this checkout is for — created ahead of time on your backend. | | `origin` | Required. Which Klappay origin to open — no default, since sandbox and production point at different hosts. | | `locale` | Forwarded to the checkout. | | `mode` | `'iframe'` | `'popup'` — forces a mode instead of the [device default](/modes). | | `onReady` | Fires once the popup/iframe signals it has loaded (`klappay:ready`). Useful for hiding a loading spinner over the trigger button. | | `onSuccess` | Fires with a `PaymentResult` once the payer completes payment. **Not proof of payment** — see [Protocol & security](/protocol#onsuccess-is-a-ux-signal-never-proof-of-payment). | | `onError` | Fires with a `KlappayOneError` — see [Errors](/errors) for every code. | | `onCancel` | Fires when the payer closes the popup/iframe without completing payment, by any means — an explicit Cancel button inside the checkout, the browser's native close button, alt-F4, swiping away a mobile popup. | ## `PaymentResult` ```ts interface PaymentResult { txHash: string walletAddress: string network: string amount: string confirmedAt: string } ``` Every field here is public on-chain data by the time this fires — a transaction hash, a wallet address, which network, how much, and when it confirmed. None of it is a secret, and none of it is a substitute for your backend's own webhook-driven fulfillment. ## `open()` ```ts interface KlappayOne { open: () => void } ``` Calling `open()` again while a checkout from a previous `open()` call is still in flight opens a second, independent popup/iframe — `createKlappayOne()` itself does no de-duplication; that's what `ui/klappay-button.ts`'s `disabled` guard and `ui/auto-wire.ts`'s `data-klappay-one-busy` guard exist for on the two markup-based entry points. If you're calling `open()` from your own click handler, guard it the same way: ```ts let busy = false button.addEventListener('click', () => { if (busy) return busy = true klappayOne.open() }) // then clear `busy` in onSuccess/onError/onCancel ``` Each call to `open()` generates its own `requestId` (`crypto.randomUUID()`) internally — that's what lets the bridge tell which `open()` call a given `postMessage` response belongs to, so more than one `createKlappayOne()` instance can safely coexist on the same page (e.g. one button per line item in a cart). See [Protocol & security](/protocol) for the full wire format. ## `configure()` / `getGlobalConfig()` ```ts import { configure, getGlobalConfig } from '@klappay/one' configure({ origin: 'https://klap.one', locale: 'en' }) getGlobalConfig() // -> { origin: 'https://klap.one', locale: 'en' } ``` Sets the page-wide default `origin`/`locale` that `` and `data-klappay-one` fall back to when they don't carry their own `origin`/`locale` attribute. `createKlappayOne()` itself never reads this — the programmatic API always requires `origin` explicitly, since it has no natural attribute to fall back to and no excuse to make a required field implicit. --- --- url: https://js-one.klappay.com/react.md --- # React `@klappay/one/react` is a thin wrapper over `core/createKlappayOne` — it doesn't reimplement anything, it just registers the same `` Custom Element used elsewhere and gives it idiomatic React props/events. `react` is a peer dependency (`>=18`, optional) — nothing here loads unless you actually `import` from the `/react` subpath. ## `` ```tsx import { KlappayButton } from '@klappay/one/react' function Checkout({ chargeId }: { chargeId: string }) { return ( { // UX signal only — see /protocol. console.log('paid', result.txHash) }} onError={(error) => console.error(error.code, error.message)} onCancel={() => console.log('payer closed the checkout')} /> ) } ``` ```ts interface KlappayButtonProps { chargeId: string origin?: string locale?: string variant?: KlappayButtonVariant size?: KlappayButtonSize onSuccess?: (result: PaymentResult) => void onError?: (error: KlappayOneError) => void onCancel?: () => void } ``` Under the hood, `` renders the real `` element (via a `ref`) and attaches/detaches native `success`/`error`/`cancel` event listeners in a `useEffect` — the same events documented in [The button](/button), just handed to you as props instead of `addEventListener` calls. `origin` is optional here only because it can come from [`configure()`](/getting-started#origin-one-way-or-another) instead; set one or the other before rendering. ## `useKlappayOne()` For a fully custom trigger — your own button, a menu item, a keyboard shortcut — skip the rendered element entirely and drive `open()` yourself: ```tsx import { useKlappayOne } from '@klappay/one/react' function BuyButton({ chargeId }: { chargeId: string }) { const klappayOne = useKlappayOne({ chargeId, origin: 'https://klap.one', onSuccess: (result) => console.log('paid', result.txHash), }) return } ``` `useKlappayOne()` keeps your latest `config` in a `ref` and always calls `createKlappayOne()` fresh on `open()` — so passing a new inline `onSuccess`/`onError` on every render (as in the example above) is fine and doesn't need `useCallback`; it never creates a stale closure over an old `chargeId` the way storing a single `createKlappayOne()` instance in `useState` on mount would. ## TypeScript: the `` JSX intrinsic Importing from `@klappay/one/react` also augments the global JSX namespace so `` type-checks if you ever render the raw element directly instead of going through ``: ```tsx import '@klappay/one/react' ; ``` You won't normally need this — `` above covers the same ground with proper React event props — but it's there for cases like server-rendering the tag name directly or interop with a non-React tree mounted alongside your app. ## Next.js and other SSR frameworks Both `@klappay/one` and `@klappay/one/react` are safe to import anywhere Node evaluates them — `registerKlappayButton()` no-ops when `customElements`/`HTMLElement` don't exist (there's no browser to define the element against), instead of throwing. Rendering ``/ `` during SSR/static generation emits the plain tag as inert markup; it becomes the real interactive button once the client bundle loads and `registerKlappayButton()` runs for real, upgrading the already-present element in place — the same progressive-enhancement behavior undefined Custom Elements get natively. That said, every example in this repo still keeps `@klappay/one` out of the server render entirely (`next/dynamic({ ssr: false })` below, Nuxt's ``, SvelteKit's `export const ssr = false` — see [Other frameworks](/frameworks#server-side-rendering)) — not to dodge a crash, but because a payment button has zero SSR value: there's nothing to index, and skipping the inert-then-upgraded render avoids a visible pop-in the instant hydration finishes. Treat the pattern below as a UX default worth keeping, not a workaround you still need: ```tsx 'use client' import dynamic from 'next/dynamic' const KlappayButton = dynamic(() => import('@klappay/one/react').then((m) => m.KlappayButton), { ssr: false, }) export function CheckoutButton({ chargeId }: { chargeId: string }) { return } ``` One more wrinkle specific to webpack (Next's default bundler, as opposed to Turbopack): `@klappay/one/react`'s `package.json` `exports` only declares `types`/`import` conditions (ESM only, no `require`) — webpack's resolver can fail on that combination specifically for a code-split `import()` like the one `next/dynamic` generates. If you hit a resolution error there, alias the subpath straight at the built file in `next.config.ts`: ```ts import path from 'node:path' import type { NextConfig } from 'next' const nextConfig: NextConfig = { webpack: (config) => { config.resolve.alias = { ...config.resolve.alias, '@klappay/one/react': path.resolve(process.cwd(), 'node_modules/@klappay/one/dist/react/index.js'), } return config }, } export default nextConfig ``` Every other framework with an SSR pass needs the equivalent of `ssr: false` — see [Other frameworks](/frameworks) for Vue's `` and SvelteKit's `export const ssr = false`. See [Examples](/examples) for a full Next.js app with a Route Handler that creates the `Charge` server-side, and the repo's [`examples/nextjs/`](https://github.com/klappay/klap-one-js/tree/main/examples/nextjs) for the complete, verified working files. --- --- url: https://js-one.klappay.com/frameworks.md --- # Other frameworks `react/index.tsx` is the only framework wrapper this package ships — not because other frameworks aren't supported, but because they don't need one. `` is a real [Custom Element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements), and `data-klappay-one` is a plain DOM attribute — both work natively in every framework's own templating, with no adapter package required. Adding a `vue/`/`svelte/` subpath here would just be a second implementation of logic that already lives once in `core/`, which is exactly the kind of duplication this package's own conventions rule out. ## Server-side rendering Importing `@klappay/one`/`@klappay/one/react` in Node (SSR, static generation, prerendering) is safe — `registerKlappayButton()` no-ops when `customElements`/`HTMLElement` don't exist instead of throwing. `` renders as inert markup server-side and becomes the real interactive button once the client bundle hydrates and registers it for real — the same progressive-enhancement behavior an undefined Custom Element gets natively in any browser. Every framework example in this repo still keeps `@klappay/one` out of the server render anyway — not to avoid a crash, but because a payment button has zero SSR value (nothing to index) and skipping the inert-then-upgraded render avoids a visible pop-in the instant hydration finishes. Each framework's own mechanism for "this component is browser-only": * **Nuxt** — wrap the component in `` (see [Nuxt](#nuxt) below). * **SvelteKit** — `export const ssr = false` in that route's `+page.ts` (see [SvelteKit](#sveltekit) below). * **Next.js** — `next/dynamic(..., { ssr: false })`, see [React](/react#next-js-and-other-ssr-frameworks) for the full pattern (plus a webpack-resolver wrinkle specific to Next's default bundler, unrelated to the SSR question). * **Plain Vue/Svelte/Angular without SSR** (a Vite SPA, for instance) — nothing to do here at all; none of this matters until something actually executes your components in Node before they reach a browser. ## Vue ```vue ``` Vue's `@success` template syntax maps directly onto the native `CustomEvent` dispatched by `` (see [The button](/button)) — no wrapper needed. If your build tooling warns about an unknown custom element, tell Vue's compiler to treat `klappay-button` as one: ```ts // vite.config.ts export default defineConfig({ plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'klappay-button', }, }, }), ], }) ``` ### Nuxt Nuxt renders every component server-side by default (Nitro's SSR). Importing `@klappay/one` there [no longer crashes](#server-side-rendering), but a payment button still has nothing worth server-rendering — wrap it in Nuxt's built-in `` to skip straight to the client-only render: ```vue ``` See the repo's [`examples/nuxt/`](https://github.com/klappay/klap-one-js/tree/main/examples/nuxt) for the complete, verified working app, including the `vue.compilerOptions.isCustomElement` config from above wired into `nuxt.config.ts`. ## Svelte ```svelte ``` Svelte's `on:success` binds the same native `CustomEvent` directly — same pattern as Vue above, no adapter package. ### SvelteKit SvelteKit server-renders every route by default too. Importing `@klappay/one` there [no longer crashes](#server-side-rendering), but same reasoning as Nuxt above — opt that specific route out of SSR in its `+page.ts` rather than render an inert button that only pops in once hydrated: ```ts // src/routes/checkout/+page.ts export const ssr = false ``` This makes the route (and everything it imports, including `@klappay/one`) render client-only, same effect as Nuxt's `` above — just expressed as a route-level flag instead of a wrapper component, since SvelteKit's SSR opt-out is per-route, not per-component. See the repo's [`examples/sveltekit/`](https://github.com/klappay/klap-one-js/tree/main/examples/sveltekit) for the complete, verified working app. ## Angular ```html ``` ```ts @Component({ selector: 'app-checkout', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], templateUrl: './checkout.component.html', }) export class CheckoutComponent { onSuccess(event: CustomEvent) { console.log('paid', event.detail.txHash) } } ``` `CUSTOM_ELEMENTS_SCHEMA` tells Angular's template compiler `klappay-button` is an intentional custom element, not a typo'd component selector. ## No framework at all The two markup-based entry points in [The button](/button) — `` and `data-klappay-one` — need nothing beyond the ` ``` See [The button](/button) for the zero-JS `` / `data-klappay-one` alternatives to the manual `createKlappayOne()` call above — either works equally well once the script tag is loaded this way. ## Next.js (App Router) A Route Handler for charge creation, a Client Component for the button — see [React](/react) for why `` needs a `'use client'` boundary. `lib/klap.ts`: ```ts import { createClient } from '@klappay/node' export const klap = createClient({ apiKey: process.env.KLAP_API_KEY!, baseUrl: process.env.KLAP_BASE_URL!, }) ``` `app/api/charges/route.ts`: ```ts import { NextResponse } from 'next/server' import { klap } from '@/lib/klap' export async function POST() { const charge = await klap.charges.create({ amount: 25, currency: 'USD', expiresIn: 3600, acceptedPayments: [{ token: 'USDC', network: 'base' }], }) return NextResponse.json({ chargeId: charge.id }) } ``` `app/checkout/CheckoutButton.tsx`: ```tsx 'use client' import { useState } from 'react' import { KlappayButton } from '@klappay/one/react' export function CheckoutButton() { const [chargeId, setChargeId] = useState(null) if (!chargeId) { return ( ) } return ( console.log('paid', result.txHash)} /> ) } ``` ## Nuxt Nitro server route for charge creation, a Vue component around the raw `` element — see [Other frameworks](/frameworks#vue) for the `isCustomElement` compiler option Nuxt needs. `server/api/charges.post.ts`: ```ts import { createClient } from '@klappay/node' const klap = createClient({ apiKey: process.env.KLAP_API_KEY!, baseUrl: process.env.KLAP_BASE_URL!, }) export default defineEventHandler(async () => { const charge = await klap.charges.create({ amount: 25, currency: 'USD', expiresIn: 3600, acceptedPayments: [{ token: 'USDC', network: 'base' }], }) return { chargeId: charge.id } }) ``` `app/components/CheckoutButton.vue`: ```vue ``` ## SvelteKit A `+server.ts` route for charge creation, the raw `` element directly in the Svelte template: `src/routes/api/charges/+server.ts`: ```ts import { json } from '@sveltejs/kit' import { createClient } from '@klappay/node' import { KLAP_API_KEY, KLAP_BASE_URL } from '$env/static/private' const klap = createClient({ apiKey: KLAP_API_KEY, baseUrl: KLAP_BASE_URL }) export async function POST() { const charge = await klap.charges.create({ amount: 25, currency: 'USD', expiresIn: 3600, acceptedPayments: [{ token: 'USDC', network: 'base' }], }) return json({ chargeId: charge.id }) } ``` `src/routes/checkout/+page.svelte`: ```svelte {#if !chargeId} {:else} {/if} ``` ## Every example uses the public `/v1` API Same as `@klappay/node`/`@klappay/checkout-kit`'s own examples: every example above calls `klap.charges.create()` against Core's public, API-key-authenticated `/v1` surface — the same access any external merchant integration has. That means each example only ever creates charges under the `KLAP_API_KEY` you provide; see each example's own README for exactly which environment variables to set before running it.