RailbedDocs

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:

payment.paid
{
  "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"
    }
  }
}
Delivered · 200
FieldTypeDescription
idstringThe event's id, evt_…. The same on every attempt and resend. Deduplicate on this
typestringOne of the types below
createdintegerWhen the event happened, in Unix seconds
livemodebooleantrue for Live events, false for Test
data.paymentobject or nullThe 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.

payment.started (trimmed)
{
  "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.
  • status is paid and paidAt is set.
  • valueCoin, coin and txidIn say what arrived; txidOut and merchantReceived say 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 txidIn and merchantReceived in a payment.updated; a notice that doesn't match is flagged on the payment's timeline in the dashboard.
  • For a held payment you accepted, merchantReceived stays null and no payment.updated follows.

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.

  • holdReason says what failed, in plain words.
  • holdAcceptable says whether you can Accept as paid in the dashboard. It's false when the evidence shows the money went somewhere else, for example to a wallet that isn't yours.
  • If you accept it, a payment.paid follows for the same payment.
payment.held (trimmed)
{
  "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.

ping
{
  "id": "evt_T2rVx9KcQm4NwLb7Ez0P",
  "type": "ping",
  "created": 1790380800,
  "livemode": false,
  "data": { "payment": null }
}

Typical sequences

What happenedEvents, in order
A normal paymentpayment.started → payment.paid (→ payment.updated)
An underpayment you acceptpayment.started → payment.held → payment.paid
A buyer who never payspayment.started → payment.expired
A buyer who never startedpayment.expired
A late paymentpayment.started → payment.expired → payment.paid or payment.held
A canceled payment linkpayment.canceled
A declined test paymentpayment.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.

FieldTypeDescription
idstringThe payment's id, pay_…. The same id the API uses
modestringlive or test
sourcestringapi (the API), checkout (a checkout page, pricing table or widget) or payment_link
checkoutIdstring or nullThe checkout it came from, chk_…, for checkout payments
planLabelstring or nullFor pricing tables: the plan and price the buyer chose, such as Pro · Yearly
descriptionstringWhat the buyer is paying for
referencestring or nullYour reference, as sent when creating the session
customerEmailstring or nullThe email the buyer entered. Buyers can type any address, so don't use it to identify an account
amountstringThe price you set, as a decimal string
currencystringThe price's currency
statusstringopen, pending, paid, held, failed or expired. See statuses
providerstring or nullThe provider the buyer chose, such as stripe
providerNamestring or nullIts display name, such as Stripe
depositAddressstring or nullThe one-time Polygon address this payment is paid into. Set when a Live payment starts
payoutWalletstring or nullYour wallet the payment is forwarded to, fixed when it starts
feeBpsintegerThe Railbed fee locked into this payment, in basis points of what arrives (150 = 1.5%)
valueCoinstring or nullHow much arrived at the deposit address, in coin
merchantReceivedstring or nullHow much was forwarded to your wallet, in coin. null until it's reported; never estimated
coinstring or nullWhat arrived, usually polygon_usdc
holdReasonstring or nullWhy the payment is held, in plain words
holdAcceptablebooleanFor a held payment: whether you can accept it as paid
txidInstring or nullThe Polygon transaction that delivered the money
txidOutstring or nullThe Polygon transaction that forwarded it to you
metadataobject or nullYour metadata, as sent when creating the session
customerNamestring or nullPayment links: who it's for
memostring or nullPayment links: the note to the payer
trackingUrlstring or nullPayment links: the read-only tracking page
canceledAtinteger or nullWhen you canceled the payment link, in ms
successUrlstring or nullThe 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
createdAtintegerWhen the payment was created, in ms
paidAtinteger or nullWhen it became paid, in ms
expiresAtintegerWhen an unpaid payment expires, in ms
urlstringThe payment's hosted checkout page

Updated · This page as Markdown