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.
On this page
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 | 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
heldfor 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.
# 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" }'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'
});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"
)<?php
function railbed_post(string $path, array $body): array {
$ch = curl_init('https://pay.railbed.io/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => 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 for the details.
Send sample webhook events
In 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
- Add your payout wallet in Settings. It must be a self-custody Polygon wallet you control, not an exchange deposit address.
- Switch the dashboard to Live and create a live key and live webhook endpoints. Live endpoints need public
https://addresses. - 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.
- Take one small real payment and follow it through:
payment.paidreceived, the order fulfilled once, USDC in your wallet.