Skip to content

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)

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

  1. Customer lands on the Rach-hosted payment modal
  2. Selects blockchain network (BSC, ETH, POL, TRX, SOL, BTC, etc.)
  3. Selects payment currency (USDT, USDC, native coin)
  4. Sees a unique deposit address + QR code
  5. Sends crypto from their wallet
  6. 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

NetworkMechanismThresholdNotes
ETH, BSC, POLBlock depth (eth_getTransactionReceipt)12 blocksconfirmations field shows real block count
BTCBlock depth (Esplora API)6 blocksconfirmations field shows real block count
TRX, SOL, LTC, BCH, XRPTime-based settlement delayconfirmations: 0 even when confirmed — expected

Do not fulfill orders based on payment.detected — always wait for payment.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} returns status: "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

HTTPcode fieldAction
402SUBSCRIPTION_EXPIREDRenew your plan at the dashboard
402TRANSACTION_LIMIT_EXCEEDEDUpgrade plan — monthly cap reached
403Individual accounts cannot create checkout sessions
404Session not found or wrong environment (test vs live key)

Rach Payments API