--- url: https://cli.klappay.com/getting-started.md --- # Getting started ## Install ```bash npm install -g @klappay/cli # or, without installing: npx @klappay/cli --help ``` The binary is `klap`. ## Log in ```bash klap login --api-key klap_test_... --base-url https://api.klap.example ``` Stores your API key and the API's base URL in `~/.klap/config.json`. There is no default base URL — same reasoning as `@klappay/node`'s `createClient()`: there is no single known production domain to guess, so it's always explicit. Use a `klap_test_...` key while you're building — it unlocks [`klap sandbox trigger`](/sandbox), which lets you simulate any charge event without moving real funds. You can also log in with a `live` key later — `klap login` auto-detects the environment from the key's own prefix and adds it alongside, without removing the one you already have: ```bash klap login --api-key klap_live_... --base-url https://api.klap.example ``` With both configured, every command that talks to the API requires `--env test` or `--env live` to pick one — no silent default. With only one configured, it's picked automatically. See [Configuration](/configuration) for the full picture, including the security trade-off of storing both. ## Your first charge ```bash klap charges create --amount 10 --accept USDC:base --expires-in 3600 ``` ``` TEST — sandbox, no real funds ch_a1b2c3d4e5 status: pending settlementStatus: null amount: 10 amountReceived: null fee: 0.1 (1%, paid by merchant) merchantAmount: 9.9 acceptedPayments: USDC:base paidWith: none yet address: 0x1234... environment: test checkoutUrl: https://pay.stage.klappay.com/c/ch_a1b2c3d4e5 ``` `checkoutUrl` is printed underlined — click it (or `Cmd`/`Ctrl`-click in most terminals) to open the hosted checkout page for that exact charge and watch it update live as it gets paid. It comes straight from the API response — the CLI never builds this URL itself, and it's `null` if the deployment you're pointed at has no hosted checkout configured. `--accept` is repeatable — pass it more than once (e.g. `--accept USDC:base --accept USDT:base`) to let the payer choose which pair to pay with: ```bash klap charges create \ --amount 49.90 \ --accept USDC:base --accept USDT:base --accept USDC:optimism \ --expires-in 3600 ``` See [Charges](/charges) for the full command reference. ## Simulate a payment without real funds ```bash klap sandbox trigger ch_a1b2c3d4e5 charge.confirmed ``` Works on any `test`-environment charge you just created — see [Sandbox](/sandbox) for every triggerable event and its preconditions. ## Receive webhooks locally ```bash klap listen --forward-to http://localhost:3000/webhooks ``` Opens a live connection and forwards every event for your organization to your own local server, signed exactly like a real webhook delivery — no tunnel, no public URL, nothing to deploy. Drop `--forward-to` to just watch events live instead, or add `--charge ` to focus on one. See [Listen](/listen) for how this works and why it's safe to run on your own machine. ## Fire a fake webhook at a handler, with no Core involved ```bash klap webhooks trigger charge.confirmed --url http://localhost:3000/webhooks --secret whsec_test123 ``` No login, no charge, no waiting for `klap listen` to see anything — signs a realistic payload and delivers it right now, so you can test your handler's signature verification and parsing in isolation. See [Webhooks](/webhooks#klap-webhooks-trigger). ## Generate test data without touching the API ```bash klap fixtures charge --status confirmed ``` No login, no network call — prints a realistic `Charge` JSON object you can pipe straight into your own tests. See [Fixtures](/fixtures). ## Where to go next * [Login / logout](/login) — credential storage, environment auto-detection * [Charges](/charges) — every flag, in depth * [Sandbox](/sandbox) — simulating charge events * [Listen](/listen) — how `klap listen` works, and why it isn't a tunnel * [Logs](/logs) — timelines and live tailing * [Webhooks](/webhooks) — registering and managing endpoints * [Fixtures](/fixtures) — realistic sample data, offline * [Configuration](/configuration) — where credentials are stored ## For LLMs and agents This site publishes [`llms.txt`](https://cli.klappay.com/llms.txt) — a link index of every doc page — and [`llms-full.txt`](https://cli.klappay.com/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 CLI's documentation without scraping HTML. Both regenerate on every deploy, so they never drift from what's on this page. --- --- url: https://cli.klappay.com/configuration.md --- # Configuration `klap login --api-key --base-url ` writes to `~/.klap/config.json`. The environment (`test`/`live`) is auto-detected from the key's own prefix — you never specify it separately. Running `klap login` again with a key for the **other** environment adds it alongside the first, it doesn't overwrite it: ```json { "baseUrl": "https://api.klap.example", "apiKeys": { "test": "klap_test_...", "live": "klap_live_..." } } ``` Both fields are optional inside `apiKeys` — store just the one you need, or both. `baseUrl` is shared across environments (one Klap API serves both `test` and `live` traffic, differentiated only by which key you present), so logging in again always updates it for both. ## Choosing which key a command uses Every command that talks to the API (`charges create`, `sandbox trigger`, `listen`, `logs`, `webhooks`) accepts `--env test` or `--env live`: ```bash klap listen --forward-to http://localhost:3000/webhooks --env live klap charges create --amount 10 --accept USDC:base --expires-in 3600 --env test klap webhooks list --env live klap logs --tail --env test ``` `klap fixtures charge` also accepts `--env`, but it's unrelated to credentials — no login is required at all for `fixtures`. It only changes the `environment` field (and the `checkoutUrl` domain) on the printed JSON. See [Fixtures](/fixtures). * **Only one key configured** — `--env` is optional, that key is used automatically. * **Both configured** — `--env` is **required**. There is no silent default (not to `test`, not to `live`) — the CLI errors and asks you to choose rather than guess. Every command that resolves an environment prints it loudly at the top of its output (`LIVE` gets a highlighted banner) specifically so you're never uncertain which one you're looking at mid-session. ## Removing credentials ```bash klap logout # remove everything — both keys and the base URL klap logout --env test # remove just the test key, keep live + baseUrl klap logout --env live # remove just the live key, keep test + baseUrl ``` `klap logout` with no `--env` removes the whole file. `klap logout --env test` (or `--env live`) removes just that one key, keeping the other and `baseUrl` intact. See [Login / logout](/login). ## Security notes Treat this file like any other stored credential (it's your real API key(s), in plaintext, at rest) — same posture as `~/.aws/credentials` or `gh`'s own local token storage. Permissions are enforced on every write: the `~/.klap` directory is `0700`, the file itself `0600` — tightened even if either already existed with looser permissions from an older version. Writes are atomic (a temp file in `~/.klap`, then a rename), so an interrupted `klap login` never leaves a half-written file behind. **Symbolic links are refused.** If `~/.klap` or `~/.klap/config.json` is a symlink, every command that reads or writes credentials (`login`, `logout`, and anything that talks to the API) stops with an error instead of following it — a link could otherwise redirect your keys to, or read them from, a location you didn't intend. Older CLI versions followed such links silently; if you relied on that (e.g. a dotfiles-managed `~/.klap`), remove the link and run `klap login` again to create a real directory. **A corrupted or invalid file is an error, not "logged out".** If `config.json` isn't valid JSON, has no `baseUrl`, or stores a key under the wrong slot (a `klap_live_...` key under `test`, or vice versa), commands fail with a message naming the file instead of silently ignoring it. Run `klap logout` to remove it, then `klap login` again. **Shared with the Klap MCP server.** `@klappay/mcp` reads the same `~/.klap/config.json`, so logging in once with the CLI is enough for both. Both go through the same storage code — this package's own [`@klappay/cli/credentials`](#programmatic-access-klappay-cli-credentials) export — so the rules above apply identically to each. **Trade-off worth knowing**: storing both keys in one file means a leak of that file exposes both environments at once, where storing only one key (the old behavior, and still what happens if you only ever `klap login` one environment) would only expose that one. This mirrors how `~/.aws/credentials` already stores multiple profiles' secrets in a single file — if an attacker can read one key from your home directory, they can usually read the other too, so splitting into separate files wouldn't meaningfully reduce real-world risk, just add complexity. If you only ever work in one environment from a given machine, only ever log in with that environment's key — nothing requires configuring both. **How you pass the key to `klap login` matters too.** `--api-key ` on the command line is simplest, but it writes the key to your shell history (`~/.bash_history`/`~/.zsh_history`) and exposes it to any local user running `ps`/`/proc`/`htop` while the `login` process is running — neither of those is protected by the `0600` config file permission above. Two alternatives avoid both: ```bash # stdin — nothing on the command line, nothing in shell history echo -n "klap_test_..." | klap login --api-key - --base-url https://api.klap.example # environment variable — same reasoning, useful in scripts/CI KLAP_API_KEY=klap_live_... klap login --base-url https://api.klap.example ``` `--api-key ` still works and isn't going away — it's the right choice for a script that already accepts this trade-off consciously (the same way `aws configure` still lets you pass `--profile`-scoped values directly in some contexts). Prefer stdin or the env var otherwise. ## Migrating from an older CLI version Config files written by a CLI version before multi-environment support look like `{ "apiKey": "...", "baseUrl": "..." }`. The CLI detects this shape automatically and treats it as if you'd logged in with just that one key (environment inferred from its prefix) — no action needed, and the file converges to the new shape the next time you run `klap login`. ## No default base URL There is no default `--base-url` — same reasoning as `@klappay/node`'s `createClient({ baseUrl })`: there's no single known production domain to fall back to, and a wrong silent default is a much harder bug to notice than a required flag. ## Programmatic access (`@klappay/cli/credentials`) The code `klap login`/`logout` and every command use to read and write `~/.klap/config.json` is also published as a subpath export, so other local tools share one store and one set of rules for picking a `test` or `live` key instead of each re-implementing them. The Klap MCP server (`@klappay/mcp`) uses it, which is why a single `klap login` is enough for both. ```bash npm install @klappay/cli ``` It's a separate, lightweight, Node-only entry point: importing `@klappay/cli/credentials` doesn't load the CLI itself (no `commander`, no `picocolors`, no `@klappay/node`), only Node built-ins. ESM only. Use it from tools that run on a developer's own machine, not from a deployed server — a backend should pass its key to `createClient()` explicitly (or via `KLAP_API_KEY`) and never touch `~/.klap`. ```ts import { createClient } from '@klappay/node' import { loadCredentials, resolveApiKey } from '@klappay/cli/credentials' const config = await loadCredentials() const { key, env } = resolveApiKey(config, 'test') const klap = createClient({ apiKey: key, baseUrl: config?.baseUrl }) console.log(`Using the ${env} key`) ``` ### File format and location `credentialsPath()` returns the absolute path (`$HOME/.klap/config.json`, resolved when called, not at import time); `CREDENTIALS_DISPLAY_PATH` is the `~/.klap/config.json` string to show in messages. `KLAP_ENVIRONMENTS` is `['test', 'live']`. ```json { "baseUrl": "https://your-klap-api-host", "apiKeys": { "test": "klap_test_...", "live": "klap_live_..." } } ``` Either slot may be absent. A key must be a string whose prefix matches its slot — a `klap_live_` key in the `test` slot (or the reverse) makes the whole file invalid rather than silently being used against the wrong environment. Unknown fields are dropped on load. The older single-key shape (see [Migrating from an older CLI version](#migrating-from-an-older-cli-version)) is read and migrated in memory into the slot its prefix names; it gets rewritten in the new shape the next time you save. ### Reading and writing | Function | Behavior | |---|---| | `loadCredentials()` | `Promise` — `null` when `~/.klap` or `config.json` doesn't exist. Throws `InvalidCredentialsFileError` for corrupted JSON or an unexpected shape, never returns `null` for it. | | `saveCredentials(config)` | Validates `config` with the same rules as loading (throws `InvalidCredentialsFileError` and writes nothing if it fails), then writes it atomically. | | `setApiKey(config, baseUrl, apiKey)` | Pure: returns a new config with `apiKey` stored in the slot its prefix names, replacing that slot only, and `baseUrl` replaced. `config` may be `null`. | | `clearApiKey(config, env)` | Pure: returns a new config without that environment's key. | | `deleteCredentials()` | Removes `config.json`; a no-op if it doesn't exist. | | `detectEnvironment(apiKey)` | `'test'` for `klap_test_`, `'live'` for `klap_live_`, otherwise throws `InvalidApiKeyPrefixError`. | ```ts import { loadCredentials, saveCredentials, setApiKey } from '@klappay/cli/credentials' const existing = await loadCredentials() await saveCredentials(setApiKey(existing, 'https://your-klap-api-host', 'klap_test_...')) ``` `saveCredentials` creates `~/.klap` with mode `0700` (resetting it if it already existed with looser permissions), writes a fresh temp file in the same directory (created exclusively, mode `0600`), flushes it, then renames it over `config.json` — a crash mid-write never leaves a half-written file, and the final file is always `0600`. The temp file is removed if anything fails. Reads open `config.json` with `O_NOFOLLOW`. If `~/.klap` or `~/.klap/config.json` is a symbolic link, `loadCredentials`/`saveCredentials` throw `SymlinkedCredentialsPathError` instead of following it, and `deleteCredentials` refuses a symlinked `~/.klap` — a key file is a high-value target, and following a planted link would read secrets from, or write them to, a place you didn't choose. ### Choosing test vs. live `resolveApiKey(config, env?)` returns `{ key, env }` and never guesses when the choice is ambiguous: | Call | Result | |---|---| | `env` given, that slot configured | that key | | `env` given, that slot empty | throws `MissingEnvironmentKeyError` (`err.environment` is the one you asked for) | | no `env`, exactly one slot configured | that key | | no `env`, both slots configured | throws `AmbiguousEnvironmentError` | | no `env`, nothing configured, or `config` is `null` | throws `NoCredentialsError` | ### Errors Every error extends `KlapCredentialsError`, which carries a stable `code` (typed as `KlapCredentialsErrorCode`) to branch on. No message ever includes any part of an API key — not even for a key with the wrong prefix, and not the JSON parser's own error text for a corrupted file (which would echo the file's contents). The CLI maps each `code` to its own wording; a library caller gets a generic message and should branch on `code`, not on the text. | Class | `code` | Thrown when | |---|---|---| | `InvalidApiKeyPrefixError` | `invalid_api_key_prefix` | A key doesn't start with `klap_test_`/`klap_live_` (`detectEnvironment`, `setApiKey`) | | `InvalidCredentialsFileError` | `invalid_credentials_file` | Corrupted JSON, unexpected shape, a key in the wrong slot, `config.json` isn't a regular file — or a config passed to `saveCredentials` fails the same rules | | `SymlinkedCredentialsPathError` | `credentials_path_symlink` | `~/.klap` or `config.json` is a symbolic link | | `NoCredentialsError` | `no_credentials` | `resolveApiKey` found no key at all | | `MissingEnvironmentKeyError` | `missing_environment_key` | `resolveApiKey` was asked for an environment that has no key; carries `environment` | | `AmbiguousEnvironmentError` | `ambiguous_environment` | `resolveApiKey` found both keys and no environment was given | Filesystem errors other than "not found" (e.g. `EACCES`) are rethrown as-is. ```ts import { AmbiguousEnvironmentError, KlapCredentialsError, loadCredentials, resolveApiKey, } from '@klappay/cli/credentials' try { const { key } = resolveApiKey(await loadCredentials()) } catch (err) { if (err instanceof AmbiguousEnvironmentError) { console.error('Both keys are configured — pick test or live explicitly.') } else if (err instanceof KlapCredentialsError) { console.error(`Credentials problem (${err.code}): ${err.message}`) } else { throw err } } ``` --- --- url: https://cli.klappay.com/login.md --- # login / logout ## `klap login` ```bash klap login --api-key --base-url ``` Stores the key locally — environment (`test`/`live`) is auto-detected from the key's own prefix, and adding one doesn't remove the other. ```bash # test key, pointed at a staging deployment klap login --api-key klap_test_abc123 --base-url https://api.stage.klap.example # add a live key later — kept alongside the test key, not replacing it klap login --api-key klap_live_xyz789 --base-url https://api.klap.example ``` **Avoid the key touching shell history or a process listing.** `--api-key ` on the command line is the simplest form, but it writes the secret to `~/.bash_history`/`~/.zsh_history` and exposes it to any local user running `ps`/`/proc` while the process runs. Two alternatives avoid both: ```bash # stdin — nothing on the command line, nothing in shell history echo -n "klap_test_abc123" | klap login --api-key - --base-url https://api.klap.example # environment variable — same reasoning, ideal for CI KLAP_API_KEY=klap_live_xyz789 klap login --base-url https://api.klap.example ``` `--api-key ` still works and isn't going away — it's the right choice for a script that already accepts this trade-off consciously. See [Configuration](/configuration) for the full security discussion. ## `klap logout` ```bash klap logout # remove everything — both keys and the base URL klap logout --env test # remove just the test key, keep live + baseUrl klap logout --env live # remove just the live key, keep test + baseUrl ``` Running it with no `--env` deletes `~/.klap/config.json` entirely. With `--env`, only that one key is cleared — the other environment (and `baseUrl`) stays intact, so you don't have to log back in for the key you're keeping. `klap logout` (with no `--env`) is also the fix when a command reports that `~/.klap/config.json` is corrupted or invalid: it removes the file without needing to read it, so `klap login` can start fresh afterward. Neither command follows a symlinked `~/.klap` — see [Configuration](/configuration#security-notes). --- --- url: https://cli.klappay.com/charges.md --- # charges ## `klap charges create` ```bash klap charges create --amount --accept --expires-in [--fee-payer merchant|payer] [--env test|live] ``` Thin wrapper over `klap.charges.create()` in `@klappay/node`. `--amount`, `--accept`, and `--expires-in` are all required — the API has no "any amount"/"never expires" charge. Every EVM network (Arc included) can be mixed freely across `--accept` flags, but `tron` can't be mixed with any of them — a charge pays out to one split address predicted up front, and TRON's split contract differs from the EVM one, so the API rejects that combination with `400 validation_error`. ```bash # $10, USDC on Base, expires in 1 hour klap charges create --amount 10 --accept USDC:base --expires-in 3600 # accept more than one (token, network) pair — repeat the flag; the payer # picks whichever they hold klap charges create \ --amount 49.90 \ --accept USDC:base --accept USDT:base --accept USDC:optimism \ --expires-in 3600 # a charge that expires in a full day klap charges create --amount 5 --accept USDC:polygon --expires-in 86400 # force a specific environment when both a test and a live key are configured klap charges create --amount 25 --accept USDC:base --expires-in 1800 --env test # gross up --amount so the payer covers Klappay's fee instead of you — # you receive the full $50, not $50 minus the fee klap charges create --amount 50 --accept USDC:base --expires-in 3600 --fee-payer payer ``` ### Output ``` TEST — sandbox, no real funds ch_a1b2c3d4e5 status: pending settlementStatus: null amount: 49.9 amountReceived: null fee: 0.499 (1%, paid by merchant) merchantAmount: 49.401 acceptedPayments: USDC:base, USDT:base, USDC:optimism paidWith: none yet address: 0x1234567890abcdef1234567890abcdef12345678 environment: test checkoutUrl: https://pay.stage.klappay.com/c/ch_a1b2c3d4e5 ``` A charge created as an escrow also prints an `escrow:` block at the end (`releaserAddress`, `releasedAt`, `refundedAt`). See [Escrow](#escrow-prepare-release-release-prepare-refund-refund). * **`acceptedPayments`** — every `(token, network)` pair you configured. * **`paidWith`** — every pair actually paid so far, comma-separated, or `none yet` until the first transfer arrives. A charge with multiple `acceptedPayments` can be paid with any combination of them; every transfer on an accepted pair sums toward the charge total. * **`fee`/`merchantAmount`** — your fee tier's `feePercent`, the resulting `feeAmount`, and who's covering it (`--fee-payer`). `merchantAmount` is always what actually lands in your payout, regardless of who paid the fee — with `--fee-payer merchant` (the default) it's `amount` minus the fee; with `--fee-payer payer`, `amount` itself is grossed up so `merchantAmount` equals the amount you asked for. * **`checkoutUrl`** — a live link to the hosted checkout page for this exact charge, printed underlined so it's easy to click straight from the terminal. It comes directly from the API response (the CLI never constructs it) and is `null` if the deployment you're pointed at has no hosted checkout configured. Open it to watch the charge update live as a payer connects a wallet and pays — the same page `klap sandbox trigger` and `klap listen` let you drive/observe from the terminal. `--env` is required only if both a `test` and a `live` key are configured — see [Configuration](/configuration). The `LIVE`/`TEST` banner above the charge is printed by every command that resolves an environment, so you're never uncertain which one you're looking at. ### Then what? ```bash # watch it live as events come in — no webhook or relay setup needed klap charges watch ch_a1b2c3d4e5 # or drive it yourself, without waiting for a real payment klap sandbox trigger ch_a1b2c3d4e5 charge.confirmed ``` See [Sandbox](/sandbox) and [Logs](/logs). ## `klap charges get` ```bash klap charges get [--env test|live] ``` Thin wrapper over `klap.charges.get()`. Prints the charge's current state in the same shape as `klap charges create` — useful for a charge created elsewhere (your backend, the checkout page) when you just want one snapshot instead of `watch`'s live stream. ## `klap charges list` ```bash klap charges list [--status ] [--token ] [--network ] \ [--since ] [--overpaid | --no-overpaid] \ [--limit ] [--cursor ] [--env test|live] ``` Thin wrapper over `klap.charges.list()`: one page of charges, one line each — id, status, amount, and `createdAt`. ```bash # every confirmed charge paid in USDC on Base since October 1st klap charges list --status confirmed --token USDC --network base --since 2026-10-01T00:00:00Z # only charges where the payer sent more than the amount klap charges list --overpaid ``` ``` TEST — sandbox, no real funds ch_a1b2c3d4e5 confirmed 10 USD 2026-10-12T14:02:11.000Z ch_f6g7h8i9j0 pending 49.9 USD 2026-10-12T13:58:40.000Z More results — rerun with the same filters plus --cursor eyJpZCI6ImNoX2Y2... ``` * **`--status`** — one of `pending`, `partially_paid`, `confirmed`, `expired`, `underpaid`. * **`--token`/`--network`** — filter on the pair actually *paid* (`paidWith`), not on what the charge accepted. * **`--since`** — an ISO 8601 datetime; filters on `createdAt`, not on when the status last changed. * **`--overpaid`/`--no-overpaid`** — only overpaid charges, or only charges that aren't. Omit both for either. * **`--limit`** — page size, 1–100 (default 20). * **`--cursor`** — when there's another page, the last line prints the cursor to fetch it. Pass it back verbatim with the same filters; it's opaque, never build or edit it yourself. Every flag is validated against `@klappay/types`'s own schema before any request is sent, so a typo'd status or a malformed date fails locally with the flag's name in the error. ## `klap charges check` ```bash klap charges check [--tx-hash --network ] [--env test|live] ``` Thin wrapper over `klap.charges.check()`: asks the API to look for payments to this charge on-chain right now instead of waiting for its background scan, then prints the result. Use it when a payment was sent but the charge hasn't moved yet. Pass `--tx-hash` and `--network` together (one without the other is rejected) when you already know the paying transaction. The check then costs a single RPC call instead of a block scan, and the output also shows **who paid**: ```bash klap charges check ch_a1b2c3d4e5 \ --tx-hash 0x9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0 \ --network base ``` ``` TEST — sandbox, no real funds ch_a1b2c3d4e5 status: confirmed ... checkoutUrl: https://pay.stage.klappay.com/c/ch_a1b2c3d4e5 payer evidence: transactionSender: 0x1111111111111111111111111111111111111111 tokenSenders: 0x2222222222222222222222222222222222222222 userOperationSenders: none ``` * **`transactionSender`** — whoever signed and submitted the transaction. That's the payer for an ordinary wallet, but it's the relayer or bundler when someone else submitted it for them. * **`tokenSenders`** — where the paying token transfers actually came from. Covers a wallet whose transaction was relayed (e.g. an EIP-7702 smart account behind a gas-sponsoring relayer). For a swap-to-pay this is the router, not the payer. * **`userOperationSenders`** — the ERC-4337 smart account whose own user operation paid, when a bundler submitted it. All three are evidence, not identity: Klappay doesn't decide who the payer is. They're only filled in when the hinted transaction actually paid this charge; otherwise the output says `payer evidence: none`. It's safe to re-run with the same hint after the charge is `confirmed` to learn who paid. If a detected transfer is still short of its network's confirmation depth, the same progress bar as `klap charges watch` is printed above the charge. ## `klap charges watch` ```bash klap charges watch [--env test|live] ``` Opens a live connection straight to this charge's own event stream and prints its current state every time `status`/`settlementStatus` changes, plus a confirmation progress bar while a detected transfer is still waiting on block confirmations. Unlike `klap logs --tail`, it needs no webhook endpoint or `klap listen` relay — just the charge id. Runs until the charge reaches a terminal state or you stop it with Ctrl+C. Not the same tool as `klap listen --charge `: that relays every raw webhook event dispatched for your org, filtered to one charge, with no confirmation progress bar. See [`watch` vs `listen`](/listen#watch-vs-listen). ``` TEST — sandbox, no real funds [#######-------------] 33% base 4/12 blocks ch_a1b2c3d4e5 status: confirmed settlementStatus: pending amount: 10 amountReceived: 10 fee: 0.1 (1%, paid by merchant) merchantAmount: 9.9 acceptedPayments: USDC:base paidWith: USDC:base address: 0x1234567890abcdef1234567890abcdef12345678 environment: test checkoutUrl: https://pay.stage.klappay.com/c/ch_a1b2c3d4e5 ``` ## Escrow: `prepare-release`, `release`, `prepare-refund`, `refund` ```bash klap charges prepare-release [--out ] [--env test|live] klap charges release --signature [--env test|live] klap charges prepare-refund [--out ] [--env test|live] klap charges refund --signature [--env test|live] ``` Thin wrappers over `klap.charges.prepareRelease()`/`release()` and `prepareRefund()`/`refund()`, for a charge created as an escrow. Its funds sit in a dedicated Safe until the escrow's releaser signs one of two transfers: * **release** — the full balance goes to the charge's split address, and the normal distribution pays out from there. * **refund** — the full balance goes back to the address that funded the charge. No distribution follows. An escrow can only ever be released *or* refunded, once. Both move real funds on-chain and can't be undone. The CLI never holds or asks for the releaser's private key. You sign somewhere else (a wallet, viem, ethers, a KMS) and pass the signature in: 1. **Prepare.** `prepare-release`/`prepare-refund` is read-only and prints the exact Safe transaction the API will execute: ``` LIVE — real funds and real data refund — sign as 0x2222222222222222222222222222222222222222 network: base (chain id 8453) amount: 49.9 USDC (49900000 base units) destination: 0x4444444444444444444444444444444444444444 safeAddress: 0x1111111111111111111111111111111111111111 tokenAddress: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 nonce: 0 safeTxHash: 0x5555555555555555555555555555555555555555555555555555555555555555 ``` Check `destination`, `amount`, `tokenAddress`, the chain id and `safeAddress` against your own records before signing. Signing commits the escrow's funds to exactly that transfer. For a refund, `destination` is the sender of the most recently credited transfer, which isn't always the buyer: an exchange withdrawal comes from the exchange's wallet, a swap from a router, and a charge paid by several wallets refunds only to the last one. 2. **Sign.** Pass `--out tx.json` to also write the full transaction, including its EIP-712 `typedData`, to a file. Sign `typedData` with the key behind the releaser address. It's plain JSON, so ethers or a KMS can take it as-is. With viem, pass it through `toSignableEscrowTypedData` from `@klappay/types` first. The result must be an EIP-712 signature from a plain externally-owned account. 3. **Submit.** ```bash klap charges refund ch_a1b2c3d4e5 --signature 0x2d0fbf1dba2878... ``` The CLI checks that `--signature` is hex before sending it. The Safe contract itself verifies it on-chain before anything moves. The updated charge is printed, with an `escrow` block showing `releasedAt`/`refundedAt`. A `charge.escrow_released` or `charge.escrow_refunded` webhook follows once the transfer executes. A signature is only valid while the escrow's balance is unchanged. If more funds arrive between prepare and submit, the API rejects with `escrow_invalid_signature`; prepare again and re-sign. Other errors come straight from the API: `escrow_already_released`/ `escrow_already_refunded`, `escrow_not_configured`, `escrow_nothing_to_release`/`escrow_nothing_to_refund` (zero balance), `escrow_multiple_payment_pairs`, and `escrow_safe_mismatch`. That last one isn't transient; contact Klappay support. All four commands need an API key with `charges:write`. --- --- url: https://cli.klappay.com/sandbox.md --- # sandbox ## `klap sandbox trigger` ```bash klap sandbox trigger [--amount ] [--env test|live] ``` `event` is one of: `charge.confirmed`, `charge.partially_paid`, `charge.overpaid`, `charge.expired`, `charge.underpaid`, `charge.settled`, `charge.settlement_failed`. Requires a `test`-environment key — a test key can only ever act on a test charge, and vice versa; passing `--env live` here reaches the server but gets rejected. ```bash # any positive transfer confirms it immediately klap sandbox trigger ch_a1b2c3d4e5 charge.confirmed # half the charge amount by default klap sandbox trigger ch_a1b2c3d4e5 charge.partially_paid # a specific partial amount klap sandbox trigger ch_a1b2c3d4e5 charge.partially_paid --amount 5 # 1.5x the charge amount by default — also dispatches charge.confirmed klap sandbox trigger ch_a1b2c3d4e5 charge.overpaid # a specific overpaid amount (must exceed the charge amount) klap sandbox trigger ch_a1b2c3d4e5 charge.overpaid --amount 75 # simulate settlement outcomes directly — no blockchain, no gas klap sandbox trigger ch_a1b2c3d4e5 charge.settled klap sandbox trigger ch_a1b2c3d4e5 charge.settlement_failed # a charge that ran out the clock unpaid klap sandbox trigger ch_a1b2c3d4e5 charge.expired # a charge that expired while only partially paid — requires it to # already be charge.partially_paid klap sandbox trigger ch_a1b2c3d4e5 charge.underpaid ``` ### Preconditions Each event has a precondition on the charge's current state — e.g. `charge.underpaid` requires it to already be `charge.partially_paid` — the API returns a clear `422 invalid_trigger_state` if you trigger one out of order. `charge.partially_paid`/`charge.overpaid` are also rejected on a charge with no target `amount` (nothing to be partial against or exceed) — `charge.confirmed` still works there. ### Why this is safe `charge.settled`/`charge.settlement_failed` never touch the blockchain — they simulate the outcome directly, matching this API's guarantee that test charges never move real funds or spend real gas. ### Combine with `listen`/`logs` ```bash klap listen --forward-to http://localhost:3000/webhooks --env test & klap sandbox trigger ch_a1b2c3d4e5 charge.confirmed ``` Drives your webhook handler with a real signed delivery, on demand, instead of waiting for an actual payment. See [Listen](/listen) and [Logs](/logs). --- --- url: https://cli.klappay.com/listen.md --- # `klap listen` — how it works, and why it's safe ```bash klap listen [--forward-to ] [--charge ] [--env test|live] ``` `--forward-to` is optional — without it, `klap listen` just prints every event live (same format as [`klap logs --tail`](/logs)); with it, every event is also signed and POSTed to that URL. `--charge ` filters either mode down to just one charge. `--env` picks which key connects (only required if you have both a `test` and a `live` key configured — see [Configuration](/configuration)); this is what determines which charge events you see, see "Environment scoping" below. ```bash # just watch everything live — no local server needed klap listen # watch just one charge klap listen --charge ch_a1b2c3d4e5 # forward everything to a local server klap listen --forward-to http://localhost:3000/webhooks # forward just one charge's events, on a different port klap listen --forward-to http://localhost:8080/hooks --charge ch_a1b2c3d4e5 # pick the environment explicitly when both a test and a live key are configured klap listen --forward-to http://localhost:3000/webhooks --env live # combine with sandbox trigger to drive events on demand instead of # waiting for a real payment klap sandbox trigger ch_a1b2c3d4e5 charge.confirmed ``` Need to fire a specific event at a handler right now, with no charge, no login, and no waiting for `klap listen` to see anything at all? See [`klap webhooks trigger`](/webhooks#klap-webhooks-trigger) instead. ## `watch` vs `listen` The names sound alike but the two commands watch different things: | | Scope | Shows | Needs | |---|---|---|---| | [`klap charges watch `](/charges#klap-charges-watch) | One charge's own event stream | That charge's status/settlement changes, plus a confirmation progress bar | Just the charge id | | `klap listen [--charge ]` | Every webhook event dispatched for your org (optionally filtered to one charge) | Raw webhook payloads as they're dispatched — any event type, not just charge state | Nothing extra, but no confirmation progress bar | If all you want is "is this specific charge done yet, and how close is it," reach for `charges watch` — it's the smaller, purpose-built tool. Reach for `listen` when you need every event type (including `webhook.*` health events) or plan to forward events with `--forward-to`. ## It is not a tunnel Tools like ngrok expose your machine to the internet — they open an **inbound** path so an outside service can reach `localhost`. `klap listen` does the opposite: the connection is always initiated **from your machine, outward**, to the Klap API. Klap's servers never make an inbound request to your laptop; they push events over a connection your own CLI process already opened. ``` your machine Klap's servers ┌─────────────┐ ┌──────────────────┐ │ klap listen │──(1) outbound conn───▶│ GET /v1/webhooks/listen│ │ │◀─(2) event pushed────│ (SSE) │ └──────┬──────┘ └──────────────────┘ │ │ (3) POST — made by your own CLI process, ▼ never by Klap's servers your local server (--forward-to) ``` Step 3 is the key: it's the CLI, running on your machine, that makes the local HTTP call — not Klap's backend. Klap's servers never see or resolve `localhost`; they only ever talk to the outbound connection your CLI opened. There is no new inbound exposure to reason about. ## Real signatures, every time you forward Each connection gets its own ephemeral signing secret (`whsec_...`), generated when you connect and **never persisted** — different from the real secret behind any webhook you've registered with `POST /v1/webhooks`. With `--forward-to`, `klap listen` signs every forwarded payload with it using the exact same scheme a real webhook delivery uses: `X-Klap-Signature: t=,v1=`. Your handler code doesn't need a special "test mode" branch — the same signature verification you ship to production works unchanged while testing locally. Without `--forward-to`, nothing is signed or sent anywhere — the secret is still printed (so you can copy it if you decide to forward later in the same session) but otherwise unused. ## What it's built on `GET /v1/webhooks/listen` (authenticated with your API key) streams every webhook event dispatched for your organization — regardless of whether you have any webhook registered — as Server-Sent Events. This is the same mechanism documented in the main API docs' `realtime.md`, generalized from "one charge" to "every event for this org." Combine it with [Sandbox](/sandbox) to drive events on demand instead of waiting for a real payment. ## Environment scoping Charge events (the `payments` category — `charge.confirmed`, etc.) are filtered server-side to match the environment of the key you connected with: a `test` key never sees `live` charge events, and vice versa. This isn't optional or client-side — it's enforced in the same request handler that streams the events, so there's no way to see across the boundary by mistake, only by deliberately reconnecting with the other key. Webhook-delivery-health events (`webhook.delivery_failed`, `webhook.delivery_recovered`, `webhook.endpoint_unhealthy`) are **not** filtered by environment — they describe a webhook endpoint's own health, not a charge, so there's no `test`/`live` distinction to make. Either key sees all of them. Use `--charge ` (above) to filter either mode down to one charge — [`klap logs --tail`](/logs) takes the same flag over the same underlying stream. ## Stopping `Ctrl-C` closes the connection cleanly — the server-side stream ends the moment it sees you disconnect. --- --- url: https://cli.klappay.com/logs.md --- # logs ```bash klap logs --charge [--env test|live] # print that charge's timeline once klap logs --tail [--env test|live] # stream every event for your org, live klap logs --tail --charge [--env test|live] # stream, filtered to one charge ``` ## Print a charge's history once ```bash klap logs --charge ch_a1b2c3d4e5 ``` ``` 2026-09-01T12:00:00.000Z charge.created 2026-09-01T12:00:15.000Z charge.transfer_detected 2026-09-01T12:00:45.000Z charge.confirmed ``` Fetches `GET /v1/charges/{id}/timeline` and prints it — requires `--charge`. ## Tail everything, live ```bash klap logs --tail ``` ``` TEST — sandbox, no real funds 2026-09-01T12:00:45.000Z charge.confirmed ch_a1b2c3d4e5 2026-09-01T12:01:02.000Z webhook.delivery_failed - 2026-09-01T12:01:10.000Z charge.settled ch_a1b2c3d4e5 ``` Connects to the same relay `klap listen` uses (`GET /v1/webhooks/listen`), printing events as they arrive instead of forwarding them anywhere. `-` in the third column means the event doesn't belong to a charge (e.g. a `webhook.*` delivery-health event). ## Tail, filtered to one charge ```bash klap logs --tail --charge ch_a1b2c3d4e5 ``` Same live stream, but only events for that specific charge are printed — useful while debugging a single payment without the rest of your org's traffic scrolling past. ## Combine with sandbox ```bash # terminal 1 klap logs --tail --charge ch_a1b2c3d4e5 # terminal 2 klap sandbox trigger ch_a1b2c3d4e5 charge.confirmed klap sandbox trigger ch_a1b2c3d4e5 charge.settled ``` Watch each simulated event land in real time. See [Sandbox](/sandbox). `Ctrl-C` stops either mode cleanly. --- --- url: https://cli.klappay.com/webhooks.md --- # webhooks Thin wrapper over `klap.webhooks` in `@klappay/node`. ## `klap webhooks create` ```bash klap webhooks create --url [--event ]... [--category ]... [--env test|live] ``` ```bash # subscribe to specific events klap webhooks create \ --url https://example.com/webhooks \ --event charge.confirmed --event charge.settled # subscribe to an entire category klap webhooks create --url https://example.com/webhooks --category payments # subscribe to everything klap webhooks create --url https://example.com/webhooks --event '*' # accept the API's own default subscription — omit both flags klap webhooks create --url https://example.com/webhooks ``` ``` TEST — sandbox, no real funds wh_f1e2d3c4b5 url: https://example.com/webhooks environment: test events: charge.confirmed, charge.settled eventCategories: none secret: whsec_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d (shown once — store it now, it is used to verify delivered payloads) ``` The signing secret is printed **only here, in full** — `klap webhooks list` never shows it again, only a masked hint. Save it immediately; you'll need it to verify incoming deliveries with `klap.webhooks.verifySignature()`/`constructEvent()` from `@klappay/node`. ## `klap webhooks list` ```bash klap webhooks list [--env test|live] ``` ``` wh_f1e2d3c4b5 url: https://example.com/webhooks environment: test events: charge.confirmed, charge.settled eventCategories: none secret: whsec_1a2b...c6d ``` ## `klap webhooks deliveries` ```bash klap webhooks deliveries [--env test|live] ``` ```bash klap webhooks deliveries wh_f1e2d3c4b5 ``` ``` del_9z8y7x6w event: charge.confirmed status: delivered attempts: 1 responseCode: 200 createdAt: 2026-09-01T12:00:45.000Z del_5v4u3t2s event: charge.settled status: failed attempts: 3 responseCode: 500 createdAt: 2026-09-01T12:01:10.000Z ``` ## `klap webhooks retry` ```bash klap webhooks retry [--env test|live] ``` ```bash klap webhooks retry wh_f1e2d3c4b5 del_5v4u3t2s ``` Re-queues a failed delivery for another attempt. ## `klap webhooks delete` ```bash klap webhooks delete [--env test|live] ``` ```bash klap webhooks delete wh_f1e2d3c4b5 ``` ## `klap webhooks trigger` ```bash klap webhooks trigger --url --secret [--charge ] [--env test|live] ``` Signs a realistic webhook payload and delivers it straight to a local URL — **no Core involved at all**, no charge, no registered webhook, no login (unless you use `--charge`, see below). From your handler's point of view it's indistinguishable from a real delivery: same `X-Klap-Signature: t=,v1=` header, same `X-Klap-Event`/`X-Klap-Delivery` headers, same JSON shape. Use it to test your handler's signature verification and parsing logic in isolation, on demand, without waiting for anything real to happen. `` is any of the 13 real event types — the 10 `charge.*` lifecycle events plus the 3 webhook-health events (`webhook.delivery_failed`, `webhook.delivery_recovered`, `webhook.endpoint_unhealthy`) — **not** `'*'`, which is a subscription filter, not a real event that gets delivered. `--secret` is required and not generated for you: it has to match whatever your handler actually verifies against — the secret `klap webhooks create` printed for a real registered webhook, or one you made up for local dev. ```bash # the basic case — no login needed at all klap webhooks trigger charge.confirmed \ --url http://localhost:3000/webhooks --secret whsec_test123 # a webhook-health event — different data shape entirely # ({ webhookId, url, failureRatio } instead of a Charge) klap webhooks trigger webhook.endpoint_unhealthy \ --url http://localhost:3000/webhooks --secret whsec_test123 # walk a handler through an entire lifecycle in one go for event in charge.confirmed charge.partially_paid charge.underpaid charge.settled; do klap webhooks trigger "$event" \ --url http://localhost:3000/webhooks --secret whsec_test123 done # against a charge you actually created, instead of a synthesized fixture klap charges create --amount 25 --accept USDC:base --expires-in 3600 # ↳ prints ch_a1b2c3d4e5 — use it below klap webhooks trigger charge.confirmed \ --url http://localhost:3000/webhooks --secret whsec_test123 \ --charge ch_a1b2c3d4e5 ``` ``` --> charge.confirmed [200] 31ms ``` Prints `[failed] ` instead of a status code if the request itself couldn't complete (connection refused, timeout after 8s) — the same format `klap listen --forward-to` already uses, since both share the same signing/delivery code underneath. ### `--charge ` — deliver a real charge's real data By default, the payload's `data` is a synthesized fixture (the same generator `klap fixtures charge` uses), shaped to match the event you asked for (e.g. `charge.settled` gets a confirmed charge with `settlementStatus: 'completed'`). Pass `--charge ` to fetch that charge for real via `GET /v1/charges/{id}` and use its actual current data instead — this is the one path that needs `klap login` and `--env`. It delivers the charge's data **exactly as it is right now** — it does not reshape it to match the event you named. Asking for `charge.confirmed` on a charge that's still `pending` delivers a `pending` charge under a `charge.confirmed` envelope; that's expected — this command tests your handler's signature/parsing logic, not a real state transition (that's what `klap sandbox trigger` is for). Only valid for a `charge.*` event — combining it with a `webhook.*` event is rejected with a clear error. ### `trigger` vs `sandbox trigger` vs `listen` | | Touches Core? | Needs login? | What it proves | |---|---|---|---| | `klap webhooks trigger` | Never | Only with `--charge` | Your handler correctly verifies signatures and parses every payload shape | | `klap sandbox trigger` | Yes — mutates a real `test` charge | Yes | The real event actually fires and is delivered end-to-end | | `klap listen` | Yes — relays real dispatched events | Yes | What's actually happening on your account right now | ## Testing without a public endpoint You don't need `klap webhooks create` at all to develop locally — see [Listen](/listen), which streams every event straight to your own `localhost` without registering anything, or `klap webhooks trigger` above for testing your handler in complete isolation. --- --- url: https://cli.klappay.com/fixtures.md --- # fixtures ## `klap fixtures charge` ```bash klap fixtures charge [--status ] [--overpaid] [--settlement ] [--amount ] [--accept ] [--fee-payer merchant|payer] [--env test|live] ``` Prints a realistic `Charge` JSON object to stdout — no API call, no `klap login` needed, no testnet funds. Useful for building and testing your own checkout UI or webhook handler against every charge state on demand, the same fixture pattern klap-checkout and klap-checkout-kit already use internally in their own tests. ```bash # defaults to a fresh pending charge, $49.90, USDC:base klap fixtures charge # every lifecycle status klap fixtures charge --status pending klap fixtures charge --status partially_paid klap fixtures charge --status confirmed klap fixtures charge --status underpaid klap fixtures charge --status expired # a confirmed, overpaid charge (isOverpaid: true, amountReceived > amount) klap fixtures charge --overpaid # a confirmed charge with a specific settlement outcome klap fixtures charge --status confirmed --settlement completed klap fixtures charge --status confirmed --settlement failed klap fixtures charge --status confirmed --settlement pending # override amount and accepted payments klap fixtures charge --status confirmed --amount 100 --accept USDT:optimism # a live-environment fixture (only changes the environment/checkoutUrl fields) klap fixtures charge --status confirmed --env live # a fixture where the payer covers the fee — merchantAmount equals amount klap fixtures charge --amount 100 --fee-payer payer ``` ### Sample output ```bash klap fixtures charge --status confirmed --settlement completed ``` ```json { "id": "ch_fixture_c5dfab0b9101", "amount": 49.9, "amountReceived": 49.9, "isOverpaid": false, "currency": "USD", "feePayer": "merchant", "feePercent": 1, "feeAmount": 0.5, "merchantAmount": 49.4, "acceptedPayments": [{ "token": "USDC", "network": "base" }], "paidWith": [{ "token": "USDC", "network": "base" }], "swapAlternatives": [], "address": "0xbf99ce5225af4e3f60c4c46620755e0b35f871c8", "status": "confirmed", "settlementStatus": "completed", "environment": "test", "checkoutUrl": "https://pay.stage.klappay.com/c/ch_fixture_c5dfab0b9101", "createdAt": "2026-09-01T12:00:00.000Z", "expiresAt": "2026-09-01T13:00:00.000Z", "confirmedAt": "2026-09-01T12:00:00.000Z", "settledAt": "2026-09-01T12:00:00.000Z", "lastActivityAt": "2026-09-01T12:00:00.000Z" } ``` ### Piping into your own tests ```bash klap fixtures charge --status underpaid > fixtures/underpaid-charge.json klap fixtures charge --status confirmed --overpaid | jq '.amountReceived' ``` `--status` defaults to `pending`; `--overpaid` forces a confirmed, overpaid charge regardless of `--status`; `--settlement` only takes effect once the charge is confirmed; `--fee-payer` defaults to `merchant` and only changes `feeAmount`/`merchantAmount` (`feePercent` is a fixed `1` in every fixture — it's a per-org configured value in production, not something to simulate variance for here).