RailbedDocs

Your own checkout

Keep buyers in your app or game. Your server starts the checkout session and gets the card providers that can take the payment; your screen shows them, and the buyer pays on the chosen provider's page.

When to build your own

Use this when the purchase belongs inside your product: an in-game store, an app's upgrade screen, a checkout with your own layout. Railbed still does the hard parts: it picks the providers that can take the payment, hands the buyer over and checks the settlement. You own everything the buyer sees before and after the provider's page.

Card details are never entered in your screen. The buyer always pays on the provider's own page, which keeps your product out of card-data scope.

The flow

  1. Your server creates the session, exactly as for the hosted checkout.
  2. Your server starts it with POST /v1/checkout_sessions/:id/start, passing the buyer's email and, if you know it, their two-letter country. The response lists the providers that can take this payment, each with a handoff_url.
  3. Your screen shows the providers. The one marked recommended is Railbed's best match for this buyer.
  4. The buyer picks one, and your page opens its handoff_url in a new tab, from the buyer's click.
  5. Your server waits for the result from the payment.paid webhook or by checking GET /v1/payments/:id, and your screen updates.

Start the session and get providers

curl -X POST \
  https://pay.railbed.io/v1/checkout_sessions/pay_7AAiYH0Ykt11ED4hmfiN/start \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "customer_email": "player1042@example.com", "country": "US" }'
Response (trimmed)
{
  "id": "pay_7AAiYH0Ykt11ED4hmfiN",
  "object": "checkout_session",
  "status": "open",
  "amount": "49.00",
  "currency": "USD",
  "started_at": 1790380611,
  "country": "US",
  "providers": [
    {
      "id": "stripe",
      "name": "Stripe",
      "note": "Card, Apple Pay or Google Pay",
      "recommended": true,
      "handoff_url": "https://pay.railbed.io/go/pay_7AAiYH0Ykt11ED4hmfiN?provider=stripe"
    },
    {
      "id": "paypal",
      "name": "PayPal",
      "note": "PayPal balance or card",
      "recommended": false,
      "handoff_url": "https://pay.railbed.io/go/pay_7AAiYH0Ykt11ED4hmfiN?provider=paypal"
    }
  ]
}
200 OK

Starting is safe to repeat: the session keeps the same deposit address and started_at, and a new email replaces the saved one. Providers change with the amount, currency and country, so fetch them when the buyer reaches your checkout screen rather than caching them.

Show providers and hand off

Render the providers however suits your screen: a list, cards or a menu. Show name and note, and highlight the one with recommended: true. When the buyer picks one, open its handoff_url in a new tab, from the click itself, so browsers don't block it, and keep your screen open to show the result.

In your checkout screen
<ul id="providers"></ul>
<p id="status" role="status">Choose how to pay.</p>
<script>
  // providers: what your server returned from the start call
  function showProviders(providers) {
    const list = document.getElementById('providers');
    for (const p of providers) {
      const a = document.createElement('a');
      a.href = p.handoff_url;
      a.target = '_blank';
      a.rel = 'noopener';
      a.textContent = p.name + (p.recommended ? ' (recommended)' : '');
      a.addEventListener('click', () => {
        document.getElementById('status').textContent =
          'Finish paying in the new tab. This page updates by itself.';
        waitForPayment();
      });
      const li = document.createElement('li');
      li.append(a, ' ', p.note);
      list.append(li);
    }
  }
</script>

Don't fetch a handoff_url from your server, frame it in an iframe or change its query. It must open in the buyer's own browser: providers check the buyer's real location and refuse to load inside frames. At the click, Railbed checks again using the buyer's own connection: if the chosen provider is no longer available or doesn't serve the country the buyer is in, the buyer lands on Railbed's page for this payment to choose another.

Show the result

Providers don't send buyers back to your app, so your screen has to find out for itself. Ask your server every few seconds while the screen is open; your server answers from the webhook it received, or from GET /v1/payments/:id.

In your checkout screen
async function waitForPayment() {
  // Your server, never Railbed directly
  const res = await fetch(`/api/orders/${orderId}/status`);
  const { status } = await res.json();
  if (status === 'paid') return showPaid();
  if (status === 'held') {
    return showMessage(
      'Your payment arrived and is being reviewed. We’ll email you.',
    );
  }
  if (status === 'failed' || status === 'expired') {
    return showMessage('The payment didn’t go through. Try again.');
  }
  setTimeout(waitForPayment, 4000);
}

Your server must also keep checking unfinished orders when nobody has the screen open, because buyers close tabs. Fulfil orders safely covers the background check.

Country

Pass country as the buyer's two-letter ISO code when you know it (for example from their account or their request's location). Railbed uses it to show only providers that serve that country. Never send your own server's location. When you leave it out, up to eight providers that fit the amount and currency are listed, including ones that serve only some countries, so pass it whenever you know it.

Errors when starting

StatusCodeMeaning
400invalid_emailNo email was saved on the session and none was sent. Send customer_email
400invalid_countrycountry isn't a two-letter code
409no_providersNo provider can take this amount in this currency right now. Nothing was started
409already_paid, held, failedThe payment already finished. Show its result
410expired, canceledThe session can no longer be paid. Create a new one
409unavailableThe account can't take payments right now (for Live, check the payout wallet)
429rate_limitedToo many starts. Wait for Retry-After seconds
502network_unavailableThe card network didn't answer. Retry in a minute

Updated · This page as Markdown