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.

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:.