---
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 `