bankroll.pay() charges the user’s Bankroll balance and settles the funds to
your wallet — the merchantWallet in your manifest.
Your page cannot name a recipient; it can only name an amount.
import { bankroll } from "@joinbankroll/sdk";
const signature = await bankroll.pay({
amountCents: 500, // whole US cents, > 0
memo: "order:1234", // optional label, see below
});
The host shows the user the charge, moves the funds to your merchant wallet, and
resolves the settled payment’s signature (an opaque string). Because the
promise resolves only once the payment has settled, your server can confirm it
the moment pay() returns. It may stay pending for a while when the host shows
native sheets (the consent sheet, settlement confirmation) — don’t wrap it in a
short timeout.
| Field | Type | Description |
|---|
amountCents | number | The amount to charge, in whole US cents. Must be a positive integer — anything else throws invalid_amount before reaching the host. |
memo | string | Optional. A short human-readable label attached to the payment (e.g. an order or item id). Trimmed and truncated to 80 characters. |
Calling pay() again creates a second charge — don’t blind-retry from the
client. If a call fails ambiguously, reconcile on your server first: every
settled payment has a unique signature, so store the signatures you’ve granted
and never grant twice.
Verify on your server
Treat the signature as a claim to check, not proof. Before you grant anything of
value, confirm on your server that the payment:
- has settled,
- was for the amount you expected,
- to your merchant wallet,
- and hasn’t already been redeemed (guard against replay by storing the
signature — it’s unique per payment).
A verification helper is provided by the Bankroll team when your app is enabled;
use it to confirm a signature’s amount and recipient. Wire your amount and
recipient checks against it before going live.
The optional memo is a short label shown with the payment; match a settled
payment back to its order by the signature pay() returns.
Handling the two non-error rejections
Every failure is a BankrollError with a stable snake_case code (the host’s
original message is preserved in message). Two codes are expected outcomes,
not errors — the host has already handled the user experience, so don’t surface
a duplicate error toast:
e.code | What happened | What to do |
|---|
insufficient_funds | The user can’t cover the charge. Bankroll already showed its “add cash” sheet and funnels them to funding. | Quietly stop — no error. |
consent_declined | The user declined the payment consent prompt. | Quietly return them to where they were. |
import { bankroll, BankrollError } from "@joinbankroll/sdk";
try {
const signature = await bankroll.pay({ amountCents: 500 });
await confirmOnServer(signature);
} catch (e) {
if (e instanceof BankrollError) {
if (e.code === "insufficient_funds" || e.code === "consent_declined") return;
}
throw e; // anything else is a real failure
}
Never grant the purchased item from the client on a resolved pay() alone. A
resolved promise means the payment settled, but only your server’s
verification of the signature (amount + recipient + not-yet-redeemed) should
release value.
Error codes
All codes identity() and pay() can throw, as BankrollError.code:
| Code | Thrown by | Meaning |
|---|
unavailable | identity, pay | Not in a Bankroll host (mirrors status()). |
update_required | identity, pay | The Bankroll app is too old for this SDK (mirrors status()). |
consent_declined | identity, pay | The user declined the consent sheet. |
insufficient_funds | pay | Balance too low — the host already showed its add-cash sheet. |
invalid_amount | pay | amountCents isn’t a positive integer. |
capability_not_registered | identity, pay | The capability isn’t declared in your manifest. |
manifest_error | identity, pay | Your /.well-known/bankroll.json is missing or malformed. |
unknown | any | An unmapped host reason — the original message is preserved. |
The code union also reserves superseded_consent, per_charge_declined,
blocked_origin, and charge_cap_exceeded — stable names you can switch on
today.