RailbedDocs

Webhooks

Railbed sends a signed HTTPS request to your server when a payment starts, settles, is held, expires or is canceled. How endpoints, deliveries, retries and the delivery log work.

How webhooks work

When something happens to a payment, Railbed sends a POST with a JSON event to each of your endpoints that subscribes to it. The request is signed with the endpoint's secret, so your server can prove it came from Railbed and wasn't changed.

One delivery
POST https://yourstore.com/webhooks/railbed
Content-Type: application/json
User-Agent: Railbed-Webhooks/1.0
Railbed-Signature: t=1790381342,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{"id":"evt_4Qm8ZsUe2VhNc7RwTb1Y","type":"payment.paid","created":1790381342,"livemode":true,"data":{"payment":{…}}}

Your server verifies the signature, records the event, answers 2xx and does the slow work afterwards. Events are queued in the same step as the payment change itself, so a payment can't become paid without its webhook being queued. Most are sent as soon as the change is saved; expiries go out within five minutes.

EventSent when
payment.startedThe buyer entered their email and was given a way to pay
payment.paidThe money arrived and passed Railbed's checks. Fulfil on this
payment.heldMoney arrived but failed a check, so it waits for your review
payment.updatedA paid payment's settlement details were filled in
payment.failedThe payment was declined (Test mode)
payment.expiredNobody paid in time
payment.canceledYou canceled a payment link
pingYou chose Send test event (or Send ping on a Live endpoint)

Add an endpoint

  1. In the dashboard, choose Test or Live, then open Developers.
  2. Choose Add endpoint and enter your server's address. Add a description if you like ("Fulfilment server"), and pick the events it should receive. It gets every event unless you choose.
  3. Copy the signing secret (whsec_…) into your server's configuration, for example RAILBED_WEBHOOK_SECRET. You can reveal it again on the endpoint later.
  4. Choose Send test event (on a Live endpoint, Send ping) and check how your server answered.

Each mode has its own endpoints and secrets: Test endpoints receive only Test events, and Live endpoints only Live events. You can have up to 10 endpoints in each mode, each with a different address.

Address rules. Endpoints use https:// with no username or password in the address. (Test endpoints also accept http://localhost, but Railbed's servers can't reach your computer that way.) Live endpoints must be on the public internet: private networks, localhost and internal hostnames are refused, and addresses are checked again at each send. While you build on your own computer, use a tunnel.

Respond to deliveries

A delivery succeeds when your endpoint answers with any 2xx status within 10 seconds. The body of your answer is ignored.

  • Answer fast. Verify, record the event, answer 200, then fulfil in a background job. A slow fulfilment that runs past 10 seconds counts as a failure and is retried, even if it later finishes.
  • Redirects aren't followed. A 3xx is a failure. Save the final address as the endpoint.
  • Reject what you can't verify with a 4xx, such as 400. Deliveries that fail are retried, so a bad secret shows up in the log instead of losing events.
  • Don't answer 2xx before the event is saved. A 2xx tells Railbed to stop sending it.

Retries

A delivery that fails is retried 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours after each failed attempt: seven attempts over about 21 hours. After the last one it's marked failed and stays in the delivery log, where you can send it again.

Every attempt carries the same event id and body, with a fresh signature and timestamp. When you edit the endpoint's address, pending retries go to the new one. When you roll its secret, later attempts are signed with the new secret.

Duplicates and order

Webhooks are delivered at least once. The same event can arrive more than once (a retry after a timeout, a resend from the log), and events for one payment can arrive in a different order than they happened, even in the same second.

  • Deduplicate on the event id. Store each id you process, with a unique constraint, in the same transaction as its effects.
  • Make fulfilment idempotent too. Grant each order once per payment id, whatever event or job arrives first. See Fulfil orders safely.
  • Don't undo a paid order on a later event. A payment.expired that arrives after payment.paid doesn't make the payment unpaid. When in doubt, read the payment: it's always current.

The delivery log

Deliveries on each endpoint lists its latest 50 deliveries: the event, when it was sent, how many attempts it took, the status code or error your server answered with, when the next retry is, and the exact body that was sent. Filter it to Failed or Retrying to see what needs attention.

  • Retry now sends a delivery that's still retrying straight away. If it fails again, its automatic retries carry on as before, so pressing it while your server is down never uses them up.
  • Resend sends a finished delivery again, once, with the same event id.
  • When the latest delivery to an endpoint failed or is retrying, its card says so and links to the log.

Finished deliveries are kept for 30 days. A payment's timeline in the dashboard also shows when its webhooks were delivered or failed.

Test events

Send test event on a Test endpoint sends one signed event, straight away, and shows the body sent and how your server answered. On a Live endpoint the button is Send ping: it sends a ping and reports the answer. Test sends are never retried.

  • Test endpoints can receive a ping or a sample of any payment event. The sample carries a made-up payment (metadata.sample is "true") that doesn't exist in your account or the API.
  • Live endpoints can receive a ping only, so a live system never receives a payment that didn't happen.

To test the whole flow, create a Test session through the API and simulate its outcome. That sends the real sequence of events for a real Test payment.

Manage endpoints

ActionWhat happens
EditChange the address, the description or the events. Pending retries follow the new address
Roll secretA new secret takes effect at once; the old one signs nothing further, retries included. Update your server straight away, or deliveries fail verification until you do
RemoveThe endpoint stops receiving events, its pending retries stop, and its history is no longer listed

A receiver, end to end

Express

import crypto from 'node:crypto';
import express from 'express';

const app = express();

// The raw body: verify exactly the bytes that were signed.
const rawJson = express.raw({ type: 'application/json' });

app.post('/webhooks/railbed', rawJson, async (req, res) => {
  const raw = req.body.toString('utf8');
  const secret = process.env.RAILBED_WEBHOOK_SECRET;
  const header = req.get('Railbed-Signature');
  if (!verify(raw, header, secret)) return res.sendStatus(400);

  const event = JSON.parse(raw);
  const isNew = await db.events.insertIfAbsent(event.id); // UNIQUE(id)
  if (isNew && event.type === 'payment.paid') {
    await queue.add('fulfil', { paymentId: event.data.payment.id });
  }
  res.sendStatus(200);
});

function verify(raw, header, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? '');
  if (!m) return false;
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${m[1]}.${raw}`)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}

The fulfilment job then reads the payment and grants the order once, as in Fulfil orders safely. Verification in more languages, and a test vector to check yours against, are in Verify signatures.

Checklist

  • The endpoint verifies Railbed-Signature against the raw body and rejects anything else with a 4xx
  • Timestamps older than five minutes are rejected
  • Event ids are stored with a unique constraint, and duplicates are acknowledged with 2xx and ignored
  • The endpoint answers within a second or two and fulfils in the background
  • Unknown event types are acknowledged with 2xx and ignored
  • Orders are granted once per payment, from paid only
  • A background job checks orders still waiting, in case a webhook never arrives
  • The Live endpoint uses the Live secret, and the Test endpoint the Test secret

Updated · This page as Markdown