Event types
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.
On this page
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",
"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 as it was when the event happened. null for ping |
data.order | object or null | The payment's order, with its items, as this event leaves it. null for ping |
data.customer | object or null | The order's customer: 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 or the 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.
{
"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 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
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.
{
"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 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. 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 this payment is an attempt at, ord_… |
The order object
data.order is the order 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, 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 |
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' amounts 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. The first 50 lines; lineCount says how many there are, and the API's order has them all |
For every payment attempt's id, read the order: the API's order lists them in payment_ids. A store order's data.order looks like this in part:
{
"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, 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.
When you delete a customer's details, 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.