# Railbed documentation > Railbed is a card checkout for online merchants that settles every sale as USDC on Polygon to a wallet the merchant controls. Start with the user guide for account setup, payment links, checkouts, pricing tables, your wallet and everyday payment management. The developer docs cover the REST API, signed webhooks and custom integrations. Every page below is also available as Markdown; visual examples include text descriptions. Essentials for building against Railbed: - API base URL: `https://pay.railbed.io/v1`. JSON over HTTPS. Authenticate every call from your server with `Authorization: Bearer `. - Keys decide the mode: `rb_test_…` keys work only with simulated Test payments (no money moves), `rb_live_…` keys take real card payments. Create and revoke keys in the dashboard under Developers (https://app.railbed.io/developers). Never put a secret key in browser or mobile code; there is no publishable key. - Each key has an access level: Full access (every endpoint; the default) or Read only (scopes `payments:read`, `orders:read`, `customers:read`). Every endpoint needs one scope; a call outside the key's access level gets `403 insufficient_scope`, naming the scope. - AI agents acting for a merchant get their own access through agent sign-in: read https://railbed.com/auth.md and follow it. Register with the merchant's email address, give them the claim link, complete the claim with the code they read back, then exchange your identity assertion for an access token (about five minutes) and renew it yourself. Send it as `Authorization: Bearer `. It carries the business, mode and access level the merchant chose. `401 invalid_token`: get a new token; `401 agent_disconnected`: ask the merchant to connect you again. Never ask a merchant to paste a secret key into a chat. - Every 401 from the API carries `WWW-Authenticate: Bearer resource_metadata="https://pay.railbed.io/.well-known/oauth-protected-resource/v1"`, the API's protected resource metadata (RFC 9728). - The flow: create a checkout session (`POST /v1/checkout_sessions`), send the buyer to its `url` (or start it with `POST /v1/checkout_sessions/:id/start` and render the returned providers in your own checkout), then fulfil only when the payment is `paid`, confirmed by a signed `payment.paid` webhook or by `GET /v1/payments/:id`. Never fulfil from the buyer's return to `success_url`. - Send an `Idempotency-Key` header (1–120 printable ASCII characters, remembered for 24 hours) on every create so a retried request never makes a second payment. - Money is a decimal string (`"49.00"`, from 1.00 to 100000.00); currencies are USD, EUR, GBP, CAD and AUD. `/v1` timestamps are Unix seconds; in webhooks the event's `created` is Unix seconds and the payment object uses camelCase with millisecond timestamps. Payment ids look like `pay_…` and are both the session id and the payment id. - Payment statuses: `open` and `pending` (wait), `paid` (fulfil, once), `held` (money arrived but failed a check; never fulfil automatically, the merchant reviews it), `failed` (Test mode decline), `expired` (can still become `paid` or `held` if money arrives late). - Webhooks are signed: `Railbed-Signature: t=,v1=.">` keyed with the endpoint's whole `whsec_…` secret. Verify against the raw body, reject timestamps more than 5 minutes off, dedupe on the event `id`, answer 2xx within 10 seconds. Failed deliveries are retried 1m, 5m, 30m, 2h, 6h and 12h after each failure. - Orders and customers: send `order` (items, discount, shipping, tax, addresses; items − discount + shipping + tax must equal `amount`) and `customer` (name, phone, your customer id) with a session. Every session belongs to an order (`ord_…`); each email is one customer (`cus_…`). Send your order id as `order.id` so a retry joins the same order; a session for an order that's already paid answers `409 order_paid`. Read them with `GET /v1/orders`, `GET /v1/orders/:id` and `GET /v1/customers/:id`; set fulfilment with `PATCH /v1/orders/:id`. An order is `paid` only when its paid payments cover its `total`. - Webhook events: `payment.started`, `payment.paid`, `payment.held`, `payment.updated`, `payment.failed`, `payment.expired`, `payment.canceled`, and `ping` for tests. Each payment event carries `data.payment`, `data.order` (with up to 50 items) and `data.customer`. Ignore types you don't handle; events can arrive more than once and out of order. - Errors are `{"error": {"code", "message", "field"?}}`; branch on `code` and the HTTP status, never the message. 429 responses carry `Retry-After: 60`; limits are about 120 requests and 12 session starts a minute per account. - Test mode: `POST /v1/payments/:id/simulate` with `outcome` `paid`, `underpaid` or `failed` settles a started Test payment through the same checks and webhooks as a live one. - Card details are always entered on a licensed card provider's page, never on Railbed's pages or yours. There are no refund, subscription or event-list endpoints; refunds are sent from the merchant's wallet. ## Start here - [Your guide to getting paid](https://railbed.com/docs/user-guide.md): Set up your business, take your first card payment, and follow every sale to your wallet. Start here, with or without a website. - [Set up your account](https://railbed.com/docs/user-guide/getting-started.md): Go from a new account to a checkout you can share, with the right business details and payout wallet in place. - [Test and Live mode](https://railbed.com/docs/user-guide/test-and-live.md): Practice the buyer experience without moving money, then prepare a separate checkout for real customers. ## Take payments - [Create and share a payment link](https://railbed.com/docs/user-guide/payment-links.md): Request one payment for one customer. Add an invoice reference, share a link or QR code, and track it until it is paid. - [Create a reusable checkout](https://railbed.com/docs/user-guide/checkout-pages.md): Sell one product at one price with a page you can share again and again. Every buyer gets a separate payment. - [Offer plans with a pricing table](https://railbed.com/docs/user-guide/pricing-tables.md): Present up to four packages side by side, highlight one recommendation, and let buyers choose the right one. - [Add Railbed to your website](https://railbed.com/docs/user-guide/widgets-and-buttons.md): Keep customers on your site while they choose what to buy. Copy a small HTML snippet; Railbed handles the checkout. - [What your customer sees](https://railbed.com/docs/user-guide/buyer-experience.md): Understand the card checkout, provider choice and return journey so you can confidently guide a customer through payment. - [Accept crypto payments](https://railbed.com/docs/user-guide/crypto-payments.md): Let buyers pay in USDC or USDT from their own wallet, beside card. It's off until you turn it on, and the money goes to your payout wallet. ## Orders and customers - [Manage orders and fulfilment](https://railbed.com/docs/user-guide/orders.md): See what a customer bought, review their payment attempts, and keep track of what still needs to be delivered. - [Understand your customers](https://railbed.com/docs/user-guide/customers.md): See a customer's order history, contact details and spending. Keep a private note and manage the personal data saved in Railbed. - [Find records across Railbed](https://railbed.com/docs/user-guide/search.md): Jump to an order, customer, payment or setting from anywhere in your dashboard. Search by a name, amount, email or record ID. ## Manage your money - [Find and understand a payment](https://railbed.com/docs/user-guide/payments.md): Search across your payment links, checkouts and API orders. See what happened, what arrived and what to do next. - [Review a held payment](https://railbed.com/docs/user-guide/held-payments.md): Understand why a payment is being reviewed or underpaid, and when you can choose to accept the amount that arrived. - [Understand your wallet and settlement](https://railbed.com/docs/user-guide/wallet-and-settlement.md): Know where the money goes, how received totals are calculated, and why your checkout price and wallet balance can differ. ## Your business - [Make Railbed your own](https://railbed.com/docs/user-guide/branding-and-settings.md): Put your business name, logo and support details on checkout, and keep your account settings accurate. - [Connect Railbed to your store](https://railbed.com/docs/user-guide/integrations.md): Choose the connection that fits your business, from Shopify or a WordPress plugin to a checkout built by your developer. - [Use Railbed on your phone](https://railbed.com/docs/user-guide/mobile.md): Manage payments in your browser, or add the dashboard to your Home Screen for quicker access. ## Help and reference - [Find your next step](https://railbed.com/docs/user-guide/troubleshooting.md): Start with the symptom, check the current payment state, and take the next action without losing track of the order. - [Railbed, in plain language](https://railbed.com/docs/user-guide/glossary.md): A short reference for the words you will see in checkout, the dashboard and your payment records. ## Get started - [Railbed developer docs](https://railbed.com/docs/index.md): Take card payments that settle as USDC to a wallet you control. Create checkouts from your server, send buyers to a hosted page or build your own, and fulfil orders from signed webhooks. - [Quickstart](https://railbed.com/docs/quickstart.md): Take your first payment in Test mode. Create a secret key, create a checkout session, pay it as a buyer, and confirm it from your server. - [How payments work](https://railbed.com/docs/how-it-works.md): The life of a Railbed payment, from the checkout to the USDC in your wallet, and what each status means for the order behind it. - [Testing](https://railbed.com/docs/testing.md): Test mode simulates payments end to end, with no card charged and no money moved. Make payments succeed, fall short or be declined on demand, send sample webhook events, and go live with confidence. ## Guides - [Hosted checkout](https://railbed.com/docs/guides/hosted-checkout.md): Create a checkout session for each order on your server, send the buyer to Railbed's hosted page, and bring them back to your store once they've paid. - [Your own checkout](https://railbed.com/docs/guides/custom-checkout.md): 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. - [Fulfil orders safely](https://railbed.com/docs/guides/fulfilment.md): 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. - [Payment links and buy buttons](https://railbed.com/docs/guides/no-code.md): Take payments without writing server code. Create payment links, checkout pages and pricing tables in the dashboard, and add a buy button or an embedded checkout to any website with two lines of HTML. - [WooCommerce](https://railbed.com/docs/guides/woocommerce.md): Add Railbed card checkout to a WordPress store with the Railbed for WooCommerce plugin. Orders complete from verified payments, and their items, customers and fulfilment appear in Railbed, with no code. ## API reference - [API reference](https://railbed.com/docs/api.md): The Railbed REST API. JSON over HTTPS, secret keys for your servers and access tokens for connected agents, scopes, idempotent creates, cursor pagination and plain error codes. - [Checkout sessions](https://railbed.com/docs/api/checkout-sessions.md): A checkout session is one payment for one order. Create it on your server, send the buyer to its page or start it for your own checkout, and read its payment to fulfil. - [Payments](https://railbed.com/docs/api/payments.md): A payment is what a checkout session became. Read it to fulfil orders, list payments to reconcile, and simulate outcomes in Test mode. - [Orders](https://railbed.com/docs/api/orders.md): An order is what a buyer tried to buy, with its items, totals, addresses and customer. Every checkout session belongs to one. Read orders to fulfil them, and report fulfilment, cancellations and refunds back from your store. - [Customers](https://railbed.com/docs/api/customers.md): A customer is one buyer's email address in one mode, with the contact details and addresses their orders brought. Read customers to see who bought what, and find one by email. - [Errors](https://railbed.com/docs/api/errors.md): Every error the Railbed API returns, with its HTTP status, what caused it and what to do next. ## Webhooks - [Webhooks](https://railbed.com/docs/webhooks.md): Railbed sends a signed HTTPS request to your server when a payment starts, settles, is held, expires or is canceled. How endpoints, deliveries, retries and the delivery log work. - [Event types](https://railbed.com/docs/webhooks/events.md): Every webhook event Railbed sends, when it's sent, what the payment, order and customer look like at that moment, and the full payload reference. - [Verify signatures](https://railbed.com/docs/webhooks/signatures.md): Prove each webhook came from Railbed and wasn't changed. The signing scheme, verification code in six languages, how to get the raw body in common frameworks, and a test vector. ## Resources - [Build with AI agents](https://railbed.com/docs/agents.md): Everything in these docs is available to AI assistants and coding agents: llms.txt, a Markdown copy of every page, a structured index, and WebMCP tools. An agent can also connect to your account through agent sign-in, with the access you choose. ## API endpoints - [`POST /v1/checkout_sessions`](https://railbed.com/docs/api/checkout-sessions.md#create-a-checkout-session): Create a checkout session - [`GET /v1/checkout_sessions/:id`](https://railbed.com/docs/api/checkout-sessions.md#retrieve-a-checkout-session): Retrieve a checkout session - [`POST /v1/checkout_sessions/:id/start`](https://railbed.com/docs/api/checkout-sessions.md#start-a-checkout-session): Start a checkout session - [`GET /v1/payments/:id`](https://railbed.com/docs/api/payments.md#retrieve-a-payment): Retrieve a payment - [`GET /v1/payments`](https://railbed.com/docs/api/payments.md#list-payments): List payments - [`POST /v1/payments/:id/simulate`](https://railbed.com/docs/api/payments.md#simulate-a-payment): Simulate a payment - [`GET /v1/orders/:id`](https://railbed.com/docs/api/orders.md#retrieve-an-order): Retrieve an order - [`GET /v1/orders`](https://railbed.com/docs/api/orders.md#list-orders): List orders - [`PATCH /v1/orders/:id`](https://railbed.com/docs/api/orders.md#update-an-order): Update an order - [`POST /v1/orders/:id/events`](https://railbed.com/docs/api/orders.md#report-a-cancellation-or-refund): Report a cancellation or refund - [`GET /v1/customers/:id`](https://railbed.com/docs/api/customers.md#retrieve-a-customer): Retrieve a customer - [`GET /v1/customers`](https://railbed.com/docs/api/customers.md#list-customers): List customers ## Webhook events - [`payment.started`](https://railbed.com/docs/webhooks/events.md#payment-started): The buyer entered their email and was given a way to pay: Railbed assigned the payment's deposit address and locked the fee and the order's USD value. The payment is still open (it becomes pending when the buyer reaches a provider), and provider is usually still null. - [`payment.paid`](https://railbed.com/docs/webhooks/events.md#payment-paid): The money arrived and passed Railbed's checks: at least 90% of the order's value in a dollar coin and, from the processor's signed notice, the right deposit address, your wallet in the payout and a transaction never used before. Or you accepted a held payment as paid. This is the event to fulfil from. - [`payment.held`](https://railbed.com/docs/webhooks/events.md#payment-held): Money arrived but failed a check, most often because less arrived than 90% of the order's value. Don't fulfil. The payment waits for you in the dashboard. - [`payment.updated`](https://railbed.com/docs/webhooks/events.md#payment-updated): A paid payment's settlement details arrived after it was confirmed: txidIn, valueCoin, coin and merchantReceived are now filled in. The status stays paid. Update your records; there's nothing to fulfil again. - [`payment.failed`](https://railbed.com/docs/webhooks/events.md#payment-failed): The payment reached a terminal failure. Today this is sent in Test mode only, for an explicit API simulation with outcome: "failed". The Test provider's declined-card action keeps the payment open and sends no failed event. Live card providers don't report declines, so a live payment the buyer never completes ends as payment.expired. After a terminal failure, let the buyer try again with a new session. - [`payment.expired`](https://railbed.com/docs/webhooks/events.md#payment-expired): Nobody paid before the payment's expiresAt: 24 hours for API sessions, or the lifetime chosen for a payment link. It's sent within five minutes of expiry. - [`payment.canceled`](https://railbed.com/docs/webhooks/events.md#payment-canceled): You canceled a payment link in the dashboard before anyone had gone to a provider. The payment's status is expired and canceledAt is set. - [`ping`](https://railbed.com/docs/webhooks/events.md#ping): Sent only when you choose Send test event and pick ping (Test endpoints) or Send ping (Live endpoints). data.payment, data.order and data.customer are null. Use it to check the address, the secret and your signature code. ## Agent sign-in - [auth.md](https://railbed.com/auth.md): how an AI agent gets its own access to a merchant's account, approved by the merchant. Start here when you need to call the API for someone - [Connect an agent to your account](https://railbed.com/docs/agents.md#connect-an-agent): the same flow from the merchant's side, the access they can choose and disconnecting - [Authentication and scopes](https://railbed.com/docs/api.md#authentication): secret keys, agent access tokens, each endpoint's scope and discovery ## Optional - [All of these docs in one file](https://railbed.com/docs/llms-full.txt) - [Railbed company overview (llms.txt)](https://railbed.com/llms.txt) - [Dashboard: create API keys and webhook endpoints](https://app.railbed.io/developers) - [Sign up](https://app.railbed.io/signup) --- # Your guide to getting paid > Set up your business, take your first card payment, and follow every sale to your wallet. Start here, with or without a website. Source: https://railbed.com/docs/user-guide/ · Updated: 2026-09-26 · Railbed by DeepWork user guide New to Railbed? [Set up your account and try a Test payment](https://railbed.com/docs/user-guide/getting-started.md). **Visual example: How a payment works.** 1. Share your checkout: Send a payment link or publish a reusable checkout. You set the price. 2. Your buyer pays by card: They choose an eligible provider and pay on its page. Provider checks may apply. 3. Confirm the payment: Read the payment status and reported USDC settlement in Railbed. Your wallet · Polygon. Deliver only when the payment is Paid. A browser return does not confirm payment. *The Live payment journey. In Test mode, the provider and settlement are simulated.* ## Start with your first payment A customer pays by card. You receive USDC on Polygon in a wallet you control. Railbed gives you the checkout, a record of each payment, and a place to manage it all. Start in **Test mode** to try the full experience without charging a card. When you are ready to take real payments, create your customer-facing links in **Live mode**. - [Set up your account](https://railbed.com/docs/user-guide/getting-started.md): Add your business, choose a payout wallet, and find your way around the dashboard. - [Try a test payment](https://railbed.com/docs/user-guide/test-and-live.md): See a successful payment, an underpayment, and a decline before going live. ## Choose how to get paid The right starting point depends on whether you are requesting one payment or selling the same thing repeatedly. | You want to… | Use | What you share | |---|---|---| | Ask one customer for a specific amount | [Payment link](https://railbed.com/docs/user-guide/payment-links.md) | A one-time link or QR code, with an optional invoice reference | | Sell one product to many buyers | [Checkout page](https://railbed.com/docs/user-guide/checkout-pages.md) | A reusable page; each buyer gets a separate payment | | Let buyers choose between packages | [Pricing table](https://railbed.com/docs/user-guide/pricing-tables.md) | A reusable page with up to four plans | | Take payments inside your website | [Widget or buy button](https://railbed.com/docs/user-guide/widgets-and-buttons.md) | A short HTML snippet copied from the dashboard | | Connect a WordPress store | [WooCommerce](https://railbed.com/docs/user-guide/integrations.md) | The Railbed plugin, connected to your account | ## Make Railbed part of your day - [Manage your orders](https://railbed.com/docs/user-guide/orders.md): See items, payment attempts and what still needs to be fulfilled. - [Know your customers](https://railbed.com/docs/user-guide/customers.md): Read purchase histories, save private notes and manage customer data. - [Understand your payments](https://railbed.com/docs/user-guide/payments.md): Find an order, read its status, and open its timeline. - [Read your wallet](https://railbed.com/docs/user-guide/wallet-and-settlement.md): Understand the difference between a balance, a sale, and the amount received. - [Make it yours](https://railbed.com/docs/user-guide/branding-and-settings.md): Set your business name, logo, support email, and checkout appearance. - [Solve a problem](https://railbed.com/docs/user-guide/troubleshooting.md): Find the next step for a missing payment, an unavailable checkout, or a buyer who needs help. Use [account search](https://railbed.com/docs/user-guide/search.md) to jump between records. Prefer reading in the dark? Choose **Dark** or **Match system** from the docs theme selector. [Appearance settings](https://railbed.com/docs/user-guide/branding-and-settings.md#choose-light-dark-or-match-system) explains the separate dashboard and checkout preferences. ## A few things to know **You control the wallet.** Railbed does not hold a balance for you to withdraw. Settlement goes to your own wallet. **Your buyer pays with a card provider.** They enter card details on that provider's page. Available providers and identity checks vary by buyer, country, currency and amount. **Payment status is what matters.** Check that a payment is **Paid** before delivering the order. A buyer returning to a thank-you page is not proof of payment. [Read the statuses](https://railbed.com/docs/user-guide/payments.md#understand-each-status). **Pricing tables are one-time purchases.** A monthly or yearly label does not create automatic recurring billing. Building your own store, app or game? Switch to the [developer docs](https://railbed.com/docs/index.md) for API examples and signed webhooks. These user guides focus on the dashboard and everyday merchant tasks. --- # Set up your account > Go from a new account to a checkout you can share, with the right business details and payout wallet in place. Source: https://railbed.com/docs/user-guide/getting-started/ · Updated: 2026-10-04 · Railbed by DeepWork user guide ## Before you begin Have a business name, a product or service to sell, and the **public address of a self-custody wallet on Polygon**. Use a wallet you control, rather than an exchange deposit address. The address tells Railbed where to send settlement; you never need to give Railbed a seed phrase or private key. Your customers see prices in USD, EUR, GBP, CAD or AUD. Settlement reaches your wallet as USDC on Polygon, regardless of the checkout currency. ## Create your account 1. Open [Railbed signup](https://app.railbed.io/signup). Continue with email or a sign-in option shown on the page, then complete any verification prompts. 2. In **Set up your business**, enter the name buyers should see, add your logo if you have one, and choose **Connect a wallet** to pick your payout wallet from MetaMask, Trust Wallet, Rabby, Rainbow or any wallet that scans a WalletConnect QR code. Railbed reads the wallet's public address and disconnects; it never asks you to sign anything or approve a transaction. No wallet yet? **Create one with Base Account**, Coinbase's free wallet that you set up with a passkey. You can also choose **Paste an address instead**: Railbed checks a pasted address for typos and shows it in groups of four so you can compare it with your wallet app. 3. In **How will you take payments?**, choose your main way to sell. You can add the others later from **Integrations**. | Choose | When | What happens next | |---|---|---| | **Send payment links or invoices** | You bill customers one at a time, or sell without a website | Make your first payment link and share it by link or QR code. You can try a test payment first. | | **Add it to your own app or website** | You have a site or app of your own | Without a developer: make a checkout and paste two lines of HTML. With one: connect your server with a Test key, then a Live key, through the [API](https://railbed.com/docs/api.md). | | **Sell on Shopify** | Your store runs on Shopify | Enter your store address, then follow the [Shopify setup](https://railbed.com/docs/user-guide/integrations.md#set-up-shopify) page's four steps. | | **Sell on WooCommerce** | Your store runs on WordPress | Download the plugin, then follow the [WooCommerce setup](https://railbed.com/docs/guides/woocommerce.md). | 4. Back on **Wallet**, the **Your first payment** card lists what's left for your way to sell and ticks each step as it happens. Hide it when you don't need it; **Settings** brings it back. 5. Check the dashboard's **Live** switch before sharing anything. Turn Live off to use Test mode. A link or checkout belongs to the mode in which you created it. > [!IMPORTANT] > Test mode and Live mode have separate checkouts and payment records. Switching the dashboard to Live does not convert a Test checkout into a real one. Create a checkout in the intended mode and share its new link. ## Find your way around | Dashboard area | What you do there | |---|---| | Wallet | Follow the steps to your first payment, and see your latest checkout, payout wallet, recent receipts and payments needing review | | Orders | Review items, customers, payment attempts and fulfilment for each purchase | | Customers | See purchase histories, contact details and your private customer notes | | Payments | Search all payment sources, filter by status and inspect a payment's details | | Payment links | Request a one-time payment, share its QR code and follow its progress | | Checkouts | Manage reusable checkout pages, pricing tables and payment widgets | | Create | Choose which payment experience to make next | | Integrations | Download the WooCommerce plugin and review ways to connect Railbed | | Developers | Create API keys (full access or read only), connect webhooks, inspect event deliveries and manage connected AI agents | | Settings | Change your business details, branding, profile photo or payout wallet | Use **Search** in the sidebar or phone navigation to find a record or jump to another page. [Account search](https://railbed.com/docs/user-guide/search.md) supports keyboard shortcuts and queries such as an order number, email or payment ID. Choose the dashboard's **Light**, **Dark** or **Match system** appearance in [Settings](https://railbed.com/docs/user-guide/branding-and-settings.md#choose-light-dark-or-match-system). You may see additional controls if your account is a platform operator. They are not needed for ordinary merchant setup. ## Make your first practice checkout 1. Turn **Live** off. Confirm the **Test mode** label is visible. 2. Choose **Create → Checkout page**. Enter an example product such as Design consultation, a price such as `49.00`, and USD. 3. Create the page, then open its **Share** tab and choose **Open checkout**. 4. Follow [Try a test payment](https://railbed.com/docs/user-guide/test-and-live.md#run-a-practice-payment) to experience the buyer's steps and confirm the result in Payments. **Visual example: A reusable checkout page.** A checkout shows Northstar Studio, Design consultation, $49.00 USD and an email field. Continue to pay leads to provider selection; card details are entered on the provider's page. *Illustrative example · your product on the left, buyer details on the right.* ## Finish your business settings In [Settings](https://app.railbed.io/settings), add your logo and support email. Buyers use those details to recognize your business and get help with their order. Choose a checkout brand color and **Save settings**. Logo and profile photo uploads save as soon as you finish cropping them. [Branding and settings](https://railbed.com/docs/user-guide/branding-and-settings.md) explains where each detail appears and which changes are shared across modes. ## When you are ready for customers Open [Test and Live mode](https://railbed.com/docs/user-guide/test-and-live.md#before-sharing-a-live-link) and work through the launch checklist. Confirm the payout address, the product price and currency, the buyer experience, and how you will deliver orders after payment. For a one-off invoice or deposit, start with a [payment link](https://railbed.com/docs/user-guide/payment-links.md). For a product you sell repeatedly, keep using a [checkout page](https://railbed.com/docs/user-guide/checkout-pages.md). --- # Test and Live mode > Practice the buyer experience without moving money, then prepare a separate checkout for real customers. Source: https://railbed.com/docs/user-guide/test-and-live/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## Check the mode before you create Use the **Live** switch in the dashboard. Live off means Test mode. The app shows a Test mode banner and labels so you can tell which records you are viewing. | What changes | Test mode | Live mode | |---|---|---| | Card payments | Simulated; no card is charged | Real payments through available card providers | | Wallet display | Simulated USDC received in the last 7 days | USDC balance read from your wallet on Polygon | | Links, checkouts, orders, customers and payments | Separate Test records | Separate Live records | | Provider list | Fixed for practice | Depends on current availability and buyer eligibility | | Webhooks | Real signed HTTP deliveries for simulated events | Real signed HTTP deliveries for actual events | Business details, branding and the payout wallet in Settings are shared across both modes. Changing those settings while viewing Test mode still changes your account settings. ## Run a practice payment 1. Turn **Live** off and create a payment link or checkout with a sample price, such as $49. 2. Open the buyer link and continue with an email address you use for testing. Choose one of the available Test providers. 3. Railbed opens a simulated provider page. Choose **Simulate successful payment**. You do not enter card details. 4. Return to the original checkout tab. It updates to the paid result, or follows your success URL if you set one. 5. Open **Payments** in Test mode. Find the payment, confirm **Paid**, and inspect the timeline and simulated receipt. You can also open [Orders](https://railbed.com/docs/user-guide/orders.md) to inspect the purchase and its attempts, then open its [customer](https://railbed.com/docs/user-guide/customers.md). These are Test records. A simulated Paid status does not represent a real charge or settlement. ## Practice the exceptions too Create a **new** Test payment for each completed-payment scenario. A declined card can be retried on the same payment. - **Simulate an underpayment:** the payment becomes **Underpaid**. Open its details to understand the hold and any available review action. - **Simulate declined card:** the payment stays open. Return to the checkout tab, choose **Choose another way to pay**, and try another provider. You can then simulate a successful payment on the same checkout. - **Leave a payment unfinished:** see how an open or in-progress payment differs from a paid one. A Live card decline happens on the provider's page and is not directly reported to Railbed. The payment may stay open or in progress until it is paid or expires. Test mode teaches the UI and integration behavior; it does not prove which providers will approve a real buyer. > [!NOTE] > A configured Test webhook still sends real requests to its endpoint. Use an endpoint intended for testing, especially if your connected system sends emails or fulfils orders automatically. ## Before sharing a Live link - [ ] Confirm the full Polygon payout address in **Settings** against the wallet you control. - [ ] Review your business name, logo, support email and displayed Railbed fee. - [ ] Switch to **Live** and create the payment link or checkout there. - [ ] Open that exact buyer link and confirm its price, currency, description and appearance. - [ ] Copy the Live embed code again if you are using a button or widget. - [ ] Confirm how you will deliver the order after **Paid**, and what you will do with a payment that needs review. - [ ] If you use a store or API integration, connect the matching Live key and Live webhook endpoint. Opening a Live checkout is different from completing a card payment. A successful Test payment does not move funds or demonstrate real settlement. ## Where did my checkout go? Check the mode first. A Test link, payment or key is not listed in Live mode, and vice versa. Switching modes changes the records you see; it does not delete them. The buyer URL already identifies its checkout or payment. Your current dashboard mode does not change what a customer sees when they open a previously shared URL. --- # Create and share a payment link > Request one payment for one customer. Add an invoice reference, share a link or QR code, and track it until it is paid. Source: https://railbed.com/docs/user-guide/payment-links/ · Updated: 2026-09-26 · Railbed by DeepWork user guide ## When to use a payment link Use a payment link for an invoice, a project deposit, a consultation or a custom order. It has a fixed amount and can be paid once. To sell the same product repeatedly, use a [checkout page](https://railbed.com/docs/user-guide/checkout-pages.md) instead. **Visual example: Create a payment link.** New payment link: Amount 49.00, Currency USD, What it is for Design consultation, Reference INV-0042, Can be paid for 7 days. The buyer sees a payment request with the amount, description and reference. *Illustrative example · a $49 consultation with a 7-day payment link.* ## Create the request 1. Check Test or Live mode, then open **Create → Payment link**, or choose **New payment link** in Payment links. 2. Enter the **Amount**, **Currency**, and **What it’s for**. The description is shown to the customer, so use a recognizable product or service name. 3. Add the optional customer and invoice details below. 4. Choose how long the link **Can be paid for**: **1 day**, **7 days**, or **30 days**. The default is 7 days. 5. Choose **Create payment link**. Review the saved details before sharing the link. | Field | What to enter | Example | |---|---|---| | Amount | A price from 1.00 to 100,000.00; provider minimums may be higher | `49.00` | | Currency | USD, EUR, GBP, CAD or AUD | `USD` | | What it’s for | A customer-facing description, up to 120 characters | Design consultation | | Reference | An optional order or invoice number, up to 64 characters | `INV-0042` | | Name | An optional customer name, up to 80 characters | Alex Morgan | | Email | Prefills the buyer's email at checkout | `alex@example.com` | | Note to the customer | An optional note shown on the payment page, up to 500 characters | Consultation on Friday at 2 pm | **Railbed does not email the payment link.** Adding a customer email prefills checkout; you still send the link yourself. A customer's name or email is not an access restriction. Share the payment link only with people who should see the request. ## Share the payment link or QR code The saved request has a **Payment link** section. Copy the link into your invoice, message or email. Use its QR code for a printed invoice or for a customer to scan from another screen. The QR code opens the same payment page; it is not a crypto transfer address. Here is a message you can adapt: Example: Example message to a customer ```text Hi Alex, Your design consultation is ready to book. The total is $49.00 USD (reference INV-0042). Pay using the Railbed link included in this message. The link is available for 7 days. Thank you, Northstar Studio ``` Paste your actual saved link into the message before sending it. Recheck the mode: a Test link cannot collect real money. ## Share progress with someone else Use the separate **Tracking link** for an accountant or collaborator who needs to see whether the request has been paid. **Visual example: A status-only tracking page.** The tracking page shows a $49 Design consultation for Alex Morgan, reference INV-0042, In progress. It omits the email, note, wallet, transactions and a way to pay. *Illustrative example · share progress without sharing payment details.* The tracking page shows the amount, status, progress, reference and billed-to name. It omits the buyer's email, your note, deposit and wallet details, transaction links and a way to pay. Anyone with the tracking link can see that limited information; treat it as a shareable link, not a login-protected report. ## Read the result Open the request in **Payment links** to see its status, details and timeline. Once paid, the buyer's payment link shows their receipt. A held payment shows that it is being reviewed. The list's **Paid** filter also includes payments being reviewed, and **Expired** includes declined payments. Always read the individual row status before delivering an order. [Payment statuses](https://railbed.com/docs/user-guide/payments.md#understand-each-status) explains the next action for each one. ## Cancel, duplicate or replace a request **Cancel link** is available only while the link is still payable and the buyer has not gone to a card provider. After that hand-off, the link stays open for the payment to arrive or expire. Canceling a request does not reverse a card payment or refund money. Saved payment links are not edited in place. To correct an amount or request payment again, choose **Duplicate**, review the copied fields, and create a new link. Duplicate clears the reference. Once the original payment has started, it also clears the saved email, so review and re-enter the details needed for the new request. Cancel the old link if that action is still available. ## Send the buyer to your thank-you page Under **More options**, fill **After payment, send them to**. You can include `{PAYMENT_ID}` and `{REFERENCE}`: Example: Example success URL ```text https://example.com/thanks?payment={PAYMENT_ID}&order={REFERENCE} ``` The original Railbed tab follows this URL after payment is confirmed. If the buyer closes the tab, the redirect cannot happen. Leaving the field empty shows Railbed's on-screen receipt; it does not send a receipt email. [The buyer experience](https://railbed.com/docs/user-guide/buyer-experience.md) explains the two-tab flow. --- # Create a reusable checkout > Sell one product at one price with a page you can share again and again. Every buyer gets a separate payment. Source: https://railbed.com/docs/user-guide/checkout-pages/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## One page, many customers A checkout page is a reusable product page hosted by Railbed. Unlike a one-time payment link, it keeps accepting new buyers until you delete it. Share it in your website, social profile or messages, or use it behind a buy button. **Visual example: A reusable checkout page.** A checkout shows Northstar Studio, Design consultation, $49.00 USD and an email field. Continue to pay leads to provider selection; card details are entered on the provider's page. *Illustrative example · your product on the left, buyer details on the right.* ## Build your checkout 1. Check the dashboard mode, then choose **Create → Checkout page**. 2. Enter the product **Name**, optional **Description**, **Price** and **Currency**. The preview shows how those details appear to buyers. 3. Choose the **Checkout theme**, Light or Dark. The editor also lets you choose a brand color; that brand setting applies across your business's checkouts. 4. Optionally change the **Button text**. Keep the next action clear, such as Continue to pay. This changes the button inside the checkout; a buy button on your website has its own label. 5. Choose the **Provider choice**: Smart Routing (the default), Buyers choose, or One provider. [How buyers reach a provider](#choose-how-buyers-reach-a-provider) explains each. 6. Under **More options**, add a success URL if you have a thank-you page. 7. Choose **Create checkout page**, then open the saved page's **Share** tab. Names can be up to 80 characters; descriptions up to 280. Prices range from 1.00 to 100,000.00 in USD, EUR, GBP, CAD or AUD. A price that Railbed accepts may still be below a provider's minimum. [Smart Routing](https://railbed.com/docs/user-guide/buyer-experience.md#how-smart-routing-chooses-providers) checks the providers available to that buyer. ## Choose how buyers reach a provider **Provider choice** in the Checkout card decides what happens after the buyer enters their email. It applies to this checkout, including any widget or buy button that uses it. - **Smart Routing** sends each buyer straight to the best card provider for their country, amount and currency. Their one click on Continue to pay opens the provider. New checkouts start here. - **Buyers choose** shows Smart Routing's ranked list, with the best match selected, and the buyer picks. Checkouts made before this setting existed use it until you change them. - **One provider** sends every buyer to the provider you pick. The editor shows the provider's minimum and warns when your price is below it (or asks you to check, when the minimum is in another currency), when the provider isn't taking payments, or when it serves only some countries. With One provider, **If it can't take a buyer, use Smart Routing instead** is on by default: a buyer the provider can't serve (another country, below its minimum, or while it's down) goes to the best provider for them, and the payment's timeline says so. Turn it off only if every buyer must use that provider. Buyers it can't serve then see a message and can't pay at this checkout, and no payment is created. In every mode, a buyer can choose **Choose another way to pay** to see the full list, including after a declined card. The payment's timeline shows who chose each provider: Smart Routing, your checkout's setting, or the buyer. Shopify stores and WooCommerce have the same setting on their setup pages ([Integrations](https://railbed.com/docs/user-guide/integrations.md)). Payment links and custom API checkouts always show the list. ## Share the saved page The **Share** tab contains the hosted URL and QR code. Open the actual checkout to check the buyer view, then copy the URL wherever customers will find it. A checkout address looks like this: Example: Example checkout address — use your saved URL ```text https://pay.railbed.io/c/your-checkout-slug ``` The slug is the final part of the saved address. It is also what the embed code uses. Use the URL Railbed gives you instead of typing a slug from the product name. To take payments on your own website, use **Add it to your site** in the Share tab. [Widgets and buy buttons](https://railbed.com/docs/user-guide/widgets-and-buttons.md) covers the available formats and copyable HTML. ## Edit an existing checkout Open **Checkouts**, choose the checkout, then select **Edit**. Change the product details and choose **Save changes** in the page header. The Share and Edit tabs keep sharing controls separate from your product settings. The hosted URL stays the same when you edit the product. Future payment creation uses the updated checkout details. Payments already created retain their saved amount and description, so do not expect an edit to rewrite an existing customer's order. Currency becomes fixed once a payment reaches a card provider, or a payment is paid or being reviewed. If the field is locked, create a new checkout to sell in a different currency. For a different checkout kind, create that kind separately. ## Use a success URL Set **After payment, send buyers to** under More options: Example: Send a confirmed buyer to your thank\-you page ```text https://example.com/thanks?payment={PAYMENT_ID} ``` Railbed fills in the payment ID after confirmation. Reusable checkout payments do not have your own order reference, so `{REFERENCE}` would be empty. Use an API integration if every order needs a reference or customer-account metadata supplied by your server. A redirect is a convenience for the buyer. For manual delivery, check the payment in the dashboard. For automatic delivery, use [verified webhooks or authenticated payment retrieval](https://railbed.com/docs/guides/fulfilment.md). ## Stop accepting new buyers Open **Edit → Delete checkout page** and confirm. The hosted checkout and any embedded button or widget using it stop accepting new payments. Past payments remain in Payments. Deleting the checkout is not a refund or a cancellation of payments that already exist. If you want to keep the page available, remove links to it from your own website instead; anyone who still has its URL can continue to open it until it is deleted. --- # Offer plans with a pricing table > Present up to four packages side by side, highlight one recommendation, and let buyers choose the right one. Source: https://railbed.com/docs/user-guide/pricing-tables/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## What a pricing table does A pricing table is a reusable checkout with multiple plans. It works for service packages, access passes, resource bundles or other choices with a fixed price. A buyer chooses a plan, enters their details, and continues to a card provider. **Visual example: Compare three one-time plans.** Starter costs $19, Studio $49 and Team $99. Studio is highlighted as recommended. Each purchase is a one-time payment; nothing renews automatically. *Illustrative example · Studio is the recommended plan.* > [!IMPORTANT] > Every purchase is one payment. Pricing tables do not create subscriptions or automatic renewals. Labels such as / mo and / yr describe what you sell; they do not schedule another card charge. ## Create the table 1. Choose the correct mode, then **Create → Pricing table**. 2. Set the table **Title**, optional **Description** and **Currency**. All plans use that currency. 3. Add between **one and four plans**. The editor starts with Starter, Pro and Team as names; enter your own prices and details. 4. For each plan, fill **Name**, optional **Description**, **Price**, optional **Label**, and **Features**. Enter one feature per line, up to eight. 5. Optionally turn on **Recommended plan** for one plan. It appears as the dark highlighted card. Only one plan can be recommended. 6. Arrange plans with their move controls. Review the preview, choose the theme, and create the pricing table. 7. Optionally change the **Provider choice** in the Checkout card. It works as it does on [checkout pages](https://railbed.com/docs/user-guide/checkout-pages.md#choose-how-buyers-reach-a-provider); with One provider, the editor compares every plan price with the provider's minimum when both are in the same currency, and asks you to check them when they aren't. A plan name can contain up to 40 characters, its description up to 140, and the price label up to 16. Keep feature descriptions short enough for buyers to compare at a glance. ## Offer two price options Turn on **Price switch** to provide two prices for every plan. Name **First option** and **Second option**, then fill in both prices for each plan. For example, a studio could sell a **Single session** or a **Three-session pack**. Switching the option changes the displayed prices across the table. Monthly and Yearly are also possible labels when the purchase buys that period of access, but Railbed will not charge again when it ends. Make both the price and what it includes explicit: | Plan | Single session | Three-session pack | |---|---|---| | Consultation | $49 | $129 | | Extended session | $99 | $269 | These are illustrative prices. You decide what you deliver and any access duration outside Railbed. ## Share or embed it Open the saved table's **Share** tab. Share its hosted URL or QR code, add a **Buy button** that opens the plans, or place the **Pricing table** directly in your website from **Add it to your site**. Example: Place your pricing table on a website ```html ``` Replace the example slug with the one copied from your saved table. No API key goes in this snippet. [Embed instructions](https://railbed.com/docs/user-guide/widgets-and-buttons.md) cover testing and site-builder behavior. ## See which plan was purchased Open **Payments** and choose the order. Its plan label identifies the selected plan and, when used, the selected price option. This detail is also available to connected integrations, so fulfil the package actually purchased. ## Change your plans later Use **Edit → Save changes** to update descriptions, features, prices, order or the recommended plan. The currency locks once a payment reaches a card provider, or is paid or being reviewed. Create a new table for another currency if the field is locked. Edits affect future purchases. Existing payment records keep the details captured when they were created. Deleting the table disables its link and embeds while preserving past payments. --- # Add Railbed to your website > Keep customers on your site while they choose what to buy. Copy a small HTML snippet; Railbed handles the checkout. Source: https://railbed.com/docs/user-guide/widgets-and-buttons/ · Updated: 2026-09-26 · Railbed by DeepWork user guide ## Choose the right format A **buy button** opens a checkout over your website when clicked. A **payment widget** places a compact checkout directly inside your page. A **pricing table** can also sit inside your page with its plans visible. These use checkouts created in the dashboard. Your website does not need an API key, a server integration or card fields. | Format | Start with | What buyers see on your site | |---|---|---| | Buy button | A checkout page or pricing table | A button; clicking opens the checkout in an overlay | | Payment widget | A payment widget checkout | The compact checkout itself, inline | | Pricing table | A pricing table checkout | Plans and the price switch, if configured | ## Copy the code from Railbed 1. Open **Checkouts** and select your saved checkout. 2. Choose **Share**, then find **Add it to your site**. 3. Where offered, choose what to add. For a button, set **Button text** and **Shape**. 4. Copy the generated code and paste it into your site's HTML or custom-code block. 5. Preview the published page. Some site builders do not run custom scripts inside their editing canvas. Start with a Test checkout. When ready, copy code from a separate Live checkout and replace the Test snippet. ## Add a buy button Include the script once per page, then put the button where you want it: Example: A button that opens your checkout ```html ``` | Attribute | What it does | |---|---| | checkout | Required. The slug from your saved checkout URL | | label | Optional. Your button text; defaults to Buy now, or See plans for a pricing table | | shape | Optional. Use pill or rounded; the default is pill | The button takes your business's brand color. Set that in [Settings](https://railbed.com/docs/user-guide/branding-and-settings.md). The button label is separate from the payment button text inside the checkout. ## Add an inline checkout For a payment widget or pricing table, use this element: Example: A checkout embedded in your page ```html ``` The embedded page adjusts its height to its content and keeps the configured Light or Dark theme. Give it enough width for the checkout to be readable; test the surrounding layout on a phone too. ## What happens when a buyer pays The buyer starts in your embedded checkout. The card provider opens in a new tab because card entry happens on the provider's own page. If the browser blocks the new tab, the provider can open in the same tab instead. After confirmation, the checkout can send the parent page to the success URL you configured. If a buyer tries to close an overlay while paying, it asks before closing. Returning to your website alone does not confirm payment: check Payments or use your connected store's verified order status. ## Troubleshoot an embed - **Nothing appears:** confirm your site builder allows custom HTML and JavaScript, includes the script, and keeps the custom element tags. Check the published page as well as the editor. - **Checkout unavailable:** check that the slug matches an existing checkout and that it has not been deleted or become unavailable. - **Still in Test mode:** copy the code from the Live checkout. Flipping your dashboard mode does not change the slug in your website. - **Provider opens elsewhere:** that is expected. Provider card entry is not embedded inside your website. Only Railbed checkout and payment pages can be framed. Do not try to embed the dashboard or a tracking page. If a custom integration needs more control, use the [developer guides](https://railbed.com/docs/guides/hosted-checkout.md). --- # What your customer sees > Understand the card checkout, provider choice and return journey so you can confidently guide a customer through payment. Source: https://railbed.com/docs/user-guide/buyer-experience/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## From your link to payment **Visual example: Explore a payment from start to finish.** Create a link: set an amount and share it. Buyer pays: the buyer opens a licensed provider in a new tab and pays by card. Payment confirmed: check the payment status and the reported settlement in your dashboard. *Illustrative Test payment. Select a step to see what happens.* 1. Your customer opens the payment link, checkout page or embed. They see your business, product and price. A pricing table first asks them to choose a plan. 2. They provide an email address and continue. A payment link can prefill the email you entered when creating it. 3. Depending on the checkout's or store's **Provider choice**, Continue to pay opens the best provider for them straight away, opens the one provider you picked, or shows eligible card providers with a recommended **Best match** for the buyer to choose. Payment links always show the list. 4. The provider opens in a new tab. The buyer enters card details and completes any checks there. 5. The original Railbed checkout tab waits for confirmation. When the payment is marked paid, it shows the result or follows your configured success URL. **Visual example: From a card payment to your wallet.** Smart Routing checks country, currency, amount and availability to show eligible card providers. The buyer pays with a provider, and settlement goes to the merchant's wallet as USDC on Polygon. *Eligibility depends on the buyer. Identity checks are decided by the provider.* ## How Smart Routing chooses providers Smart Routing considers the buyer's country, checkout currency, amount, provider minimums and availability. It filters for eligible card methods and ranks the available choices. You do not need to configure a routing rule for each country. A checkout on Smart Routing skips the list and sends the buyer to the top choice; [Provider choice](https://railbed.com/docs/user-guide/checkout-pages.md#choose-how-buyers-reach-a-provider) sets this for each checkout. The provider list can differ between buyers and change over time. Test mode uses a fixed example list, so it is not a live availability check. A low checkout amount may have no eligible provider even though Railbed accepted the product price. **Best match is a recommendation, not an approval guarantee.** The provider decides whether to accept the card, what fees or conversion terms to show, and which identity checks to require. Railbed cannot promise that a buyer will never be asked for ID. ## Why there are two tabs Keeping the Railbed tab open gives the customer a page that can report the verified payment result and return them to your store. The provider's own final page may have a different destination that your Railbed success URL does not control. Tell customers to keep the original checkout tab open. After finishing on the provider page, they can return to it to see progress. If their browser blocks new tabs, payment can continue in the original tab, and they may need to navigate back to Railbed to see the result. ## If a card is declined A Live decline is shown by the card provider. Railbed does not receive a direct decline signal, so your dashboard may still show **Open** or **In progress**. The buyer can use **Choose another way to pay** to return to the full provider list before the payment expires, including on checkouts that sent them straight to a provider. Avoid assuming that an in-progress payment means the card was charged. If the buyer believes they were charged, inspect the payment and the provider's confirmation before asking them to make a separate payment. ## If the page says Being reviewed or Underpaid Payment evidence arrived but did not pass all the checks needed for normal confirmation. **Underpaid** tells the buyer that less than the full amount arrived and that you will contact them. Ask the buyer to wait while you [review the payment](https://railbed.com/docs/user-guide/held-payments.md). A held payment is not an instruction to top up the original payment. ## Thank-you pages and receipts A success URL runs from the original checkout after payment confirmation. If that tab is closed, Railbed cannot redirect it. Closing the page does not cancel money already sent. Without a success URL, Railbed shows an on-screen receipt. Automatic buyer receipt emails are not available. If your store sends order emails, those are handled by the store or your own integration. For manual order delivery, verify **Paid** in your dashboard. For automatic delivery, connect [signed webhooks or authenticated payment checks](https://railbed.com/docs/guides/fulfilment.md). Never use the buyer's screenshot, browser redirect or thank-you page as the only payment proof. --- # Accept crypto payments > Let buyers pay in USDC or USDT from their own wallet, beside card. It's off until you turn it on, and the money goes to your payout wallet. Source: https://railbed.com/docs/user-guide/crypto-payments/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## How it works Card stays the main way to pay. When you turn crypto payments on, your checkout pages, payment links and store checkouts show a **Card | Crypto wallet** switch, with Card selected. A buyer who picks **Crypto wallet** enters their email, chooses a coin and network, and sends the exact amount to a one-time address shown with a QR code. The page updates when the money arrives, and the payment settles to your payout wallet like a card payment. Buyers can send: | Coin | Networks | |---|---| | USDC | Polygon, Base, Arbitrum, Optimism, BNB Chain, Ethereum | | USDT | Polygon, Base, Arbitrum, Optimism, BNB Chain, Ethereum | One coin counts as one US dollar. An order only offers the networks it can use: each network has a minimum, and networks that cost more to forward (Ethereum and Base) only appear on larger orders, so the network cost never takes much of your payment. ## Before you turn it on Crypto payments arrive at your payout wallet on whichever network the buyer used. Your wallet must be a **self-custody wallet** (one where you hold the recovery phrase, such as MetaMask, Rabby, Trust Wallet or a hardware wallet). The same address then works on every network above. > [!WARNING] > Don't turn crypto payments on if your payout wallet is an exchange deposit address. An exchange may not credit money sent on a network it doesn't support for that address, and it can be lost. ## Turn it on 1. Open **Settings** and find **Crypto payments**. 2. Turn it on, and confirm your payout wallet is a self-custody wallet that works on every network. 3. Your checkout pages, payment links and store checkouts now offer **Crypto wallet**. If you change your payout wallet later, you confirm the new one the same way. To keep one checkout card only, open it, and turn off **Accept crypto**. ## Fees Crypto payments have their own Railbed fee, shown in **Settings**, usually lower than the card fee. It's taken from what arrives, the same way as for card payments, together with the network cost of forwarding the money to your wallet. ## Short and late payments The buyer must send the full amount. A payment that arrives at least 99% complete counts as paid. Less is marked **Underpaid**, and you decide what to do from the payment, as for any [held payment](https://railbed.com/docs/user-guide/held-payments.md). A second, separate transfer isn't added to the first. Money that arrives after the checkout closed still settles, as with card. ## Test mode In Test mode the address is an example: never send real funds to it. Use **Simulate payment** or **Simulate underpayment** on the page to see what your buyers and your dashboard show. ## In the dashboard and integrations A crypto payment reads like **Crypto · USDC on Base** in Payments, Orders and alerts, with links to that network's block explorer. Webhooks and the API say `method: crypto` and name the `network` ([payment fields](https://railbed.com/docs/api/payments.md), [webhook fields](https://railbed.com/docs/webhooks/events.md)). Crypto addresses can't be created through the API yet: an API checkout session offers crypto on its hosted payment page. --- # Manage orders and fulfilment > See what a customer bought, review their payment attempts, and keep track of what still needs to be delivered. Source: https://railbed.com/docs/user-guide/orders/ · Updated: 2026-09-26 · Railbed by DeepWork user guide ## An order brings the purchase together Open [Orders](https://app.railbed.io/orders) for the purchase itself: the items, customer, total, addresses, payment attempts and fulfilment. Use [Payments](https://railbed.com/docs/user-guide/payments.md) when you need the evidence for a particular attempt to pay. One order can have several payment attempts. A retry does not always mean a second purchase, and one customer can have many orders. Always match the order and payment before delivering. **Visual example: How customers, orders and payments relate.** Customer: Alex Morgan. Their history can include many orders. alex@example.com. Order #1001: Design consultation. One purchase, with its own total and fulfilment record. $49.00 USD. Payment attempts: One retry, one purchase. An order can be paid after an earlier attempt expires. Attempt 1: Expired · Attempt 2: Paid. Fulfil the order once; inspect individual attempts when reviewing payment evidence. *Illustrative records. A customer can have many orders, and an order can have several payment attempts.* Orders appear from payment links, reusable checkouts and sessions created by your store or the API. Store integrations can send item details, shipping, tax, discounts and addresses. A simple checkout creates an order for its product or plan. Missing details mean the source did not provide them; Railbed does not invent an address or product catalog entry. ## Find the orders that need you 1. Check **Test** or **Live** mode. Orders, customers and payments are separate in each mode. 2. Choose a view in **Orders**, then narrow by **Source** if you want one store or checkout type. 3. Search for an order number, customer name, email, product, reference, or order/payment ID. **Clear filters** returns to the broader list when nothing matches. 4. Open a row to review the order. Use **Show more** to load more results. | View | What it includes | |---|---| | All | Orders matching your current source, customer and search filters | | Not fulfilled | Paid orders recorded as waiting to be fulfilled | | Being reviewed | Orders whose payment is held for review | | Awaiting payment | Open or In progress orders | | Ended unpaid | Declined, Expired or Canceled orders | The counts on **Not fulfilled** and **Being reviewed** stop at **100+**. That is a capped count, not a claim that there are exactly 100 orders. Filtered page totals can also show **10,000+**. Filters stay in the page URL, so refreshing or using Back keeps your view. An order link still requires sign-in and the matching account and mode. ## Read an order The drawer keeps the list behind it so you can move through your work. Use **Previous order** and **Next order**, or **K** and **J** while focused in the drawer and outside a text field. - **Items and total:** products, quantities, variants, discounts, shipping and tax when sent by the source. The order total is what the customer was charged in the order's currency. - **Customer and address:** open the linked customer for their wider history. Copy the shipping address when one is present. Addresses are stored as provided; they have not been checked against a postal service. - **Attempts:** inspect each attempt to pay. One failed or expired attempt does not undo another attempt that is Paid. - **Settlement:** reported USDC receipts describe what reached your wallet. They are different from the order total. See [wallet and settlement](https://railbed.com/docs/user-guide/wallet-and-settlement.md). - **Timeline:** follow the order, payment and fulfilment events in context. If the drawer warns that the buyer paid more than once, inspect each payment under **Attempts**. Deliver the intended order once, then agree any duplicate-payment refund with the buyer. Marking fulfilment does not refund a payment. ## Payment and fulfilment are separate A payment status answers whether the order was paid. Fulfilment records whether you have delivered it. **Mark as fulfilled** cannot turn an unpaid order into a paid one and does not send money. | Fulfilment label | Meaning | |---|---| | No shipping | The order was created without a shipping requirement, such as a service or digital product | | Not fulfilled | The order is waiting to be shipped or delivered | | Fulfilled | You or the connected store recorded delivery or dispatch | Do not treat **No shipping** as proof that a digital item was delivered. Your business or connected system still handles delivery. Likewise, a fulfilment label alone is not proof of payment. ## Mark an order fulfilled For an order managed in Railbed: 1. Open the order and confirm **Paid**. Check the items, customer and delivery details. 2. Deliver the goods or service through your normal process. 3. Choose **Mark as fulfilled**. To correct the record later, use **Mark as not fulfilled**. For several orders, select the rows and choose **Mark as fulfilled** in the selection bar. You can select up to 100 orders. Railbed checks each order; unpaid orders and orders whose fulfilment is controlled by a store are skipped. Read the result message and review anything that was skipped. > [!IMPORTANT] > A store-connected order says **Fulfilment comes from the store**. Update it in that store. Use **Open in WooCommerce**, or the source's available order link, to find it. Railbed shows the state sent by the integration. If an order is **Being reviewed**, choose **Review payment** and follow [the held-payment guide](https://railbed.com/docs/user-guide/held-payments.md). Do not mark it fulfilled to bypass a payment review. ## Export orders Choose **Export CSV** to download orders matching the current filters. The export includes the order number, customer, items, amounts, currency, payment/fulfilment labels and available source details. A selection export downloads the selected orders instead. Each full export contains up to 10,000 orders. Narrow the filters if you need a smaller set. Keep the currency column with every amount; do not add USD, EUR and other currencies as if they were one unit. **Received (USDC)** is a separate settlement measure. Exports contain customer details when available. Store and share them only where you intend to keep those details. Deleting customer data in Railbed does not recall a CSV you already downloaded. ## When something looks wrong | What you see | What to check | |---|---| | An order is missing | Confirm the mode, clear filters and search by its order or payment ID | | A customer tried again | Open Attempts; several attempts can belong to the same order | | An expired order later becomes Paid | Late settlement can update the order; read its current status before sending a new request | | Fulfilment cannot be changed here | Check whether the order is controlled by its source store, or has not been paid | | The store shows a different fulfilment state | Check the integration and latest sync; make changes in the source store | | An address or item is missing | Check what the store or API sent when the order was created | - [Understand your customers](https://railbed.com/docs/user-guide/customers.md): See each customer's orders, contact details and private note. - [Find records across Railbed](https://railbed.com/docs/user-guide/search.md): Search by order number, email, payment ID or transaction. --- # Understand your customers > See a customer's order history, contact details and spending. Keep a private note and manage the personal data saved in Railbed. Source: https://railbed.com/docs/user-guide/customers/ · Updated: 2026-09-26 · Railbed by DeepWork user guide ## How customers appear [Customers](https://app.railbed.io/customers) brings together purchases associated with the same email address in the current mode. Railbed creates a customer when a checkout or store order provides an email. You do not need to enter each customer by hand. Email matching ignores letter case. **Test** and **Live** records are separate. Names, phone numbers and addresses appear when the checkout or connected source supplies them. Later orders can update those saved details; omitted details keep their earlier value. A customer first created when a buyer starts payment may contain only their email. Open the order to inspect any additional addresses or details supplied with the purchase. > [!IMPORTANT] > A checkout email is supplied by the buyer or your store. A matching customer record is purchase history, not proof that the buyer owns an account in your own system. ## Find a customer 1. Open **Customers** and check the mode. 2. Search by name, email or phone. You can also use the customer's full ID. 3. Choose a view to narrow the list, then open a customer to see their details. | View | What it means | |---|---| | All | Customers matching the current search | | Returning | At least two paid orders | | New | First seen in the last 30 days | | Never paid | No paid orders yet | Sort by recent activity or **Spent** using the list's column controls. The Spent sort compares spending on a USD basis; displayed amounts retain their original currencies. They are not a converted wallet balance. Choose **Show more** when more records are available. If nothing matches, use **Clear filters** and check whether the customer belongs to the other mode. ## Read the customer page The page links the person to their orders and the information supplied with them: - **Spent:** the amounts of their paid payment attempts, grouped by currency. Unpaid attempts are not included. If the buyer paid twice for one order, both paid amounts count here even though there is still one order. - **Reached your wallet:** reported net receipts in USDC from their payment activity. This can differ from Spent because of fees, conversion, unreported receipts or multiple attempts. - **Orders:** the number of paid orders alongside all their orders, with a compact history of payment outcomes. - **Needs you:** a shortcut to an order needing payment review or fulfilment among the orders shown. - **Contact and address:** available email, phone, source-store customer IDs and the latest saved shipping or billing address. Open an order to inspect its items, payment attempts and timeline without leaving the customer page. For a long history, **All their orders** opens the Orders list filtered to that customer. The detail page shows the latest 50 orders first. **Email** opens your device's email app with the customer's address. It does not send a message by itself. **Copy address** copies the displayed address for use in your delivery workflow. ## Keep a private note Use the **Note** card for context that helps you serve the customer, such as a preferred delivery instruction. The customer never sees this note, and it is not included in API customer responses or merchant webhooks. Choose **Add note** or **Edit note**, write up to 2,000 characters, then **Save note**. **Cancel** keeps the saved version. Clearing the text and saving removes the note. The note belongs to this Railbed record. It does not update your store's customer profile, and changes to a store profile do not edit this private note. ## Export your customer list Choose **Export CSV** on Customers. It follows the current search, view and sort, and exports up to 10,000 records. The file includes available contact details, order counts, spending by currency, reported USDC receipts and customer IDs. Deleted customers are left out of the customer list and its export. Previously downloaded files remain wherever you saved them. ## Delete customer data Open the customer, choose **Delete customer data**, then read the confirmation carefully. This action cannot be undone. It removes the saved name, email, phone, addresses, store customer identifiers and private note from the customer record, and removes the associated personal-detail fields from orders, payments and stored webhook delivery bodies. Orders, payment records and amounts remain for your records, with the person shown as **Deleted customer**. After deletion: - The customer leaves the normal customer list. Existing order links can still show the anonymized record. - The same email in a future purchase creates a new customer record. - Webhooks already delivered, emails sent and files exported cannot be recalled. Handle those copies in the systems where they were saved. - Custom order descriptions, references and integration metadata are separate fields. Do not assume they have been scrubbed if you or an integration placed personal information in them. Deleting customer data does not refund, cancel or fulfil an order. Review the relevant order or payment separately if the buyer also needs help with a purchase. - [Manage orders and fulfilment](https://railbed.com/docs/user-guide/orders.md): Review what was bought and what still needs to be delivered. - [Understand payment statuses](https://railbed.com/docs/user-guide/payments.md): Distinguish payment confirmation from delivery. --- # Find records across Railbed > Jump to an order, customer, payment or setting from anywhere in your dashboard. Search by a name, amount, email or record ID. Source: https://railbed.com/docs/user-guide/search/ · Updated: 2026-09-26 · Railbed by DeepWork user guide ## Open search Choose **Search** in the dashboard sidebar or phone navigation. On a keyboard, press **⌘K** on Mac, **Ctrl+K** on Windows/Linux, or **/** when you are not typing in a field. Before you type, search shows shortcuts for the current page, recent searches, other pages and common actions. Type at least two characters to search your records. Results are grouped by type, with the current page's group prioritized. Use **↑** and **↓** to choose a result, **Enter** to open it, and **Escape** to close search. Selecting a page or action takes you to that workflow; it does not complete a payment, export or record change by itself. A mode-switch action changes the dashboard's current mode. ## What you can look for | Search for | Example | What Railbed looks for | |---|---|---| | Order number | `#1042` | A Railbed or store order number | | Customer | `alex@example.com` | The exact email, or matching names/phone text when you type those instead | | Product or reference | `consultation` | Matching records and their descriptions | | Record ID | `pay_example`, `ord_example`, `cus_example` | The matching payment, order or customer ID; replace the example with a real ID | | Amount | `49.00` or `$49.00` | The amount across currencies; check each result's currency | | Transaction or address | A full Polygon transaction hash or deposit address | Matching payment evidence | | Shortened chain reference | `0x1234…abcd` | A matching beginning and ending, when enough characters are supplied | | Page or action | `customers`, `settings`, `create payment link` | Dashboard pages and available shortcuts | The search field explains recognized queries, such as an exact email, order number or transaction. Read that hint when results are different from what you expected. A currency symbol in an amount search does not limit the results to that currency. Search can find orders, customers, payments, payment links, checkouts and developer records such as named API keys, endpoints and delivery events. Search API keys by their names or IDs; do not paste a secret key or webhook signing secret. ## Test and Live stay separate Record results belong to the current dashboard mode. If nothing matches, check the mode before assuming the record is missing. Search can offer **Search Test mode instead** or **Search Live mode instead** when appropriate. Changing the mode clears the old mode's results. Your records have not been deleted or moved. [Test and Live mode](https://railbed.com/docs/user-guide/test-and-live.md) explains the separation. ## See more results For supported record groups, **Show all matching …** opens the relevant page with the search applied. Some developer groups show only the newest matching results; use the Developers page to inspect the full context. If a result opens the wrong view, clear that page's filters or search directly using the full record ID. A payment can belong to an order, so open the related order when you need the purchase and fulfilment details. ## Recent searches and privacy Railbed remembers up to five searches that you used to open a result. They are saved in this browser. Choose **Clear recent searches** from the search dialog to remove the saved list, especially on a shared device. Queries that look like a Railbed secret key or webhook signing secret are refused and are not saved as recent searches. Do not rely on search as a place to test or store credentials. ## When search cannot load Check your connection, then use the retry action. You can still navigate through the sidebar and search within a specific list. Recent results are not shown as if they answered a different query or belonged to another mode. The search box on this documentation site searches public articles and code examples. It does not read your account or customer data. --- # Find and understand a payment > Search across your payment links, checkouts and API orders. See what happened, what arrived and what to do next. Source: https://railbed.com/docs/user-guide/payments/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## Find an order Open [Payments](https://app.railbed.io/payments) and check the mode. The list includes payments from payment links, reusable checkouts and the API in that mode. For the full purchase, including items, customer and fulfilment, open [Orders](https://railbed.com/docs/user-guide/orders.md). A payment is one attempt to pay; several attempts can belong to the same order. Use the search field for a buyer email, product name or payment ID. Choose a status filter to narrow the list, and **Show more** when there are additional results. Open a payment to view its details drawer. **Visual example: Read the status of each payment.** The sample payment list shows a $49 Paid consultation, a $250 In progress deposit and $19 Being reviewed resources. Only the Paid order is ready for normal fulfilment. *Illustrative records · read each status before delivering an order.* ## Understand each status Choose the label you see in the dashboard. The explanation below shows what it means and the next step to take. **Visual example: Choose a status to find your next step.** **Open: The payment is ready for your customer.** A payment exists, but the buyer has not reached the provider hand-off yet. Your next step: Share the link or wait for the buyer. Keep the order unfulfilled. Check that the payment is in the right Test or Live mode. **In progress: Your buyer has reached a card provider.** Railbed is still waiting for payment confirmation. Opening the provider or returning to your site does not confirm a payment. Your next step: Wait and follow the payment timeline. Do not deliver yet. A Live card decline may stay In progress until the payment expires. **Paid: This order is ready for fulfilment.** The payment passed the applicable confirmation checks, or you explicitly accepted an eligible hold. Your next step: Match the payment to the order, then deliver once. Review the reported settlement when needed. A Paid Test payment is a simulation. It does not represent money received. **Being reviewed: Payment evidence needs your attention.** Evidence arrived, but the amount or another confirmation check needs review. Your next step: Open the payment and read the hold reason. Do not fulfil automatically. Accept as paid is available only for eligible holds. Read the held-payment guide before accepting. [Review a held payment](https://railbed.com/docs/user-guide/held-payments.md) **Underpaid: Less than the full amount arrived.** Money arrived, but for less than the order’s value allows. It is a held payment with a named reason. Your next step: Open the payment and compare what arrived with the order. Do not fulfil automatically. You can accept what arrived as paid, or ask for a new payment. Railbed has no top-up for the difference. [Review a held payment](https://railbed.com/docs/user-guide/held-payments.md) **Declined: This simulated payment was declined.** Declined is the Test-mode outcome. Live card declines are handled by the provider and are not directly reported to Railbed. Your next step: Start a new Test payment to try another outcome. An unsuccessful Live attempt can remain Open or In progress, then expire. **Expired: The payment’s time limit has passed.** The checkout can no longer start a new payment attempt. Late settlement evidence can still change its status. Your next step: Recheck the current status and timeline before sending a replacement request. An expired payment can still become Paid or Being reviewed if money arrives late. **Canceled: You canceled this payment link.** The link can no longer be used to start a new payment. Canceling a link does not reverse a transfer already in progress. Your next step: Check the payment timeline for late settlement before requesting payment again. Use a new payment link if another request is needed. *Explanations only. This example does not read or change a real payment.* Live card declines are handled by the provider and are not directly reported to Railbed. An unsuccessful Live attempt may remain open or in progress and later expire. A hold caused only by a short amount reads **Underpaid**; every other hold reads **Being reviewed**. The **Being reviewed** filter lists all held payments, and **Underpaid** narrows it to short amounts. A payment that expires can still receive late settlement evidence. Recheck the current status before acting on an old screenshot or list view. ## Read the details drawer The payment drawer brings together the information saved for the order and the evidence received afterwards. Depending on the payment, it includes: - The description, original price and currency, customer email, source and payment ID. - Your reference, selected pricing plan or integration metadata when present. - Provider information, the amount sent by the provider and the amount reported as received by your wallet. - The payout address locked for that payment and available settlement transaction links. - A timeline of creation, payment start, provider hand-off, confirmation, review and webhook delivery events. Use **Copy** next to the payment ID when matching an order or asking for help. A shortened wallet or transaction address can be opened in the linked explorer for its full details on Live payments. ## Follow the timeline The timeline explains the sequence. A checkout being created, a buyer entering their email and a buyer going to a provider are all earlier steps than a confirmed payment. A **Webhook delivered** entry concerns the notification to your connected system. It does not mean the connected system has completed delivery to the customer. If the payment is Paid but the store has not updated, inspect the endpoint's delivery log in [Developers](https://app.railbed.io/developers), then check the receiving system. ## Read the money correctly The order amount is the customer's checkout price. **You received** is the reported net amount that reached your payout wallet. Fees and conversion can make those amounts different. **Not reported yet** means the net receipt is not yet available in that record. Do not treat it as zero, or substitute the order price as the amount received. Some Paid payments are not included in received totals until the receipt amount is reported. [Wallet and settlement](https://railbed.com/docs/user-guide/wallet-and-settlement.md) explains how those totals work. ## Decide whether to deliver For manual fulfilment, check that the payment is **Paid** and that it matches the intended order. For connected systems, use the [fulfilment guide](https://railbed.com/docs/guides/fulfilment.md) so repeated notifications cannot fulfil the same order twice. For **Being reviewed** or **Underpaid**, read [Review a held payment](https://railbed.com/docs/user-guide/held-payments.md) before making a decision. Do not deliver automatically from Open, In progress, Being reviewed, Underpaid or an unconfirmed browser return. --- # Review a held payment > Understand why a payment is being reviewed or underpaid, and when you can choose to accept the amount that arrived. Source: https://railbed.com/docs/user-guide/held-payments/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## What Being reviewed and Underpaid mean Railbed received payment evidence that did not pass all the checks needed to confirm the payment normally. Both labels mark a held payment: - **Underpaid:** the only problem is that less than the expected amount arrived. Your buyer's page says so too. - **Being reviewed:** any other hold, involving the currency, address or transaction evidence. A hold is a review state in Railbed's record. It does not mean Railbed is holding a balance that you can release from the dashboard. Whether money reached your wallet depends on the evidence for that payment. ## Open the review 1. On **Wallet**, use the review notice, or open **Payments → Being reviewed**. It lists every held payment; **Underpaid** shows only the short amounts. 2. Select the payment and read the reason at the top of the drawer. 3. Match the payment ID, amount, currency and reference to the order you expected. 4. Check the settlement details and your payout wallet. Use the available transaction link for a Live payment. 5. Decide your next step based on the reason and the controls Railbed makes available. A **Needs review** tag identifies holds that the dashboard allows you to accept. The **Payment links → Paid** list filter also includes held payments, so that filter alone is not a confirmation that an order should be delivered. ## When Accept as paid is available For an eligible hold, the drawer offers **Accept as paid**. Use it only after checking your wallet and deciding that the amount received is sufficient for this order. The confirmation marks the payment as Paid with what arrived and sends the normal paid notification to connected integrations. That can trigger order fulfilment. It does not charge the customer for a difference, create another transfer or increase the amount in your wallet. > [!IMPORTANT] > Accept as paid is an order decision. Check the wallet and intended order first. Do not use it just to make a warning disappear. ## When the action is unavailable Some holds cannot be accepted in the dashboard because the evidence does not establish a payment that is safe to accept there. The drawer explains that the money may not have reached your wallet. Keep the order undelivered while you investigate. When asking for help, include the payment ID, the exact reason shown, the order amount and whether it is Test or Live. Do not send an API key, signing secret, seed phrase or private key. ## Do not top up the original payment Railbed does not offer a partial-payment top-up flow. Do not ask a buyer to send a difference to the original payment address. If a new payment is needed, make a new request after establishing what already arrived and what the new request is for. A later valid confirmation can resolve a held payment automatically. Refresh its current status before taking another action, especially if you are returning to an older review. ## Practice before you need it Create a new Test payment and choose **Simulate an underpayment** on the simulated provider page. Find the resulting hold, read its details and try the review flow with simulated money. [Test and Live mode](https://railbed.com/docs/user-guide/test-and-live.md) has the full walkthrough. --- # Understand your wallet and settlement > Know where the money goes, how received totals are calculated, and why your checkout price and wallet balance can differ. Source: https://railbed.com/docs/user-guide/wallet-and-settlement/ · Updated: 2026-09-26 · Railbed by DeepWork user guide ## Your wallet is yours Railbed sends settlement to the Polygon wallet saved for the payment. It does not keep a custodial balance or require you to request a payout from the dashboard. The **Wallet** page is a view of your checkout activity and payout wallet, not a separate Railbed bank account. Use a self-custody Polygon wallet whose keys you control. Your public address belongs in Settings; your seed phrase and private key never do. **Visual example: Read your wallet balance and receipts.** The sample wallet balance is 182.40 USDC. The sample last-24-hours Railbed receipts are 46.31 USDC. The wallet can include other transfers; Railbed receipts include reported net settlements. *Illustrative amounts, not a fee quote. Wallet balance and Railbed receipts measure different things.* ## Balance and received totals are different | Number | What it represents | |---|---| | USDC balance in Live mode | A balance read from your payout wallet on Polygon; it can include funds from elsewhere and reflect transfers out | | Received in 24 hours or 7 days | Reported net receipts from Railbed payments in that period | | Simulated USDC in Test mode | Simulated receipts in the last 7 days; no money in a real wallet | | Original order amount | The price the buyer was asked to pay, in the checkout currency | | You received on a payment | The reported net settlement for that specific payment | A wallet balance can go down if you send funds from that wallet. The Railbed received total is a record of receipts, so those two numbers do not need to match. ## Why the amount received differs Your checkout might be priced in USD, EUR, GBP, CAD or AUD, but settlement is in USDC on Polygon. Provider pricing, conversion and fees affect what arrives. Railbed's fee is shown in **Settings → Payout wallet**; use the rate shown for your account instead of assuming every merchant has the same rate. The fee and payout wallet are locked when a payment starts. Later changes apply to payments started afterwards. Use the payment's own details when investigating an older transaction. An illustrative dollar amount in these docs is not a promise of the net amount you will receive. Review the buyer's provider quote and the payment's reported settlement for the actual figures. ## What Not reported yet means A payment can be marked Paid before all settlement detail has been reported. The dashboard keeps the net receipt as **Not reported yet** rather than guessing from the order price or the amount the provider sent. Those payments may be omitted from received totals until the amount reaching the wallet is reported. The Wallet page shows a note when paid payments are missing from those totals. Open the payment for its latest details and any available transaction information. ## Check a particular receipt 1. Open **Payments** and choose the payment. 2. Confirm its mode, payment ID, order amount and current status. 3. Read **You received** and the payout address for that payment. 4. For a Live payment with a settlement transaction, use **View on Polygonscan** to inspect it. 5. If the payment is Being reviewed, follow the [review guide](https://railbed.com/docs/user-guide/held-payments.md) before delivering the order. A real-time balance read can fail independently of your payment list. If Wallet shows a balance error, choose **Try again**. The error does not itself mean that a payment failed or that the wallet is empty. ## Change the payout wallet Go to **Settings → Payout wallet**. Choose **Connect a different wallet** and pick the wallet, or paste the new public address into **Polygon address**. Then choose **Save settings** and review the full address in the confirmation before choosing **Change wallet**. Settings shows which wallet app a connected address came from. Payments already in progress continue using their saved address. Keep access to the old wallet for those payments. Changing the setting does not move its existing balance to the new wallet. ## Refunds and card disputes Railbed has no automatic refund control that reverses a settled payment. A refund must be arranged separately by the merchant. Recording a refund in WooCommerce does not send funds. Card disputes are handled by the card provider; a payment status in Railbed is not a dispute-management service. Establish what was paid and what arrived before deciding how to handle a customer request. Never interpret deleting a checkout or canceling a payment link as a refund. --- # Make Railbed your own > Put your business name, logo and support details on checkout, and keep your account settings accurate. Source: https://railbed.com/docs/user-guide/branding-and-settings/ · Updated: 2026-09-26 · Railbed by DeepWork user guide ## Business details buyers recognize Open [Settings](https://app.railbed.io/settings). The Business section controls the identity customers see on Railbed payment pages. | Setting | Where it is used | |---|---| | Business name | Identifies your business on buyer-facing checkout | | Logo | Appears on checkouts, payment links and tracking pages | | Support email | Gives buyers a way to contact your business about their order | | Checkout brand color | Applies your selected accent across your business's checkouts and buy buttons | | Profile photo | Identifies your signed-in user in the dashboard | | Your name | Shows your teammates who you are on the **Team** page | Use a support email you monitor. This is your business's support address, not a setting that makes Railbed send receipts or payment-link emails. ## Save text and color changes 1. Change your business name, support email or brand color. 2. Review the **You have unsaved changes** bar. 3. Choose **Save settings**, or **Discard** to return to the saved values. 4. Open a buyer checkout to confirm the saved appearance. Settings are shared across Test and Live mode. A brand or wallet change made while viewing Test mode is still a real change to the business settings. ## Upload a logo or profile photo Use the image control, choose your image, and adjust the crop. The app re-encodes the cropped image before upload. **The image saves as soon as you finish cropping it**; you do not need to press Save settings afterwards. Choose a simple, legible logo that still works at small sizes. Use a supported raster image such as PNG, JPEG or WebP. SVG uploads are not accepted. If an image is rejected, follow the uploader's message and try an ordinary raster export. The business logo and profile photo are separate. Changing your profile photo does not replace the logo customers see. ## Your name and photo If you sign in with Google or GitHub, Railbed fills in your name and profile picture from that account when you haven’t set them yet. Change either one in **Settings → Account**: type your name and choose **Save name**, or upload a different photo. A photo you remove stays removed; signing in again doesn't bring it back. ## Set an individual checkout's appearance Open **Checkouts → your checkout → Edit**. The **Checkout theme** chooses Light or Dark for that checkout. The editor's brand color control changes the merchant brand color, rather than creating a separate color for that one page. For checkout pages and widgets, **Button text** changes the payment button inside the checkout. For a buy button embedded on your website, set its label under **Share → Add it to your site** or in the `label` attribute of its snippet. Save the checkout, then inspect its actual buyer page and any embed using it. The editor preview is useful for composition, but the buyer page shows the full flow. ## Choose Light, Dark or Match system Open **Settings → Appearance** and choose a theme: | Choice | What happens | |---|---| | Light | Uses Railbed's paper background and dark text; the default | | Dark | Uses charcoal surfaces and light text throughout the dashboard | | Match system | Follows this device's appearance preference, including changes made while Railbed is open | The choice applies immediately, without **Save settings**. It is saved in this browser. Other dashboard tabs in the same browser follow the change; another browser or device can have its own preference. If your browser blocks storage, the choice lasts for the current page and the app explains that it cannot save it. Dashboard appearance does not change your customer's checkout, payment link or tracking page. A checkout's own **Checkout theme** remains the buyer-facing setting. Logos and checkout previews keep the backgrounds they need to remain readable. These docs also offer **Light**, **Dark** and **Match system** in the theme selector beside the collection links. The docs site remembers its own choice separately from the dashboard. Code windows remain dark in both reading themes, and the visual examples follow the docs palette while keeping every status readable. ## Keep the payout address accurate The Payout wallet section shows your Polygon address and your Railbed fee. Changing the wallet requires confirmation of the full new address. Payments already in progress still use their saved wallet. Read [Wallet and settlement](https://railbed.com/docs/user-guide/wallet-and-settlement.md#change-the-payout-wallet) before making a wallet change. Never paste a seed phrase or private key into the address field. ## Account access Railbed uses its sign-in flow to verify your identity. Follow the email or social sign-in prompts shown when you log in. If access fails, see [Troubleshooting](https://railbed.com/docs/user-guide/troubleshooting.md#i-cannot-sign-in). Owners can invite other people on the **Team** page and give each one a role: | Role | What they can do | |---|---| | Owner | Everything, including the payout wallet, business settings and the team | | Manager | Checkouts, payment links, orders and customers, and accepting held payments | | Developer | API keys, webhooks, integrations and [AI agents](https://railbed.com/docs/agents.md#connect-an-agent), and reading records | | Viewer | Reading records only | Only owners can invite people or change roles. Only owners and developers can create API keys or connect AI agents. Removing someone, or moving them to a role that can't manage keys, disconnects the AI agents they approved for good; connect them again if they're still needed. Use a [tracking link](https://railbed.com/docs/user-guide/payment-links.md#share-progress-with-someone-else) when someone only needs to see the status of a particular payment request. --- # Connect Railbed to your store > Choose the connection that fits your business, from Shopify or a WordPress plugin to a checkout built by your developer. Source: https://railbed.com/docs/user-guide/integrations/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## Choose a supported connection Open [Integrations](https://app.railbed.io/integrations) to see the current options. The ready-made store connections are **Railbed Checkout for Shopify** and **Railbed for WooCommerce**. For other websites, use a hosted checkout, a buy button, a widget or a custom API integration. | Your setup | Start here | |---|---| | No website, or occasional invoices | [Payment links](https://railbed.com/docs/user-guide/payment-links.md) | | A website with an HTML or custom-code block | [Widgets and buy buttons](https://railbed.com/docs/user-guide/widgets-and-buttons.md) | | A Shopify store | [Set up Shopify](#set-up-shopify) | | A WordPress store using WooCommerce | [Install WooCommerce](#install-woocommerce) | | A custom store, app or game | Give your developer the [API quickstart](https://railbed.com/docs/quickstart.md) | For a different store platform, choose a website embed or ask your developer to use the API. ## Set up Shopify Railbed Checkout for Shopify keeps buyers in your store's cart, then takes them to Railbed's checkout to pay by card. Shopify prices the order, including shipping, tax and discounts, and each paid checkout becomes a real Shopify order with the customer attached. Before you start: - You're the store owner, or staff with the App development permission. - Your store sells in USD, EUR, GBP, CAD or AUD and has at least one product. - Your store uses a theme. Headless storefronts (Hydrogen) aren't supported. - You can stop and come back at any step; Railbed keeps your progress. Open [Integrations → Shopify](https://app.railbed.io/integrations/shopify) and follow its four steps. Railbed checks each one on your store. 1. **Connect your store.** Type your store's .myshopify.com address, your own domain or any link to the store. 2. **Create the Railbed app in Shopify.** In Shopify's Dev Dashboard, create an app manually using the name, App URL and permissions the setup page shows (each has a copy button), and release it. Under the app's **API access requests**, request **Protected customer data** access and select Name, Email, Phone and Address, each for **App functionality**. Install the app on your store, then paste its Client ID and secret into Railbed. 3. **Add Railbed to your theme.** In the theme editor, add a **Custom Liquid** section to the footer, paste the code from the setup page, set its top and bottom padding to 0 and save. 4. **Try it, then go live.** Place a test order through the preview link, then switch the store to Live. Live needs every app check passed and a payout wallet. Until you switch it, the store stays in Test mode: only browsers that opened the preview link see Railbed's checkout, and test orders arrive in Shopify tagged `railbed-test`. Your buyers keep Shopify's checkout. **Checkout settings** on the setup page also sets the store's **Provider choice**, as on [checkout pages](https://railbed.com/docs/user-guide/checkout-pages.md#choose-how-buyers-reach-a-provider): new stores send buyers straight to the best provider with Smart Routing, and Pay opens it in a new tab. Choose **Save provider choice** after changing it. Some carts go to Shopify's own checkout instead, such as subscriptions, gift cards, bundles or a currency other than your store's. Checkout apps such as upsells don't run on Railbed's checkout. The setup page's **Good to know** lists the rest. ## Install WooCommerce The plugin requires WordPress 6.9 or later, WooCommerce 10.9 or later and PHP 8.3 or later. It supports classic checkout and Checkout Blocks. Use HTTPS for the store, including Test mode outside a local WordPress environment. Open [Integrations → WooCommerce](https://app.railbed.io/integrations/woocommerce) in Railbed and follow its four steps. Railbed checks each one as you go. The page follows the dashboard's mode: set up in Live to take real payments straight away, or turn Live off to try it in Test first. 1. Download the plugin ZIP from that page. In WordPress, open **Plugins → Add new plugin → Upload plugin**, choose the ZIP, then **Install now** and **Activate**. 2. Create an API key on the setup page and copy it (it's shown once), and add your store's webhook by typing your store's address. In **WooCommerce → Settings → Payments → Railbed**, choose the same mode, paste the key and the webhook's secret into that mode's fields and save. Keep both values private. 3. Send the test webhook from the setup page. Reload the plugin settings and confirm **Connection verified**. 4. Tick **Enable** to offer Railbed at checkout. In Live mode, your first order completes this step. In Test mode, open the plugin's preview link (Test mode is shown only to store managers and to browsers with that link), place a Test order and complete **Simulate successful payment**. Confirm the order changes to Processing, or Completed for an order whose items are all virtual and downloadable. The setup page's **Checkout settings** card holds the **Provider choice** for all your WooCommerce orders, in Test and Live mode. Your first WooCommerce key starts it on Smart Routing, so buyers go straight to the best provider after Continue to pay. The plugin needs no change. [The WooCommerce guide](https://railbed.com/docs/guides/woocommerce.md) contains the connection details, requirements and recovery notes. Keep that guide open alongside the plugin settings. ## Move the store to Live If you set up in Test first, confirm your payout wallet, switch Railbed to Live mode and repeat the setup page's steps using a **Live key**, a **Live endpoint** and the plugin's **Live** fields. A Live endpoint uses **Send ping** rather than a simulated payment event. Keep the Enable setting on only when the connection has been verified and you are ready to accept customers. Test orders retain their Test mode when you change the plugin mode. A mode switch does not turn an old Test order into a real payment. ## What connects the payment to the order The plugin creates a payment for the order, directs the customer to checkout, and verifies the result before marking the order paid. A signed webhook starts the check; the plugin then retrieves the payment through the authenticated API. The customer's return to the store is not the proof that releases an order. If the store and Railbed disagree, compare the payment ID, mode and status, then check webhook delivery and WordPress cron. See [Troubleshooting](https://railbed.com/docs/user-guide/troubleshooting.md#railbed-says-paid-but-my-store-does-not). ## Follow store orders and customers With Railbed for WooCommerce 1.1 or later, new checkouts also send the purchase details to [Orders](https://railbed.com/docs/user-guide/orders.md) and the buyer details to [Customers](https://railbed.com/docs/user-guide/customers.md). The order can include item quantities and variants, coupon discounts, shipping, tax, addresses and a link back to WooCommerce. Manage fulfilment in WooCommerce. **Completed** marks the linked Railbed order fulfilled; moving it back to **Processing** or **On hold** marks it not fulfilled. Updates run in the background and retry when Railbed cannot be reached. If an update still fails, inspect the order's private note and use **Order actions → Send fulfilment status to Railbed**. Cancelling an order or recording a refund in WooCommerce shows on the linked Railbed order's timeline; it never moves money. In Test mode, the plugin offers Railbed only to signed-in store managers and to browsers that opened the preview link in its settings, and from version 1.1.1 WordPress offers plugin updates like any other plugin. Details: [WooCommerce](https://railbed.com/docs/guides/woocommerce.md). Orders placed before the 1.1 upgrade are not linked for this fulfilment sync. If a store extension makes the line totals inconsistent, the plugin can send the order as a single total instead; check its private order note and the WooCommerce order for the original breakdown. The payment confirmation process still applies. The [WooCommerce developer guide](https://railbed.com/docs/guides/woocommerce.md) covers upgrade and sync details. A custom API integration can provide the same order and customer information when creating a session. ## Give your developer the right resources You do not need to learn the API to use payment links. When you need automatic order delivery, a custom purchase screen, or customer-account metadata, share these guides with the person connecting your system: - [Developer quickstart](https://railbed.com/docs/quickstart.md): Create and check a payment from a server. - [Fulfil orders safely](https://railbed.com/docs/guides/fulfilment.md): Confirm the right order and prevent duplicate delivery. - [Webhooks](https://railbed.com/docs/webhooks.md): Receive signed payment events and recover missed deliveries. - [Your own checkout](https://railbed.com/docs/guides/custom-checkout.md): Build a custom purchase screen while card entry stays with a provider. Create keys and endpoints in the matching mode. API keys and signing secrets belong in your server or store configuration, never in a public web page, widget snippet or customer message. A key that only needs to look things up, such as one for a reporting tool, can be **Read only**. ### Let an AI agent connect If you or your developer use an AI coding assistant, it can ask for its own access instead of being given a key: it reads `railbed.com/auth.md`, gives you a link, and you approve it on a Railbed page, choosing the business, Test or Live and full access or read only. Owners and developers can approve agents. Connected agents are listed under **Developers → AI agents**, where you can disconnect them. [How agent sign-in works](https://railbed.com/docs/agents.md#connect-an-agent). --- # Use Railbed on your phone > Manage payments in your browser, or add the dashboard to your Home Screen for quicker access. Source: https://railbed.com/docs/user-guide/mobile/ · Updated: 2026-09-26 · Railbed by DeepWork user guide ## Use the dashboard anywhere Open [the dashboard](https://app.railbed.io/) in your browser and sign in. On smaller screens, use the navigation menu to move between Wallet, Payments, Payment links, Checkouts and Settings. Payment lists adapt to the screen so the amount and status remain readable. You can share a payment link from your phone, open a request to check its progress, or review payment details without installing a separate store app. ## Add the dashboard to your device In **Settings → Railbed on your device**, follow the instructions for your browser: - **iPhone or iPad:** open Railbed in Safari, use the Share menu and choose Add to Home Screen. If offered, leave Open as Web App on. - **Android or desktop:** use your browser's Install app or Add to Home screen command when available. - **Safari on Mac:** use File → Add to Dock when available. Browser wording can vary. The installed shortcut opens the same Railbed dashboard and uses the same account. Buyer checkout links remain ordinary browser pages rather than becoming the installed merchant dashboard. ## Stay connected when managing payments An internet connection is required to load current payment and wallet information or to save changes. The offline fallback explains that you are offline; it is not an offline copy of your payment history and does not queue payment actions for later. If the connection drops, reconnect and refresh the relevant payment before deciding whether to deliver an order. Do not use an old screen as confirmation of a payment's current status. ## Help a buyer returning from a provider A provider can open a new browser tab when the buyer pays. They should keep the original Railbed checkout tab available and return to it after finishing. If the browser blocks the new tab, payment may continue in the original tab. The exact tab and app-switching experience depends on the device and browser. See [The buyer experience](https://railbed.com/docs/user-guide/buyer-experience.md#why-there-are-two-tabs) for what the buyer should expect and why a return page alone does not prove payment. --- # Find your next step > Start with the symptom, check the current payment state, and take the next action without losing track of the order. Source: https://railbed.com/docs/user-guide/troubleshooting/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## My link or payment is missing Check the dashboard's **Live** switch. Links, checkouts, payments, API keys and webhook endpoints belong to either Test or Live mode. Switch to the mode in which the item was created, then search Payments using the payment ID or customer email. If you still cannot find it, confirm that you are signed in to the intended Railbed account. An existing buyer URL is not changed by your current dashboard mode. ## The customer did not receive an email Railbed does not send payment links by email. The Email field on a payment request prefills the buyer's checkout. Copy and send the saved link yourself. Railbed also does not send automatic buyer receipt emails. The confirmed checkout shows an on-screen receipt, or follows your success URL. Store order emails are handled by your store or integration. ## The payment is stuck In progress Open its details and timeline. In progress means the buyer went to a provider; it does not mean Railbed has confirmed payment. Ask the buyer whether the provider completed the payment, requested another step or showed a decline. Keep the original checkout tab available. Live provider declines are not sent directly to Railbed, so an unsuccessful attempt can stay in progress until it expires. Avoid creating another charge request solely because the buyer's browser has not updated. For an API or reusable-checkout payment, the normal payment window is 24 hours. Payment links use their configured expiry. Late payment evidence can still change the result; check the latest status before following up. ## The payment says Being reviewed or Underpaid Open **Payments → Being reviewed** and read the reason on that payment. **Underpaid** means less than the full amount arrived; **Being reviewed** covers every other cause. Follow [Review a held payment](https://railbed.com/docs/user-guide/held-payments.md). Only use **Accept as paid** when it is offered and you have checked what arrived in your wallet. That action can trigger fulfilment. Do not ask a customer to top up the original payment address. ## Railbed says Paid but my store does not Match the order to the correct Railbed payment ID and mode. In **Developers**, inspect the endpoint's delivery log. A failing endpoint may need a corrected URL, public access to that URL, or the correct signing secret in your store. After fixing the cause, use the available retry or resend action for the event. Your receiver must handle duplicate events safely. In WooCommerce, also check the plugin's connection status and that WordPress cron is running. A delivered webhook means the endpoint accepted the request. Your own store still needs to process it correctly. [Webhook delivery and retries](https://railbed.com/docs/webhooks.md) and [WooCommerce](https://railbed.com/docs/guides/woocommerce.md) explain the integration checks. ## My wallet balance does not match my sales The Live wallet balance is read from Polygon and can include non-Railbed deposits or transfers out. Railbed's received totals include reported net receipts, not the full original order prices. If a Paid payment shows **Not reported yet**, it is missing a reported net settlement amount and may not be included in the received totals. If the wallet balance itself failed to load, use **Try again**. [Read the balance correctly](https://railbed.com/docs/user-guide/wallet-and-settlement.md). ## There are no available card providers Provider eligibility depends on the buyer's country, currency, amount, minimums and current availability. Test mode's fixed providers do not prove that the same choices are available for a Live buyer. A checkout set to **One provider** with Smart Routing turned off stops buyers that provider can't serve, with a message naming it. Turn the Smart Routing option back on, or choose Smart Routing, to let those buyers pay. Check the amount and currency you intended to charge, and that the checkout is in the intended mode. Do not promise a specific provider or an ID-free checkout to every customer. If asking for help, include the country, price, currency, mode and checkout URL; do not include card details. ## A button or widget does not appear Check that your site contains the embed script and the matching `railbed-button` or `railbed-checkout` element. Use the slug copied from the saved checkout, and verify that the checkout still exists. Some website editors strip scripts or do not execute them in preview. Test the published page and use the site's supported custom-code block. [Widgets and buy buttons](https://railbed.com/docs/user-guide/widgets-and-buttons.md#troubleshoot-an-embed) has copyable examples. ## The buyer did not reach my thank-you page Check that the success URL was saved on the right link or checkout. Railbed redirects from the original checkout tab only after payment confirmation. A buyer who closes it or remains on the provider's page may not reach your URL. Check payment status in Railbed instead of treating a missing redirect as a failed payment. Do not alter a provider's own return links. [The two-tab flow](https://railbed.com/docs/user-guide/buyer-experience.md#why-there-are-two-tabs) explains the behavior. ## My AI agent can't connect or stopped working An AI agent connects through a link it gives you, which you approve on a Railbed page ([how it works](https://railbed.com/docs/agents.md#connect-an-agent)). What the page or the agent says usually tells you which of these it is: | What you see | What to do | |---|---| | The request expired, or the code didn't work | The link and the code each last only a few minutes. Ask your agent to start connecting again, then use the new link straight away | | The agent asked with a different email address | Sign in with the email address you gave the agent, or ask the agent to start again with yours | | Only owners and developers can connect agents | Ask an owner to make you a developer, or to connect the agent themselves | | Agents use the Checkout API, which is off | The API is turned off for this business. Contact support to ask about turning it on | | The agent reports `agent_disconnected` | Someone disconnected it under **Developers → AI agents**, or the person who approved it left the business or can no longer manage keys. Connect it again | | The agent reports `insufficient_scope` | It was approved with **Read only**. Connect it again with **Full access** if it needs to create checkouts or update orders | | The agent can't find a payment you can see | It works in the mode it was approved for. An agent connected in Test sees only Test data | Railbed never asks you for the code. If someone you didn't ask contacts you about one, don't share it. ## I cannot sign in Return to [Railbed sign-in](https://app.railbed.io/login) and follow the currently offered email or social sign-in flow. Use the email/account associated with your merchant account and complete the verification prompts shown. If your session expired, sign in again before retrying the action. An offline page or failed connection cannot confirm current account access. Do not share a one-time code, password or session details in a support message. ## Prepare a useful support request Include the payment ID, Test or Live mode, the time of the issue, what you expected, and the exact message shown. For a website issue, include the checkout URL and browser/device. For a store issue, include the store order reference and its matching payment ID. Leave out passwords, seed phrases, private keys, card numbers, API keys and webhook signing secrets. Screenshots should be cropped to the issue and any unrelated customer details removed. --- # Railbed, in plain language > A short reference for the words you will see in checkout, the dashboard and your payment records. Source: https://railbed.com/docs/user-guide/glossary/ · Updated: 2026-10-05 · Railbed by DeepWork user guide ## Payment and checkout terms | Term | Meaning | |---|---| | Payment link | A request for one payment with a set amount; useful for an invoice or deposit | | Checkout page | A reusable product page; every buyer creates a separate payment | | Pricing table | A reusable checkout with several plans and optional two-way price choices | | Payment widget | A compact checkout placed inside your own website | | Buy button | A website button that opens a Railbed checkout in an overlay | | Tracking link | A shareable, status-only view of a payment request | | Reference | Your own invoice or order label, attached to a payment | | Payment ID | Railbed's identifier for one payment, beginning with pay_ | | Order | The purchase record containing items, totals, customer and one or more payment attempts; see [Orders](https://railbed.com/docs/user-guide/orders.md) | | Customer | The purchase history associated with one email address in one mode; see [Customers](https://railbed.com/docs/user-guide/customers.md) | | Payment attempt | One try at paying an order; a retry can belong to the same order | | Success URL | The thank-you destination opened from checkout after payment confirmation | | Smart Routing | The checks and preferences that choose eligible card providers for a buyer, and the checkout setting that sends buyers straight to the top one | | Provider | The licensed service whose page takes the buyer's card details and payment | ## Wallet and money terms | Term | Meaning | |---|---| | USDC | The dollar-denominated stablecoin used for merchant settlement | | Polygon | The blockchain network used for that settlement | | Self-custody wallet | A wallet whose private keys you control | | Payout wallet | Your saved public Polygon address for receiving settlement | | Deposit address | A payment-specific intermediate address; it is not the wallet setting you should change | | Settlement | The funds transfer associated with a confirmed payment | | Net receipt | The reported amount that reached your wallet after applicable fees | | Held payment | A payment marked Being reviewed or Underpaid because confirmation checks need attention | | Underpaid | A held payment whose only problem is that less than the full amount arrived | | Accept as paid | Your explicit decision to accept an eligible held payment with the amount that arrived | ## Integration terms | Term | Meaning | |---|---| | Test mode | A separate environment of simulated payments; no card is charged | | Live mode | The mode for real card payments | | API key | A private credential that lets your server or plugin access Railbed in a specific mode, with full access or read only | | AI agent connection | Access you approved for an AI agent (such as a coding assistant) to use your account's API in one mode, with full access or read only. You can disconnect it under **Developers → AI agents**; see [Connect an agent](https://railbed.com/docs/agents.md#connect-an-agent) | | Webhook | A signed notification from Railbed to a server when a payment event happens | | Signing secret | A private value your receiver uses to verify a webhook | | Fulfilment | Your delivery record, separate from payment status; see [Orders and fulfilment](https://railbed.com/docs/user-guide/orders.md#payment-and-fulfilment-are-separate) | | Idempotency | Retrying the same operation without accidentally creating the same order's payment twice | For the meaning of a specific status and what to do next, see [Payments and statuses](https://railbed.com/docs/user-guide/payments.md#understand-each-status). For API fields and event payloads, use the [developer reference](https://railbed.com/docs/api.md). --- # Railbed developer docs > Take card payments that settle as USDC to a wallet you control. Create checkouts from your server, send buyers to a hosted page or build your own, and fulfil orders from signed webhooks. Source: https://railbed.com/docs/ · Updated: 2026-10-05 · Railbed by DeepWork developer docs ## What you can build Using the dashboard without an integration? Start with the [Railbed user guide](https://railbed.com/docs/user-guide.md) for account setup, payment links, checkouts and everyday payment management. Railbed gives your store a card checkout that never holds your money. A buyer pays by card through a licensed on-ramp provider, and the sale settles as USDC on Polygon straight to your own wallet. Your integration decides where the buyer pays and when an order counts as paid; Railbed handles the checkout, the provider hand-off and the settlement checks. - [Quickstart](https://railbed.com/docs/quickstart.md): Create a key, make a checkout session and take a simulated payment. - [How payments work](https://railbed.com/docs/how-it-works.md): The life of a payment, from checkout to settlement, and what each status means for your order. - [API reference](https://railbed.com/docs/api.md): Authentication, idempotency, pagination, errors and every endpoint, with examples. - [Webhooks](https://railbed.com/docs/webhooks.md): Signed events for every step of a payment, retries, and the delivery log. ## Choose an integration Every integration ends the same way: the buyer pays on a provider's page and the money settles to your wallet. They differ in how much you build. | Integration | You build | Best for | |---|---|---| | [Payment links and buy buttons](https://railbed.com/docs/guides/no-code.md) | Nothing. Create links, checkout pages and pricing tables in the dashboard; paste a buy button into any site | Invoices, one product, getting started this afternoon | | [WooCommerce](https://railbed.com/docs/guides/woocommerce.md) | Nothing. Install the plugin and connect it with a key and a webhook secret | WordPress stores | | [Hosted checkout](https://railbed.com/docs/guides/hosted-checkout.md) | One API call per order, then a redirect | Custom stores and apps that want Railbed's checkout page | | [Your own checkout](https://railbed.com/docs/guides/custom-checkout.md) | The whole buyer screen: your server starts the session and shows the card providers | Apps and games with their own purchase flow | Whichever you choose, fulfil orders the same way: from a verified `payment.paid` webhook or an authenticated status check. [Fulfil orders safely](https://railbed.com/docs/guides/fulfilment.md) shows how. ## The API at a glance The API is JSON over HTTPS at `https://pay.railbed.io/v1`. Your server authenticates with a secret key; buyers never see it. Example: Create a checkout session ```bash curl https://pay.railbed.io/v1/checkout_sessions \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_1042" \ -d '{ "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042" }' ``` Example: Response · response 201 Created ```json { "id": "pay_7AAiYH0Ykt11ED4hmfiN", "object": "checkout_session", "url": "https://pay.railbed.io/p/pay_7AAiYH0Ykt11ED4hmfiN", "status": "open", "livemode": false, "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042", "customer_email": null, "created": 1790380525, "expires_at": 1790466925, "started_at": null, "metadata": null, "order_id": "ord_M4tQx8ZkP1vRn6WcLs2B" } ``` Send the buyer to `url`. When they pay, Railbed sends a signed `payment.paid` event to your server, and `GET /v1/payments/pay_…` reports `"status": "paid"`. Every session belongs to an [order](https://railbed.com/docs/api/orders.md): send its items, totals and addresses as `order`, and your orders and customers appear in the dashboard and in every webhook. ## Test mode and live mode Every account has two separate modes. **Test mode** simulates payments: no card is charged, no money moves, and you can make a payment succeed, fall short or be declined on demand. **Live mode** takes real card payments. Keys, webhook endpoints, checkouts and payments belong to one mode, and a test key can never touch a live payment. Build and test everything in Test mode first; [Testing](https://railbed.com/docs/testing.md) covers the tools. ## What Railbed doesn't do Card details are always entered on the provider's own page, never on Railbed's pages or yours, so your integration stays out of card-data scope. Railbed can't reverse a settled payment: refunds are sent from your wallet, and card disputes are handled by the provider that charged the card. There are no subscription, refund or event-list endpoints today. > [!TIP] > Building with an AI assistant? Every page here has a Markdown copy, and [/docs/llms.txt](https://railbed.com/docs/llms.txt) summarises the whole API. See [For AI agents](https://railbed.com/docs/agents.md). --- # Quickstart > Take your first payment in Test mode. Create a secret key, create a checkout session, pay it as a buyer, and confirm it from your server. Source: https://railbed.com/docs/quickstart/ · Updated: 2026-10-06 · Railbed by DeepWork developer docs ## Before you start You need a Railbed account ([sign up](https://app.railbed.io/signup)) and a terminal. Everything below runs in **Test mode**: no card is charged and no money moves. The same code works in Live mode with a live key. ## 1. Create a secret key In the dashboard, switch to **Test** mode, open [Developers](https://app.railbed.io/developers) and choose **Create key**. Name it after the server that will use it, for example "Store backend", and keep **Full access** (a **Read only** key can't create checkouts). The key starts with `rb_test_` and is shown once: copy it into your server's environment. Using an AI coding agent? It can [connect to your account itself](https://railbed.com/docs/agents.md#connect-an-agent) through `railbed.com/auth.md`, so you never paste a key into a chat. Example: Your server's environment ```bash export RAILBED_SECRET_KEY="rb_test_…" ``` > [!IMPORTANT] > A secret key can create checkouts and read your payments. Keep it on your server. Never put it in a web page, a mobile app or a repository. If one leaks, revoke it in Developers and create another. ## 2. Create a checkout session A checkout session is one payment for one order: a fixed amount, a currency, and your order's reference. Send an `Idempotency-Key` so a retried request can never create a second payment. Example: cURL ```bash curl https://pay.railbed.io/v1/checkout_sessions \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_1042" \ -d '{ "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042", "customer_email": "buyer@example.com", "success_url": "https://yourstore.com/thanks?order={REFERENCE}" }' ``` Example: Node\.js ```js const res = await fetch('https://pay.railbed.io/v1/checkout_sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'order_1042', }, body: JSON.stringify({ amount: '49.00', currency: 'USD', description: 'Pro Membership', reference: 'order_1042', customer_email: 'buyer@example.com', success_url: 'https://yourstore.com/thanks?order={REFERENCE}', }), }); const session = await res.json(); console.log(session.url); ``` Example: Python ```python import os, requests session = requests.post( "https://pay.railbed.io/v1/checkout_sessions", headers={ "Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}", "Idempotency-Key": "order_1042", }, json={ "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042", "customer_email": "buyer@example.com", "success_url": "https://yourstore.com/thanks?order={REFERENCE}", }, timeout=15, ).json() print(session["url"]) ``` Example: PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', 'Idempotency-Key: order_1042', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => '49.00', 'currency' => 'USD', 'description' => 'Pro Membership', 'reference' => 'order_1042', 'customer_email' => 'buyer@example.com', 'success_url' => 'https://yourstore.com/thanks?order={REFERENCE}', ]), ]); $session = json_decode(curl_exec($ch), true); echo $session['url']; ``` The response is the new session. Save its `id` (`pay_…`) with your order. Example: Response · response 201 Created ```json { "id": "pay_7AAiYH0Ykt11ED4hmfiN", "object": "checkout_session", "url": "https://pay.railbed.io/p/pay_7AAiYH0Ykt11ED4hmfiN", "status": "open", "livemode": false, "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042", "customer_email": "buyer@example.com", "created": 1790380525, "expires_at": 1790466925, "started_at": null, "metadata": null, "order_id": "ord_M4tQx8ZkP1vRn6WcLs2B" } ``` ## 3. Pay as a buyer Open the session's `url` in a browser. Continue with the email, pick a card provider and choose **Pay**. In Test mode the provider's page is replaced by Railbed's test provider, where **Simulate successful payment** stands in for entering a card. The checkout tab then shows the payment as paid and takes the buyer to your `success_url`, with `{REFERENCE}` filled in. ## 4. Confirm the payment from your server The redirect is for the buyer's comfort, not proof of payment. Confirm it from your server, with the same key: Example: cURL ```bash curl https://pay.railbed.io/v1/payments/pay_7AAiYH0Ykt11ED4hmfiN \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" ``` Example: Node\.js ```js const res = await fetch(`https://pay.railbed.io/v1/payments/${paymentId}`, { headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` }, }); const payment = await res.json(); if (payment.status === 'paid') fulfil(payment.reference); ``` Example: Python ```python payment = requests.get( f"https://pay.railbed.io/v1/payments/{payment_id}", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, timeout=15, ).json() if payment["status"] == "paid": fulfil(payment["reference"]) ``` Example: PHP ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY')], ]); $payment = json_decode(curl_exec($ch), true); if ($payment['status'] === 'paid') fulfil($payment['reference']); ``` Example: Response (trimmed) · response 200 OK ```json { "id": "pay_7AAiYH0Ykt11ED4hmfiN", "object": "payment", "status": "paid", "livemode": false, "amount": "49.00", "currency": "USD", "reference": "order_1042", "paid_at": 1790381342, "provider": "stripe", "settlement": { "coin": "polygon_usdc", "value_coin": "47.53", "merchant_received": "44.915850", "txid_in": "0x0c65…582e", "txid_out": "0x8202…356f", "payout_wallet": "0xF977814e90dA44bFA03b6295A0616a897441aceC" } } ``` Check that `status` is `paid` and that the id, amount, currency and reference match the order you saved, then fulfil it once. ## 5. Get told instead of asking Polling works, but webhooks tell you the moment something happens. In [Developers](https://app.railbed.io/developers), choose **Add endpoint** and enter your server's `https://` address (while you build on your own computer, [use a tunnel](https://railbed.com/docs/testing.md#receive-webhooks-on-your-own-computer)). Copy the signing secret, then choose **Send test event** to see exactly what your server receives. [Webhooks](https://railbed.com/docs/webhooks.md) covers the events and [verifying signatures](https://railbed.com/docs/webhooks/signatures.md). ## Next - [Fulfil orders safely](https://railbed.com/docs/guides/fulfilment.md): The checks that make fulfilment correct even with retries, late payments and reviews. - [Your own checkout](https://railbed.com/docs/guides/custom-checkout.md): Show card providers in your own screen instead of redirecting. - [Testing](https://railbed.com/docs/testing.md): Simulate underpayments and declines, and send sample events. - [Going live](https://railbed.com/docs/testing.md#going-live): The short checklist before real payments. --- # How payments work > The life of a Railbed payment, from the checkout to the USDC in your wallet, and what each status means for the order behind it. Source: https://railbed.com/docs/how-it-works/ · Updated: 2026-10-04 · Railbed by DeepWork developer docs ## The journey of one payment 1. **Your server creates a checkout session** for an order: amount, currency, description and your reference. Nothing is charged yet, and nothing is reserved. 2. **The buyer starts paying.** On Railbed's hosted page (or your own screen) they enter an email and see the card providers that can take this payment in their country, ranked by Smart Routing. Starting assigns the payment a one-time deposit address and locks the fee and the order's value in USD. 3. **The buyer pays on the provider's page.** The provider (Stripe, Coinbase, PayPal and others) runs its own card checkout and any identity check it requires, then buys USDC with the card payment and sends it to the payment's deposit address. 4. **The USDC is forwarded to your wallet.** Your share goes straight to the payout wallet saved in your dashboard, on the Polygon network. Railbed never holds it. 5. **Railbed checks the settlement** before calling the payment paid: the right amount reached the right wallet, and the transaction hasn't been counted before. Then the payment becomes `paid`, and your webhooks and API report it. The buyer's checkout tab waits while they pay and sends them to your `success_url` once the payment is confirmed. Providers don't send buyers back on their own, so your order should never depend on the redirect: fulfil from the [webhook or the API](https://railbed.com/docs/guides/fulfilment.md). ## Statuses | Status | Meaning | What to do | |---|---|---| | `open` | Created, or started but not yet handed to a provider | Wait. The buyer hasn't paid | | `pending` | The buyer went to a provider to pay | Wait. How long depends on the provider; see [timing](#timing) | | `paid` | The money arrived and passed Railbed's checks | Fulfil the order, once | | `held` | Money arrived but failed a check, usually because less arrived than expected | Don't fulfil yet. Review it in the dashboard, where you can accept it as paid | | `failed` | Terminal Test failure. Live providers do not report declines | Let the buyer try again with a new session | | `expired` | Nobody paid within the session's time (24 hours for API sessions) | Treat as abandoned, but keep listening: money that arrives late still settles and the payment becomes `paid` or `held` | A payment link the merchant cancels before the payer has gone to a card provider also reads `expired`, with `canceled_at` set. ## What arrives in your wallet Providers charge the buyer the order's price in their currency and deliver USDC for it. The amount that arrives (`value_coin`) is lower than the price because the provider keeps its own fee and spread. From that, a 1% network fee and the Railbed fee shown in your dashboard come off, and the rest (`merchant_received`) is forwarded to your wallet. The fee that applies is locked when the buyer starts paying, so a later fee change never affects a payment already in progress. A payment counts as paid when what arrived is at least 90% of the order's value in USD. Anything less is `held` for your review rather than paid, so an order is never fulfilled on a large shortfall by accident. Orders in EUR, GBP, CAD or AUD are converted to USD at the rate when the buyer starts. > [!NOTE] > Settlement is usually USDC on Polygon (`polygon_usdc`). If a payment ever arrives in another coin that can't be valued in dollars, it's held for review instead of counted. ## Identity checks Each provider decides whether to ask the buyer for identification, usually based on amount, country and history. Smart Routing ranks the providers least likely to ask first, but it can't promise that a provider won't. The buyer's identity details stay with the provider. ## Refunds and disputes A settled payment can't be reversed by Railbed: the money is already in your wallet. To refund a buyer, send the funds back yourself. Card disputes and chargebacks are handled by the provider that charged the card, under its own terms. ## Timing | Step | Typical time | |---|---| | Checkout session lifetime (API) | 24 hours from creation | | Payment link lifetime | 1–30 days, chosen when it's created | | Card payment on the provider's page | A few minutes | | Settlement after the provider sends USDC | As soon as the forwarding is reported; if that report is late, Railbed checks unsettled payments on a schedule for up to two days | | Webhook retries | For about a day: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, then 12 hours | --- # Testing > Test mode simulates payments end to end, with no card charged and no money moved. Make payments succeed, fall short or be declined on demand, send sample webhook events, and go live with confidence. Source: https://railbed.com/docs/testing/ · Updated: 2026-10-04 · Railbed by DeepWork developer docs ## Test mode Test mode is a complete, separate copy of your account. Test keys start with `rb_test_`, and everything they create is simulated: checkouts, payments, settlement and webhooks all behave as in Live mode, but no card is charged and no money moves. Switch the dashboard between Test and Live with the toggle at the top of each page. | | Test mode | Live mode | |---|---|---| | Secret keys | `rb_test_…` | `rb_live_…` | | Card payments | Simulated on Railbed's test provider page | Real, on the provider's page | | Settlement | Simulated, with the same checks as live | USDC to your payout wallet | | Webhook endpoints | Any `https://` address (for your own computer, a tunnel) | Public `https://` addresses only | | Webhooks | Real, signed HTTP requests to your endpoints | The same | | `POST /v1/payments/:id/simulate` | Available | Refused | | [AI agents](https://railbed.com/docs/agents.md#connect-an-agent) | The default when you approve one | Only if you choose Live when you approve it | Test and live data never mix: a test key can't read or change a live payment, and each mode has its own endpoints and keys. ## Simulate an outcome as a buyer Open a test session's `url` (or any test checkout or payment link), enter an email and choose **Pay**. The test provider page offers three outcomes: - **Simulate successful payment**: about 97% of the order's value arrives, as a real provider's fee would leave it, and the payment becomes `paid`. - **Simulate an underpayment**: half arrives, so the payment is `held` for review, exactly as a live shortfall would be. - **Simulate declined card**: the payment stays open. Return to checkout and choose another way to pay on the same payment. ## Simulate an outcome from your server To test without a browser, start the session and simulate the outcome through the API. Use `declined` to test trying another provider on the same payment. Use `failed` to test a terminal failure and its `payment.failed` webhook. Example: cURL ```bash # Start it (the buyer would normally do this by choosing a provider) 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": "buyer@example.com" }' # Choose an outcome: "paid", "underpaid", "declined" or "failed" curl -X POST \ https://pay.railbed.io/v1/payments/pay_7AAiYH0Ykt11ED4hmfiN/simulate \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "outcome": "paid" }' ``` Example: Node\.js ```js const api = (path, body) => fetch(`https://pay.railbed.io/v1${path}`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }).then((r) => r.json()); await api(`/checkout_sessions/${id}/start`, { customer_email: 'buyer@example.com', }); const payment = await api(`/payments/${id}/simulate`, { outcome: 'paid', // or 'underpaid', 'declined', 'failed' }); ``` Example: Python ```python def api(path, body): return requests.post( f"https://pay.railbed.io/v1{path}", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, json=body, timeout=15, ).json() api(f"/checkout_sessions/{id}/start", {"customer_email": "buyer@example.com"}) payment = api( f"/payments/{id}/simulate", {"outcome": "paid"}, # or "underpaid", "declined", "failed" ) ``` Example: PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($body), ]); return json_decode(curl_exec($ch), true); } railbed_post("/checkout_sessions/$id/start", [ 'customer_email' => 'buyer@example.com', ]); $payment = railbed_post("/payments/$id/simulate", [ 'outcome' => 'paid', // or 'underpaid', 'declined', 'failed' ]); ``` Simulated money runs through the same settlement checks and webhooks as a live payment. A `declined` outcome records the card decline without a failed webhook or a final payment state, so the same session can be retried. Simulating again on a finished payment returns it unchanged. See [Simulate a payment](https://railbed.com/docs/api/payments.md#simulate-a-payment) for the details. ## Send sample webhook events In [Developers](https://app.railbed.io/developers), choose **Send test event** on a Test endpoint and pick any event. Railbed sends a signed, realistic event with a made-up payment, order and customer (the payment's `metadata.sample` is `"true"`, the order is `ord_sample` and the customer `cus_sample`, and none of them is in your account or the API), then shows the exact body your server received and how it answered. Live endpoints can receive a `ping` only, so a live system never sees a payment that didn't happen. Every delivery, test or real, appears in the endpoint's delivery log with its body, and can be sent again. ## Receive webhooks on your own computer Railbed sends webhooks from the internet, so it can't reach a server that only listens on your computer. While you build, expose your local server through a tunnel that gives it a public `https://` address (for example Cloudflare Tunnel or ngrok), and add that address as a Test endpoint. When the tunnel's address changes, edit the endpoint: pending retries go to the new address. ## A test plan worth running Before going live, check that your integration handles each of these without a human: | Scenario | How to cause it | Expected result | |---|---|---| | A normal payment | Simulate `paid` | Order fulfilled once | | The same event twice | Resend a delivery from the delivery log | Nothing changes the second time | | An underpayment | Simulate `underpaid` | Order not fulfilled; `payment.held` received | | A card decline | Simulate `declined` | Order not fulfilled; buyer can retry another provider on the same session | | A terminal failure | Simulate `failed` | Order not fulfilled; `payment.failed` received; a new session is needed | | A buyer who never pays | Create a session and leave it | Order not fulfilled; `payment.expired` after 24 hours | | A lost create response | Send the same create request twice with one `Idempotency-Key` | One payment, the same `id` both times | | A second try at one order | Create two sessions with the same `order.id` and new keys, and pay the second | One order, both payments in its `payment_ids`, fulfilled once | | A paid order sent again | Create another session for an `order.id` that's paid | `409 order_paid`; the buyer isn't asked to pay twice | | Your server is down | Point the endpoint at a failing address, then fix it | Deliveries retry; **Retry now** in the log delivers it | | A forged webhook | Send a request with a wrong signature | Your endpoint rejects it with a 4xx | ## Going live 1. Add your payout wallet in [Settings](https://app.railbed.io/settings). It must be a self-custody Polygon wallet you control, not an exchange deposit address. 2. Switch the dashboard to **Live** and create a live key and live webhook endpoints. Live endpoints need public `https://` addresses. 3. Put the live key and the live endpoint's signing secret on your production server. Keep the test ones for your test environment. An AI agent you connected in Test stays in Test: if it should work on live data, connect it again and choose **Live**. 4. Take one small real payment and follow it through: `payment.paid` received, the order fulfilled once, USDC in your wallet. --- # Hosted checkout > Create a checkout session for each order on your server, send the buyer to Railbed's hosted page, and bring them back to your store once they've paid. Source: https://railbed.com/docs/guides/hosted-checkout/ · Updated: 2026-09-27 · Railbed by DeepWork developer docs ## How it works Your server creates a [checkout session](https://railbed.com/docs/api/checkout-sessions.md) for the order and redirects the buyer to its `url`. Railbed's page asks for the buyer's email, shows the card providers that can take this payment in their country (the recommended one first), and opens the chosen provider in a new tab. The checkout tab waits there while the buyer pays, then sends them to your `success_url`. 1. **Order placed.** Your server saves the order, then calls `POST /v1/checkout_sessions`. 2. **Redirect.** Your server sends the buyer to the session's `url`. 3. **Payment.** The buyer pays on the provider's page, in its own tab. 4. **Return.** The Railbed tab sees the payment confirmed and sends the buyer to your `success_url`. 5. **Fulfilment.** Your server fulfils the order from the `payment.paid` webhook or `GET /v1/payments/:id`, never from the return alone. ## Create the session Create one session per order attempt, with an `Idempotency-Key` saved alongside the order before you call. A retry after a timeout then returns the same session instead of creating a second one. Example: Node\.js ```js // POST /checkout on your server, after saving the order app.post('/checkout', async (req, res) => { const order = await orders.create({ userId: req.user.id, sku: 'pro-monthly', price: '49.00', currency: 'USD', }); const response = await fetch('https://pay.railbed.io/v1/checkout_sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': `order_${order.id}`, }, body: JSON.stringify({ amount: order.price, currency: order.currency, description: 'Pro Membership · monthly', reference: `order_${order.id}`, customer_email: req.user.email, metadata: { user_id: String(req.user.id) }, success_url: `https://yourstore.com/orders/${order.id}/thanks?payment={PAYMENT_ID}`, cancel_url: `https://yourstore.com/cart`, }), }); if (!response.ok) { return res.status(502).send( 'Checkout is unavailable. Try again in a moment.', ); } const session = await response.json(); await orders.update(order.id, { paymentId: session.id }); res.redirect(303, session.url); }); ``` Example: Python ```python # Flask: after saving the order @app.post("/checkout") def checkout(): order = orders.create( user_id=current_user.id, sku="pro-monthly", price="49.00", currency="USD", ) r = requests.post( "https://pay.railbed.io/v1/checkout_sessions", headers={ "Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}", "Idempotency-Key": f"order_{order.id}", }, json={ "amount": order.price, "currency": order.currency, "description": "Pro Membership · monthly", "reference": f"order_{order.id}", "customer_email": current_user.email, "metadata": {"user_id": str(current_user.id)}, "success_url": ( f"https://yourstore.com/orders/{order.id}" f"/thanks?payment={{PAYMENT_ID}}" ), "cancel_url": "https://yourstore.com/cart", }, timeout=15, ) if not r.ok: return "Checkout is unavailable. Try again in a moment.", 502 session = r.json() orders.update(order.id, payment_id=session["id"]) return redirect(session["url"], code=303) ``` Example: PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', 'Idempotency-Key: order_' . $order['id'], ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => $order['price'], 'currency' => $order['currency'], 'description' => 'Pro Membership · monthly', 'reference' => 'order_' . $order['id'], 'customer_email' => $user['email'], 'metadata' => ['user_id' => (string) $user['id']], 'success_url' => 'https://yourstore.com/orders/' . $order['id'] . '/thanks?payment={PAYMENT_ID}', 'cancel_url' => 'https://yourstore.com/cart', ]), ]); $session = json_decode(curl_exec($ch), true); if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 300) { http_response_code(502); exit('Checkout is unavailable.'); } save_payment_id($order['id'], $session['id']); header('Location: ' . $session['url'], true, 303); ``` > [!NOTE] > In Python f-strings, `{{PAYMENT_ID}}` writes the literal `{PAYMENT_ID}` placeholder. Railbed fills it in, not your code. > [!TIP] > Send the order's items, totals and shipping address as `order`, with your order's id in `order.id`. The order then shows in the dashboard and in every webhook, a buyer's second try joins the same order, and an order that's already paid can't be paid again. See [Orders and customers](https://railbed.com/docs/api/checkout-sessions.md#orders-and-customers). ## Return URLs | Field | When the buyer sees it | |---|---| | `success_url` | Once the payment is confirmed, the Railbed checkout tab sends the buyer here. `{PAYMENT_ID}` and `{REFERENCE}` in the address are replaced with the payment's id and your reference | | `cancel_url` | A link back to your store, named after your business, while the buyer is paying and after a decline or an expiry | Both are optional, must be full `https://` addresses in Live mode (Test also accepts `http://`), up to 1,000 characters, and can't contain a username or password. Without a `success_url`, the buyer sees Railbed's own confirmation. > [!IMPORTANT] > A buyer can open a `success_url` without paying, and a buyer who pays can close the tab before it loads. Treat the return page as a status screen: show "Payment confirmed" only after your server has seen the payment as `paid`, and fulfil from your server. See [Fulfil orders safely](https://railbed.com/docs/guides/fulfilment.md). ## What the buyer sees - **The order.** Your business name and logo, the description and the price, in the currency you set. - **Their email.** Prefilled when you send `customer_email`. Providers use it for their receipt. - **Card providers.** Only those that serve the buyer's country, take the currency and accept the amount, the recommended one first. The buyer pays on the provider's page. - **Waiting.** The checkout tab says to finish paying in the provider's tab and updates by itself. If the browser blocks the new tab, the provider opens in the same tab instead. - **The result.** Paid, being reviewed (held), declined or expired, each with a clear next step. A session can be paid for 24 hours. After that it's `expired`; create a new one if the buyer comes back. ## When there's no provider Providers have minimum amounts and regional limits. When none can take a payment, the checkout says so and asks the buyer to try later. You can check in advance by starting the session through the API: `POST /v1/checkout_sessions/:id/start` answers `409 no_providers`. Orders of a few dollars are the most likely to hit provider minimums. --- # 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. Source: https://railbed.com/docs/guides/custom-checkout/ · Updated: 2026-09-26 · Railbed by DeepWork developer docs ## 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](https://railbed.com/docs/guides/hosted-checkout.md). 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 Example: cURL ```bash 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" }' ``` Example: Node\.js ```js // Your server: POST /api/purchase/:orderId/providers // (called by your checkout screen) const res = await fetch( `https://pay.railbed.io/v1/checkout_sessions/${order.paymentId}/start`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ customer_email: buyer.email, country: buyer.country, // from the buyer's request, if known }), }, ); if (res.status === 409) { const { error } = await res.json(); // e.g. no_providers, already_paid return reply.status(409).send({ code: error.code, message: error.message }); } const started = await res.json(); // Send only what the screen needs. Never send your API key to the browser. reply.send( started.providers.map(({ id, name, note, recommended, handoff_url }) => ({ id, name, note, recommended, handoff_url, })), ); ``` Example: Python ```python r = requests.post( f"https://pay.railbed.io/v1/checkout_sessions/{order.payment_id}/start", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, json={ "customer_email": buyer.email, "country": buyer.country, # from the buyer's request, if known }, timeout=15, ) if r.status_code == 409: error = r.json()["error"] # e.g. no_providers, already_paid return {"code": error["code"], "message": error["message"]}, 409 providers = [ {k: p[k] for k in ("id", "name", "note", "recommended", "handoff_url")} for p in r.json()["providers"] ] ``` Example: PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'customer_email' => $buyer['email'], 'country' => $buyer['country'], ]), ]); $started = json_decode(curl_exec($ch), true); if (curl_getinfo($ch, CURLINFO_HTTP_CODE) === 409) { http_response_code(409); exit(json_encode($started['error'])); // e.g. no_providers, already_paid } $fields = array_flip(['id', 'name', 'note', 'recommended', 'handoff_url']); echo json_encode(array_map( fn ($p) => array_intersect_key($p, $fields), $started['providers'], )); ``` Example: Response (trimmed) · response 200 OK ```json { "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" } ] } ``` 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. Example: In your checkout screen ```html

    Choose how to pay.

    ``` 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`. Example: In your checkout screen ```js 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](https://railbed.com/docs/guides/fulfilment.md) 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 | Status | Code | Meaning | |---|---|---| | 400 | `invalid_email` | No email was saved on the session and none was sent. Send `customer_email` | | 400 | `invalid_country` | `country` isn't a two-letter code | | 409 | `no_providers` | No provider can take this amount in this currency right now. Nothing was started | | 409 | `already_paid`, `held`, `failed` | The payment already finished. Show its result | | 410 | `expired`, `canceled` | The session can no longer be paid. Create a new one | | 409 | `unavailable` | The account can't take payments right now (for Live, check the payout wallet) | | 429 | `rate_limited` | Too many starts. Wait for `Retry-After` seconds | | 502 | `network_unavailable` | The card network didn't answer. Retry in a minute | --- # 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. Source: https://railbed.com/docs/guides/fulfilment/ · Updated: 2026-09-27 · Railbed by DeepWork developer docs ## 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. Send the order itself as [`order`](https://railbed.com/docs/api/checkout-sessions.md#orders-and-customers), with your order's id in `order.id`: a later session for the same order then joins it, and one for an order that's already paid is refused with `409 order_paid`. ## 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. When you ship goods, key it on the order: a buyer who paid two sessions for one order has two paid payments, and should get one parcel and a refund. Resolve the customer from your own order, never from the payment's `customer_email`: buyers can type any email at checkout. Example: Node\.js: one grant per payment, in one transaction ```js 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); }); } ``` > [!NOTE] > The webhook payload uses camelCase names (`reference`, `amount`, `currency`, `mode`, `paidAt` in milliseconds), while the API uses snake_case (`livemode`, `paid_at` in seconds). Many integrations read the webhook only as a signal and then fetch `GET /v1/payments/:id`, so all their checks run on one shape. ## Ship what the order says Every payment belongs to an [order](https://railbed.com/docs/api/orders.md) with the items, the addresses and the [customer](https://railbed.com/docs/api/customers.md), as your store sent them in [`order` and `customer`](https://railbed.com/docs/api/checkout-sessions.md#orders-and-customers). If your store keeps its own orders, ship from those, checked as above. If your fulfilment works from Railbed instead (a warehouse connected to your webhooks, for example), take what to ship from the order: - **In a webhook:** `data.order`, with `items` and `shipTo` (camelCase). See [the order object](https://railbed.com/docs/webhooks/events.md#the-order-object). - **From the API:** `GET /v1/orders/:id` with the payment's `order_id`, with `items` and `shipping_address`. The checks above still apply, plus one: **the paid payment's `amount` and `currency` equal the order's `total` and `currency`.** An order's items and totals follow its latest session, so a late payment for an older session can be for a different basket. Hold those for a person to look at. Ship each order once, keyed on the order's id, then report it with [Update an order](https://railbed.com/docs/api/orders.md#update-an-order) so the dashboard shows it as fulfilled: Example: Node\.js: ship a paid order once, then report it ```js // railbed() is the small helper from /docs/api/errors/ async function shipOrder(paymentId) { const payment = await railbed(`/payments/${paymentId}`); if (payment.status !== 'paid') return; const order = await railbed(`/orders/${payment.order_id}`); // The order follows its latest session: check this payment covers it if ( payment.amount !== order.total || payment.currency !== order.currency ) { return flagForReview(order, payment); } await db.transaction(async (tx) => { // UNIQUE(order_id): a second paid payment for this order ships nothing const isNew = await tx.shipments.insertIfAbsent({ orderId: order.id }); if (!isNew) return; await tx.shipments.queue({ to: order.shipping_address, lines: order.items.map((i) => ({ sku: i.sku, quantity: i.quantity })), }); }); } // Later, when the parcel leaves the warehouse async function markShipped(orderId) { await railbed(`/orders/${orderId}`, { method: 'PATCH', body: JSON.stringify({ fulfillment: 'fulfilled' }), }); } ``` `fulfillment` is a record for you and your team; it doesn't touch the payment. Orders you sent with `order.id` get their fulfilment only from your store, so report every change; send `unfulfilled` if a shipment is recalled. The [WooCommerce plugin](https://railbed.com/docs/guides/woocommerce.md#orders-customers-and-fulfilment) does this for you. ## Webhooks, the API, or both | Signal | Strengths | Watch out for | |---|---|---| | `payment.paid` webhook | Arrives the moment the payment settles; retried for about a day | Your endpoint must be reachable, answer 2xx quickly and verify signatures | | `GET /v1/payments/:id` | Always current; no public endpoint needed | You 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. --- # Payment links and buy buttons > Take payments without writing server code. Create payment links, checkout pages and pricing tables in the dashboard, and add a buy button or an embedded checkout to any website with two lines of HTML. Source: https://railbed.com/docs/guides/no-code/ · Updated: 2026-09-27 · Railbed by DeepWork developer docs ## What you can make in the dashboard For a complete walkthrough of the dashboard, use the [user guide](https://railbed.com/docs/user-guide.md). It includes visual examples, payment-link setup, branding, payment review and troubleshooting. Everything here is created under **Create** in the dashboard, in Test or Live mode. Each one produces a Railbed-hosted page, and every payment appears in Payments and sends your webhooks, exactly like an API payment. Each buyer's attempt is also an order in **Orders**, with one item (the product, the plan or what the link is for), and each buyer's email a customer in **Customers**. | Kind | What it is | Address | |---|---|---| | Payment link | One payment for one payer: an invoice or a deposit, with an optional note, reference and expiry (1–30 days). Comes with a read-only tracking link you can share | `https://pay.railbed.io/p/pay_…` | | Checkout page | One product at one price that any number of buyers can pay | `https://pay.railbed.io/c/your-slug` | | Pricing table | Plans side by side, with an optional monthly/yearly switch. The buyer picks a plan, then pays | `https://pay.railbed.io/c/your-slug` | | Payment widget | The checkout itself in a compact card, made to sit inside your page | `https://pay.railbed.io/c/your-slug` | Every checkout has a success URL (where buyers go once paid, with `{PAYMENT_ID}` filled in; checkout payments carry no reference, so `{REFERENCE}` is empty) and a light or dark theme. Checkout pages and widgets also take the button's text. ## Add a buy button to any site Paste the script once, anywhere in the page, then place a button wherever you want one. `checkout` is the slug at the end of the checkout's address. Example: A buy button that opens the checkout over your page ```html ``` The button takes your brand colour, and screen readers hear the product and its price. Two attributes change it: | Attribute | Values | Default | |---|---|---| | `checkout` | The checkout's slug | required | | `label` | The button text, for example `Get the notes` | "Buy now" ("See plans" for a pricing table) | | `shape` | `pill` or `rounded` | `pill` | If the checkout is deleted or can't take payments, the button reads "Checkout unavailable" and is disabled. Clicking opens the checkout in a dialog over your page. The buyer pays on the provider's page in a new tab; once the payment is confirmed, your page goes to the checkout's success URL. If the buyer tries to close the dialog while paying, it asks first. ## Put the checkout in your page For a payment widget or a pricing table, place the checkout itself in your layout: Example: The checkout, in your page ```html ``` It sizes itself to its content and keeps its theme. Card providers never open inside the frame: they open in a new tab or, if the browser blocks that, in place of your page. ## Good to know - The script has no dependencies, sets no cookies and stores nothing in the buyer's browser. It's plain JavaScript that works in current browsers; older ones show nothing rather than a broken button. - Your page needs no Railbed key: the button only knows the public checkout slug. - Only `/c/` and `/p/` pages can be embedded; the dashboard and tracking pages can't be framed. - Delete a checkout in the dashboard to stop new payments. Past payments stay. - Fulfil from webhooks, as with every integration: payments from buttons carry the checkout's id (`checkoutId`) and, for pricing tables, the chosen plan (`planLabel`). The event's `data.order` and `data.customer` say who bought what; see [event payloads](https://railbed.com/docs/webhooks/events.md#the-order-object). - Mark orders fulfilled in **Orders** once they're paid, or with [Update an order](https://railbed.com/docs/api/orders.md#update-an-order). --- # WooCommerce > Add Railbed card checkout to a WordPress store with the Railbed for WooCommerce plugin. Orders complete from verified payments, and their items, customers and fulfilment appear in Railbed, with no code. Source: https://railbed.com/docs/guides/woocommerce/ · Updated: 2026-10-05 · Railbed by DeepWork developer docs ## What the plugin does Railbed for WooCommerce adds Railbed as a payment method at your store's checkout. When a buyer places an order, the plugin creates a checkout session and sends the buyer to Railbed's hosted page. The order completes only after the plugin has verified the payment with Railbed: a signed webhook starts the check, and the plugin confirms it with an authenticated status request before marking the order paid. Card details never touch WordPress. From version 1.1, each checkout also sends the order's items, totals and addresses and the buyer's contact details, so the order and its customer appear in Railbed's **Orders** and **Customers**, and the plugin keeps the order's fulfilment in step with WooCommerce. See [Orders, customers and fulfilment](#orders-customers-and-fulfilment). **Requirements:** WordPress 6.9 or later, WooCommerce 10.9 or later, PHP 8.3 or later. Works with classic checkout and Checkout Blocks, and with both order storage modes. Order currencies USD, EUR, GBP, CAD and AUD; amounts from 1.00 to 100,000.00. ## Install and connect The dashboard walks you through it: open **Integrations → WooCommerce** in the [dashboard](https://app.railbed.io/integrations/woocommerce). It has four steps, one open at a time, and Railbed checks each one as you go (the plugin using your key, the test webhook answered, the first order in). 1. **Install the plugin.** Download the ZIP from that page. In WordPress, open **Plugins → Add new → Upload plugin**, upload it and activate it alongside WooCommerce. 2. **Connect Railbed.** Create an API key on the setup page (it's shown once: copy it) and add your store's webhook by typing your store's address (or pasting the webhook URL the plugin's settings show; it ends in `?wc-api=railbed_webhook`). Then open **WooCommerce → Settings → Payments → Railbed**, choose **Test** mode, paste the key into **Test API key** and the webhook's secret into **Test webhook secret**, and save. Saving checks the key. 3. **Send a test webhook** from the setup page, then reload the plugin settings: they say **Connection verified**. If something is missing, the plugin's settings list it, including why the last webhook was refused. 4. **Try a test order.** Tick **Enable** (Offer Railbed at checkout) and save. In Test mode, shoppers don't see Railbed: only store managers who are signed in, and browsers that open the **preview link** shown in the plugin's settings (for seven days). Open it in the browser you test with, place an order and pay it with **Simulate successful payment**. The order waits as **Pending payment**, then becomes Processing (or Completed when every item is virtual and downloadable), as with any paid order. **Make a new link** retires the old one. ## Provider choice **Checkout settings** on the setup page decides how your WooCommerce buyers reach a card provider, as on [checkout pages](https://railbed.com/docs/user-guide/checkout-pages.md#choose-how-buyers-reach-a-provider): Smart Routing (the best provider for each buyer, opened straight from Continue to pay), Buyers choose (the ranked list) or One provider. It covers Test and Live orders, and the plugin needs no change. Your first WooCommerce key starts it on Smart Routing. ## Going live Add your payout wallet in Railbed. Switch the dashboard to **Live**: the setup page follows, and you repeat steps 2 and 3 with a live key and a live webhook (a live webhook takes a ping as its test). Then set the plugin's **Payment mode** to Live and save; saving checks the payout wallet, and without one the plugin says so and keeps Railbed off your checkout. The payment method is offered only on an HTTPS store, in Test mode too (unless WordPress's environment type is `local`). Orders keep the mode they were placed in, so switching the plugin to Live doesn't affect test orders in progress. ## Order status while the buyer pays - **Pending payment** while the buyer is on Railbed's page, like any payment method that sends the buyer away to pay. WooCommerce holds the stock for its usual hold time and sends no email yet. - **Processing** (or Completed for downloads) when Railbed confirms the payment: WooCommerce reduces stock and sends its **New order** email to you and **Processing order** email to the buyer. - **On hold** when money arrived but Railbed is [reviewing it](https://railbed.com/docs/user-guide/held-payments.md). Don't ship until it's confirmed. - **Failed** when the checkout closed unpaid (after 24 hours). If a slow provider's payment still arrives and nobody changed the order, it completes as usual. A changed or cancelled order gets a private note to review instead. From version 1.1.2, a buyer returning from a closed checkout can place a fresh order with the cart still in their browser. Both classic checkout and Checkout Blocks create a new order for that retry. If the cart is empty, the buyer returns to the shop to choose the items again. The old order keeps its original payment record. ## Orders, customers and fulfilment Each checkout sends Railbed the WooCommerce order along with the payment. Nothing needs setting up. - **What's sent.** The items (with SKU, quantity and the variation's attributes), fees, the discount and coupon codes, shipping and its method, tax, the billing and shipping addresses, the order number and a link back to the order, and the buyer's name, email, phone and WooCommerce customer ID. In the dashboard the order carries WooCommerce's number and an **Open in WooCommerce** button. - **When the totals don't add up.** If the lines, discount, shipping and tax don't come to the order total to the cent (tax rounding, or an extension that changes totals), the order is sent without its items and totals and shows as one line for the total. The WooCommerce order gets a private note. Payment works the same either way. - **Existing payment attempts.** A buyer's second try at the same WooCommerce order joins its order in Railbed. If Railbed already has a paid payment for that order, or one held for review, no new checkout is created: the buyer is asked to contact you, and the order gets a private note to review. - **Fulfilment.** Orders that ship start as **Not fulfilled**; orders with only virtual products have nothing to ship. Marking an order **Completed** marks it fulfilled in Railbed, and moving it from Completed back to **Processing** or **On hold** marks it not fulfilled. Orders WooCommerce completes on payment (downloads) are marked fulfilled the same way. - **Fulfilment updates run in the background** and are retried four times over about two and a half hours if Railbed can't be reached. If one still fails, the order gets a private note, and **Order actions → Send fulfilment status to Railbed** sends it again. The dashboard shows these orders' fulfilment but doesn't change it: change it in WooCommerce. - **Cancellations and refunds.** Cancelling an order in WooCommerce, or recording a refund, shows on the Railbed order as **Canceled** or **Refund recorded** with the amount. It doesn't change the payment: Railbed can't send money back, so arrange any refund with the buyer yourself. These are sent in the background and retried like fulfilment; **Order actions → Send cancellations and refunds to Railbed** sends one again. See [Report a cancellation or refund](https://railbed.com/docs/api/orders.md#report-a-cancellation-or-refund). - **Orders placed before 1.1** aren't linked to a Railbed order, so their fulfilment, cancellations and refunds aren't sent. For day-to-day tasks, see [Orders and fulfilment](https://railbed.com/docs/user-guide/orders.md) and [Customers](https://railbed.com/docs/user-guide/customers.md). The plugin uses the API's [order fields](https://railbed.com/docs/api/checkout-sessions.md#orders-and-customers) and [Update an order](https://railbed.com/docs/api/orders.md#update-an-order), so a store you build yourself can do the same. ## Updates From version 1.1.1, WordPress shows new Railbed versions on its **Plugins** and **Updates** screens like any other plugin: choose **Update now**, or turn on automatic updates for it. The plugin installs a download only if its checksum matches the one Railbed publishes. To check for new versions it contacts Railbed about twice a day and sends nothing about your store. To get to 1.1.1 from an earlier version, download the plugin from **Integrations** in the [dashboard](https://app.railbed.io/integrations), upload it in **Plugins → Add new → Upload plugin**, and choose **Replace current with uploaded**. Settings, keys and orders carry over, and nothing needs reconnecting. Orders placed before the upgrade finish as they would have. ## Staging copies If you copy your site (to staging, or by restoring a backup somewhere else), the copy notices its new address and pauses Railbed: no checkout, webhooks or background updates there until an administrator answers the notice at the top of the WordPress admin. Choose **Same store, new address** after moving your store, then send another test event. Choose **A copy (staging): disconnect it** on a copy: it removes the copy's keys so it can't change your real store's Railbed orders. Connect a staging copy with Test keys if you want to test there. ## Store settings that matter - Deleting the plugin in WordPress removes its keys and secrets and keeps your orders' payment records. WooCommerce's REST API never returns the saved keys. - Your store must accept a public POST to its webhook address without a login, cache or bot challenge, and keep the `Railbed-Signature` header and the request body unchanged. Security plugins and CDN rules sometimes block it; allow the address. - Run WordPress cron regularly (a real cron job is best). The plugin uses it to re-check orders whose webhook was missed. - If you change the webhook address, send another test event before new checkouts are offered. - When you roll the signing secret in Railbed, paste the new one into the plugin straight away. From the roll on, deliveries are signed with the new secret and the plugin rejects them until it has it; they're retried for about a day, and the plugin's status checks keep orders moving meanwhile. After saving the new secret, send another test event: until a delivery signed with it arrives, the payment method isn't offered at checkout. ## Moving from another gateway Install Railbed as a separate payment method. Keep your old gateway active until its outstanding orders finish, then disable it for new purchases. Orders aren't moved between gateways. --- # API reference > The Railbed REST API. JSON over HTTPS, secret keys for your servers and access tokens for connected agents, scopes, idempotent creates, cursor pagination and plain error codes. Source: https://railbed.com/docs/api/ · Updated: 2026-10-05 · Railbed by DeepWork developer docs ## Base URL Example: Base URL ```text https://pay.railbed.io/v1 ``` Every request uses HTTPS. You create [checkout sessions](https://railbed.com/docs/api/checkout-sessions.md), one for each attempt at paying, and read their [payments](https://railbed.com/docs/api/payments.md) to fulfil orders. A session and its payment share one id (`pay_…`). Each session belongs to an [order](https://railbed.com/docs/api/orders.md), which holds what was bought and where it ships, and each order to a [customer](https://railbed.com/docs/api/customers.md). | Endpoint | What it does | |---|---| | `POST /v1/checkout_sessions` | [Create a checkout session](https://railbed.com/docs/api/checkout-sessions.md#create-a-checkout-session) | | `GET /v1/checkout_sessions/:id` | [Retrieve a checkout session](https://railbed.com/docs/api/checkout-sessions.md#retrieve-a-checkout-session) | | `POST /v1/checkout_sessions/:id/start` | [Start a checkout session](https://railbed.com/docs/api/checkout-sessions.md#start-a-checkout-session) and get its card providers | | `GET /v1/payments/:id` | [Retrieve a payment](https://railbed.com/docs/api/payments.md#retrieve-a-payment) | | `GET /v1/payments` | [List payments](https://railbed.com/docs/api/payments.md#list-payments) | | `POST /v1/payments/:id/simulate` | [Simulate a payment](https://railbed.com/docs/api/payments.md#simulate-a-payment) (Test mode) | | `GET /v1/orders/:id` | [Retrieve an order](https://railbed.com/docs/api/orders.md#retrieve-an-order) | | `GET /v1/orders` | [List orders](https://railbed.com/docs/api/orders.md#list-orders), or find one by your store's order id | | `PATCH /v1/orders/:id` | [Update an order](https://railbed.com/docs/api/orders.md#update-an-order)'s fulfilment | | `POST /v1/orders/:id/events` | [Report a cancellation or refund](https://railbed.com/docs/api/orders.md#report-a-cancellation-or-refund) on the order's timeline | | `GET /v1/customers/:id` | [Retrieve a customer](https://railbed.com/docs/api/customers.md#retrieve-a-customer) | | `GET /v1/customers` | [List customers](https://railbed.com/docs/api/customers.md#list-customers), or find one by email | ## Authentication Send a bearer token on every request: a secret key from your server, or the access token of an AI agent you've [connected to your account](https://railbed.com/docs/agents.md#connect-an-agent). The API treats both the same way. Example: An authenticated request ```bash curl "https://pay.railbed.io/v1/payments?limit=1" \ -H "Authorization: Bearer rb_test_…" ``` The credential decides everything about the request: which account it belongs to, whether it works on Test or Live data, and which endpoints it may use. Nothing else can switch the mode. A suspended account gets `403 suspended`. ### Secret keys Keys are created and revoked in [Developers](https://app.railbed.io/developers), and each is shown once, when it's created. `rb_test_…` keys see only Test data and `rb_live_…` keys only Live data. A missing, malformed or revoked key gets `401 invalid_api_key`. Each key has an access level, chosen when you create it: | Access level | Scopes | What the key can do | |---|---|---| | **Full access** (the default) | All five [scopes](#scopes) | Use every endpoint | | **Read only** | `payments:read`, `orders:read`, `customers:read` | Retrieve and list payments, checkout sessions, orders and customers. Nothing that creates or changes | Keys created before access levels existed have full access. WooCommerce keys always do, because the plugin creates checkout sessions. > [!IMPORTANT] > Secret keys belong on your server. The API doesn't accept requests from browsers (it sends no CORS headers), and there's no publishable key: your web pages and apps call your server, and your server calls Railbed. ### Agent access tokens An AI agent gets access through [agent sign-in](https://railbed.com/docs/agents.md#connect-an-agent). It reads [railbed.com/auth.md](https://railbed.com/auth.md), a person who can manage keys approves it in the dashboard, and the agent receives its own access tokens. Each lasts about five minutes, and the agent gets the next one itself from the token endpoint that auth.md describes. A token carries the business, the mode and the access level (**Full access** or **Read only**) the person chose. Example: An agent's request ```bash curl "https://pay.railbed.io/v1/payments?limit=1" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` | Response | Cause | What the agent should do | |---|---|---| | `401 invalid_token` | The token expired, is malformed or wasn't issued for Railbed | Get a new access token | | `401 agent_disconnected` | The agent was disconnected in the dashboard, or the person who connected it no longer manages keys for the business | Ask the person to connect it again | ### Scopes Every endpoint needs one scope. A key or agent whose access level doesn't include it gets `403 insufficient_scope`, and the message names the scope. | Scope | Endpoints | |---|---| | `payments:read` | `GET /v1/payments`, `GET /v1/payments/:id`, `GET /v1/checkout_sessions/:id` | | `checkouts:write` | `POST /v1/checkout_sessions`, `POST /v1/checkout_sessions/:id/start`, `POST /v1/payments/:id/simulate` | | `orders:read` | `GET /v1/orders`, `GET /v1/orders/:id` | | `orders:write` | `PATCH /v1/orders/:id`, `POST /v1/orders/:id/events` | | `customers:read` | `GET /v1/customers`, `GET /v1/customers/:id` | ### Discovery Every `401` from the API tells an agent where to find out how to get access, in a `WWW-Authenticate` header ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)): Example: A request without a credential · response 401 Unauthorized ```http WWW-Authenticate: Bearer resource_metadata="https://pay.railbed.io/.well-known/oauth-protected-resource/v1" ``` When a credential was sent and refused, the header adds `error="invalid_token"`. A `403 insufficient_scope` carries `error="insufficient_scope", scope="…"`, naming the scope the endpoint needs. The protected resource metadata names the API, its authorization server, the scopes and these docs. The same document is also at `https://pay.railbed.io/.well-known/oauth-protected-resource`. An agent starting out should read [auth.md](https://railbed.com/auth.md), which takes it through the rest. Example: The protected resource metadata · response 200 OK ```json { "resource": "https://pay.railbed.io/v1", "resource_name": "Railbed API", "authorization_servers": ["https://astounding-resonance-81.authkit.app"], "scopes_supported": ["payments:read", "checkouts:write", "orders:read", "orders:write", "customers:read"], "bearer_methods_supported": ["header"], "resource_documentation": "https://railbed.com/docs/api/" } ``` ## Requests and responses - Send JSON bodies with `Content-Type: application/json`, up to 64 KiB. Other types get `415`, larger bodies `413`, and bodies that aren't a JSON object `400 invalid_json`. - Responses are JSON and are never cached (`Cache-Control: no-store`). - Unknown fields in a request are ignored. Build against the fields documented here. - **Money** is a decimal string, never a number, so no amount is ever rounded in transit. Prices have two places (`"49.00"`); settlement amounts in the coin can have more (`merchant_received` has six). - **Currencies** are `USD`, `EUR`, `GBP`, `CAD` and `AUD`. - **Timestamps** in the API are Unix seconds. (A webhook event's `created` is Unix seconds too, but the payment, order and customer inside it use milliseconds; see [event payloads](https://railbed.com/docs/webhooks/events.md#the-payment-object).) - **Ids** are prefixed: `pay_` for sessions and payments, `ord_` for orders, `cus_` for customers, `evt_` for webhook events. ## Idempotency Networks fail. To retry a create safely, send an `Idempotency-Key` header: a value unique to the order attempt, 1–120 printable ASCII characters. Save it with your order before the first call. | Situation | Response | |---|---| | First request with a key | `201` and the new session | | Same key, same body (a retry) | `200` and the same session, with its current status | | Same key, different body | `409 idempotency_conflict`. Use a new key for a different order | | Same key and body while the first request is still running | The same session: one request gets `201`, the others `200`. Rarely, `409 idempotency_conflict` asks you to retry in a moment | | Key longer than 120 characters, blank, or with other characters | `400 invalid_idempotency_key` (never truncated) | Keys are scoped to your account and the key's mode, and remembered for 24 hours; after that the same key creates a new session. Bodies are compared including `order` and `customer`, and metadata key order doesn't matter. Without the header, every create makes a new session, and your `reference` alone doesn't prevent duplicates. ## Pagination The list endpoints (`GET /v1/payments`, `GET /v1/orders` and `GET /v1/customers`) return the newest first, up to `limit` (1–100, default 20) at a time: Example: A page · response 200 OK ```json { "data": [{ "id": "pay_…", "object": "payment", "status": "paid" }], "has_more": true, "next_cursor": "pay_Q3cNtwPT0bRqGmS5eZkB" } ``` Pass `next_cursor` as `starting_after` to get the next page, until `next_cursor` is `null`. The order is stable, even for objects created in the same second. A `limit` outside 1–100, or a cursor that isn't an object from the same list in this mode, gets `400` rather than a silent first page. ## Rate limits Each account can make about **120 requests a minute** across all its keys and modes, and **12 session starts a minute**. Each connected AI agent has its own 120 a minute, so an agent can't use up your servers' budget; its live session starts still count toward the account's 12. Requests with a missing or invalid secret key are limited to about 60 a minute from one address. Over the limit, requests get `429 rate_limited` with a `Retry-After: 60` header. Queue requests on your server and back off with a little randomness; never make a request per animation frame or per page view. The limits protect the service rather than set a quota: they're enforced per region and are approximate, so plan well below them. ## Errors Errors use HTTP status codes and a JSON body with a stable `code`, a sentence for people, and sometimes the `field` at fault: Example: An error · response 400 Bad Request ```json { "error": { "code": "invalid_amount", "message": "amount must be a decimal string between \"1.00\" and \"100000.00\".", "field": "amount" } } ``` Branch on `code` and the status, never on `message`, which may be reworded. [Errors](https://railbed.com/docs/api/errors.md) lists every code. ## Versioning The API is `v1`. Changes within `v1` only add things: new fields in responses, new optional request fields, new error codes, new webhook event types. Write your integration to ignore fields and event types it doesn't know. A change that could break an integration would come as a new version. --- # Checkout sessions > A checkout session is one payment for one order. Create it on your server, send the buyer to its page or start it for your own checkout, and read its payment to fulfil. Source: https://railbed.com/docs/api/checkout-sessions/ · Updated: 2026-09-27 · Railbed by DeepWork developer docs ## The checkout session object | Field | Type | Description | |---|---|---| | `id` | string | The session's id, `pay_…`. The same id identifies its [payment](https://railbed.com/docs/api/payments.md) | | `object` | string | `"checkout_session"` | | `url` | string | The hosted checkout page for this session. Send the buyer here | | `status` | string | `open`, `pending`, `paid`, `held`, `failed` or `expired`. See [statuses](https://railbed.com/docs/how-it-works.md#statuses) | | `livemode` | boolean | `false` for Test mode, `true` for Live mode | | `amount` | string | The price, as set at creation: `"49.00"` | | `currency` | string | `USD`, `EUR`, `GBP`, `CAD` or `AUD` | | `description` | string | What the buyer is paying for, shown on the checkout | | `reference` | string or null | Your own id for the order | | `customer_email` | string or null | The buyer's email, lowercased | | `created` | integer | When it was created, in Unix seconds | | `expires_at` | integer | When it stops accepting new payments: 24 hours after creation | | `started_at` | integer or null | When the buyer (or your server) started it. Set once | | `metadata` | object or null | Your key-value pairs, returned unchanged | | `order_id` | string or null | The [order](https://railbed.com/docs/api/orders.md) this session is a payment attempt for, `ord_…`. Every session has one (`null` only on records from before orders existed) | ## Create a checkout session `POST /v1/checkout_sessions` Creates a session for one order. Nothing is charged and no provider is contacted until the buyer starts paying. Send an [`Idempotency-Key`](https://railbed.com/docs/api.md#idempotency) so a retry returns the same session. | Field | Type | Description | |---|---|---| | `amount` | string · required | The price as a decimal string with at most two decimals, from `"1.00"` to `"100000.00"`. `"49"` and `"49.0"` become `"49.00"`. Numbers, exponents and negative values are refused | | `currency` | string · required | `USD`, `EUR`, `GBP`, `CAD` or `AUD`, in any letter case | | `description` | string · required | What the buyer is paying for, up to 120 characters | | `customer_email` | string or null | The buyer's email. Optional here, but needed before the session can start | | `reference` | string or null | Your order id, up to 120 characters. Returned on the payment and every webhook. Not unique: Railbed doesn't stop two sessions sharing one | | `metadata` | object or null | Up to 20 keys (1–40 characters, not starting with `__`) with string values up to 500 characters. For your own ids, never secrets or card details | | `success_url` | string or null | Where the checkout sends the buyer once the payment is confirmed. `{PAYMENT_ID}` and `{REFERENCE}` are filled in. Up to 1,000 characters | | `cancel_url` | string or null | Where the checkout's "Cancel and return to …" and "Back to …" links lead (they name your business). Up to 1,000 characters | | `customer` | object or null | Who is buying: name, phone and your customer id. See [Orders and customers](#orders-and-customers) | | `order` | object or null | What they're buying: items, discount, shipping, tax, addresses and your order id. See [Orders and customers](#orders-and-customers) | Return URLs must be full `http://` or `https://` addresses (Live mode: `https://` only) with no username or password. In Live mode your account needs a payout wallet first, or the request gets `409 no_payout_wallet`. Example: cURL ```bash curl https://pay.railbed.io/v1/checkout_sessions \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_1042" \ -d '{ "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042", "customer_email": "buyer@example.com", "metadata": { "user_id": "player_1042" }, "success_url": "https://yourstore.com/thanks?order={REFERENCE}&payment={PAYMENT_ID}", "cancel_url": "https://yourstore.com/cart" }' ``` Example: Node\.js ```js const res = await fetch('https://pay.railbed.io/v1/checkout_sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'order_1042', }, body: JSON.stringify({ amount: '49.00', currency: 'USD', description: 'Pro Membership', reference: 'order_1042', customer_email: 'buyer@example.com', metadata: { user_id: 'player_1042' }, success_url: 'https://yourstore.com/thanks?order={REFERENCE}&payment={PAYMENT_ID}', cancel_url: 'https://yourstore.com/cart', }), }); if (!res.ok) throw new Error((await res.json()).error.code); const session = await res.json(); ``` Example: Python ```python r = requests.post( "https://pay.railbed.io/v1/checkout_sessions", headers={ "Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}", "Idempotency-Key": "order_1042", }, json={ "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042", "customer_email": "buyer@example.com", "metadata": {"user_id": "player_1042"}, "success_url": ( "https://yourstore.com/thanks?order={REFERENCE}&payment={PAYMENT_ID}" ), "cancel_url": "https://yourstore.com/cart", }, timeout=15, ) r.raise_for_status() session = r.json() ``` Example: PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', 'Idempotency-Key: order_1042', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => '49.00', 'currency' => 'USD', 'description' => 'Pro Membership', 'reference' => 'order_1042', 'customer_email' => 'buyer@example.com', 'metadata' => ['user_id' => 'player_1042'], 'success_url' => 'https://yourstore.com/thanks?order={REFERENCE}&payment={PAYMENT_ID}', 'cancel_url' => 'https://yourstore.com/cart', ]), ]); $session = json_decode(curl_exec($ch), true); ``` Example: Response · response 201 Created ```json { "id": "pay_7AAiYH0Ykt11ED4hmfiN", "object": "checkout_session", "url": "https://pay.railbed.io/p/pay_7AAiYH0Ykt11ED4hmfiN", "status": "open", "livemode": false, "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042", "customer_email": "buyer@example.com", "created": 1790380525, "expires_at": 1790466925, "started_at": null, "metadata": { "user_id": "player_1042" }, "order_id": "ord_M4tQx8ZkP1vRn6WcLs2B" } ``` A retry with the same `Idempotency-Key` and body answers `200 OK` with the same session. ### Orders and customers Every session belongs to an [order](https://railbed.com/docs/api/orders.md). Send `order` and `customer` with the session, and the order has your items, totals, addresses and store order id, and the [customer](https://railbed.com/docs/api/customers.md) has the buyer's name and phone. They appear in the dashboard's **Orders** and **Customers** and in every [webhook](https://railbed.com/docs/webhooks/events.md#the-order-object). Without them, the order has one line, your `description`, for the whole amount. The buyer's checkout page still shows your `description` and the `amount`. The order's details are for your records, not for the buyer to confirm. Example: cURL ```bash curl https://pay.railbed.io/v1/checkout_sessions \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_1042-attempt-1" \ -d '{ "amount": "135.31", "currency": "USD", "description": "Order #1042", "reference": "order_1042", "customer_email": "maya@example.com", "customer": { "name": "Maya Okafor", "phone": "+1 503 555 0142", "id": "88" }, "order": { "id": "1042", "number": "1042", "url": "https://yourstore.com/admin/orders/1042", "store": { "id": "yourstore", "name": "yourstore.com", "url": "https://yourstore.com" }, "items": [ { "name": "Magnesium glycinate", "variant": "120 capsules", "sku": "HN-MG-120", "quantity": 2, "unit_amount": "32.00", "amount": "64.00" }, { "name": "Daily greens", "sku": "HN-DG-30", "quantity": 1, "amount": "54.00" }, { "name": "Shaker bottle", "quantity": 1, "amount": "12.00" } ], "discount": "13.00", "discount_code": "WELCOME10", "shipping": "8.95", "shipping_method": "Standard", "tax": "9.36", "shipping_address": { "name": "Maya Okafor", "line1": "418 Linden Avenue", "line2": "Apt 3B", "city": "Portland", "region": "OR", "postal_code": "97214", "country": "US" } }, "success_url": "https://yourstore.com/orders/1042/thanks" }' ``` Example: Node\.js ```js const res = await fetch('https://pay.railbed.io/v1/checkout_sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'order_1042-attempt-1', }, body: JSON.stringify({ amount: '135.31', currency: 'USD', description: 'Order #1042', reference: 'order_1042', customer_email: 'maya@example.com', customer: { name: 'Maya Okafor', phone: '+1 503 555 0142', id: '88' }, order: { id: '1042', number: '1042', url: 'https://yourstore.com/admin/orders/1042', store: { id: 'yourstore', name: 'yourstore.com', url: 'https://yourstore.com', }, items: [ { name: 'Magnesium glycinate', variant: '120 capsules', sku: 'HN-MG-120', quantity: 2, unit_amount: '32.00', amount: '64.00', }, { name: 'Daily greens', sku: 'HN-DG-30', quantity: 1, amount: '54.00' }, { name: 'Shaker bottle', quantity: 1, amount: '12.00' }, ], discount: '13.00', discount_code: 'WELCOME10', shipping: '8.95', shipping_method: 'Standard', tax: '9.36', shipping_address: { name: 'Maya Okafor', line1: '418 Linden Avenue', line2: 'Apt 3B', city: 'Portland', region: 'OR', postal_code: '97214', country: 'US', }, }, success_url: 'https://yourstore.com/orders/1042/thanks', }), }); if (!res.ok) throw new Error((await res.json()).error.code); const session = await res.json(); ``` Example: Python ```python r = requests.post( "https://pay.railbed.io/v1/checkout_sessions", headers={ "Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}", "Idempotency-Key": "order_1042-attempt-1", }, json={ "amount": "135.31", "currency": "USD", "description": "Order #1042", "reference": "order_1042", "customer_email": "maya@example.com", "customer": { "name": "Maya Okafor", "phone": "+1 503 555 0142", "id": "88", }, "order": { "id": "1042", "number": "1042", "url": "https://yourstore.com/admin/orders/1042", "store": { "id": "yourstore", "name": "yourstore.com", "url": "https://yourstore.com", }, "items": [ { "name": "Magnesium glycinate", "variant": "120 capsules", "sku": "HN-MG-120", "quantity": 2, "unit_amount": "32.00", "amount": "64.00", }, { "name": "Daily greens", "sku": "HN-DG-30", "quantity": 1, "amount": "54.00", }, {"name": "Shaker bottle", "quantity": 1, "amount": "12.00"}, ], "discount": "13.00", "discount_code": "WELCOME10", "shipping": "8.95", "shipping_method": "Standard", "tax": "9.36", "shipping_address": { "name": "Maya Okafor", "line1": "418 Linden Avenue", "line2": "Apt 3B", "city": "Portland", "region": "OR", "postal_code": "97214", "country": "US", }, }, "success_url": "https://yourstore.com/orders/1042/thanks", }, timeout=15, ) r.raise_for_status() session = r.json() ``` Example: PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', 'Idempotency-Key: order_1042-attempt-1', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => '135.31', 'currency' => 'USD', 'description' => 'Order #1042', 'reference' => 'order_1042', 'customer_email' => 'maya@example.com', 'customer' => [ 'name' => 'Maya Okafor', 'phone' => '+1 503 555 0142', 'id' => '88', ], 'order' => [ 'id' => '1042', 'number' => '1042', 'url' => 'https://yourstore.com/admin/orders/1042', 'store' => [ 'id' => 'yourstore', 'name' => 'yourstore.com', 'url' => 'https://yourstore.com', ], 'items' => [ [ 'name' => 'Magnesium glycinate', 'variant' => '120 capsules', 'sku' => 'HN-MG-120', 'quantity' => 2, 'unit_amount' => '32.00', 'amount' => '64.00', ], [ 'name' => 'Daily greens', 'sku' => 'HN-DG-30', 'quantity' => 1, 'amount' => '54.00', ], ['name' => 'Shaker bottle', 'quantity' => 1, 'amount' => '12.00'], ], 'discount' => '13.00', 'discount_code' => 'WELCOME10', 'shipping' => '8.95', 'shipping_method' => 'Standard', 'tax' => '9.36', 'shipping_address' => [ 'name' => 'Maya Okafor', 'line1' => '418 Linden Avenue', 'line2' => 'Apt 3B', 'city' => 'Portland', 'region' => 'OR', 'postal_code' => '97214', 'country' => 'US', ], ], 'success_url' => 'https://yourstore.com/orders/1042/thanks', ]), ]); $session = json_decode(curl_exec($ch), true); ``` Example: Response · response 201 Created ```json { "id": "pay_Rt5Wm2KxQ8zLb3NvYc7P", "object": "checkout_session", "url": "https://pay.railbed.io/p/pay_Rt5Wm2KxQ8zLb3NvYc7P", "status": "open", "livemode": true, "amount": "135.31", "currency": "USD", "description": "Order #1042", "reference": "order_1042", "customer_email": "maya@example.com", "created": 1790380525, "expires_at": 1790466925, "started_at": null, "metadata": null, "order_id": "ord_Vd3kR8mQ1xTn6LpZs0Wa" } ``` [Retrieve the order](https://railbed.com/docs/api/orders.md#retrieve-an-order) with `order_id` to see what was saved. ### The customer field | Field | Type | Description | |---|---|---| | `email` | string or null | The buyer's email, with the same rules as `customer_email`. Send either one; when you send both, they must be the same address | | `name` | string or null | The buyer's name, up to 120 characters. Without it, the name on the shipping (or billing) address is used | | `phone` | string or null | Up to 40 characters, in any format | | `id` | string or null | The buyer's customer id in your store, up to 64 characters. It appears in the dashboard, on the order's `store.customer_id` and in the customer's [`store_accounts`](https://railbed.com/docs/api/customers.md) | Name and phone reach the customer with the buyer's email: when the session is created with it, or once the buyer gives it and starts paying. See [how customers are made](https://railbed.com/docs/api/customers.md#how-customers-are-made). ### The order field | Field | Type | Description | |---|---|---| | `id` | string or null | Your order id, up to 64 characters. A later session with the same `id` joins this order instead of making another. See [Store orders and retries](#store-orders-and-retries) | | `number` | string or null | The order number your buyers see, up to 32 characters, such as `1042` (a leading `#` is dropped). Used only with `id` | | `url` | string or null | The order's page in your admin, up to 1,000 characters. Used only with `id`, and only when it's an `https://` address | | `store` | object or null | Which of your stores the order is from. Needs `id`: `store` without it is `400 invalid_order`. See below | | `items` | array or null | What's being bought, 1–100 items. Leave it out for a single line. See below | | `discount` | string or null | Taken off the items' total. Default `"0.00"`. It can be more than the items when store credit covers shipping or tax too, as long as the order still adds up to `amount` | | `discount_code` | string or null | The code the buyer used, up to 64 characters | | `shipping` | string or null | Shipping charged. Default `"0.00"` | | `shipping_method` | string or null | Up to 120 characters, such as `Standard` | | `tax` | string or null | Tax charged. Default `"0.00"` | | `shipping_address` | object or null | Where to ship the order. See below | | `billing_address` | object or null | The buyer's billing address | | `fulfillment` | string or null | `none`, `unfulfilled` or `fulfilled`. Default `unfulfilled` when there's a shipping address, otherwise `none`. See [fulfilment](https://railbed.com/docs/api/orders.md#fulfilment) | Money in `order` is written like `amount`: a decimal string with at most two decimal places, never a number. Zero is allowed. Text fields are trimmed, and a blank one counts as left out. Each item in `items`: | Field | Type | Description | |---|---|---| | `name` | string · required | Up to 200 characters | | `quantity` | integer · required | A whole number from 1 to 100,000 | | `amount` | string · required | The line's total for all its units, such as `"64.00"`. Zero is allowed | | `unit_amount` | string or null | One unit's price, for display. It isn't checked against `amount` | | `sku` | string or null | Up to 64 characters | | `variant` | string or null | Which version, up to 120 characters, such as `120 capsules` | `store` tells your stores apart, so two of them can use the same order ids. With one store, leave it out. | Field | Type | Description | |---|---|---| | `id` | string · required | A stable id for the store, up to 64 characters. Keep it the same: it's how later sessions and [list filters](https://railbed.com/docs/api/orders.md#list-orders) find the order | | `platform` | string | `woocommerce`, `shopify` or `other` (the default) | | `name` | string or null | The store's name, up to 120 characters | | `url` | string or null | The store's address, up to 1,000 characters. Kept only when it's `http://` or `https://` | `shipping_address` and `billing_address` take the same fields, each optional: | Field | Description | |---|---| | `name` | Up to 120 characters | | `line1`, `line2` | Up to 200 characters each | | `city` | Up to 120 characters | | `region` | A state, province or county, up to 120 characters | | `postal_code` | Up to 32 characters | | `country` | A two-letter code such as `US`, in any letter case. Saved in capitals | An address with nothing in it counts as none. Any field that breaks these rules is `400 invalid_order`, with `field` naming it, such as `order.items[2].quantity`. A `customer` that isn't an object, or a `customer.email` that differs from `customer_email`, is `400 invalid_customer`. ### Totals With `items`, the parts must add up to `amount` exactly, to the cent: the items' `amount`s, less `discount`, plus `shipping` and `tax`. For the order above: | Part | Amount | |---|---| | Magnesium glycinate × 2 | 64.00 | | Daily greens | 54.00 | | Shaker bottle | 12.00 | | Discount, `WELCOME10` | −13.00 | | Shipping | +8.95 | | Tax | +9.36 | | **`amount`** | **135.31** | A session whose parts don't add up is `400 invalid_order_totals`, and nothing is created; the error's message shows the sum. Line amounts come before the order's discount: when your store discounts single lines, send either the discounted line totals or the whole discount in `discount`, not both. Without `items`, the order has one line named after your `description`, for whatever makes the parts add up: `amount` + `discount` − `shipping` − `tax`. If that comes to less than zero, the session is `400 invalid_order_totals`. ### Store orders and retries A session with `order.id` pays for an order in your store. Railbed keeps one order for each `order.id` (within its `store.id`, in each mode): - **The first session creates the order.** Later sessions with the same `order.id` join it as more attempts at paying, for example after the buyer's card was declined or their session expired. - **The latest session describes the order.** Its items, totals and addresses replace the order's, and so does its customer when it has an email. The order's `number` and fulfilment stay, and its timeline in the dashboard notes when the total or items changed. - **A paid order refuses a new session.** When the order is `paid`, or `held` with money you could accept as paid or that part-paid it, a new session gets `409 order_paid` and nothing is created. Check the order before asking the buyer to pay again. An order held only for money you can't accept (it went somewhere other than your wallet) takes new sessions, so the buyer can pay. - **Earlier sessions stay open.** Joining doesn't close them, and one that's still open can still be paid. Send the buyer only to the newest session's `url`. A buyer who pays twice leaves two paid payments in the order's `payment_ids`; refund one from your wallet. - **Use a new `Idempotency-Key` for each attempt.** Repeating a request with the same key and body returns its original session, even after the order is paid: a replay is never refused with `order_paid`. The key covers `order` and `customer` too, so the same key with a different order is `409 idempotency_conflict`. Without `order.id`, every session creates a new order. ## Retrieve a checkout session `GET /v1/checkout_sessions/:id` Returns a session of yours in the key's mode. Another account's session, or one in the other mode, is `404 not_found`. Reading a session past its `expires_at` marks it `expired`. Example: cURL ```bash curl https://pay.railbed.io/v1/checkout_sessions/pay_7AAiYH0Ykt11ED4hmfiN \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" ``` Example: Node\.js ```js const session = await fetch(`https://pay.railbed.io/v1/checkout_sessions/${id}`, { headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` }, }).then((r) => r.json()); ``` Example: Python ```python session = requests.get( f"https://pay.railbed.io/v1/checkout_sessions/{id}", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, timeout=15, ).json() ``` Example: PHP ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), ], ]); $session = json_decode(curl_exec($ch), true); ``` The response is the [checkout session object](#the-checkout-session-object). To fulfil, read the [payment](https://railbed.com/docs/api/payments.md#retrieve-a-payment) instead: it adds the payment and settlement details. ## Start a checkout session `POST /v1/checkout_sessions/:id/start` For [your own checkout](https://railbed.com/docs/guides/custom-checkout.md): starts the session as the buyer would on the hosted page, and returns the card providers that can take it, each with a `handoff_url` to open in the buyer's browser. Starting assigns the payment's deposit address and locks the fee and the order's value in USD. | Field | Type | Description | |---|---|---| | `customer_email` | string | The buyer's email. Optional when the session already has one; a new one replaces it | | `country` | string | The buyer's two-letter country code (`US`, `DE`), any letter case. Optional. Use the buyer's country, never your server's | Send `{}` when the saved email is enough. Example: cURL ```bash 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 '{ "country": "US" }' ``` Example: Node\.js ```js const url = `https://pay.railbed.io/v1/checkout_sessions/${id}/start`; const started = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ country: 'US' }), }).then((r) => r.json()); ``` Example: Python ```python started = requests.post( f"https://pay.railbed.io/v1/checkout_sessions/{id}/start", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, json={"country": "US"}, timeout=15, ).json() ``` Example: PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['country' => 'US']), ]); $started = json_decode(curl_exec($ch), true); ``` Example: Response · response 200 OK ```json { "id": "pay_7AAiYH0Ykt11ED4hmfiN", "object": "checkout_session", "url": "https://pay.railbed.io/p/pay_7AAiYH0Ykt11ED4hmfiN", "status": "open", "livemode": false, "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042", "customer_email": "buyer@example.com", "created": 1790380525, "expires_at": 1790466925, "started_at": 1790380611, "metadata": { "user_id": "player_1042" }, "order_id": "ord_M4tQx8ZkP1vRn6WcLs2B", "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": "cashapp", "name": "Cash App", "note": "Cash App balance or card", "recommended": false, "handoff_url": "https://pay.railbed.io/go/pay_7AAiYH0Ykt11ED4hmfiN?provider=cashapp" } ] } ``` The response is the session with two more fields: | Field | Type | Description | |---|---|---| | `country` | string or null | The country you sent, uppercased | | `providers` | array | The providers that can take this payment, best match first. Each has `id`, `name`, `note` (a short line on how the buyer pays), `recommended` (true for one) and `handoff_url` | Starting again is safe: `started_at` and the deposit address stay the same. Providers depend on the amount, currency and country, so fetch them when the buyer reaches your checkout, and open a `handoff_url` only from the buyer's click, in a new tab. | Status | Code | When | |---|---|---| | 400 | `invalid_email` | The session has no email and none was sent | | 400 | `invalid_country` | `country` isn't two letters | | 404 | `not_found` | Not a session of yours in this mode | | 409 | `no_providers` | No provider can take this amount and currency now. Nothing was started | | 409 | `already_paid`, `held`, `failed` | The payment has finished | | 409 | `unavailable` | The account can't take payments now (Live: no payout wallet) | | 410 | `expired`, `canceled` | The session can no longer be paid | | 429 | `rate_limited` | More than about 12 starts a minute | | 502 | `network_unavailable` | The card network didn't answer. Retry in a minute | --- # Payments > A payment is what a checkout session became. Read it to fulfil orders, list payments to reconcile, and simulate outcomes in Test mode. Source: https://railbed.com/docs/api/payments/ · Updated: 2026-10-04 · Railbed by DeepWork developer docs ## The payment object A payment has every field of its [checkout session](https://railbed.com/docs/api/checkout-sessions.md#the-checkout-session-object), with `object` set to `"payment"`, plus: | Field | Type | Description | |---|---|---| | `paid_at` | integer or null | When it became `paid`, in Unix seconds | | `provider` | string or null | The provider the buyer chose, such as `stripe` or `paypal`. Null when the buyer paid in crypto | | `method` | string | How the money that settled (or is held) arrived: `card`, or `crypto` when the buyer sent stablecoins from their own wallet ([crypto payments](https://railbed.com/docs/user-guide/crypto-payments.md)) | | `hold_reason` | string or null | Why it's `held`, in plain words. Null otherwise | | `hold_acceptable` | boolean | For a `held` payment: whether you can accept it as paid in the dashboard | | `canceled_at` | integer or null | When you canceled it (payment links only). A canceled payment's status is `expired` | | `settlement` | object | What arrived and where it went. See below | The `settlement` object: | Field | Type | Description | |---|---|---| | `network` | string or null | Crypto payments: the network the money arrived on, such as `base` or `polygon`. Null for card, which always settles on Polygon | | `coin` | string or null | What was delivered, usually `polygon_usdc` (`polygon_usdt` is also possible). Crypto payments name their network, such as `base_usdc` | | `value_coin` | string or null | How much arrived at the payment's deposit address, before fees | | `merchant_received` | string or null | How much was forwarded to your wallet, in the coin (six decimal places). Can be null for a while after `paid` and fill in later. Stays null for a held payment you accepted as paid | | `txid_in` | string or null | The transaction that delivered the money (on Polygon, or on `network` for crypto) | | `txid_out` | string or null | The transaction that forwarded it to your wallet | | `payout_wallet` | string or null | The wallet it went to, fixed when the buyer started | `amount` and `currency` are always the price you set; `value_coin` and `merchant_received` are what actually moved. Look up both transactions on a Polygon block explorer to see them for yourself. ## Retrieve a payment `GET /v1/payments/:id` Returns the current state of a payment of yours in the key's mode. This is the authority for fulfilment: when it says `paid`, the money arrived and passed Railbed's [settlement checks](https://railbed.com/docs/webhooks/events.md#payment-paid). What to deliver is on its [order](https://railbed.com/docs/api/orders.md#retrieve-an-order), from `order_id`. Example: cURL ```bash curl https://pay.railbed.io/v1/payments/pay_7AAiYH0Ykt11ED4hmfiN \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" ``` Example: Node\.js ```js const payment = await fetch(`https://pay.railbed.io/v1/payments/${id}`, { headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` }, }).then((r) => r.json()); ``` Example: Python ```python payment = requests.get( f"https://pay.railbed.io/v1/payments/{id}", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, timeout=15, ).json() ``` Example: PHP ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), ], ]); $payment = json_decode(curl_exec($ch), true); ``` Example: Response · response 200 OK ```json { "id": "pay_7AAiYH0Ykt11ED4hmfiN", "object": "payment", "url": "https://pay.railbed.io/p/pay_7AAiYH0Ykt11ED4hmfiN", "status": "paid", "livemode": true, "amount": "49.00", "currency": "USD", "description": "Pro Membership", "reference": "order_1042", "customer_email": "buyer@example.com", "created": 1790380525, "expires_at": 1790466925, "started_at": 1790380611, "metadata": { "user_id": "player_1042" }, "order_id": "ord_M4tQx8ZkP1vRn6WcLs2B", "paid_at": 1790381342, "provider": "stripe", "hold_reason": null, "hold_acceptable": false, "canceled_at": null, "settlement": { "coin": "polygon_usdc", "value_coin": "47.53", "merchant_received": "46.341750", "txid_in": "0x0c651ba1d59c7a32e8b1f4bd2c7e0e4f96a55d13a6b0f2d1c8e7a9b4f3d29b58", "txid_out": "0x8202d1373e0a9c4f1b6d5e2c7a8f9b0e1d2c3b4a5f6e7d8c9b0a1f2e3d7356ff", "payout_wallet": "0xF977814e90dA44bFA03b6295A0616a897441aceC" } } ``` A held payment looks like this in part: Example: A held payment (trimmed) · response 200 OK ```json { "id": "pay_Kx81mQv2PzR0dT7eWcYa", "object": "payment", "status": "held", "amount": "49.00", "currency": "USD", "paid_at": null, "hold_reason": "The provider sent 24.50 USDC, below 90% of the order’s 49.00 USD value.", "hold_acceptable": true, "settlement": { "coin": "polygon_usdc", "value_coin": "24.50", "merchant_received": null } } ``` Reading a payment past its `expires_at` marks an unpaid one `expired`. ## List payments `GET /v1/payments` Returns your payments in the key's mode, newest first. Use it to reconcile, not to find new payments quickly: it lists by creation time, so recheck unfinished payments you saved by id. | Parameter | Type | Description | |---|---|---| | `limit` | integer | 1–100. Default 20 | | `starting_after` | string | A payment id from the previous page's `next_cursor` | Example: cURL ```bash curl "https://pay.railbed.io/v1/payments?limit=50" \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" ``` Example: Node\.js ```js // Every payment, page by page let cursor = null; do { const url = new URL('https://pay.railbed.io/v1/payments'); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('starting_after', cursor); const page = await fetch(url, { headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` }, }).then((r) => r.json()); for (const payment of page.data) reconcile(payment); cursor = page.next_cursor; } while (cursor); ``` Example: Python ```python cursor = None while True: params = {"limit": 100, **({"starting_after": cursor} if cursor else {})} page = requests.get( "https://pay.railbed.io/v1/payments", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, params=params, timeout=15, ).json() for payment in page["data"]: reconcile(payment) cursor = page["next_cursor"] if not cursor: break ``` Example: PHP ```php 100, 'starting_after' => $cursor, ])); $ch = curl_init('https://pay.railbed.io/v1/payments?' . $query); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), ], ]); $page = json_decode(curl_exec($ch), true); foreach ($page['data'] as $payment) reconcile($payment); $cursor = $page['next_cursor']; } while ($cursor); ``` Example: Response · response 200 OK ```json { "data": [ { "id": "pay_7AAiYH0Ykt11ED4hmfiN", "object": "payment", "status": "paid", "amount": "49.00", "currency": "USD" }, { "id": "pay_Kx81mQv2PzR0dT7eWcYa", "object": "payment", "status": "held", "amount": "49.00", "currency": "USD" } ], "has_more": true, "next_cursor": "pay_Kx81mQv2PzR0dT7eWcYa" } ``` Each item is a full [payment object](#the-payment-object) (trimmed here). An invalid `limit` is `400 invalid_limit`; a cursor that isn't one of your payments in this mode is `400 invalid_cursor`. ## Simulate a payment `POST /v1/payments/:id/simulate` **Test mode only.** Simulates an outcome for a started test payment. Simulated money goes through the same settlement checks and webhooks as a live payment. A card decline keeps the payment open for retry. | Field | Type | Description | |---|---|---| | `outcome` | string · required | `paid` (about 97% of the value arrives), `underpaid` (half arrives, so it's `held`), `declined` (stays open for another provider, no failed webhook), or `failed` (terminal Test failure with a `payment.failed` webhook) | Example: cURL ```bash curl -X POST \ https://pay.railbed.io/v1/payments/pay_7AAiYH0Ykt11ED4hmfiN/simulate \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "outcome": "paid" }' ``` Example: Node\.js ```js const payment = await fetch(`https://pay.railbed.io/v1/payments/${id}/simulate`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ outcome: 'paid' }), }).then((r) => r.json()); ``` Example: Python ```python payment = requests.post( f"https://pay.railbed.io/v1/payments/{id}/simulate", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, json={"outcome": "paid"}, timeout=15, ).json() ``` Example: PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['outcome' => 'paid']), ]); $payment = json_decode(curl_exec($ch), true); ``` The response is the [payment](#the-payment-object) after the outcome. A `declined` outcome leaves it open for another provider on the same session. Simulating a finished payment returns it unchanged. | Status | Code | When | |---|---|---| | 400 | `invalid_outcome` | `outcome` is missing or not one of the four supported values | | 403 | `live_payment` | The payment is a Live payment | | 409 | `not_started` | Start the session first, with [Start a checkout session](https://railbed.com/docs/api/checkout-sessions.md#start-a-checkout-session) | --- # Orders > An order is what a buyer tried to buy, with its items, totals, addresses and customer. Every checkout session belongs to one. Read orders to fulfil them, and report fulfilment, cancellations and refunds back from your store. Source: https://railbed.com/docs/api/orders/ · Updated: 2026-10-04 · Railbed by DeepWork developer docs ## The order object | Field | Type | Description | |---|---|---| | `id` | string | The order's id, `ord_…` | | `object` | string | `"order"` | | `livemode` | boolean | `false` for Test mode, `true` for Live mode | | `number` | integer | Railbed's order number, counting from 1001 in each mode. Your store's own number is `store.order_number` | | `status` | string | `open`, `pending`, `paid`, `held`, `failed`, `expired` or `canceled`, from its payments. See [Status](#status) | | `needs_review` | boolean | `true` when it's `held` and waits on you: a held payment you could accept as paid, or paid payments that don't cover `total`. Such an order refuses new sessions with `409 order_paid` | | `fulfillment` | string | `none`, `unfulfilled` or `fulfilled`. See [Fulfilment](#fulfilment) | | `fulfilled_at` | integer or null | When it was marked `fulfilled`, in Unix seconds | | `customer_id` | string or null | Its [customer](https://railbed.com/docs/api/customers.md), `cus_…`. Null until the buyer has given an email | | `currency` | string | The currency of every amount on the order | | `subtotal` | string | The items' `amount`s added up | | `discount` | string | Taken off the subtotal, and larger than it only when store credit also covered shipping or tax. `"0.00"` when there's none | | `discount_code` | string or null | The code or codes the buyer used | | `shipping` | string | Shipping charged | | `shipping_method` | string or null | How it ships, such as `Standard` | | `tax` | string | Tax charged | | `total` | string | `subtotal` − `discount` + `shipping` + `tax`: the latest session's `amount` | | `amount_paid` | string | What its paid payments in `currency` add up to. `"0.00"` until one is paid | | `items` | array | What was bought, in the order you sent it. See [Items](#items) | | `shipping_address` | object or null | Where to send it. See [Addresses](#addresses) | | `billing_address` | object or null | The buyer's billing address | | `store` | object or null | Where the order lives in your systems, for orders sent with `order.id`. See [Store](#store) | | `payment_ids` | array | Every payment attempt at this order, newest first (`pay_…`) | | `created` | integer | When the order was created, in Unix seconds | | `paid_at` | integer or null | When its first payment became `paid`, in Unix seconds | Amounts are decimal strings, like every price in the API. They're what the buyer was charged in `currency`, not what arrived in your wallet: that's on each [payment](https://railbed.com/docs/api/payments.md#the-payment-object). Example: An order from a store · response 200 OK ```json { "id": "ord_Vd3kR8mQ1xTn6LpZs0Wa", "object": "order", "livemode": true, "number": 1187, "status": "paid", "needs_review": false, "fulfillment": "unfulfilled", "fulfilled_at": null, "customer_id": "cus_Hc7nQ2wVz9KpT4mRb1Ye", "currency": "USD", "subtotal": "130.00", "discount": "13.00", "discount_code": "WELCOME10", "shipping": "8.95", "shipping_method": "Standard", "tax": "9.36", "total": "135.31", "amount_paid": "135.31", "items": [ { "name": "Magnesium glycinate", "variant": "120 capsules", "sku": "HN-MG-120", "quantity": 2, "unit_amount": "32.00", "amount": "64.00" }, { "name": "Daily greens", "variant": null, "sku": "HN-DG-30", "quantity": 1, "unit_amount": null, "amount": "54.00" }, { "name": "Shaker bottle", "variant": null, "sku": null, "quantity": 1, "unit_amount": null, "amount": "12.00" } ], "shipping_address": { "name": "Maya Okafor", "line1": "418 Linden Avenue", "line2": "Apt 3B", "city": "Portland", "region": "OR", "postal_code": "97214", "country": "US" }, "billing_address": null, "store": { "platform": "other", "id": "yourstore", "name": "yourstore.com", "url": "https://yourstore.com", "order_id": "1042", "order_number": "1042", "order_url": "https://yourstore.com/admin/orders/1042", "customer_id": "88" }, "payment_ids": ["pay_Rt5Wm2KxQ8zLb3NvYc7P"], "created": 1790380525, "paid_at": 1790381342 } ``` ### Items | Field | Type | Description | |---|---|---| | `name` | string | What the line is | | `variant` | string or null | Which version, such as `120 capsules` or `Blue, L` | | `sku` | string or null | Your stock-keeping code | | `quantity` | integer | How many units | | `unit_amount` | string or null | One unit's price, when it was sent. For display only | | `amount` | string | The line's total. The order's totals add up from these | An order made without `order.items` has one line, named after the session's `description`, with quantity 1. Its amount is whatever makes the totals add up: `amount` + `discount` − `shipping` − `tax`, which is the whole amount when you send no totals. ### Addresses `shipping_address` and `billing_address` have the same fields, each a string or null: `name`, `line1`, `line2`, `city`, `region` (a state, province or county), `postal_code` and `country` (a two-letter code such as `US`). They're stored as sent, not checked against a postal service. ### Store For orders you sent with `order.id`, `store` says where the order lives, so you can find it again and link back to it: | Field | Type | Description | |---|---|---| | `platform` | string | `woocommerce`, `shopify` or `other` | | `id` | string | The `order.store.id` you sent. Empty (`""`) when you sent no `store` | | `name` | string or null | The store's name | | `url` | string or null | The store's address | | `order_id` | string | Your order id, as sent in `order.id` | | `order_number` | string or null | The number your buyers see | | `order_url` | string or null | The order's page in your admin | | `customer_id` | string or null | The buyer's id in your store: the `customer.id` you sent | `store` is null for orders sent without `order.id`, and for orders from Railbed's own checkouts and payment links. ## How orders are made Every checkout session belongs to an order, and its `order_id` says which. The order is created in the same step as the session, so a session never exists without one. - **From the API.** Send [`order` and `customer`](https://railbed.com/docs/api/checkout-sessions.md#orders-and-customers) when you create a session, and the order has your items, totals and addresses. Without them, the order has one line, the session's `description`, for the whole amount. - **From your store's order id.** A session sent with `order.id` joins the order that already has that id, if there is one. That's how a buyer's second try at paying the same order stays one order. See [Store orders and retries](https://railbed.com/docs/api/checkout-sessions.md#store-orders-and-retries). - **From Railbed's checkouts and payment links.** Checkout pages, pricing tables, buy buttons and payment links make orders too, with one item: the product, plan or what the link is for. A buyer who tries the same checkout again within a day, with the same email, price and plan, joins their unpaid order. Each payment is one attempt at paying for its order. An order can have several, in `payment_ids`, and more than one of them can be paid if the buyer paid twice. The API reads orders and updates their fulfilment. It doesn't create orders on their own, edit their items or delete them: an order's contents change only when a new session for the same `order.id` arrives. ## Status An order's `status` comes from its payments, and changes in the same step as theirs: 1. `paid` when its paid payments cover `total` (they add up to at least `total` in the order's `currency`). 2. Otherwise `held` when any of its payments is held for review, or is paid but doesn't cover `total` (a part-payment after the order grew, or one in another currency). 3. Otherwise the latest payment's status: `open`, `pending`, `failed` or `expired`, or `canceled` when it's a payment link you canceled. | Status | Meaning | What to do | |---|---|---| | `open` | Created, or started but not yet handed to a provider | Wait | | `pending` | The buyer went to a provider to pay | Wait | | `paid` | Payments for it arrived, passed Railbed's checks and cover `total` | Fulfil it, once | | `held` | Money arrived but failed a check, or what's paid doesn't cover `total` (`amount_paid`) | Don't fulfil. Review it in the dashboard, and check with the buyer | | `failed` | The latest attempt ended in a terminal Test failure | Let the buyer try again | | `expired` | The latest attempt expired unpaid | Treat as abandoned; a late payment can still make it `paid` | | `canceled` | You canceled its payment link | Nothing to do | The status can't be set through the API. Like a payment, an order can become `paid` after `expired` when money arrives late, and a `held` order becomes `paid` when you accept the payment. A `paid` order stays `paid`. ## Fulfilment `fulfillment` is your record of whether the order has been delivered. It doesn't affect the payment. | Value | Meaning | |---|---| | `none` | Nothing to ship: digital goods, memberships, services. The default without a shipping address | | `unfulfilled` | Waiting to be shipped or delivered. The default when the order has a shipping address | | `fulfilled` | Delivered or on its way. `fulfilled_at` says when it was marked | You can set the starting value with `order.fulfillment` when you create the session. After that: - **Orders sent with `order.id`** get their fulfilment from your store, through [Update an order](#update-an-order). The dashboard shows it but doesn't change it, so the two never disagree. The [WooCommerce plugin](https://railbed.com/docs/guides/woocommerce.md#orders-customers-and-fulfilment) does this for you. - **Other orders** can be marked fulfilled in the dashboard's **Orders** once they're paid, or through the API. ## Retrieve an order `GET /v1/orders/:id` Returns an order of yours in the key's mode. Another account's order, or one in the other mode, is `404 not_found`. Example: cURL ```bash curl https://pay.railbed.io/v1/orders/ord_Vd3kR8mQ1xTn6LpZs0Wa \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" ``` Example: Node\.js ```js const order = await fetch(`https://pay.railbed.io/v1/orders/${id}`, { headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` }, }).then((r) => r.json()); ``` Example: Python ```python order = requests.get( f"https://pay.railbed.io/v1/orders/{id}", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, timeout=15, ).json() ``` Example: PHP ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), ], ]); $order = json_decode(curl_exec($ch), true); ``` The response is the [order object](#the-order-object). To find the order behind a payment or webhook, use the payment's `order_id` (`orderId` in webhooks); webhooks also carry the [order itself](https://railbed.com/docs/webhooks/events.md#the-order-object). ## List orders `GET /v1/orders` Returns your orders in the key's mode, newest first by creation. Filter by customer, or find the order you sent with a given `order.id`. | Parameter | Type | Description | |---|---|---| | `limit` | integer | 1–100. Default 20 | | `starting_after` | string | An order id from the previous page's `next_cursor` | | `customer_id` | string | Only this customer's orders | | `store_order_id` | string | Only the order you sent with this `order.id` | | `store_id` | string | Only orders sent with this `order.store.id`. With `store_order_id`, the one order you sent with both; leave it out for orders sent without `store` | Example: cURL ```bash curl "https://pay.railbed.io/v1/orders?store_id=yourstore&store_order_id=1042" \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" ``` Example: Node\.js ```js const url = new URL('https://pay.railbed.io/v1/orders'); url.searchParams.set('store_id', 'yourstore'); url.searchParams.set('store_order_id', '1042'); const page = await fetch(url, { headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` }, }).then((r) => r.json()); const order = page.data[0] ?? null; ``` Example: Python ```python page = requests.get( "https://pay.railbed.io/v1/orders", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, params={"store_id": "yourstore", "store_order_id": "1042"}, timeout=15, ).json() order = page["data"][0] if page["data"] else None ``` Example: PHP ```php 'yourstore', 'store_order_id' => '1042', ]); $ch = curl_init('https://pay.railbed.io/v1/orders?' . $query); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), ], ]); $page = json_decode(curl_exec($ch), true); $order = $page['data'][0] ?? null; ``` Example: Response (trimmed) · response 200 OK ```json { "data": [ { "id": "ord_Vd3kR8mQ1xTn6LpZs0Wa", "object": "order", "number": 1187, "status": "paid", "fulfillment": "unfulfilled", "total": "135.31" } ], "has_more": false, "next_cursor": null } ``` Each item is a full [order object](#the-order-object) (trimmed here). Page through with `next_cursor` as on [List payments](https://railbed.com/docs/api/payments.md#list-payments). A `store_order_id` matches at most one order in each mode. An invalid `limit` is `400 invalid_limit`; a cursor that isn't one of your orders in this mode is `400 invalid_cursor`. ## Update an order `PATCH /v1/orders/:id` Records the order's fulfilment. This is how your store tells Railbed an order has shipped, or that it's waiting again. Marking it `fulfilled` or `unfulfilled` appears on the order's timeline in the dashboard. | Field | Type | Description | |---|---|---| | `fulfillment` | string · required | `none`, `unfulfilled` or `fulfilled` | Example: cURL ```bash curl -X PATCH \ https://pay.railbed.io/v1/orders/ord_Vd3kR8mQ1xTn6LpZs0Wa \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "fulfillment": "fulfilled" }' ``` Example: Node\.js ```js const order = await fetch(`https://pay.railbed.io/v1/orders/${id}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ fulfillment: 'fulfilled' }), }).then((r) => r.json()); ``` Example: Python ```python order = requests.patch( f"https://pay.railbed.io/v1/orders/{id}", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, json={"fulfillment": "fulfilled"}, timeout=15, ).json() ``` Example: PHP ```php 'PATCH', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['fulfillment' => 'fulfilled']), ]); $order = json_decode(curl_exec($ch), true); ``` The response is the updated [order](#the-order-object). - `fulfilled` sets `fulfilled_at` to now; `unfulfilled` and `none` clear it. Sending the value the order already has changes nothing, so retrying is safe. - It works on any order of yours in this mode, from any source, paid or not. Railbed doesn't check the payment here, so report only what you've actually shipped. - No webhook is sent for a fulfilment change. | Status | Code | When | |---|---|---| | 400 | `invalid_fulfillment` | `fulfillment` is missing or not one of the three | | 404 | `not_found` | Not an order of yours in this mode | ## Report a cancellation or refund `POST /v1/orders/:id/events` Records that your store canceled the order or recorded a refund, so the order's timeline in the dashboard says so ("Canceled in …", "Refund recorded in …"). It's a note, not an instruction: the order's `status` still comes from its payments, and Railbed moves no money. Arrange any refund with the buyer yourself. | Field | Type | Description | |---|---|---| | `type` | string · required | `canceled` or `refunded` | | `id` | string · required | Your own id for this event, such as `refund-5521`: 1 to 64 printable characters without spaces. The same id is recorded once, so a retry is safe | | `amount` | string · optional | `refunded` only: how much you refunded, in the order's `currency`, from `"0.01"` up to its `total` | Example: cURL ```bash curl https://pay.railbed.io/v1/orders/ord_Vd3kR8mQ1xTn6LpZs0Wa/events \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "refunded", "id": "refund-5521", "amount": "12.00" }' ``` Example: Node\.js ```js const event = await fetch(`https://pay.railbed.io/v1/orders/${id}/events`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ type: 'refunded', id: 'refund-5521', amount: '12.00' }), }).then((r) => r.json()); ``` Example: Python ```python event = requests.post( f"https://pay.railbed.io/v1/orders/{id}/events", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, json={"type": "refunded", "id": "refund-5521", "amount": "12.00"}, timeout=15, ).json() ``` Example: PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['type' => 'refunded', 'id' => 'refund-5521', 'amount' => '12.00']), ]); $event = json_decode(curl_exec($ch), true); ``` Example: The recorded event · response 201 Created ```json { "id": "oev_Tb8vN3qLx5WmR2kZc9Hd", "object": "order_event", "order_id": "ord_Vd3kR8mQ1xTn6LpZs0Wa", "type": "refunded", "store_event_id": "refund-5521", "amount": "12.00", "currency": "USD", "created": 1790467742 } ``` - The answer is `201 Created` the first time and `200 OK`, with the same event, when that `id` was already recorded. Send a new `id` for each cancellation or refund. - It works on any order of yours in this mode, paid or not. No webhook is sent. - Don't put personal details in `id`: it's shown to anyone who can see the order. | Status | Code | When | |---|---|---| | 400 | `invalid_event_type` | `type` is missing or not `canceled` or `refunded` | | 400 | `invalid_event_id` | `id` is missing, longer than 64 characters or has spaces | | 400 | `invalid_amount` | `amount` isn't a decimal string within the order's `total`, or was sent with `canceled` | | 404 | `not_found` | Not an order of yours in this mode | | 409 | `event_conflict` | That `id` was already recorded with a different `type` or `amount` | --- # Customers > A customer is one buyer's email address in one mode, with the contact details and addresses their orders brought. Read customers to see who bought what, and find one by email. Source: https://railbed.com/docs/api/customers/ · Updated: 2026-09-27 · Railbed by DeepWork developer docs ## The customer object | Field | Type | Description | |---|---|---| | `id` | string | The customer's id, `cus_…` | | `object` | string | `"customer"` | | `livemode` | boolean | `false` for Test mode, `true` for Live mode | | `email` | string or null | Their email, lowercased. Null after you [delete their details](#deleted-customers) | | `name` | string or null | Their name, from their latest order that had one | | `phone` | string or null | Their phone number, as your store sent it | | `shipping_address` | object or null | Their latest shipping address, in the [order's address shape](https://railbed.com/docs/api/orders.md#addresses) | | `billing_address` | object or null | Their latest billing address | | `store_accounts` | array | Their ids in your stores, one per store: `platform`, `store_id`, `store_name` and `customer_id` (the `customer.id` you sent). Empty when you sent none | | `orders_count` | integer | How many orders they have, paid or not | | `paid_orders_count` | integer | How many of those are `paid` | | `spent` | object | What their paid payments add up to, by currency, such as `{ "USD": "135.31" }`. Empty until a payment is paid | | `created` | integer | When they first appeared, in Unix seconds | | `last_order_at` | integer or null | When their latest order was created, in Unix seconds | | `erased` | boolean | `true` once you've deleted their details | `spent` adds up the `amount` of each of their payments that's `paid`, in its currency: what they paid, including a part-payment on an order that's `held`, and both payments when they paid one order twice. `paid_orders_count` counts only orders that are `paid`. What reached your wallet is on each [payment](https://railbed.com/docs/api/payments.md#the-payment-object). Example: A customer · response 200 OK ```json { "id": "cus_Hc7nQ2wVz9KpT4mRb1Ye", "object": "customer", "livemode": true, "email": "maya@example.com", "name": "Maya Okafor", "phone": "+1 503 555 0142", "shipping_address": { "name": "Maya Okafor", "line1": "418 Linden Avenue", "line2": "Apt 3B", "city": "Portland", "region": "OR", "postal_code": "97214", "country": "US" }, "billing_address": null, "store_accounts": [], "orders_count": 3, "paid_orders_count": 2, "spent": { "USD": "224.31" }, "created": 1787702525, "last_order_at": 1790380525, "erased": false } ``` ## How customers are made - **One customer per email address, in each mode.** Case doesn't matter: `Maya@Example.com` and `maya@example.com` are one customer. Test and Live customers are separate. - **The email comes from the session.** A session's `customer_email` (or `customer.email`) makes or finds the customer when it's created; a session created without one gets its customer from the email given when it starts. Railbed's checkouts and payment links use the email the buyer enters, or the one you set on the link. - **The latest details win.** Each order's `customer.name`, `customer.phone` and addresses replace the saved ones. Details an order leaves out keep their earlier value. - **Details wait for the email.** A session created without an email keeps its name, phone and addresses on the order, and saves them to the customer once the buyer gives their email and starts paying. - **Orders follow the email.** When a session starts with a different email from the one it was created with, its order moves to the customer with the new email. The API doesn't create, edit or delete customers: they change only through orders. The dashboard lets you keep a private note on each customer, which is never returned by the API or sent in webhooks. > [!IMPORTANT] > Buyers type their own email at checkout. Use a customer to see who bought what, never to decide which account in your system gets an order: resolve that from your own order, as in [Fulfil orders safely](https://railbed.com/docs/guides/fulfilment.md). ## Deleted customers When a buyer asks you to delete their data, open the customer in the dashboard's **Customers** and choose **Delete customer data**. It removes their name, email, phone and addresses from the customer, from their orders and payments, and from the bodies stored in your webhook delivery log. Their orders, amounts and payments stay for your records. It can't be undone. - The customer keeps its `id`, with `erased: true` and its contact details and addresses `null`. Their orders keep `customer_id`, and their payments' `customer_email` becomes `null`. - Their email is no longer on file, so [listing by email](#list-customers) doesn't find them, and the same email later starts a new customer. - Webhooks already delivered can't be recalled. Delete the details from your own systems too. ## Retrieve a customer `GET /v1/customers/:id` Returns a customer of yours in the key's mode. Another account's customer, or one in the other mode, is `404 not_found`. Example: cURL ```bash curl https://pay.railbed.io/v1/customers/cus_Hc7nQ2wVz9KpT4mRb1Ye \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" ``` Example: Node\.js ```js const customer = await fetch(`https://pay.railbed.io/v1/customers/${id}`, { headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` }, }).then((r) => r.json()); ``` Example: Python ```python customer = requests.get( f"https://pay.railbed.io/v1/customers/{id}", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, timeout=15, ).json() ``` Example: PHP ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), ], ]); $customer = json_decode(curl_exec($ch), true); ``` The response is the [customer object](#the-customer-object). An order's `customer_id` leads here; for their orders, [list orders](https://railbed.com/docs/api/orders.md#list-orders) with `customer_id`. ## List customers `GET /v1/customers` Returns your customers in the key's mode, newest first by `created`. Pass `email` to find one buyer. | Parameter | Type | Description | |---|---|---| | `limit` | integer | 1–100. Default 20 | | `starting_after` | string | A customer id from the previous page's `next_cursor` | | `email` | string | Only the customer with this email, in any letter case. Matches the whole address | Example: cURL ```bash curl -G https://pay.railbed.io/v1/customers \ --data-urlencode "email=maya@example.com" \ -H "Authorization: Bearer $RAILBED_SECRET_KEY" ``` Example: Node\.js ```js const url = new URL('https://pay.railbed.io/v1/customers'); url.searchParams.set('email', 'maya@example.com'); const page = await fetch(url, { headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` }, }).then((r) => r.json()); const customer = page.data[0] ?? null; ``` Example: Python ```python page = requests.get( "https://pay.railbed.io/v1/customers", headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"}, params={"email": "maya@example.com"}, timeout=15, ).json() customer = page["data"][0] if page["data"] else None ``` Example: PHP ```php 'maya@example.com']); $ch = curl_init('https://pay.railbed.io/v1/customers?' . $query); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'), ], ]); $page = json_decode(curl_exec($ch), true); $customer = $page['data'][0] ?? null; ``` Example: Response (trimmed) · response 200 OK ```json { "data": [ { "id": "cus_Hc7nQ2wVz9KpT4mRb1Ye", "object": "customer", "email": "maya@example.com", "name": "Maya Okafor", "paid_orders_count": 2 } ], "has_more": false, "next_cursor": null } ``` Each item is a full [customer object](#the-customer-object) (trimmed here). With `email`, the list has at most one customer; an empty `data` means no customer has that email in this mode. Page through the whole list with `next_cursor` as on [List payments](https://railbed.com/docs/api/payments.md#list-payments). An invalid `limit` is `400 invalid_limit`; a cursor that isn't one of your customers in this mode is `400 invalid_cursor`. --- # Errors > Every error the Railbed API returns, with its HTTP status, what caused it and what to do next. Source: https://railbed.com/docs/api/errors/ · Updated: 2026-10-05 · Railbed by DeepWork developer docs ## The error object Every error has an HTTP status of 400 or above and the same JSON body: Example: An error · response 409 Conflict ```json { "error": { "code": "no_payout_wallet", "message": "Add a payout wallet in the dashboard before taking live payments." } } ``` | Field | Type | Description | |---|---|---| | `code` | string | Stable and machine-readable. Branch on this | | `message` | string | A sentence for people. It can be reworded at any time, so show it or log it, but never parse it | | `field` | string | The request field at fault, when there is one: `amount`, `metadata`, `order.items[0].amount`, `starting_after` | ## Handling errors Decide by status first, then by `code` where it matters: | Status | Meaning | What your code should do | |---|---|---| | `400` | The request was invalid | Fix the request. Retrying it unchanged fails the same way | | `401` | The key or agent access token is missing, malformed, expired or revoked | For a secret key, check the key and its mode, and don't retry. An agent gets a new access token on `invalid_token`, and asks its person to connect it again on `agent_disconnected` | | `403` | Not allowed for this account, key or agent | Don't retry. On `insufficient_scope`, use a key or agent with **Full access** | | `404` | Not found in this account and mode | Check the id and whether the key's mode matches the object's | | `409` | The object's state doesn't allow this | Read the object and act on its current status | | `410` | The session can't be paid any more | Create a new session | | `413`, `415` | The body is too large or isn't JSON | Fix the request | | `429` | Rate limited | Wait for `Retry-After` seconds, then retry | | `500`, `502`, `503` | Something failed on our side or upstream | Retry with backoff. Creates are safe to retry with the same `Idempotency-Key` | Example: A small error handler ```js async function railbed(path, init = {}) { const res = await fetch(`https://pay.railbed.io/v1${path}`, { ...init, headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`, 'Content-Type': 'application/json', ...init.headers, }, }); const body = await res.json(); if (res.ok) return body; const err = Object.assign(new Error(body.error.message), { status: res.status, code: body.error.code, field: body.error.field, }); err.retryable = res.status === 429 || res.status >= 500; err.retryAfter = Number(res.headers.get('Retry-After')) || null; throw err; } ``` ## Every code ### 400 Bad Request | Code | Endpoint | Cause | |---|---|---| | `invalid_json` | Any with a body | The body isn't a JSON object | | `invalid_amount` | Create | `amount` isn't a string, or isn't from `"1.00"` to `"100000.00"` with at most two decimal places | | `invalid_currency` | Create | `currency` isn't `USD`, `EUR`, `GBP`, `CAD` or `AUD` | | `missing_description` | Create | `description` is missing, blank or not a string | | `too_long` | Create | `description` or `reference` is over 120 characters. See `field` | | `invalid_reference` | Create | `reference` isn't a string | | `invalid_email` | Create, Start | `customer_email` or `customer.email` isn't a valid address, or Start needs one and none is on the session | | `invalid_url` | Create | `success_url` or `cancel_url` isn't a full `http(s)://` address, has a username or password, is over 1,000 characters, or isn't `https://` in Live mode | | `invalid_metadata` | Create | Over 20 keys, a blank key, a key over 40 characters or starting with `__`, or a value that isn't a string of at most 500 characters | | `invalid_idempotency_key` | Create | The `Idempotency-Key` header is blank, over 120 characters or has characters other than printable ASCII | | `invalid_customer` | Create | `customer` isn't an object, or `customer.email` and `customer_email` are different addresses | | `invalid_order` | Create | A field in `order` has the wrong type, is too long or is out of range, such as a `quantity` that isn't a whole number from 1 to 100,000 or an item without `name` or `amount`. See `field` | | `invalid_order_totals` | Create | The items less `discount` plus `shipping` and `tax` don't equal `amount` to the cent, or without items, `shipping` and `tax` less `discount` come to more than `amount` | | `invalid_country` | Start | `country` isn't a two-letter code such as `US` | | `invalid_limit` | Any list | `limit` isn't a whole number from 1 to 100 | | `invalid_cursor` | Any list | `starting_after` isn't one of your payments, orders or customers (whichever you're listing) in this mode | | `invalid_outcome` | Simulate | `outcome` isn't `paid`, `underpaid`, `declined` or `failed` | | `invalid_fulfillment` | Update an order | `fulfillment` is missing or isn't `none`, `unfulfilled` or `fulfilled` | ### 401 Unauthorized Every `401` carries a `WWW-Authenticate` header that points to the API's [protected resource metadata](https://railbed.com/docs/api.md#discovery), where an agent can find out how to get access. | Code | Cause | |---|---| | `invalid_api_key` | No `Authorization: Bearer` header, a malformed key, or a revoked one. Create a key in [Developers](https://app.railbed.io/developers) | | `invalid_token` | An agent's access token expired, is malformed, or wasn't issued for Railbed. Get a new one from the token endpoint described in [auth.md](https://railbed.com/auth.md) | | `agent_disconnected` | The agent was disconnected in the dashboard, or the person who connected it no longer manages keys for the business. Ask an owner or developer to [connect it again](https://railbed.com/docs/agents.md#connect-an-agent) | ### 403 Forbidden | Code | Cause | |---|---| | `suspended` | The account is suspended. Email support@railbed.com | | `insufficient_scope` | The key's or agent's access level doesn't include the [scope](https://railbed.com/docs/api.md#scopes) this endpoint needs, such as a **Read only** key creating a checkout session. The message and the `WWW-Authenticate` header (`error="insufficient_scope", scope="…"`) name the scope. Use a key or agent with **Full access** | | `live_payment` | Simulate was called on a Live payment. Only Test payments can be simulated | ### 404 Not Found | Code | Cause | |---|---| | `not_found` | No such session, payment, order or customer in your account in this key's mode, or no such endpoint. A Test key can't see Live objects and the other way round | ### 409 Conflict | Code | Endpoint | Cause | |---|---|---| | `idempotency_conflict` | Create | The key was used with a different body. Rarely, the first request with the key hadn't finished saving: retry in a moment | | `no_payout_wallet` | Create | Live mode needs a payout wallet. Add one in [Settings](https://app.railbed.io/settings) | | `order_paid` | Create | The store order in `order.id` is already paid, or held with money you could accept as paid or that part-paid it. Nothing was created. Check the order before asking the buyer to pay again. See [Store orders and retries](https://railbed.com/docs/api/checkout-sessions.md#store-orders-and-retries) | | `unavailable` | Start | The account can't take payments now | | `no_providers` | Start | No card provider can take this amount and currency right now. Nothing was started; try later or a different amount | | `already_paid` | Start | The payment is complete | | `held` | Start | The money arrived and the payment is held for review | | `failed` | Start | The payment was declined. Create a new session | | `not_started` | Simulate | Start the session before simulating | ### 410 Gone | Code | Cause | |---|---| | `expired` | The session passed its `expires_at` without being paid | | `canceled` | The payment link was canceled in the dashboard | ### Size, type and rate | Status | Code | Cause | |---|---|---| | 413 | `request_too_large` | The body is over 64 KiB | | 415 | `unsupported_media_type` | The body isn't sent as `Content-Type: application/json` | | 429 | `rate_limited` | Over about 120 requests a minute (each connected agent has its own 120, and a business's agents share 600), 12 starts a minute, or about 60 requests a minute from one address with a missing or invalid key. Wait for `Retry-After` | ### 5xx | Status | Code | Cause | |---|---|---| | 500 | `internal` | Something failed on our side. Retry with backoff; email support@railbed.com if it continues | | 502 | `network_unavailable` | The card network didn't answer while starting. Retry in a minute | | 503 | `misconfigured` | Card payments are briefly unavailable. Retry later | > [!NOTE] > New codes can be added within `v1`. Treat an unknown `code` by its status: an unknown `4xx` is a request to fix, an unknown `5xx` a reason to retry. --- # Webhooks > Railbed sends a signed HTTPS request to your server when a payment starts, settles, is held, expires or is canceled. How endpoints, deliveries, retries and the delivery log work. Source: https://railbed.com/docs/webhooks/ · Updated: 2026-10-04 · Railbed by DeepWork developer docs ## How webhooks work When something happens to a payment, Railbed sends a `POST` with a JSON [event](https://railbed.com/docs/webhooks/events.md) to each of your endpoints that subscribes to it. The event carries the payment, its [order](https://railbed.com/docs/webhooks/events.md#the-order-object) with the items and addresses, and the [customer](https://railbed.com/docs/webhooks/events.md#the-customer-object). The request is signed with the endpoint's secret, so your server can prove it came from Railbed and wasn't changed. Example: One delivery ```text POST https://yourstore.com/webhooks/railbed Content-Type: application/json User-Agent: Railbed-Webhooks/1.0 Railbed-Signature: t=1790381342,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd {"id":"evt_4Qm8ZsUe2VhNc7RwTb1Y","type":"payment.paid",…,"data":{"payment":{…},"order":{…},"customer":{…}}} ``` Your server verifies the signature, records the event, answers `2xx` and does the slow work afterwards. Events are queued in the same step as the payment change itself, so a payment can't become paid without its webhook being queued. Most are sent as soon as the change is saved; expiries go out within five minutes. | Event | Sent when | |---|---| | [`payment.started`](https://railbed.com/docs/webhooks/events.md#payment-started) | The buyer entered their email and was given a way to pay | | [`payment.paid`](https://railbed.com/docs/webhooks/events.md#payment-paid) | The money arrived and passed Railbed's checks. **Fulfil on this** | | [`payment.held`](https://railbed.com/docs/webhooks/events.md#payment-held) | Money arrived but failed a check, so it waits for your review | | [`payment.updated`](https://railbed.com/docs/webhooks/events.md#payment-updated) | A paid payment's settlement details were filled in | | [`payment.failed`](https://railbed.com/docs/webhooks/events.md#payment-failed) | The payment ended in a terminal Test failure | | [`payment.expired`](https://railbed.com/docs/webhooks/events.md#payment-expired) | Nobody paid in time | | [`payment.canceled`](https://railbed.com/docs/webhooks/events.md#payment-canceled) | You canceled a payment link | | [`ping`](https://railbed.com/docs/webhooks/events.md#ping) | You chose **Send test event** (or **Send ping** on a Live endpoint) | ## Add an endpoint 1. In the dashboard, choose **Test** or **Live**, then open [Developers](https://app.railbed.io/developers). 2. Choose **Add endpoint** and enter your server's address. Add a description if you like ("Fulfilment server"), and pick the events it should receive. It gets every event unless you choose. 3. Copy the **signing secret** (`whsec_…`) into your server's configuration, for example `RAILBED_WEBHOOK_SECRET`. You can reveal it again on the endpoint later. 4. Choose **Send test event** (on a Live endpoint, **Send ping**) and check how your server answered. Each mode has its own endpoints and secrets: Test endpoints receive only Test events, and Live endpoints only Live events. You can have up to 10 endpoints in each mode, each with a different address. **Address rules.** Endpoints use `https://` with no username or password in the address. (Test endpoints also accept `http://localhost`, but Railbed's servers can't reach your computer that way.) Live endpoints must be on the public internet: private networks, `localhost` and internal hostnames are refused, and addresses are checked again at each send. While you build on your own computer, [use a tunnel](https://railbed.com/docs/testing.md#receive-webhooks-on-your-own-computer). > [!NOTE] > Endpoints added before September 26, 2026 receive only `payment.paid` and `payment.failed` until you edit them and choose their events, so existing integrations see no new traffic unannounced. ## Respond to deliveries A delivery succeeds when your endpoint answers with any `2xx` status within **10 seconds**. The body of your answer is ignored. - **Answer fast.** Verify, record the event, answer `200`, then fulfil in a background job. A slow fulfilment that runs past 10 seconds counts as a failure and is retried, even if it later finishes. - **Redirects aren't followed.** A `3xx` is a failure. Save the final address as the endpoint. - **Reject what you can't verify** with a `4xx`, such as `400`. Deliveries that fail are retried, so a bad secret shows up in the log instead of losing events. - **Don't answer `2xx` before the event is saved.** A `2xx` tells Railbed to stop sending it. ## Retries A delivery that fails is retried **1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours** after each failed attempt: seven attempts over about 21 hours. After the last one it's marked failed and stays in the delivery log, where you can send it again. Every attempt carries the same event `id` and body, with a fresh signature and timestamp. When you edit the endpoint's address, pending retries go to the new one. When you roll its secret, later attempts are signed with the new secret. ## Duplicates and order Webhooks are delivered at least once. The same event can arrive more than once (a retry after a timeout, a resend from the log), and events for one payment can arrive in a different order than they happened, even in the same second. - **Deduplicate on the event `id`.** Store each id you process, with a unique constraint, in the same transaction as its effects. - **Make fulfilment idempotent too.** Grant each order once per payment id, whatever event or job arrives first. See [Fulfil orders safely](https://railbed.com/docs/guides/fulfilment.md). - **Don't undo a paid order on a later event.** A `payment.expired` that arrives after `payment.paid` doesn't make the payment unpaid. When in doubt, [read the payment](https://railbed.com/docs/api/payments.md#retrieve-a-payment): it's always current. ## The delivery log **Deliveries** on each endpoint lists its latest 50 deliveries: the event, when it was sent, how many attempts it took, the status code or error your server answered with, when the next retry is, and the exact body that was sent. Filter it to **Failed** or **Retrying** to see what needs attention. - **Retry now** sends a delivery that's still retrying straight away. If it fails again, its automatic retries carry on as before, so pressing it while your server is down never uses them up. - **Resend** sends a finished delivery again, once, with the same event id. - When the latest delivery to an endpoint failed or is retrying, its card says so and links to the log. Finished deliveries are kept for 30 days. A payment's timeline in the dashboard also shows when its webhooks were delivered or failed. ## Test events **Send test event** on a Test endpoint sends one signed event, straight away, and shows the body sent and how your server answered. On a Live endpoint the button is **Send ping**: it sends a ping and reports the answer. Test sends are never retried. - **Test endpoints** can receive a `ping` or a sample of any payment event. The sample carries a made-up payment (`metadata.sample` is `"true"`), order (`ord_sample`, two items shipping to San Francisco) and customer (`cus_sample`), none of which exist in your account or the API. Use them to build your order handling before a real sale. - **Live endpoints** can receive a `ping` only, so a live system never receives a payment that didn't happen. To test the whole flow, create a Test session through the API and [simulate its outcome](https://railbed.com/docs/testing.md). That sends the real sequence of events for a real Test payment. ## Manage endpoints | Action | What happens | |---|---| | **Edit** | Change the address, the description or the events. Pending retries follow the new address | | **Roll secret** | A new secret takes effect at once; the old one signs nothing further, retries included. Update your server straight away, or deliveries fail verification until you do | | **Remove** | The endpoint stops receiving events, its pending retries stop, and its history is no longer listed | ## A receiver, end to end Example: Node\.js · Express ```js import crypto from 'node:crypto'; import express from 'express'; const app = express(); // The raw body: verify exactly the bytes that were signed. const rawJson = express.raw({ type: 'application/json' }); app.post('/webhooks/railbed', rawJson, async (req, res) => { const raw = req.body.toString('utf8'); const secret = process.env.RAILBED_WEBHOOK_SECRET; const header = req.get('Railbed-Signature'); if (!verify(raw, header, secret)) return res.sendStatus(400); const event = JSON.parse(raw); const isNew = await db.events.insertIfAbsent(event.id); // UNIQUE(id) if (isNew && event.type === 'payment.paid') { await queue.add('fulfil', { paymentId: event.data.payment.id }); } res.sendStatus(200); }); function verify(raw, header, secret) { const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? ''); if (!m) return false; if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false; const expected = crypto .createHmac('sha256', secret) .update(`${m[1]}.${raw}`) .digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2])); } ``` Example: Python · Flask ```python import hashlib, hmac, os, re, time from flask import Flask, request app = Flask(__name__) @app.post("/webhooks/railbed") def railbed_webhook(): raw = request.get_data() # the raw bytes, before any JSON parsing header = request.headers.get("Railbed-Signature", "") if not verify(raw, header, os.environ["RAILBED_WEBHOOK_SECRET"]): return "", 400 event = request.get_json() is_new = db.events.insert_if_absent(event["id"]) if is_new and event["type"] == "payment.paid": queue.enqueue("fulfil", event["data"]["payment"]["id"]) return "", 200 def verify(raw: bytes, header: str, secret: str) -> bool: m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "") if not m: return False expected = hmac.new( secret.encode(), m[1].encode() + b"." + raw, hashlib.sha256 ).hexdigest() return ( abs(time.time() - int(m[1])) < 300 and hmac.compare_digest(expected, m[2]) ) ``` Example: PHP ```php Every webhook event Railbed sends, when it's sent, what the payment, order and customer look like at that moment, and the full payload reference. Source: https://railbed.com/docs/webhooks/events/ · Updated: 2026-10-04 · Railbed by DeepWork developer docs ## The event object Every delivery's body is one event: Example: payment\.paid · response Delivered · 200 ```json { "id": "evt_4Qm8ZsUe2VhNc7RwTb1Y", "type": "payment.paid", "created": 1790381342, "livemode": true, "data": { "payment": { "id": "pay_7AAiYH0Ykt11ED4hmfiN", "mode": "live", "source": "api", "checkoutId": null, "planLabel": null, "description": "Pro Membership", "reference": "order_1042", "customerEmail": "buyer@example.com", "amount": "49.00", "currency": "USD", "status": "paid", "provider": "stripe", "providerName": "Stripe", "depositAddress": "0x5b0e8a3f2d1c4b7a9e6f0d3c2b1a4e7f8d9c0b1a", "payoutWallet": "0xF977814e90dA44bFA03b6295A0616a897441aceC", "feeBps": 150, "valueCoin": "47.53", "merchantReceived": "46.341750", "coin": "polygon_usdc", "holdReason": null, "holdAcceptable": false, "txidIn": "0x0c651ba1d59c7a32e8b1f4bd2c7e0e4f96a55d13a6b0f2d1c8e7a9b4f3d29b58", "txidOut": "0x8202d1373e0a9c4f1b6d5e2c7a8f9b0e1d2c3b4a5f6e7d8c9b0a1f2e3d7356ff", "metadata": { "user_id": "player_1042" }, "customerName": null, "memo": null, "trackingUrl": null, "canceledAt": null, "successUrl": "https://yourstore.com/thanks?order={REFERENCE}&payment={PAYMENT_ID}", "createdAt": 1790380525000, "paidAt": 1790381342000, "expiresAt": 1790466925000, "url": "https://pay.railbed.io/p/pay_7AAiYH0Ykt11ED4hmfiN", "orderId": "ord_M4tQx8ZkP1vRn6WcLs2B" }, "order": { "id": "ord_M4tQx8ZkP1vRn6WcLs2B", "mode": "live", "number": 1186, "displayNumber": "#1186", "source": "api", "checkoutId": null, "store": null, "status": "paid", "needsReview": false, "fulfillment": "none", "fulfilledAt": null, "customer": { "id": "cus_Q9wLm3TzX7bKc2VnR5Hd", "email": "buyer@example.com", "name": null, "erased": false }, "summary": "Pro Membership", "lineCount": 1, "unitCount": 1, "currency": "USD", "subtotal": "49.00", "discount": "0.00", "discountCode": null, "shipping": "0.00", "shippingMethod": null, "tax": "0.00", "total": "49.00", "amountPaid": "49.00", "received": "46.341750", "shipTo": null, "billTo": null, "reference": "order_1042", "paymentId": "pay_7AAiYH0Ykt11ED4hmfiN", "attemptCount": 1, "createdAt": 1790380525000, "updatedAt": 1790381342000, "paidAt": 1790381342000, "items": [ { "name": "Pro Membership", "variant": null, "sku": null, "quantity": 1, "unitAmount": "49.00", "amount": "49.00" } ] }, "customer": { "id": "cus_Q9wLm3TzX7bKc2VnR5Hd", "mode": "live", "email": "buyer@example.com", "name": null, "phone": null, "location": null, "shipTo": null, "billTo": null, "storeAccounts": [], "firstSeenAt": 1790380525000, "erased": false } } } ``` | Field | Type | Description | |---|---|---| | `id` | string | The event's id, `evt_…`. The same on every attempt and resend. **Deduplicate on this** | | `type` | string | One of the types below | | `created` | integer | When the event happened, in Unix **seconds** | | `livemode` | boolean | `true` for Live events, `false` for Test | | `data.payment` | object or null | The [payment](#the-payment-object) as it was when the event happened. `null` for `ping` | | `data.order` | object or null | The payment's [order](#the-order-object), with its items, as this event leaves it. `null` for `ping` | | `data.customer` | object or null | The order's [customer](#the-customer-object): contact details and addresses. `null` for `ping`, and until the buyer has given an email | The payment, order and customer in an event are a snapshot. Events can arrive late or out of order, so when you need the current state, [read the payment](https://railbed.com/docs/api/payments.md#retrieve-a-payment) or [the order](https://railbed.com/docs/api/orders.md#retrieve-an-order). The order's `status` already includes the event: on `payment.paid` it's `paid`. It follows all of the order's payments, so an event about one attempt can carry an order that another attempt has already paid, such as a `payment.expired` for a buyer's abandoned first try. Decide what to do from the event's `type` and the payment; use the order for what to deliver and where. ## Events ### payment.started The buyer entered their email and was given a way to pay: Railbed assigned the payment's deposit address and locked the fee and the order's USD value. The payment is still `open` (it becomes `pending` when the buyer reaches a provider), and `provider` is usually still `null`. Use it for abandoned-checkout follow-ups, or to show "awaiting payment" in your system. Never fulfil from it. A buyer who entered their email only now appears here first: `data.customer` and `data.order.customer` are set from it. Example: payment\.started (trimmed) ```json { "id": "evt_9sPq2XbLr5TtVn0KcWmE", "type": "payment.started", "created": 1790380611, "livemode": true, "data": { "payment": { "id": "pay_7AAiYH0Ykt11ED4hmfiN", "status": "open", "customerEmail": "buyer@example.com", "amount": "49.00", "currency": "USD", "provider": null, "depositAddress": "0x5b0e8a3f2d1c4b7a9e6f0d3c2b1a4e7f8d9c0b1a", "feeBps": 150 } } } ``` In Test mode, `depositAddress` is `null`: no real address is created. ### payment.paid The money arrived and passed Railbed's checks: at least 90% of the order's value in a dollar coin and, from the processor's signed notice, the right deposit address, your wallet in the payout and a transaction never used before. Or you [accepted a held payment](#payment-held) as paid. **This is the event to fulfil from.** - The full payload is the example in [The event object](#the-event-object). - `status` is `paid` and `paidAt` is set. - `valueCoin`, `coin` and `txidIn` say what arrived; `txidOut` and `merchantReceived` say what was forwarded to you. - If the processor's notice is late, Railbed's scheduled status check can confirm the payment on the coin and amount first. The notice's details are checked when it arrives and fill in `txidIn` and `merchantReceived` in a [`payment.updated`](#payment-updated); a notice that doesn't match is flagged on the payment's timeline in the dashboard. - For a held payment you accepted, `merchantReceived` stays `null` and no `payment.updated` follows. ### payment.held Money arrived but failed a check, most often because less arrived than 90% of the order's value. **Don't fulfil.** The payment waits for you in the dashboard. - `holdReason` says what failed, in plain words. - `holdAcceptable` says whether you can **Accept as paid** in the dashboard. It's `false` when the evidence shows the money went somewhere else, for example to a wallet that isn't yours. - If you accept it, a `payment.paid` follows for the same payment. Example: payment\.held (trimmed) ```json { "id": "evt_Hq3nW8ZkT1cVbR6sYp0M", "type": "payment.held", "created": 1790381342, "livemode": true, "data": { "payment": { "id": "pay_Kx81mQv2PzR0dT7eWcYa", "status": "held", "amount": "49.00", "currency": "USD", "provider": "stripe", "valueCoin": "24.50", "coin": "polygon_usdc", "holdReason": "The provider sent 24.50 USDC, below 90% of the order’s 49.00 USD value.", "holdAcceptable": true, "paidAt": null } } } ``` ### payment.updated A paid payment's settlement details arrived after it was confirmed: `txidIn`, `valueCoin`, `coin` and `merchantReceived` are now filled in. The status stays `paid`. Update your records; there's nothing to fulfil again. ### payment.failed The payment reached a terminal failure. Today this is sent in **Test mode only**, for an explicit API simulation with `outcome: "failed"`. The Test provider's declined-card action keeps the payment open and sends no failed event. Live card providers don't report declines, so a live payment the buyer never completes ends as [`payment.expired`](#payment-expired). After a terminal failure, let the buyer try again with a new session. ### payment.expired Nobody paid before the payment's `expiresAt`: 24 hours for API sessions, or the lifetime chosen for a payment link. It's sent within five minutes of expiry. It's not always the end. A provider that delivers late still settles the payment, and a `payment.paid` or `payment.held` follows. Release reserved stock if you like, but keep the order able to complete. ### payment.canceled You canceled a payment link in the dashboard before anyone had gone to a provider. The payment's `status` is `expired` and `canceledAt` is set. ### ping Sent only when you choose **Send test event** and pick `ping` (Test endpoints) or **Send ping** (Live endpoints). `data.payment`, `data.order` and `data.customer` are `null`. Use it to check the address, the secret and your signature code. Example: ping ```json { "id": "evt_T2rVx9KcQm4NwLb7Ez0P", "type": "ping", "created": 1790380800, "livemode": false, "data": { "payment": null, "order": null, "customer": null } } ``` ## Typical sequences | What happened | Events, in order | |---|---| | A normal payment | `payment.started` → `payment.paid` (→ `payment.updated`) | | An underpayment you accept | `payment.started` → `payment.held` → `payment.paid` | | A buyer who never pays | `payment.started` → `payment.expired` | | A buyer who never started | `payment.expired` | | A late payment | `payment.started` → `payment.expired` → `payment.paid` or `payment.held` | | A canceled payment link | `payment.canceled` | | A declined test payment | `payment.started` → `payment.failed` | | A second try at a store order | `payment.started` → `payment.expired` for the first session, then `payment.started` → `payment.paid` for the second. Every event carries the same `data.order.id` | Events can arrive out of order, so handle each one on its own merits, and never undo a `paid` order because of a later event. When you ship goods, deliver each order once, keyed on `data.order.id`: a buyer who pays two sessions for one order sends two `payment.paid` events for it. ## The payment object Webhook payloads carry the payment, order and customer in the dashboard's shape: camelCase names and timestamps in **milliseconds** (the event's own `created` is Unix seconds). The [API's payment](https://railbed.com/docs/api/payments.md#the-payment-object) has the same facts in snake_case with Unix seconds. | Field | Type | Description | |---|---|---| | `id` | string | The payment's id, `pay_…`. The same id the API uses | | `mode` | string | `live` or `test` | | `source` | string | `api` (the API), `checkout` (a checkout page, pricing table or widget) or `payment_link` | | `checkoutId` | string or null | The checkout it came from, `chk_…`, for `checkout` payments | | `planLabel` | string or null | For pricing tables: the plan and price the buyer chose, such as `Pro · Yearly` | | `description` | string | What the buyer is paying for | | `reference` | string or null | Your reference, as sent when creating the session | | `customerEmail` | string or null | The email the buyer entered. Buyers can type any address, so don't use it to identify an account | | `amount` | string | The price you set, as a decimal string | | `currency` | string | The price's currency | | `status` | string | `open`, `pending`, `paid`, `held`, `failed` or `expired`. See [statuses](https://railbed.com/docs/how-it-works.md#statuses) | | `provider` | string or null | The provider the buyer chose, such as `stripe`. Null when the buyer paid in crypto | | `providerName` | string or null | Its display name, such as `Stripe`, or the coin and network for crypto, such as `USDC on Base` | | `method` | string | `card`, or `crypto` when the buyer sent stablecoins from their own wallet | | `network` | string or null | Crypto payments: the network the money arrived on, such as `base`. Null for card | | `depositAddress` | string or null | The one-time address this payment is paid into: on Polygon for card, set when a Live payment starts; on `network` for crypto | | `payoutWallet` | string or null | Your wallet the payment is forwarded to, fixed when it starts | | `feeBps` | integer | The Railbed fee locked into this payment, in basis points of what arrives (150 = 1.5%) | | `valueCoin` | string or null | How much arrived at the deposit address, in `coin` | | `merchantReceived` | string or null | How much was forwarded to your wallet, in `coin`. `null` until it's reported; never estimated | | `coin` | string or null | What arrived, usually `polygon_usdc` | | `holdReason` | string or null | Why the payment is held, in plain words | | `holdAcceptable` | boolean | For a held payment: whether you can accept it as paid | | `txidIn` | string or null | The Polygon transaction that delivered the money | | `txidOut` | string or null | The Polygon transaction that forwarded it to you | | `metadata` | object or null | Your metadata, as sent when creating the session | | `customerName` | string or null | Payment links: who it's for | | `memo` | string or null | Payment links: the note to the payer | | `trackingUrl` | string or null | Payment links: the read-only tracking page | | `canceledAt` | integer or null | When you canceled the payment link, in ms | | `successUrl` | string or null | The success URL as you set it, with its `{PAYMENT_ID}` and `{REFERENCE}` placeholders left in (percent-encoded if they're in the path). The buyer's checkout fills them in | | `createdAt` | integer | When the payment was created, in ms | | `paidAt` | integer or null | When it became `paid`, in ms | | `expiresAt` | integer | When an unpaid payment expires, in ms | | `url` | string | The payment's hosted checkout page | | `orderId` | string or null | The [order](#the-order-object) this payment is an attempt at, `ord_…` | > [!TIP] > Want one shape everywhere? Treat the webhook as a signal: verify it, then fetch `GET /v1/payments/:id` (and `GET /v1/orders/:id` for what to deliver) and run all your checks on the API's answers. ## The order object `data.order` is the [order](https://railbed.com/docs/api/orders.md) the payment belongs to, with its items. It has the API order's facts in camelCase, plus a few the dashboard uses. | Field | Type | Description | |---|---|---| | `id` | string | The order's id, `ord_…`. The same id the API uses | | `mode` | string | `live` or `test` | | `number` | integer | Railbed's order number, counting from 1001 in each mode | | `displayNumber` | string | The number to show people, such as `#1042`: your store's number when you sent one, otherwise Railbed's | | `source` | string | Where its first payment came from: `api`, `checkout` or `payment_link` | | `checkoutId` | string or null | The checkout it came from, `chk_…` | | `store` | object or null | For orders sent with `order.id`: `platform`, `id`, `name`, `url`, `orderId`, `orderNumber`, `orderUrl` and `customerId` (the `customer.id` you sent) | | `status` | string | The order's [status](https://railbed.com/docs/api/orders.md#status), including this event | | `needsReview` | boolean | `true` when it's `held` and needs you: a held payment can be accepted as paid in the dashboard, or what's paid doesn't cover `total` | | `fulfillment` | string | `none`, `unfulfilled` or `fulfilled`. See [fulfilment](https://railbed.com/docs/api/orders.md#fulfilment) | | `fulfilledAt` | integer or null | When it was marked `fulfilled`, in ms | | `customer` | object or null | The customer at a glance: `id`, `email`, `name` and `erased` | | `summary` | string | The first item and how many more lines, such as `Magnesium glycinate · 120 capsules +2` | | `lineCount` | integer | How many item lines | | `unitCount` | integer | How many units across all lines | | `currency` | string | The currency of every amount on the order | | `subtotal` | string | The items' `amount`s added up | | `discount` | string | Taken off the subtotal | | `discountCode` | string or null | The code the buyer used | | `shipping` | string | Shipping charged | | `shippingMethod` | string or null | How it ships | | `tax` | string | Tax charged | | `total` | string | `subtotal` − `discount` + `shipping` + `tax` | | `amountPaid` | string | What its paid payments in `currency` add up to. Below `total` when a part-payment left it `held` | | `received` | string or null | USDC that reached your wallet from its paid payments, with six decimal places like the payment's `merchantReceived`. `null` until it's reported | | `shipTo` | object or null | The shipping address: `name`, `line1`, `line2`, `city`, `region`, `postalCode`, `country` | | `billTo` | object or null | The billing address, in the same shape | | `reference` | string or null | Your `reference` from its latest payment | | `paymentId` | string or null | The payment that decides its status: the paid one, else the held one, else the latest | | `attemptCount` | integer | How many payments it has | | `createdAt` | integer | When the order was created, in ms | | `updatedAt` | integer | When it last changed, in ms | | `paidAt` | integer or null | When its first payment became `paid`, in ms | | `items` | array | Each line's `name`, `variant`, `sku`, `quantity`, `unitAmount` (or `null`) and `amount`, as on the [API's order](https://railbed.com/docs/api/orders.md#items). The first 50 lines; `lineCount` says how many there are, and the [API's order](https://railbed.com/docs/api/orders.md#retrieve-an-order) has them all | For every payment attempt's id, [read the order](https://railbed.com/docs/api/orders.md#retrieve-an-order): the API's order lists them in `payment_ids`. A store order's `data.order` looks like this in part: Example: data\.order for a store order (trimmed) ```json { "id": "ord_Vd3kR8mQ1xTn6LpZs0Wa", "displayNumber": "#1042", "store": { "platform": "woocommerce", "id": "3f6c1a2e-8b4d-4e9a-9c7f-2d5b8e1a4c60", "name": "yourstore.com", "url": "https://yourstore.com/", "orderId": "1042", "orderNumber": "1042", "orderUrl": "https://yourstore.com/wp-admin/post.php?post=1042&action=edit", "customerId": "88" }, "status": "paid", "fulfillment": "unfulfilled", "total": "135.31", "shipTo": { "name": "Maya Okafor", "line1": "418 Linden Avenue", "line2": "Apt 3B", "city": "Portland", "region": "OR", "postalCode": "97214", "country": "US" }, "items": [ { "name": "Magnesium glycinate", "variant": "120 capsules", "sku": "HN-MG-120", "quantity": 2, "unitAmount": "32.00", "amount": "64.00" } ] } ``` ## The customer object `data.customer` is the order's [customer](https://railbed.com/docs/api/customers.md), with their contact details as they were when the event happened. It's `null` until the buyer has given an email. | Field | Type | Description | |---|---|---| | `id` | string | The customer's id, `cus_…`. The same id the API uses | | `mode` | string | `live` or `test` | | `email` | string or null | Their email, lowercased. `null` once you've deleted their details | | `name` | string or null | Their name | | `phone` | string or null | Their phone number | | `location` | string or null | Where they are, such as `Portland, OR, US`, from the shipping address or else the billing address | | `shipTo` | object or null | Their latest shipping address, in the order's address shape | | `billTo` | object or null | Their latest billing address | | `storeAccounts` | array | Their customer ids in your stores: `platform`, `storeId`, `storeName` and `customerId` for each | | `firstSeenAt` | integer | When they first appeared, in ms | | `erased` | boolean | `true` once you've deleted their details | Their order counts, what they've spent and your private note are never sent in webhooks. For counts and totals, [read the customer](https://railbed.com/docs/api/customers.md#retrieve-a-customer). When you [delete a customer's details](https://railbed.com/docs/api/customers.md#deleted-customers), they're also removed from the bodies kept in your delivery log: a resend carries `null` in their place. Events already delivered can't be recalled. --- # Verify signatures > Prove each webhook came from Railbed and wasn't changed. The signing scheme, verification code in six languages, how to get the raw body in common frameworks, and a test vector. Source: https://railbed.com/docs/webhooks/signatures/ · Updated: 2026-09-26 · Railbed by DeepWork developer docs ## How deliveries are signed Every delivery carries a `Railbed-Signature` header: Example: The header ```text Railbed-Signature: t=1790381342,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` - `t` is when this attempt was signed, in Unix seconds. - `v1` is the hex HMAC-SHA256 of the string `{t}.{raw body}`, keyed with the endpoint's signing secret: the whole `whsec_…` value, as text. To verify a delivery: 1. Read the header and split out `t` and `v1`. Reject the request if the header is missing or doesn't match `t=,v1=<64 hex characters>`. 2. Reject it if `t` is more than five minutes from your clock. This stops someone replaying an old delivery they captured. 3. Compute HMAC-SHA256 over `t`, a full stop and the **raw request body**, with your secret as the key, and hex-encode it. 4. Compare your result with `v1` in constant time. Reject the request if they differ. Each attempt is signed afresh, so a retry has a new `t` and `v1` for the same body. ## Verify a delivery Each function returns `true` only for a genuine, recent delivery. Check yours against the [test vector](#test-vector) below. Example: Node\.js ```js import crypto from 'node:crypto'; export function verifyRailbedSignature( rawBody, header, secret, toleranceSeconds = 300, ) { const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? ''); if (!match) return false; const [, timestamp, signature] = match; const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (age > toleranceSeconds) return false; const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); } ``` Example: Web Crypto · Cloudflare Workers, Deno, Bun, Next\.js route handlers ```ts export async function verifyRailbedSignature( rawBody: string, header: string | null, secret: string, toleranceSeconds = 300, ) { const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? ''); if (!match) return false; const [, timestamp, signature] = match; const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (age > toleranceSeconds) return false; const enc = new TextEncoder(); const key = await crypto.subtle.importKey( 'raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'], ); const bytes = new Uint8Array( signature.match(/../g)!.map((h) => parseInt(h, 16)), ); const signedPayload = enc.encode(`${timestamp}.${rawBody}`); // subtle.verify compares in constant time return crypto.subtle.verify('HMAC', key, bytes, signedPayload); } ``` Example: Python ```python import hashlib, hmac, re, time def verify_railbed_signature( raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300, ) -> bool: match = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "") if not match: return False timestamp, signature = match.groups() if abs(time.time() - int(timestamp)) > tolerance_seconds: return False expected = hmac.new( secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) ``` Example: PHP ```php $toleranceSeconds) return false; $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); return hash_equals($expected, $signature); } ``` Example: Ruby ```ruby require 'openssl' require 'rack/utils' # secure_compare; Rails and Sinatra already load it def verify_railbed_signature(raw_body, header, secret, tolerance_seconds = 300) match = /\At=(\d+),v1=([0-9a-f]{64})\z/.match(header.to_s) return false unless match timestamp, signature = match.captures return false if (Time.now.to_i - timestamp.to_i).abs > tolerance_seconds expected = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{raw_body}") Rack::Utils.secure_compare(expected, signature) end ``` Example: Go ```go package railbed import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "regexp" "strconv" "time" ) var signatureHeader = regexp.MustCompile(`^t=(\d+),v1=([0-9a-f]{64})$`) func VerifySignature( rawBody []byte, header, secret string, tolerance time.Duration, ) bool { m := signatureHeader.FindStringSubmatch(header) if m == nil { return false } ts, err := strconv.ParseInt(m[1], 10, 64) if err != nil { return false } if age := time.Since(time.Unix(ts, 0)); age > tolerance || age < -tolerance { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(m[1] + ".")) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(m[2])) } ``` ## Use the raw body The signature covers the exact bytes Railbed sent. If your framework parses the JSON and you serialize it again, spacing, key order or escaping can change and the signature won't match. Read the body as text or bytes, verify it, then parse it. | Framework | The raw body | |---|---| | Express | `express.raw({ type: 'application/json' })` on the webhook route, then `req.body.toString('utf8')` | | Fastify | Add a content-type parser with `parseAs: 'string'` for the route, or use `fastify-raw-body` | | Next.js (App Router) | `const raw = await request.text()` in the route handler | | Cloudflare Workers, Deno, Bun | `const raw = await request.text()` | | Flask | `request.get_data()` | | Django | `request.body` | | FastAPI | `raw = await request.body()` | | Laravel | `$request->getContent()` | | Plain PHP, WordPress | `file_get_contents('php://input')` | | Rails | `request.raw_post` | | Go `net/http` | `io.ReadAll(r.Body)` before anything else reads it | Example: A complete Cloudflare Worker or Next\.js route handler ```ts export async function POST(request: Request) { const raw = await request.text(); const header = request.headers.get('Railbed-Signature'); const secret = process.env.RAILBED_WEBHOOK_SECRET!; if (!(await verifyRailbedSignature(raw, header, secret))) { return new Response('Invalid signature', { status: 400 }); } const event = JSON.parse(raw); // Record event.id with a unique constraint, queue the work, then answer. return new Response(null, { status: 200 }); } ``` ## Test vector Check your code against these known values. The secret is an example only; it isn't a real endpoint's. | Input | Value | |---|---| | Secret | `whsec_4mJ9pQx2VtR7cY1nKs8LwZ3bHd6fGa0e` | | Timestamp `t` | `1790380800` | | Raw body | `{"id":"evt_ExampleEventId0001","type":"ping","created":1790380800,"livemode":false,"data":{"payment":null}}` | | String to sign | `1790380800.{"id":"evt_ExampleEventId0001",…}`: the timestamp, a full stop, then the body | | Expected `v1` | `0f48db16650bc9a4d07b56d6db5b8f7cd53e778317e31492ed32c8fb0749fd0f` | Example: Reproduce it with OpenSSL ```bash printf '%s' '1790380800.' \ '{"id":"evt_ExampleEventId0001","type":"ping","created":1790380800,' \ '"livemode":false,"data":{"payment":null}}' \ | openssl dgst -sha256 -hmac 'whsec_4mJ9pQx2VtR7cY1nKs8LwZ3bHd6fGa0e' ``` The timestamp is in the past, so a verifier with a five-minute tolerance rejects this header. To test with it, pass a very large tolerance (every function above takes one), or test the HMAC step on its own. Change one byte of the body and the result must be `false`. For an end-to-end check, choose **Send test event** (Test) or **Send ping** (Live) on your endpoint in [Developers](https://app.railbed.io/developers): the result shows whether your server accepted it. ## Replays and clock skew The five-minute window assumes your server's clock is right; keep it synced with NTP. A captured delivery replayed within the window still has a valid signature, which is why you also [deduplicate on the event `id`](https://railbed.com/docs/webhooks.md#duplicates-and-order): a replay of an event you've processed then changes nothing. ## Rolling a secret **Roll secret** on the endpoint replaces its secret at once, and every later delivery, retries included, is signed with the new one. Update your server's secret straight away. Deliveries that fail verification in between are retried, and any that run out of retries can be sent again from the [delivery log](https://railbed.com/docs/webhooks.md#the-delivery-log). A roll can't be seamless: from the moment you roll, every delivery is signed with the new secret, and you see it only then. Deliveries that reach your server before it has the new secret fail and are retried a few minutes later, so update promptly and nothing is lost. ## Troubleshooting | Symptom | Likely cause | |---|---| | Every delivery fails verification | The wrong secret (Test and Live endpoints have different ones, and each endpoint has its own), or a secret with extra spaces or quotes around it | | Only some deliveries fail | The body was parsed and re-serialized before verifying. Verify the raw body | | Test events pass, real ones fail | A proxy, CDN or security plugin changes the body or strips the `Railbed-Signature` header on real traffic. Allow the webhook path through untouched | | Fails after a while | Your server's clock has drifted beyond five minutes | | Your server answered `2xx` but verification failed | Check the order: verify first, then answer. A `2xx` stops retries | --- # Build with AI agents > Everything in these docs is available to AI assistants and coding agents: llms.txt, a Markdown copy of every page, a structured index, and WebMCP tools. An agent can also connect to your account through agent sign-in, with the access you choose. Source: https://railbed.com/docs/agents/ · Updated: 2026-10-05 · Railbed by DeepWork developer docs ## Give your assistant the docs Point your coding assistant at one of these, depending on how much context it can take: | Resource | What it is | |---|---| | [/docs/llms.txt](https://railbed.com/docs/llms.txt) | A summary of the API and its rules, with a link to every page. Start here | | [/docs/llms-full.txt](https://railbed.com/docs/llms-full.txt) | Every docs page in one Markdown file | | `/docs/.md` | Any page as Markdown: add `.md` to its path, as in [/docs/quickstart.md](https://railbed.com/docs/quickstart.md). The overview is [/docs/index.md](https://railbed.com/docs/index.md) | | Any page, asking for Markdown | A request whose `Accept` header prefers `text/markdown`, as coding agents send, gets the page's Markdown copy instead of its HTML | | [/agents/docs.json](https://railbed.com/agents/docs.json) | A structured index: every page, section and endpoint, with links | | [/auth.md](https://railbed.com/auth.md) | Agent sign-in: how an agent asks for its own access to your account, which you approve. See [Connect an agent](#connect-an-agent) | Each page also has **Copy page for AI** in its sidebar, which copies the page's Markdown for pasting into a chat. ### Any Railbed link leads here You can give your assistant any Railbed address, not only these docs, and it will find its way: - An assistant that asks for Markdown gets the page's Markdown copy, and every copy names `/docs/llms.txt` near its top, so one page leads to the rest. - Every page's HTML also carries a short note for AI assistants, following the [inline llms.txt](https://vercel.com/blog/a-proposal-for-inline-llm-instructions-in-html) proposal. Browsers don't show it; an assistant reading the raw page gets links to the page's Markdown copy and to `llms.txt`. - Dashboard and checkout links (`app.railbed.io`, `pay.railbed.io`) draw their pages in JavaScript, so their HTML carries a hidden note pointing to these docs, and both hosts serve an `/llms.txt` that does too. Example: A prompt that works well ```text Read https://railbed.com/docs/llms.txt and the pages it links to that you need. Then add Railbed card checkout to this app: create a checkout session on the server when the user clicks Buy, redirect to its url, and fulfil the order from a verified payment.paid webhook. Use my test key from RAILBED_SECRET_KEY and my webhook secret from RAILBED_WEBHOOK_SECRET. Deduplicate on the event id. ``` > [!IMPORTANT] > Keep secret keys and webhook secrets out of prompts, chats and code. Put them in your server's environment and let the assistant refer to them by name. To let an agent use the API on your account, [connect it](#connect-an-agent) instead of pasting a key into the chat. ## Rules worth giving an agent These are the mistakes that matter most in a payments integration. They're in `llms.txt` too. - Call the API only from a server, with the key in an environment variable. There is no publishable key. - Fulfil only when a payment is `paid`, confirmed by a verified webhook or `GET /v1/payments/:id`. Never from the buyer's return to `success_url`. - Verify webhook signatures against the raw body, and deduplicate on the event `id`. - Send an `Idempotency-Key` with every create. - Treat `held` as not paid, and `expired` as possibly paid later. - Send the order as `order`, with items whose total less `discount` plus `shipping` and `tax` equals `amount` to the cent, and your order id in `order.id`. On `409 order_paid`, don't ask the buyer to pay again. - Ship each order once, keyed on its order id, and report it with `PATCH /v1/orders/:id`. - Start in Test mode (`rb_test_…` keys) and [simulate outcomes](https://railbed.com/docs/testing.md); no money moves. ## Connect an agent to your account An AI agent can use the Railbed API on your account without a secret key. You approve it once in the dashboard, it gets its own short-lived access, and you can disconnect it at any time. This is agent sign-in, and everything the agent needs to know is at [railbed.com/auth.md](https://railbed.com/auth.md). ### How connecting works 1. Ask your agent to connect to your Railbed account, and tell it the email address you sign in with. It reads [railbed.com/auth.md](https://railbed.com/auth.md) and follows it. 2. Your agent gives you a link to approve it. It opens **Connect an AI agent** on `app.railbed.io`. Sign in with the same email address you gave the agent. The link works for a few minutes; if it has expired, ask your agent for a new one. 3. Check the business, give the agent a name you'll recognize, and choose the mode and the access level. **Test** and **Full access** are selected to start with. 4. Choose **Connect agent**. The page shows **Your agent's code**. Give it to your agent, in the conversation where you asked it to connect. The code works once, for a few minutes. 5. The agent finishes connecting and starts using the API. It's listed in [Developers](https://app.railbed.io/developers) under **AI agents**, in the mode you chose. Only owners and developers of a business can connect an agent to it, the same people who can create secret keys. If a link has expired or was already used, ask the agent to start again. > [!WARNING] > The code lets an agent into your account. Give it only to your own agent, right after you asked that agent to connect. Never type it into a website, and never give it to someone who contacted you and asked for it, by email, message or phone, whoever they say they are. If you didn't just ask an agent to connect, close the page. ### The access you choose | Choice | What the agent can do | |---|---| | **Test** | Work with Test data only. Payments are simulated and no money moves, so it's the place to try an agent out | | **Live** | Work with your real payments, orders and customers | | **Full access** | Everything the API allows: create and start checkout sessions, read payments, orders and customers, and update orders | | **Read only** | Look up payments, checkout sessions, orders and customers. It can't create or change anything | A connection works in one mode, like a secret key. An agent connected in Test can't see Live data. It uses access tokens that last about five minutes and gets new ones itself, so there's no key for you to create, paste or rotate. ### Disconnect an agent In [Developers](https://app.railbed.io/developers), **AI agents** lists the agents connected in the current mode, with who approved each one and when it was last used. Choose **Disconnect** next to an agent and confirm. Its next request is refused. To use it again, it has to ask to connect again, and someone has to approve it. An agent also loses its access for good when the person who approved it leaves the business, moves to a role that can't manage keys, or has their access disabled. Putting them back doesn't restore it: an owner or developer connects the agent again. ### For agent developers [auth.md](https://railbed.com/auth.md) describes the whole flow, with request templates. WorkOS, which runs Railbed's sign-in, hosts the registration, claim and token endpoints it lists. The API itself is `https://pay.railbed.io/v1`. - Register with the person's email address (`service_auth`). Anonymous registration isn't offered. - Give the person the claim link, wait for the code they read back to you, and complete the claim. - Exchange your identity assertion for an access token. Access tokens last about five minutes. Get the next one yourself, as auth.md describes. - Call the API with `Authorization: Bearer `, the way a server sends a secret key. The token carries the business, the mode and the access level the person chose. [Authentication](https://railbed.com/docs/api.md#authentication) lists each endpoint's scope. - On `401 invalid_token`, get a new access token. On `401 agent_disconnected`, ask the person to connect you again. On `403 insufficient_scope`, the person chose **Read only**: tell them what you need and why. ## WebMCP tools [WebMCP](https://webmachinelearning.github.io/webmcp/) lets a web page offer tools to an AI agent working in the visitor's browser, so the agent can act through the page's own logic instead of reading the screen. Railbed's pages offer tools in browsers that support it; elsewhere nothing changes. ### On railbed.com and these docs Every page on railbed.com, including these docs, offers tools for learning about Railbed and building with it. All are read-only except the two that open signup or login. | Tool | What it does | |---|---| | `railbed_status` | Current [service availability](https://railbed.com/status/), observation times and check coverage; optionally filter by service. It fetches fresh results, not saved browser history. This does not verify a payment or settlement | | `railbed_docs_search` | Search the developer docs; returns the best matching sections with links | | `railbed_docs_read` | A docs page, or one section of it, as Markdown | | `railbed_api_reference` | Every API endpoint with its method, path and reference link, plus the base URL and the rules for authentication, idempotency, money and errors | | `railbed_webhook_reference` | The webhook event types, when each is sent, the signature scheme and the retry schedule | | `railbed_code_samples` | The docs' code examples, filtered by topic and language | | `railbed_overview`, `railbed_faq`, `railbed_search`, `railbed_read_page`, `railbed_contact` | What Railbed is, the FAQ, and the company and legal pages | | `railbed_start_signup`, `railbed_open_login` | Take the visitor to signup (email optionally filled in) or login. Nothing is submitted for them | ### In the dashboard When a merchant is signed in, onboarding and the **Wallet**, **Developers**, **Integrations**, **Shopify** and **WooCommerce** pages offer tools that work on their account, in the mode the dashboard is in (Test or Live). They run with the merchant's own session, in their browser, so an agent can do only what the merchant could. | Tool | Page | What it does | |---|---|---| | `railbed_onboarding_status` | Onboarding | Where setup stands: whether the business name and payout wallet are saved, the main way to sell, and the ways the account can choose, with what each involves | | `railbed_prepare_sales_channel` | Onboarding | Open **How will you take payments?** with one way selected, for the merchant to confirm with Continue. Tools never fill in the payout wallet | | `railbed_setup_checklist` | Wallet | The **Your first payment** steps for the merchant's way to sell: each step, whether it's done, where it's done or who can do it, and the optional extras | | `railbed_developer_status` | Developers | The mode, the setup steps done so far, and a summary of keys, endpoints and connected AI agents | | `railbed_list_api_keys` | Developers | The keys, masked (`rb_test_••••••••3f9a`), with their access level and when each was created and last used | | `railbed_list_agent_connections` | Developers | The AI agents connected in this mode: name, access level, who approved it, and when it connected and was last used | | `railbed_list_webhook_endpoints` | Developers | Endpoints with their addresses, labels, events and latest delivery. Signing secrets are never included | | `railbed_list_webhook_deliveries` | Developers | An endpoint's recent deliveries: status, attempts, your server's answer and, if asked, the body sent | | `railbed_send_test_webhook` | Developers | Send a signed test event to an endpoint and report how it answered. Test endpoints take any event type; Live endpoints take `ping` only | | `railbed_prepare_api_key` | Developers | Open **Create key** with a name filled in. The merchant creates it; the secret is shown only to them | | `railbed_prepare_webhook_endpoint` | Developers | Open **Add endpoint** with the address, description and events filled in, for the merchant to review and save | | `railbed_open_webhook_deliveries` | Developers | Open an endpoint's delivery log, optionally filtered to failed or retrying deliveries, so the merchant can resend | | `railbed_list_integrations` | Integrations | The ways to take payments (links, checkouts, the API, store plugins), their status and where to set each up | | `railbed_list_card_providers` | Integrations | The card providers Railbed routes to, with their status and minimum amounts (in Test mode, the fixed test list) | | `railbed_shopify_status` | Shopify | Each connected store's setup steps and the checks behind them, its own Test or Live mode, ad-tracking ids set, failed syncs, and the exact values to paste into Shopify. Never the client secret or tracking tokens | | `railbed_list_shopify_checkouts` | Shopify | A store's recent checkouts, newest first: the buyer's email, items, total, payment status, and how far the Shopify draft or order got, with the error when a sync failed. Optionally only failed syncs | | `railbed_send_shopify_test_event` | Shopify | Send one test event to an ad platform or affiliate postback set up in the Ad tracking card, and report whether it was accepted. It never counts as a sale | | `railbed_prepare_shopify_store` | Shopify | Open **Connect a store** with the address filled in, for the merchant to confirm | | `railbed_prepare_shopify_retry` | Shopify | Open the confirmation to send a checkout whose sync failed to Shopify again | | `railbed_prepare_shopify_mode` | Shopify | Open the confirmation to take a store Live or back to Test. Refused, with what's missing, when the store isn't ready | | `railbed_woocommerce_setup` | WooCommerce | Where setup stands: the plugin download, the four steps, the store's webhook address and last delivery, and recent WooCommerce orders. Never keys or secrets | Some things are deliberately left to the merchant. Tools never return a secret key or a signing secret, and they never create or revoke keys, roll secrets, remove endpoints or resend real events: those open the dashboard's own dialog for the merchant to confirm, or aren't offered at all. > [!NOTE] > Content returned by the delivery log and the Shopify checkout list includes data your buyers typed, such as their email, and your server's answers. Agents receive it marked as untrusted content.