# Integrate Railbed into an existing app

> The short path for adding Railbed to an app that already sells. Who handles the keys, the smallest safe webhook receiver, what each status means, safe retries, what the API can't do yet, reconciliation and what buyers see.

Source: https://railbed.com/docs/guides/existing-app/ · Updated: 2026-10-11 · Railbed by MediaGlobe developer docs

For AI assistants: the index of every docs page, with the rules for an integration, is https://railbed.com/docs/llms.txt

## Who does what

Two secrets connect your app to Railbed, and a person who owns or develops the business handles both in the dashboard. Your coding agent never needs to see them: it writes code that reads them from your server's environment by name.

| Step | Who | Where |
|---|---|---|
| Create a secret key: `rb_test_…` to build, `rb_live_…` to go live | An owner or developer | [Developers](https://app.railbed.io/developers) → **Create key** |
| Add the webhook endpoint | An owner or developer, or an assistant through the [MCP server](https://railbed.com/docs/mcp.md) with your confirmation | Developers → **Add endpoint** |
| Reveal the endpoint's signing secret (`whsec_…`) | An owner or developer | The endpoint's card → **Reveal signing secret** |
| Put both into the app's server configuration, such as `RAILBED_SECRET_KEY` and `RAILBED_WEBHOOK_SECRET` | An owner or developer | Your host's secret settings or environment |
| Write and test the code | You or your coding agent | Your app |

Keep both secrets out of chats, prompts and repositories. If one ends up somewhere it shouldn't, replace it in Developers (create a new key and revoke the old one, or roll the endpoint's secret), then update your server. For configuration checks, [Secret keys](https://railbed.com/docs/api.md#secret-keys) gives the key's format and [How deliveries are signed](https://railbed.com/docs/webhooks/signatures.md#how-deliveries-are-signed) the signing secret's.

An assistant connected through the MCP server can add or edit the endpoint (you confirm each change in Railbed), read payments and orders, and list an endpoint's deliveries with each one's outcome and HTTP status. It never returns secret keys or signing secrets.

### Secret keys and agent tokens

The API accepts two kinds of credential. A coding agent connected through [agent sign-in](https://railbed.com/docs/agents.md#connect-an-agent) gets its own access tokens, which last about five minutes and which it renews itself. Approved in Test with **Full access**, it can create sessions, simulate payments and read the results while you build, with no key to paste. Your production server uses a secret key: it belongs to the business and works until someone revokes it. An agent's access ends for good when the person who approved it leaves the business or loses the role that manages keys, so don't run production on it.

## The minimum safe receiver

1. Read the request's raw body before any JSON parsing, and verify `Railbed-Signature` against it with the signing secret. Reject a bad signature, or a timestamp more than five minutes from your clock, with a `4xx`. See [Verify signatures](https://railbed.com/docs/webhooks/signatures.md).
2. Deduplicate on the event `id`: record it with a unique constraint together with the event's effects (or with the queued job that carries them out), and answer a duplicate with `2xx`. Recorded on its own first, an event whose handling then failed would be skipped when it comes again.
3. Beyond its `id`, take only the event's `type`, `livemode` and `data.payment.id` (and, for a Test event, its payment's `metadata.sample`, to skip [samples](#testing)). An endpoint only receives events of its own mode, so a `livemode` that doesn't match your key's mode means the server has the other mode's key or secret: fail the delivery so it shows in the log.
4. Read the payment with `GET /v1/payments/:id` and act on that answer. Fulfil only when it says `paid`, once per payment, after matching it to your saved order ([the checks](https://railbed.com/docs/guides/fulfilment.md#check-before-you-grant)).
5. Answer `2xx` within 10 seconds, only after the event is handled. A slower answer, or a redirect, counts as a failure and is retried. If handling fails, answer `5xx` and the same event comes again.

If fulfilment can take more than a second or two, record the event, answer `2xx` and run step 4 in a background job instead, as the [receivers on the Webhooks page](https://railbed.com/docs/webhooks.md#a-receiver-end-to-end) do.

> [!IMPORTANT]
> The event's `data.payment` and the API's payment are two shapes. The event uses camelCase names and timestamps in milliseconds (`paidAt`); the API uses snake_case and Unix seconds (`paid_at`). Re-reading means you parse one shape, and you always act on the current state.

Example: Node\.js: after the signature is verified

```js
// railbed() is the helper from /docs/api/errors/,
// fulfilFromPayment() is from /docs/guides/fulfilment/
const LIVE = process.env.RAILBED_SECRET_KEY.startsWith('rb_live_');

async function handleEvent(event) {
  if (event.livemode !== LIVE) {
    // This endpoint's mode isn't the key's: fail, so the delivery log shows it
    throw new Error('mode mismatch: check RAILBED_SECRET_KEY and RAILBED_WEBHOOK_SECRET');
  }
  if (!event.type.startsWith('payment.')) return; // ping, or a type you don't handle
  let payment;
  try {
    payment = await railbed(`/payments/${event.data.payment.id}`);
  } catch (err) {
    // Test samples from "Send test event" carry an id the API doesn't know
    const sample = event.data.payment.metadata?.sample === 'true';
    if (err.status === 404 && !event.livemode && sample) return;
    throw err; // answer 5xx, so the event is retried
  }
  await fulfilFromPayment(payment); // does nothing unless status is 'paid'
}
```

## Every status, and what to do

| Status | What it means | What to do |
|---|---|---|
| `open` | Created, or started but not yet at a provider | Wait |
| `pending` | The buyer went to a provider to pay, or was shown a crypto address | Wait |
| `paid` | The money arrived and passed Railbed's checks | Fulfil, once |
| `held` | Money arrived but failed a check | Don't fulfil. Review it: when `hold_acceptable` is true, **Accept as paid** in the dashboard makes it `paid` and sends `payment.paid` |
| `failed` | A terminal failure (Test mode only today) | Close the attempt. A retry needs a new session |
| `expired` | Not paid in time: 24 hours for API sessions | Close it on your side, but keep listening: money that arrives late still settles, and the payment becomes `paid` or `held` |
| Anything else | A status this table doesn't list | Treat it as not paid and keep reconciling |

[How payments work](https://railbed.com/docs/how-it-works.md#statuses) explains each status. A read right after an event can already be past it: [Reading the payment back](https://railbed.com/docs/webhooks/events.md#reading-back) lists what to expect after each one. Right after `payment.started`, for example, the payment still reads `open`.

Railbed only adds to `v1`, and your code should expect that: ignore fields and event types it doesn't know, branch on the error codes it knows and fall back on the HTTP status for others, and never treat a status other than `paid` as paid. See [Versioning](https://railbed.com/docs/api.md#versioning).

## Retry creates safely

One rule: save the exact request body with its `Idempotency-Key` before the first call, and send that same body on every retry.

- `409 idempotency_in_progress`: the first request with that key hasn't finished. Wait `Retry-After` seconds and send the same request again.
- `409 idempotency_conflict`: the body differs from the first one. Never retry it with the same key.

The key's fingerprint covers `amount`, `currency`, `description`, the buyer's email (`customer_email` or `customer.email`), `reference`, `success_url`, `cancel_url` and `metadata` (key order doesn't matter; spaces inside values do), plus every field of `order` and `customer` when the request sent either. Values are compared after normalization, so `"49"` and `"49.00"` match, but rebuilding a retry from current data turns a changed email, a renamed item or a new return-URL domain into a conflict.

If a create timed out before you saved the `pay_…` id, retry it with the same key and body within 24 hours: you get the session if it was made, or it's created then if it wasn't. After 24 hours, or if the key was lost, `GET /v1/payments?reference=…` lists the sessions created with your reference, so check it before creating another. [Idempotency](https://railbed.com/docs/api.md#idempotency) has the full rules.

## What the API can't do yet

### Cancel a checkout session

There's no cancel endpoint for checkout sessions. To close one, mark it closed in your own system and stop offering its `url`. Its hosted page stays payable until the session's `expires_at`, 24 hours after it was created, and a buyer who started paying before then can still finish later. So when a session you closed turns `paid`, honour it for that session: fulfil it, or refund it from your wallet. When you create a newer session for the same purchase, send the buyer only to the newest `url`.

### Refunds

There's no refund endpoint, and Railbed can't reverse a settled payment. Refund the buyer yourself, then record the refund on the order with `POST /v1/orders/:id/events`, type `refunded`, under your own stable event id. The record changes no status, sends no webhook and moves no money; it puts the refund on the order's timeline for your team. [Record a refund](https://railbed.com/docs/guides/fulfilment.md#refunds) has the steps and code.

## Reconcile in the background

Webhooks can miss: a server that was down for longer than the retries, or a deploy that dropped a request. Run a background job that rechecks your saved, unresolved payment ids with `GET /v1/payments/:id`: everything `open`, `pending` or `held`, and anything that `expired` in the last two days.

Budget it. The limit is about 120 requests a minute per mode, shared by all of the account's secret keys. It's approximate, and responses carry no rate-limit headers today, so stay well below it: for example, one pass every 5 minutes over a bounded batch, and buyer status screens that read Railbed at most once per payment every 10 seconds. On a `429`, wait `Retry-After` seconds. [Check unfinished orders in the background](https://railbed.com/docs/guides/fulfilment.md#check-unfinished-orders-in-the-background) has the schedule.

## What buyers can pay with

Buyers pay by card, or with another method the provider they use supports, such as Apple Pay, Google Pay, PayPal or a bank transfer, always on the provider's own page. Each provider in a [start](https://railbed.com/docs/api/checkout-sessions.md#start-a-checkout-session) response has an `id` and a `note` saying how buyers pay with it, as Railbed's checkout shows it. Some of today's:

| `id` | `note` |
|---|---|
| `stripe` | Card, Apple Pay or Google Pay |
| `paypal` | PayPal balance or card |
| `cashapp` | Cash App balance or card |
| `coinbase` | Card or Coinbase account |
| `rampnetwork` | Card, Apple Pay or bank transfer |

Which providers a buyer sees depends on their country, the amount, the currency and each provider's availability, so the way to know is what the hosted page or the start response offers. When the merchant turns on [crypto payments](https://railbed.com/docs/user-guide/crypto-payments.md), the hosted page also lets buyers send stablecoins; card stays first, and an API session reaches crypto only on its hosted `url`.

### Minimum amounts

Sessions accept amounts from `"1.00"`, but each provider has its own minimum. When an amount is below every eligible provider's minimum, `POST /v1/checkout_sessions/:id/start` answers `409 no_providers` and starts nothing, and the hosted page tells the buyer that no card provider can take the amount right now. The minimums are the providers' and they change: in Live mode, **Integrations** → **Card providers** in the dashboard shows each one's current minimum (Test mode shows a fixed list). Check your lowest price against it.

## What the buyer sees

A hosted checkout involves three tabs:

1. **Your page.** The buyer chooses to buy, and you send them to the session's `url`.
2. **Railbed's checkout tab.** It shows your business, the description and the price, takes the buyer's email and, from the buyer's click, opens the chosen provider in a new tab. It stays open and updates by itself.
3. **The provider's tab.** The buyer pays there. Providers don't send buyers back: their last page may be the provider's own or another company's.

Once the verified settlement marks the payment `paid`, Railbed's tab sends the buyer to your `success_url`. If the buyer closed that tab, no redirect happens, so your pages should get the status from your server. If the browser blocks the new tab, the provider opens in the same tab, and the buyer sees your page only if they come back. [Hosted checkout](https://railbed.com/docs/guides/hosted-checkout.md#what-the-buyer-sees) has the details.

- **Card statement.** The provider charges the card under its own terms and sets the statement descriptor, so the charge carries the provider's name. Say so near your buy button or in your order email, so buyers recognize the charge.
- **Receipts.** Railbed doesn't email buyers a receipt. Its checkout shows an on-screen confirmation when there's no `success_url`, and the provider may send its own receipt to the email the buyer gave. Send your own order confirmation from your app.

## Test without moving money

- Use a Test key and a Test endpoint. Everything is simulated, and Test webhooks are real, signed HTTP requests to your endpoint.
- Start a session and call `POST /v1/payments/:id/simulate` with `paid`, `underpaid`, `declined` or `failed` to run each path through the same checks and events as a live payment. [Testing](https://railbed.com/docs/testing.md) has the test plan.
- **Send test event** in Developers sends a sample with a made-up payment: its `metadata.sample` is `"true"` and its `pay_…` id isn't in the API, so a receiver that re-reads it gets `404 not_found`. Acknowledge a sample with `2xx` instead of failing, or the delivery shows as failed. See [Test events](https://railbed.com/docs/webhooks.md#test-events).
- A coding agent connected through agent sign-in, in Test with **Full access**, can create and simulate payments on its own.

## Next

- [Fulfil orders safely](https://railbed.com/docs/guides/fulfilment.md): The checks before a grant, background reconciliation and refunds.
- [Webhooks](https://railbed.com/docs/webhooks.md): Endpoints, retries, the delivery log and a receiver in three languages.
- [Hosted checkout](https://railbed.com/docs/guides/hosted-checkout.md): Create a session per order and send the buyer to its page.
- [Connect an AI assistant](https://railbed.com/docs/mcp.md): Let an assistant set up your endpoint and diagnose deliveries, with your confirmation.
