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:

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",
      "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
    }
  }
}
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
data.orderobject or nullThe payment's order, with its items, as this event leaves it. null for ping
data.customerobject or nullThe 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.

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 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.

ping
{
  "id": "evt_T2rVx9KcQm4NwLb7Ez0P",
  "type": "ping",
  "created": 1790380800,
  "livemode": false,
  "data": { "payment": null, "order": null, "customer": 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
A second try at a store orderpayment.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.

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. Null when the buyer paid in crypto
providerNamestring or nullIts display name, such as Stripe, or the coin and network for crypto, such as USDC on Base
methodstringcard, or crypto when the buyer sent stablecoins from their own wallet
networkstring or nullCrypto payments: the network the money arrived on, such as base. Null for card
depositAddressstring or nullThe one-time address this payment is paid into: on Polygon for card, set when a Live payment starts; on network for crypto
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
orderIdstring or nullThe 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.

FieldTypeDescription
idstringThe order's id, ord_…. The same id the API uses
modestringlive or test
numberintegerRailbed's order number, counting from 1001 in each mode
displayNumberstringThe number to show people, such as #1042: your store's number when you sent one, otherwise Railbed's
sourcestringWhere its first payment came from: api, checkout or payment_link
checkoutIdstring or nullThe checkout it came from, chk_…
storeobject or nullFor orders sent with order.id: platform, id, name, url, orderId, orderNumber, orderUrl and customerId (the customer.id you sent)
statusstringThe order's status, including this event
needsReviewbooleantrue 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
fulfillmentstringnone, unfulfilled or fulfilled. See fulfilment
fulfilledAtinteger or nullWhen it was marked fulfilled, in ms
customerobject or nullThe customer at a glance: id, email, name and erased
summarystringThe first item and how many more lines, such as Magnesium glycinate · 120 capsules +2
lineCountintegerHow many item lines
unitCountintegerHow many units across all lines
currencystringThe currency of every amount on the order
subtotalstringThe items' amounts added up
discountstringTaken off the subtotal
discountCodestring or nullThe code the buyer used
shippingstringShipping charged
shippingMethodstring or nullHow it ships
taxstringTax charged
totalstringsubtotal − discount + shipping + tax
amountPaidstringWhat its paid payments in currency add up to. Below total when a part-payment left it held
receivedstring or nullUSDC that reached your wallet from its paid payments, with six decimal places like the payment's merchantReceived. null until it's reported
shipToobject or nullThe shipping address: name, line1, line2, city, region, postalCode, country
billToobject or nullThe billing address, in the same shape
referencestring or nullYour reference from its latest payment
paymentIdstring or nullThe payment that decides its status: the paid one, else the held one, else the latest
attemptCountintegerHow many payments it has
createdAtintegerWhen the order was created, in ms
updatedAtintegerWhen it last changed, in ms
paidAtinteger or nullWhen its first payment became paid, in ms
itemsarrayEach 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:

data.order for a store order (trimmed)
{
  "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.

FieldTypeDescription
idstringThe customer's id, cus_…. The same id the API uses
modestringlive or test
emailstring or nullTheir email, lowercased. null once you've deleted their details
namestring or nullTheir name
phonestring or nullTheir phone number
locationstring or nullWhere they are, such as Portland, OR, US, from the shipping address or else the billing address
shipToobject or nullTheir latest shipping address, in the order's address shape
billToobject or nullTheir latest billing address
storeAccountsarrayTheir customer ids in your stores: platform, storeId, storeName and customerId for each
firstSeenAtintegerWhen they first appeared, in ms
erasedbooleantrue 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.

Updated · This page as Markdown