Custom checkout
Build your own stablecoin payment interface on top of a Paybytoken Checkout Session.
Custom checkout keeps the order, supported tokens, payment destination and payment lifecycle inside Paybytoken while your application owns every customer-facing component.
Choose it when you need a payment experience that looks and behaves like the rest of your product and you are prepared to build token selection, chain selection, transfer instructions, QR display, expiration and error states.
How custom checkout works
- Your server creates a Checkout Session with
ui_mode: 'custom'. - Your browser displays the session's supported currencies and chains.
- The customer chooses one exact
supported_tokens[].id. - Your server confirms that token using the session CSRF token.
- Paybytoken returns the exact amount, chain and one-time payment address.
- Your interface displays wallet or manual-transfer instructions.
- Your server fulfills only after a verified
payment_intent.succeededevent.
The browser never receives your secret API key or the session CSRF token.
1. Create the session on your server
import Paybytoken from '@paybytoken/node'
const paybytoken = new Paybytoken(process.env.PAYBYTOKEN_SECRET_KEY!)
const session = await paybytoken.checkoutSessions.create({
currency: 'usd',
line_items: [
{
quantity: 1,
unit_amount: '50.00',
product_data: {
name: 'Account balance top-up',
},
},
],
supported_tokens: [
{ currency: 'USDC', chain: 'base' },
{ currency: 'USDC', chain: 'ethereum' },
{ currency: 'USDT', chain: 'base' },
],
ui_mode: 'custom',
metadata: {
order_id: 'order_1234',
},
})Store the session ID and csrf_token in server-side storage scoped to the authenticated customer.
Return only the browser-safe fields:
return {
session_id: session.id,
supported_tokens: session.supported_tokens,
expired_at: session.expired_at,
}Do not send csrf_token to the browser, place it in a cookie readable by JavaScript or include it
in a URL.
2. Render currency and chain choices
The response contains one supported-token object for each enabled currency and chain combination:
{
"id": "tok_base_usdc",
"currency": "USDC",
"name": "USD Coin",
"chain": "base",
"chain_name": "Base",
"chain_id": 8453,
"decimals": 6,
"currency_logo_url": "https://cdn.example.com/tokens/usdc.png",
"chain_logo_url": "https://cdn.example.com/chains/base.png"
}You can group these objects into separate currency and chain controls, but preserve the selected
object's id. Send that ID back to your server; do not reconstruct it from user-provided currency
or chain strings.
3. Confirm on your server
Your browser posts only the session ID and selected token ID to an authenticated merchant route:
const stored = await checkoutDrafts.get({
customerId: authenticatedCustomer.id,
sessionId: body.session_id,
})
if (!stored) throw new Error('Checkout session not found')
const instructions = await paybytoken.checkoutSessions.confirm(
stored.sessionId,
{ selected_token_id: body.selected_token_id },
stored.csrfToken,
)
return instructionsThe Paybytoken API checks that the session belongs to the merchant, is still open, has not expired and includes the selected token.
4. Display payment instructions
Confirmation returns the values needed to render the transfer step:
{
"session_id": "chk_test_RzULZl7bkylpMyo7bkHxinc2",
"payment_intent_id": "pay_t24c9qLoGieNDhvHVHWo",
"payment_address": "0x4A20...9C1F",
"amount": "50.00",
"amount_atomic": "50000000",
"currency": "USDC",
"chain": "base",
"token_id": "tok_base_usdc",
"contract_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6,
"chain_id": 8453,
"expired_at": "2026-07-29T10:30:00.000Z"
}For a manual-transfer interface, show:
- the exact decimal amount and currency;
- the selected chain name;
- the one-time payment address;
- a scannable QR code containing the payment address;
- the expiration time; and
- a clear warning not to change the token, chain or amount.
Use amount_atomic, contract_address and chain_id when constructing a connected-wallet
transaction. They are implementation values and do not need to appear in a customer-facing manual
transfer screen.
5. Track and fulfill
Keep the order pending while Paybytoken watches the selected chain. A copied address, wallet submission or browser confirmation is not proof of payment.
Verify the signed webhook and fulfill the order exactly once after
payment_intent.succeeded. Follow Fulfill orders with webhooks.
Customer states to design
Your custom interface should handle:
- loading and session-creation failure;
- currency and chain selection;
- confirmation in progress;
- exact transfer instructions;
- session expiration;
- payment detected and confirming;
- payment succeeded;
- cancellation; and
- wrong-chain, wrong-token or insufficient-amount guidance.
Keep safety warnings, exact values and payment status visually distinct from merchant-controlled branding.
Security checklist
- Create and confirm sessions only from authenticated server routes.
- Bind the stored session and CSRF token to the current merchant customer.
- Accept only a token ID present in the server-created session.
- Never accept prices, destination addresses or token contracts from browser input.
- Never reuse a Checkout Session for another customer or payment attempt.
- Use a verified webhook or authenticated retrieval for fulfillment.
- Redact credentials, CSRF tokens and payment addresses from analytics and general-purpose logs.
Try the complete flow in the Paybytoken Playground, then review the Checkout Sessions API.
Did this page answer your question?
Your feedback helps us improve the integration path.