Payments
A payment is what a checkout session became. Read it to fulfil orders, list payments to reconcile, and simulate outcomes in Test mode.
The payment object
A payment has every field of its checkout session, 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 |
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 |
|---|---|---|
coin | string or null | What was delivered, usually polygon_usdc (polygon_usdt is also possible) |
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 Polygon transaction that delivered the money |
txid_out | string or null | The Polygon 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.
curl https://pay.railbed.io/v1/payments/pay_7AAiYH0Ykt11ED4hmfiN \
-H "Authorization: Bearer $RAILBED_SECRET_KEY"const payment = await fetch(`https://pay.railbed.io/v1/payments/${id}`, {
headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` },
}).then((r) => r.json());payment = requests.get(
f"https://pay.railbed.io/v1/payments/{id}",
headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"},
timeout=15,
).json()<?php
$ch = curl_init('https://pay.railbed.io/v1/payments/' . rawurlencode($id));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'),
],
]);
$payment = json_decode(curl_exec($ch), true);{
"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" },
"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:
{
"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 |
curl "https://pay.railbed.io/v1/payments?limit=50" \
-H "Authorization: Bearer $RAILBED_SECRET_KEY"// 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);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<?php
$cursor = null;
do {
$query = http_build_query(array_filter([
'limit' => 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);{
"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 (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. Decides how a started test payment ends, as a buyer on the test provider page would. The outcome goes through the same settlement checks and sends the same webhooks as a live payment.
| Field | Type | Description |
|---|---|---|
outcome | string Required | paid (about 97% of the value arrives), underpaid (half arrives, so it's held) or failed (declined) |
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 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());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()<?php
$ch = curl_init(
'https://pay.railbed.io/v1/payments/' . rawurlencode($id) . '/simulate'
);
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(['outcome' => 'paid']),
]);
$payment = json_decode(curl_exec($ch), true);The response is the payment after the outcome. A payment can end only once: simulating a finished payment returns it as it is. Use a new session for each scenario.
| Status | Code | When |
|---|---|---|
| 400 | invalid_outcome | outcome is missing or not one of the three |
| 403 | live_payment | The payment is a Live payment |
| 409 | not_started | Start the session first, with Start a checkout session |