Payments
Hosted checkout
The fastest way to collect a payment: send the payer to a page that connects their own wallet and submits the on-chain payment for you. You write no wallet code and never handle a private key.
Where it lives
After you create an intent, redirect the payer to the hosted checkout, keyed by the intent_id:
https://demo.rsends.io/pay/pi_9f1c4a7e2b5d8036c1e94ab7d2508f63The intent_id is the only credential the page needs — it is 128 bits of randomness, so it is not guessable, but it is the key to that intent's checkout. Treat the link as you would a payment link anywhere else.
What the page does
The checkout re-fetches the intent's onchain instructions and walks the payer through a normal wallet flow:
- Connect a wallet. The payer connects their own wallet (MetaMask, Coinbase Wallet, WalletConnect, …). The funds and the keys are always theirs.
- Approve, then pay. For an ERC-20 token, the wallet approves the router for
onchain.total, then callspay(...). Native ETH skips the approval and callspayNative(...)withmsg.valueequal to the total. Tokens that support EIP-2612 (onchain.permitType: "eip2612", which includes USDC) can sign a permit instead of sending a separate approval transaction. - Settle atomically. One transaction: the router moves your
amountto your wallet and emitsPaymentMade. Your principal is never touched — on the currently-deployed router the payer additionally covers the flat fee, which is why the approval is fortotal, notamount.
// Read straight from intent.onchain — nothing is invented client-side.
router.pay(
invoiceId, // onchain.invoiceId (bytes32, derived from your reference)
merchant, // onchain.merchant (your destination wallet)
token, // onchain.token (e.g. USDC)
amount, // onchain.amount (base units — what YOU receive)
maxFee // onchain.maxFee (payer's ceiling; reverts above it)
)
// routerVersion 2 (mainnet cutover) drops the fee argument entirely:
// router.pay(invoiceId, merchant, token, amount)Don’t hardcode the ABI — read routerVersion
onchain.routerVersion tells you which one you are on, and onchain.calldata ships ready-to-send bytes for whichever it is. Building against the field rather than the shape means the mainnet cutover needs no change on your side.No card fields, no processor
The golden rule
When the payment lands, the browser will redirect or show a success screen. That is a UI hint, not proof of payment. A redirect can be faked, refreshed, or lost.
Never mark an order paid from the browser redirect
payment.completed webhook — verified by its signature — or the on-chain PaymentMade event read directly. Fulfil only after you have reconciled against one of those.Recommended flow
| Where | Do |
|---|---|
| Browser | Show a friendly “processing / thank you” state on redirect. Do not fulfil yet. |
| Server (webhook) | Receive payment.completed, verify the signature, then mark the order paid and fulfil. |
| Anywhere | Optionally cross-check the PaymentMade event on-chain for full independence. |
Build your own checkout?
You don't have to use the hosted page. Because the intent response hands you the full onchain object (router, routerVersion, invoiceId, token, amount, total, calldata, permitType), you can drive pay(...) from your own front-end. The settlement and reconciliation model is identical — the webhook and the chain remain the source of truth.
Your front-end can also read the payer-facing view of an intent directly, without an API key: GET /api/v1/public/payment-intent/{intent_id} returns status, amount, currency, chain, expires_at, merchant_name, tx_hash and the same onchain object — and nothing else. It is what the hosted checkout itself polls. The intent_id is the credential, so keep the link private; rate limit is 40/min per IP, plus a 240/min ceiling per intent across all callers.