Checkout Flow
A full walkthrough of the payment lifecycle — from session creation to order fulfillment.
Payment Lifecycle
Session Status: pending ──► paid
│ │
│ └── webhook: payment.confirmed
│
└──► expired (no payment within session window)
│
└──► failed (deposit detected but unconfirmed)Option A: Hosted Payment Page (Recommended)
The simplest integration. Rach hosts the entire payment UI for your customers.
Step 1: Create session & redirect
javascript
// Backend
const session = await rach.checkout.create({
amount: 99.99,
currency: 'USD',
reference: 'ORDER-42',
customerEmail: req.user.email,
callbackUrl: 'https://yoursite.com/webhooks/rach',
});
// Redirect customer to hosted page
res.redirect(session.payment_url);
// → https://...run.app/pay/a1b2c3d4-e5f6-...Step 2: Customer experience on hosted page
- Customer lands on the Rach-hosted payment modal
- Selects blockchain network (BSC, ETH, POL, TRX, SOL, BTC, etc.)
- Selects payment currency (USDT, USDC, native coin)
- Sees a unique deposit address + QR code
- Sends crypto from their wallet
- Page polls for confirmation and shows a success screen
Step 3: Receive webhook and fulfill
javascript
app.post('/webhooks/rach', express.raw({ type: '*/*' }), async (req, res) => {
// 1. Verify signature
const sig = req.headers['x-webhook-signature'];
const mac = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body).digest('hex');
if (sig !== mac) return res.status(401).end();
// 2. Parse event
const { event, data } = JSON.parse(req.body);
// 3. Act
if (event === 'payment.confirmed') {
await db.orders.markPaid(data.reference, {
sessionId: data.session_id,
amount: data.amount,
currency: data.currency,
paidAt: data.paid_at,
});
await sendOrderConfirmationEmail(data.customer_email);
}
res.sendStatus(200);
});Option B: Custom Payment UI
Build your own payment screen using the Public Checkout API. Gives full control over the UX.
Step 1: Create session
Same as Option A — POST /api/v1/checkout/create.
Step 2: Load session on your frontend
javascript
// Frontend — no API key needed
const session = await fetch(`/api/v1/checkout/${sessionId}`).then(r => r.json());
// Display amount, currency, business name
renderPaymentPage(session);Step 3: Let customer select network
javascript
async function onNetworkSelect(network, currency) {
const { address, amount_crypto, qr_code, expires_at } =
await fetch(`/api/v1/checkout/${sessionId}/select-network`, {
method: 'POST',
body: JSON.stringify({ network, currency }),
headers: { 'Content-Type': 'application/json' }
}).then(r => r.json());
renderAddress(address, qr_code, amount_crypto);
startCountdown(expires_at);
}Step 4: Poll for confirmation
javascript
async function pollPaymentStatus() {
const { status, paid_at, paid_amount } =
await fetch(`/api/v1/checkout/${sessionId}/verify-now`).then(r => r.json());
if (status === 'paid') {
showSuccessScreen(paid_at, paid_amount);
} else if (status === 'expired') {
showExpiredScreen();
} else {
// Still pending — poll again in 10 seconds
setTimeout(pollPaymentStatus, 10_000);
}
}Partial Payments
If a customer sends less than the required amount, verify-now returns status: "partial" and the paid_amount. You can decide to:
- Accept the partial amount (update order total, fulfill partially)
- Show a "you need to send X more" message on your UI
- Expire the session and ask the customer to create a new order
Confirmation Thresholds
| Network | Mechanism | Threshold | Notes |
|---|---|---|---|
| ETH, BSC, POL | Block depth (eth_getTransactionReceipt) | 12 blocks | confirmations field shows real block count |
| BTC | Block depth (Esplora API) | 6 blocks | confirmations field shows real block count |
| TRX, SOL, LTC, BCH, XRP | Time-based settlement delay | — | confirmations: 0 even when confirmed — expected |
Do not fulfill orders based on
payment.detected— always wait forpayment.confirmed.
Session Expiry
Payment sessions expire after a fixed window (shown in expires_at). After expiry:
- The hosted payment page shows an "expired" message
GET /verify/{sessionId}returnsstatus: "expired"- No funds are expected — any late deposits should be handled via support
Create a new session if the customer wants to retry.
Error Codes
| HTTP | code field | Action |
|---|---|---|
402 | SUBSCRIPTION_EXPIRED | Renew your plan at the dashboard |
402 | TRANSACTION_LIMIT_EXCEEDED | Upgrade plan — monthly cap reached |
403 | — | Individual accounts cannot create checkout sessions |
404 | — | Session not found or wrong environment (test vs live key) |
