Event types
Every webhook event Railbed sends, when it's sent, what the payment looks like at that moment, and the full payload reference.
The event object
Every delivery's body is one event:
{
"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"
}
}
}| 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 as it was when the event happened. null for ping |
The payment in an event is a snapshot. Events can arrive late or out of order, so when you need the current state, read the payment.
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.
{
"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 as paid. This is the event to fulfil from.
- The full payload is the example in The event object.
statusispaidandpaidAtis set.valueCoin,coinandtxidInsay what arrived;txidOutandmerchantReceivedsay 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
txidInandmerchantReceivedin apayment.updated; a notice that doesn't match is flagged on the payment's timeline in the dashboard. - For a held payment you accepted,
merchantReceivedstaysnulland nopayment.updatedfollows.
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.
holdReasonsays what failed, in plain words.holdAcceptablesays whether you can Accept as paid in the dashboard. It'sfalsewhen the evidence shows the money went somewhere else, for example to a wallet that isn't yours.- If you accept it, a
payment.paidfollows for the same payment.
{
"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 was declined. Today this is sent in Test mode only, when you simulate a decline: live card providers don't report declines, so a live payment the buyer never completes ends as payment.expired. 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 is null. Use it to check the address, the secret and your signature code.
{
"id": "evt_T2rVx9KcQm4NwLb7Ez0P",
"type": "ping",
"created": 1790380800,
"livemode": false,
"data": { "payment": 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 |
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.
The payment object
Webhook payloads carry the payment in the dashboard's shape: camelCase names and timestamps in milliseconds (the event's own created is Unix seconds). The API's payment 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 |
provider | string or null | The provider the buyer chose, such as stripe |
providerName | string or null | Its display name, such as Stripe |
depositAddress | string or null | The one-time Polygon address this payment is paid into. Set when a Live payment starts |
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 |