React checkout widget
@payitgo/react shows a payment on your own page, in a modal: the
customer chooses the coin, sees the exact amount, network, QR code, address
(and XRP destination tag), pays from their wallet, and watches the payment
confirm. It needs no key. Your server creates the payment and hands the
browser its checkout_token; the widget calls the
public checkout API and follows the
payment live.
| Install | npm install @payitgo/react |
| Source | github.com/payitgo/react-sdk |
| Needs | React 18 or 19; it brings qrcode.react and socket.io-client |
| Module | ESM, with TypeScript types. The components are marked 'use client' |
Show a payment
In the browser, a button that opens the checkout:
import { PayitgoButton, payitgoCheckout } from "@payitgo/react";
const adapter = payitgoCheckout({
baseUrl: "https://api.payitgo.co",
// Ask your server for the payment and return its checkout_token.
createPayment: async () =>
(await fetch("/orders/42/payitgo", { method: "POST" }).then((r) => r.json())).checkout_token,
});
export function PayButton() {
return (
<PayitgoButton
adapter={adapter}
payment={{ title: "Order #42", amount: { value: 49.9, currency: "USD" } }}
onSuccess={(result) => showThanks(result.paymentId)}
>
Pay with crypto
</PayitgoButton>
);
}
On your server, the route the widget calls creates the payment with your
secret key (Node.js library here; any library, or
POST /v1/payments, does the same) and
answers with its checkout_token:
import { Payitgo } from "@payitgo/node";
const payitgo = new Payitgo(process.env.PAYITGO_KEY!, { baseUrl: "https://api.payitgo.co" });
app.post("/orders/:id/payitgo", async (req, res) => {
const order = await orders.get(req.params.id);
// No token: the customer chooses the coin in the widget.
const payment = await payitgo.payments.create({ order_id: order.id, amount_usd: order.total });
res.json({ checkout_token: payment.checkout_token });
});
Never put a secret key in browser code. The checkout token only shows the payer's view of one payment.
Each time the widget opens it calls createPayment() again. While the order
has an open payment, creating it again returns that same payment
(safe retries), so a customer who closes and
reopens the widget sees the same address and amount.
onSuccess runs in the customer's browser and is not proof of payment.
Fulfil orders from the payment.confirmed webhook.
Without a server: a payment link
A payment link needs no key to
pay, so a page without a server of its own can start its payment from the
browser with POST /links/{id}/checkout:
const adapter = payitgoCheckout({
baseUrl: "https://api.payitgo.co",
createPayment: async () => {
const r = await fetch(`https://api.payitgo.co/links/${linkId}/checkout`, { method: "POST" });
const body = await r.json();
if (!r.ok) throw new Error(body.error); // "this payment link can no longer be paid"…
return body.checkout_token;
},
});
Next.js
Render the widget from a client component: the adapter and onSuccess are
functions, which a server component cannot pass down. Nothing renders on the
server; the modal opens in the browser.
The adapter
payitgoCheckout(options: PayitgoCheckoutOptions): PayitgoAdapter
Connects the widget to payitgo.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
createPayment |
(payment: PayitgoPayment) => Promise<string> |
yes | Called when the widget opens (and on "Start again"): returns the payment's checkout_token, as it is |
|
baseUrl |
string |
no | The address the package was released with | The gateway's address, https://api.payitgo.co. Trailing slashes are removed |
pollIntervalMs |
number |
no | 4000 |
How often to read the payment while the live connection is down |
fetch |
typeof fetch |
no | The global fetch |
For tests, or older runtimes |
What it calls, all from the browser, with no credentials:
| Call | When |
|---|---|
GET {baseUrl}/checkout/{checkout_token} |
When the widget opens, and every pollIntervalMs while the socket is down |
POST {baseUrl}/checkout/{checkout_token}/token |
The customer chose the coin (a 409, chosen already, reads the payment instead) |
POST {baseUrl}/checkout/{checkout_token}/simulate |
A test payment's simulate buttons |
socket.io at {baseUrl}/socket.io/, default namespace |
subscribe with the checkout token on every connection; each payment:update carries the whole payment |
All of them answer any origin, so no proxy is needed. A POST answered
429 is sent again once, a second later. If you put a proxy in front of the
gateway anyway (baseUrl: "https://shop.example/payitgo"), it must forward
WebSocket upgrades on /payitgo/socket.io/: a plain HTTP rewrite cannot carry
the WebSocket (the widget then polls instead).
PayitgoError
What the adapter throws: the API's message (capitalised) and HTTP status.
| Property | Type | Description |
|---|---|---|
message |
string |
"Payment not found", "This payment can no longer be paid"… |
status |
number |
The HTTP status, or 0 when the gateway could not be reached |
The widget shows them itself, with a way to try again; you only meet them in a custom adapter or around your own calls.
Components
PayitgoButton
A button that opens the checkout. It takes every
PayitgoCheckout prop except open, plus:
| Prop | Type | Description |
|---|---|---|
children |
ReactNode |
The label. Default: "Pay with crypto" |
className, style |
Your own styles for the button | |
disabled |
boolean |
|
onClose |
() => void |
The customer closed the checkout |
PayitgoCheckout
The modal itself, controlled: render it anywhere, it portals to
document.body while open. Every opening starts with createPayment().
| Prop | Type | Required | Description |
|---|---|---|---|
open |
boolean |
yes | |
onClose |
() => void |
yes | Escape, the close button, a click outside, or "Done" |
payment |
PayitgoPayment |
yes | Display text: title, amount: { value, currency: "USD" }, and optionally description, period, badge, lineItem, due. Once loaded, the amount shown is the payment's own |
adapter |
PayitgoAdapter |
no | payitgoCheckout({…}). Default: demoAdapter, an offline simulation |
onSuccess |
(result: PayitgoResult) => void |
no | Once, when the payment is confirmed: { paymentId, status: "confirmed", token, amount, txId }, amount a decimal string in token |
theme |
{ mode?: "dark" | "light", accent?: string } |
no | Dark by default; accent is any CSS colour |
returnLinks |
boolean |
no | Show the payment's success_url and cancel_url as "Back to the shop" links. Default false: on your own page, closing the modal is the way back |
What the customer sees
status |
The widget |
|---|---|
pending, no coin yet |
The payment's token_options, each at its price (cheapest first when you pass the fees on), and the time left to choose |
pending |
The exact amount and network, a QR code of the address, the address, the XRP destination tag (marked required), each with a copy button, and the countdown. At zero: "Time is up, checking…" until the gateway closes the payment |
partially_paid |
What arrived and what is still missing, to the same address |
detecting |
"Payment received, confirming…" with its confirmations; no address, so nobody pays twice |
confirmed |
"Paid", what arrived, the transaction |
expired |
"This payment expired". It keeps following the payment: money that arrives within 24 hours still completes it. A new payment is offered at once only if no coin was chosen; after an address was shown, only once the customer confirms they sent nothing |
underpaid |
What arrived in time, and that the rest can still be sent within 24 hours |
failed |
"The payment could not be confirmed", with the order id |
Every transfer in deposits[] is listed with its confirmations, marked
"Not counted" (another coin) or "Late". A payment
priced in your currency shows
that price next to the dollars. An unknown checkout token shows "Payment not
found", with no retry and no new payment.
Test payments
A test payment shows a "Test payment" banner and buttons that play what the customer does, through the real processing: deposits, status changes and your webhooks (Test mode). Live payments never show them.
| Button | Outcome |
|---|---|
| Pay in full | confirmed |
| Overpay | confirmed with overpaid: true |
| Pay half | partially_paid; the payment stays open |
| Underpay | partially_paid, then underpaid |
| Let it expire | expired (the only one before a coin is chosen) |
| Drop the transaction | detecting, then failed |
| Pay in another coin | A deposit in another coin (not on Bitcoin, Litecoin, Dogecoin or XRP) |
| Pay late | partially_paid, underpaid, then confirmed with paid_late: true |
Custom adapters
PayitgoAdapter is an interface: start(payment) returns a checkout token,
get(checkoutToken) and chooseToken(checkoutToken, token) return the
payment as GET /checkout/{token} does.
Two optional methods turn on more: subscribe(checkoutToken, { onPayment, onConnection, onNotFound }) follows the payment live (it returns the
function that stops), and simulate(checkoutToken, outcome) shows the
simulate buttons on test payments. Without subscribe, the widget polls
get() every pollIntervalMs.
demoAdapter simulates everything offline, with addresses no wallet accepts:
for previews and design work.
Content Security Policy
The widget adds one <style> element to the page the first time it opens,
and styles some elements inline: a style-src must allow 'unsafe-inline'.
connect-src must allow the gateway, over https: and wss:.