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.
On this page
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 → Create key |
| Add the webhook endpoint | An owner or developer, or an assistant through the MCP server 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 gives the key's format and 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 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
- Read the request's raw body before any JSON parsing, and verify
Railbed-Signatureagainst it with the signing secret. Reject a bad signature, or a timestamp more than five minutes from your clock, with a4xx. See Verify signatures. - 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 with2xx. Recorded on its own first, an event whose handling then failed would be skipped when it comes again. - Beyond its
id, take only the event'stype,livemodeanddata.payment.id(and, for a Test event, its payment'smetadata.sample, to skip samples). An endpoint only receives events of its own mode, so alivemodethat 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. - Read the payment with
GET /v1/payments/:idand act on that answer. Fulfil only when it sayspaid, once per payment, after matching it to your saved order (the checks). - Answer
2xxwithin 10 seconds, only after the event is handled. A slower answer, or a redirect, counts as a failure and is retried. If handling fails, answer5xxand 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 do.
// 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 explains each status. A read right after an event can already be past it: Reading the payment 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.
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. WaitRetry-Afterseconds 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 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 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 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 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, 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:
- Your page. The buyer chooses to buy, and you send them to the session's
url. - 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.
- 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 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/simulatewithpaid,underpaid,declinedorfailedto run each path through the same checks and events as a live payment. Testing has the test plan. - Send test event in Developers sends a sample with a made-up payment: its
metadata.sampleis"true"and itspay_…id isn't in the API, so a receiver that re-reads it gets404 not_found. Acknowledge a sample with2xxinstead of failing, or the delivery shows as failed. See 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 safelyThe checks before a grant, background reconciliation and refunds.
- WebhooksEndpoints, retries, the delivery log and a receiver in three languages.
- Hosted checkoutCreate a session per order and send the buyer to its page.
- Connect an AI assistantLet an assistant set up your endpoint and diagnose deliveries, with your confirmation.