RailbedDocs

Fulfil orders safely

Deliver each order exactly once, only for money that really arrived. The checks that keep fulfilment correct through retries, duplicate events, late payments and reviews.

The rule

Fulfil an order only when your server has seen its payment as paid from Railbed itself: a webhook whose signature you verified, or an authenticated GET /v1/payments/:id. Never fulfil from the buyer's return to your success_url, a query parameter, or anything the buyer's browser reports.

Save before you create

Before calling Railbed, save the order with everything that decides what the buyer gets: the user, the items, the price, the currency and the Idempotency-Key you'll send. After the create call, save the returned pay_… id on the order. If the call times out, retry it with the same key: you get the same session back, never a second payment.

Check before you grant

When a payment reports paid, look up your order by the payment's reference (or its id) and check, in this order:

  1. It's the right payment. The payment id matches the one saved on the order, and the mode matches (livemode in the API, livemode on the event).
  2. It's for this order. reference, and any metadata you set, match the order.
  3. It's the price you asked. amount and currency match the order. They can't change after creation, so a mismatch means you're looking at the wrong payment.
  4. It's paid. status is paid. Not held, not pending.
  5. It isn't done already. Record the grant with a unique constraint on the payment id (or the order), in the same database transaction as the delivery itself. A second event for the same payment then changes nothing.

Resolve the customer from your own order, never from the payment's customer_email: buyers can type any email at checkout.

Node.js: one grant per payment, in one transaction
async function fulfilFromPayment(payment) {
  // Held, pending, expired: nothing to deliver yet
  if (payment.status !== 'paid') return;
  const order = await db.orders.findByPaymentId(payment.id);
  // Not ours (another store sharing the endpoint), or not saved yet
  if (!order) return;
  if (payment.livemode !== order.livemode) {
    throw new Error('mode mismatch');
  }
  if (payment.reference !== order.reference) {
    throw new Error('reference mismatch');
  }
  if (payment.amount !== order.price || payment.currency !== order.currency) {
    throw new Error('amount mismatch');
  }

  await db.transaction(async (tx) => {
    // UNIQUE(payment_id): a duplicate event or a second worker fails here
    // and delivers nothing
    const inserted = await tx.grants.insertIfAbsent({
      paymentId: payment.id,
      orderId: order.id,
    });
    if (!inserted) return;
    await tx.inventory.deliver(order);
    await tx.orders.markPaid(order.id, payment.paid_at);
  });
}

Webhooks, the API, or both

SignalStrengthsWatch out for
payment.paid webhookArrives the moment the payment settles; retried for about a dayYour endpoint must be reachable, answer 2xx quickly and verify signatures
GET /v1/payments/:idAlways current; no public endpoint neededYou decide when to ask: poll gently, with backoff

The most robust integrations use both: fulfil on the webhook, and run a background job that checks orders still waiting. The job catches anything a webhook missed (your server was down for a day, a deploy dropped a request) without anyone watching.

Check unfinished orders in the background

Run a job every few minutes that checks each order still waiting on a payment, gently:

  • Check recent orders often and older ones less often (for example after 1, 5, 15 and 60 minutes, then hourly).
  • Include orders whose payment expired in the last two days. Money can arrive after the 24-hour window, and the payment then becomes paid or held.
  • Stop checking a payment once it's paid, failed, or has been expired for two days.
  • Respect the shared rate limit: 120 requests a minute per account. On a 429, wait for Retry-After seconds.
  • Survive restarts: keep the queue in your database, not in memory.

Held payments

held means money arrived but didn't pass a check, most often because the provider delivered less than 90% of the order's value. Don't fulfil. You can review the payment in the dashboard and Accept as paid when the shortfall is fine with you; the payment then becomes paid and payment.paid is sent, so your normal fulfilment path handles it. Payments held because the money may have gone somewhere else can't be accepted; contact support.

Listen for payment.held if you want to tell the buyer their payment is being reviewed.

Late payments

A buyer can open a provider's page, leave, and come back to finish after the session's 24 hours. The payment was expired, and then becomes paid (or held). If your system cancelled the order on payment.expired, decide what a late payment means for you: fulfil it, or refund it from your wallet. Either way, don't ignore a payment.paid because the order was once marked abandoned.

Refunds

Railbed can't reverse a settled payment; the money is already in your wallet. To refund, send USDC from your wallet to the buyer (ask them for an address), or refund in another way you both agree. Card disputes are handled by the provider that charged the card.

Updated · This page as Markdown