# 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.io/docs/testing/ · Updated: 2026-09-26 · 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 |

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 becomes `failed`.

## Simulate an outcome from your server

To test without a browser, start the session and simulate the outcome through the API. This is how automated tests should drive payments.

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" }'

# Then decide how it ends: "paid", "underpaid" 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" }'
```

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', 'failed'
});
```

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", "failed"
)
```

PHP:

```php
<?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', 'failed'
]);
```

The simulated outcome runs the same settlement checks and sends the same webhooks as a live payment. Each session can end only once: simulating again on a finished payment returns it unchanged, so use a new session for each scenario. See [Simulate a payment](https://railbed.io/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 (its `metadata.sample` is `"true"`, and it isn't in your payments 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 decline | Simulate `failed` | Order not fulfilled; buyer can start again |
| 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 |
| 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.
4. Take one small real payment and follow it through: `payment.paid` received, the order fulfilled once, USDC in your wallet.
