# Zapier

> Connect Railbed to thousands of apps with Zapier, using Zapier's own Webhooks and Code steps. Receive signed payment events, read payments, orders and customers, mark orders fulfilled and send checkout links, with ready-made recipes.

Source: https://railbed.com/docs/guides/zapier/ · Updated: 2026-10-12 · Railbed by MediaGlobe developer docs

For AI assistants: the index of every docs page, with the rules for an integration, is https://railbed.com/docs/llms.txt

## What you need

These steps use apps Zapier already has, so they work with no Railbed app in Zapier:

- **Webhooks by Zapier**, to receive Railbed's events and to call the Railbed API. It's one of Zapier's premium apps, so check that your Zapier plan includes it.
- **Code by Zapier**, to check each event's signature and turn it into simple fields. On Zapier's Free plan a Code step may run for one second; paid plans allow longer.
- A Railbed secret key or a webhook endpoint, both made in [Developers](https://app.railbed.io/developers).

Build and test everything in **Test mode** first (turn it on in the business block at the top of the sidebar). Test payments move no money, and Test and Live each have their own keys, endpoints and secrets.

## Receive events in a Zap

Railbed sends a signed [event](https://railbed.com/docs/webhooks/events.md) when a payment starts, is paid, needs your review, or closes unpaid. A Zap receives it with **Catch Raw Hook**, and a Code step checks that Railbed sent it before anything else runs.

1. In Zapier, create a Zap with the trigger **Webhooks by Zapier → Catch Raw Hook**, and copy its webhook URL. Use Catch Raw Hook, not Catch Hook: the signature covers the exact body Railbed sent, and only Catch Raw Hook keeps that body and the request's headers.
2. In Railbed, open [Developers](https://app.railbed.io/developers) → **Add endpoint**. Paste the Zapier URL, add a description such as "Zapier: paid orders to Sheets", choose the events this Zap needs (often just `payment.paid`) and save. Copy the endpoint's **signing secret** (`whsec_…`).
3. On the endpoint, choose **Send test event** and pick `payment.paid`. Back in Zapier, test the trigger: the request appears with a **Raw Body** and its headers.
4. Add an action step **Code by Zapier → Run JavaScript**. Under **Input Data**, add these names and values, then paste the code below into **Code** and test the step:

| Input Data name | Value |
|---|---|
| `raw_body` | The trigger's **Raw Body** |
| `signature` | The trigger's `Railbed-Signature` header, which Zapier lists as **Headers Http Railbed Signature** |
| `secret` | The endpoint's signing secret, pasted as text |
| `max_age_seconds` | Optional. How old a delivery may be, in seconds; 86400 (a day) when empty |

Example: Code by Zapier: check the signature and read the event

```js
// Railbed → Zapier: check a webhook's signature, then turn the event into simple fields.
//
// Paste this whole file into a "Code by Zapier → Run JavaScript" step that follows
// "Webhooks by Zapier → Catch Raw Hook". Fill in the step's Input Data:
//
//   raw_body         Catch Raw Hook's "Raw Body"
//   signature        Catch Raw Hook's Railbed-Signature header ("Headers Http Railbed Signature")
//   secret           the endpoint's signing secret from Railbed (whsec_…)
//   max_age_seconds  optional; how old a delivery may be, in seconds (default 86400, a day)
//
// A delivery whose signature doesn't match stops the Zap with an error, so nothing after
// this step runs for it. Guide: https://railbed.com/docs/guides/zapier/

const crypto = require('crypto');

// How old a delivery may be when this step runs. Zapier can run this step long after the
// delivery arrived: it holds runs when your plan's task limit is reached or many arrive at
// once, until you replay them. A day covers most of those. The signature must still match,
// so an old delivery can only repeat an event Railbed really sent. Set max_age_seconds
// lower (such as 3600) if you'd rather refuse late runs.
const DEFAULT_MAX_AGE_SECONDS = 86400;

const rawBody = typeof inputData.raw_body === 'string' ? inputData.raw_body : '';
const header = String(inputData.signature || '').trim();
const secret = String(inputData.secret || '').trim();
const maxAge = inputData.max_age_seconds ? Number(inputData.max_age_seconds) : DEFAULT_MAX_AGE_SECONDS;

if (!/^whsec_[A-Za-z0-9_-]+$/.test(secret)) {
  throw new Error('Add the endpoint\'s signing secret (it starts with whsec_) to this step\'s "secret" input.');
}
if (!Number.isFinite(maxAge) || maxAge <= 0) {
  throw new Error('max_age_seconds must be a number of seconds, such as 86400.');
}

// Railbed-Signature: t=<Unix seconds>,v1=<hex HMAC>[,v1=<hex HMAC>] (two v1 values for 24 hours after a secret roll).
const match = /^t=(\d{1,12})((?:,v1=[0-9a-f]{64})+)$/.exec(header);
if (!match) {
  throw new Error('This request has no valid Railbed-Signature header, so it was not sent by Railbed. Nothing after this step ran.');
}
const timestamp = match[1];
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (age > maxAge) {
  throw new Error(`This delivery was signed ${Math.round(age)} seconds ago, longer than max_age_seconds (${maxAge}). If Zapier held this run, check the payment in Railbed, or resend the delivery from Developers.`);
}
const expected = Buffer.from(crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`, 'utf8').digest('hex'));
const genuine = match[2].slice(1).split(',').some((part) => {
  const given = Buffer.from(part.slice(3));
  return given.length === expected.length && crypto.timingSafeEqual(expected, given);
});
if (!genuine) {
  throw new Error('The Railbed-Signature doesn\'t match this body and secret. Check that the secret belongs to this endpoint and mode (Test and Live differ), and that "raw_body" is mapped from Catch Raw Hook. Nothing after this step ran.');
}

const event = JSON.parse(rawBody);
const data = event.data || {};
const payment = data.payment || null;
const order = data.order || null;
const customer = data.customer || null;
const iso = (ms) => (typeof ms === 'number' ? new Date(ms).toISOString() : null);
const address = (prefix, a) => ({
  [`${prefix}_name`]: a ? a.name : null,
  [`${prefix}_line1`]: a ? a.line1 : null,
  [`${prefix}_line2`]: a ? a.line2 : null,
  [`${prefix}_city`]: a ? a.city : null,
  [`${prefix}_region`]: a ? a.region : null,
  [`${prefix}_postal_code`]: a ? a.postalCode : null,
  [`${prefix}_country`]: a ? a.country : null,
});

return {
  verified: true,
  event_id: event.id,
  event_type: event.type,
  event_created_at: typeof event.created === 'number' ? new Date(event.created * 1000).toISOString() : null,
  mode: event.livemode ? 'live' : 'test',
  // Send test event in Developers uses a made-up payment that the API doesn't know.
  is_sample: !!(payment && payment.metadata && payment.metadata.sample === 'true'),

  payment_id: payment ? payment.id : null,
  payment_status: payment ? payment.status : null,
  amount: payment ? payment.amount : null,
  currency: payment ? payment.currency : null,
  description: payment ? payment.description : null,
  reference: payment ? payment.reference : null,
  customer_email: payment ? payment.customerEmail : null,
  provider: payment ? payment.provider : null,
  provider_name: payment ? payment.providerName : null,
  method: payment ? payment.method : null,
  hold_reason: payment ? payment.holdReason : null,
  hold_acceptable: payment ? !!payment.holdAcceptable : null,
  net_received: payment ? payment.merchantReceived : null,
  checkout_url: payment ? payment.url : null,
  created_at: payment ? iso(payment.createdAt) : null,
  paid_at: payment ? iso(payment.paidAt) : null,
  metadata: payment && payment.metadata ? payment.metadata : {},

  order_id: order ? order.id : null,
  order_number: order ? order.displayNumber : null,
  order_status: order ? order.status : null,
  order_needs_review: order ? !!order.needsReview : null,
  order_fulfillment: order ? order.fulfillment : null,
  order_summary: order ? order.summary : null,
  order_subtotal: order ? order.subtotal : null,
  order_discount: order ? order.discount : null,
  order_shipping: order ? order.shipping : null,
  order_tax: order ? order.tax : null,
  order_total: order ? order.total : null,
  order_store_order_id: order && order.store ? order.store.orderId : null,
  order_items: order && Array.isArray(order.items)
    ? order.items.map((item) => ({
        name: item.name,
        variant: item.variant,
        sku: item.sku,
        quantity: item.quantity,
        unit_amount: item.unitAmount,
        amount: item.amount,
      }))
    : [],
  ...address('shipping', order ? order.shipTo : null),

  customer_id: customer ? customer.id : null,
  customer_name: customer ? customer.name : null,
  customer_phone: customer ? customer.phone : null,
  // They joined your email list; null while the payment has no customer (no email yet).
  customer_accepts_marketing: customer ? !!customer.acceptsMarketing : null,
};
```

Next, always add a **Filter by Zapier** step that continues only when the Code step's **Event Type** exactly matches the event this Zap handles, such as `payment.paid`, even when the endpoint sends only that event. A `ping`, or a test event of another type from **Send test event**, has no payment, so without the Filter the later steps would run with empty fields. Then add the steps that do the work, mapping the Code step's fields below.

A request that isn't from Railbed, or that was changed on the way, stops the Zap with an error at the Code step, so nothing after it runs. Zapier tells you about errors, so a wrong secret shows up on the first event. The Code step allows a delivery up to a day old rather than the usual [five minutes](https://railbed.com/docs/webhooks/signatures.md#how-deliveries-are-signed), because Zapier can run the step long after the request arrived: it holds runs when your plan's task limit is reached or many arrive at once, until you replay them. The signature must still match. Set `max_age_seconds` for a shorter limit.

| Field | What it is |
|---|---|
| `event_id`, `event_type` | The event's id (`evt_…`) and type, such as `payment.paid` |
| `mode` | `live` or `test` |
| `is_sample` | `true` for the made-up payment that **Send test event** sends. It isn't in your account, so skip any step that looks it up |
| `payment_id`, `payment_status` | The payment (`pay_…`) and its status, such as `paid` or `held` |
| `amount`, `currency` | The price you set, such as `49.00` and `USD` |
| `description`, `reference`, `metadata` | What the buyer paid for, your reference and your metadata |
| `customer_email` | The email the buyer typed. Fine for a receipt, but don't use it to decide whose account to credit |
| `provider`, `provider_name`, `method` | How the buyer paid |
| `hold_reason`, `hold_acceptable` | For a payment that needs your review: why, and whether you can accept it as paid |
| `net_received` | What reached your payout wallet after fees, in dollars. It can be empty on a `payment.paid` when the payout's details are reported later: they then arrive in a [`payment.updated`](https://railbed.com/docs/webhooks/events.md#payment-updated) event for the same payment |
| `paid_at`, `created_at`, `checkout_url` | When it was paid and created, and its checkout page |
| `order_id`, `order_number`, `order_status`, `order_fulfillment`, `order_summary`, `order_total` | The [order](https://railbed.com/docs/webhooks/events.md#the-order-object) the payment belongs to |
| `order_items` | The order's lines, as Zapier line items: `name`, `variant`, `sku`, `quantity`, `unit_amount`, `amount` |
| `shipping_name` … `shipping_country` | Where the order ships |
| `customer_id`, `customer_name`, `customer_phone` | The [customer](https://railbed.com/docs/webhooks/events.md#the-customer-object) |
| `customer_accepts_marketing` | `true` once the buyer joined your email list (the box at a checkout that offers it, or your own edit), `false` if they haven't, empty while the payment has no customer |

> [!IMPORTANT]
> The signing secret is saved in the Code step, so anyone who can edit the Zap can read it. With it they could sign events this Zap would accept, but they can't reach your account or your money. If it may have been exposed, choose **Roll secret** on the endpoint and paste the new secret into the step within 24 hours: until then, deliveries carry signatures from both secrets.

Fulfil only on `payment.paid`. Events can arrive more than once and Zapier doesn't drop repeats, so when a step must happen once (a shipment, a licence key), check first in the destination for the `payment_id`, for example with a **Lookup Spreadsheet Row** step. Turning the Zap off doesn't remove the endpoint: remove it in Developers, since each mode allows 10.

## Call the API from a Zap

Use **Webhooks by Zapier → Custom Request** for every call. Its settings:

| Setting | Value |
|---|---|
| **Method** | As in each request below |
| **URL** | As below, with the ids mapped from earlier steps |
| **Data** | The JSON body, for requests that send one; empty otherwise |
| **Headers** | `Authorization`: `Bearer` followed by a space and your secret key. Add `Content-Type`: `application/json` when you send data |

The key is saved in the step, where anyone who can edit the Zap can read it. Use a **Read only** key for steps that only read, and a **Full access** key only in steps that create sessions or update orders. Zapier's **API by Zapier** app (in beta, on paid plans) keeps credentials in a connection instead of the step; if your plan has it, its **API Request** action takes the same method, URL and body. Every response is JSON, which Zapier turns into fields for later steps; an error has an `error.code` and an `error.message` ([Errors](https://railbed.com/docs/api/errors.md)).

### Create a checkout session and send the link

**POST** `https://pay.railbed.io/v1/checkout_sessions`. Add the header `Idempotency-Key` and map something unique to this order into it, such as the form entry's id: if Zapier runs the step again, you get the same session back instead of a second one.

Example: Data

```json
{
  "amount": "49.00",
  "currency": "USD",
  "description": "Pro Membership",
  "customer_email": "buyer@example.com",
  "reference": "order_1042",
  "success_url": "https://yourstore.com/thanks"
}
```

Map your own values in place of the examples: `amount` is text with two decimal places, from `1.00` to `100000.00`, and `currency` is `USD`, `EUR`, `GBP`, `CAD` or `AUD`. The response's `url` is the checkout page to send the buyer, and `id` is the payment to look up later. Every field is in [Create a checkout session](https://railbed.com/docs/api/checkout-sessions.md#create-a-checkout-session).

### Read payments

| Request | What it gives you |
|---|---|
| **GET** `https://pay.railbed.io/v1/payments/pay_…` | One [payment](https://railbed.com/docs/api/payments.md#the-payment-object), as it is now |
| **GET** `https://pay.railbed.io/v1/payments?status=paid&limit=20` | The newest paid payments, newest first, in `data` |
| **GET** `https://pay.railbed.io/v1/payments?reference=order_1042` | Payments with your reference |

Put the payment's id at the end of the first URL. The API's names are snake_case and its times are Unix seconds (`paid_at`), unlike the event's fields; see [List payments](https://railbed.com/docs/api/payments.md#list-payments) for every filter.

### Read orders and customers

| Request | What it gives you |
|---|---|
| **GET** `https://pay.railbed.io/v1/orders/ord_…` | One [order](https://railbed.com/docs/api/orders.md#the-order-object), with every item and address |
| **GET** `https://pay.railbed.io/v1/orders?limit=20` | The newest orders |
| **GET** `https://pay.railbed.io/v1/orders?store_order_id=1042` | The order your system sent with `order.id` 1042 |
| **GET** `https://pay.railbed.io/v1/customers/cus_…` | One [customer](https://railbed.com/docs/api/customers.md#the-customer-object), with order counts and what they spent per currency |
| **GET** `https://pay.railbed.io/v1/customers?email=buyer@example.com` | The customer with that email |

### Mark an order fulfilled

**PATCH** `https://pay.railbed.io/v1/orders/ord_…` with this Data. Use `unfulfilled` to undo it. The order's timeline in Railbed records the change; see [Update an order](https://railbed.com/docs/api/orders.md#update-an-order).

Example: Data

```json
{ "fulfillment": "fulfilled" }
```

### Record a cancellation or refund

**POST** `https://pay.railbed.io/v1/orders/ord_…/events`. This only records on the order what your store did; Railbed can't send money back to a buyer. `id` is your own id for the event, so a repeated step records it once. See [Report a cancellation or refund](https://railbed.com/docs/api/orders.md#report-a-cancellation-or-refund).

Example: Data

```json
{ "type": "refunded", "id": "refund_5521", "amount": "12.00" }
```

### Simulate a payment in Test mode

With a `rb_test_…` key, create a session, then **POST** `https://pay.railbed.io/v1/checkout_sessions/pay_…/start` with Data `{ "customer_email": "buyer@example.com" }`, then **POST** `https://pay.railbed.io/v1/payments/pay_…/simulate` with Data `{ "outcome": "paid" }`. Your Test endpoint receives the real sequence of events for that payment. Other outcomes are in [Simulate a payment](https://railbed.com/docs/api/payments.md#simulate-a-payment).

## Recipes

Each recipe starts with the [receiving steps](#receive-events) (Catch Raw Hook, then the Code step) unless it says otherwise. Choose only the events the recipe needs on its endpoint.

| When | Then | Events and steps |
|---|---|---|
| A payment is paid | Add a row to a sheet | `payment.paid` → **Google Sheets: Create Spreadsheet Row** with `paid_at`, `order_number`, `customer_email`, `amount`, `currency`, `net_received` and `payment_id` |
| A payment is paid | Tell the team | `payment.paid` → **Slack: Send Channel Message**, such as "Paid: order_summary, amount currency, from customer_email" |
| A payment is paid | Email the buyer a receipt | `payment.paid` → **Gmail: Send Email** to `customer_email`, with `order_items`, `order_total` and your reference |
| A payment is paid | Ship it, then mark it fulfilled | `payment.paid` → your shipping or fulfilment app → a Custom Request that [marks the order fulfilled](#mark-fulfilled) with `order_id` |
| A payment needs your review | Get an alert | `payment.held` → **Slack** or **SMS by Zapier**, with `hold_reason` and `checkout_url`. Don't fulfil: decide in [Payments](https://app.railbed.io/payments) ([held payments](https://railbed.com/docs/user-guide/held-payments.md)) |
| A buyer pays | Add them to your CRM | `payment.paid` → **HubSpot: Create or Update Contact** with `customer_email` and `customer_name`. Create or Update keeps one contact per buyer when they pay again |
| A buyer who joined your email list pays | Add them to your mailing list | `payment.paid` → **Filter by Zapier** that continues only when `customer_accepts_marketing` is true → **Mailchimp: Add/Update Subscriber** with `customer_email` and `customer_name`. Without that Filter, buyers who didn't agree to marketing email would join the list |
| A form is sent or a deal is won | Send a checkout link | Your form or CRM trigger (no receiving steps) → a Custom Request that [creates a session](#create-a-session), with the form entry's id as `Idempotency-Key` → **Gmail: Send Email** with the response's `url` |
| Every morning | Post yesterday's sales | **Schedule by Zapier: Every Day** (no receiving steps) → the Code step below → **Slack** or **Gmail** with `sales_summary`, `paid_count` and `net_received` |

### The daily summary

This Code step reads your paid payments from the API and adds up those that became paid in the period: the 24 whole hours before the current hour began, in UTC (`period_start` and `period_end` say which). Schedule the Zap for the same time every day, such as just after midnight, and each period starts where the previous one ended, so a payment is counted in one day's summary only. Give it a **Read only** secret key as `secret_key` under **Input Data**; optionally `hours` (a whole number of hours, default 24) and `lookback_days` (default 2), how far back to look for payments created earlier and paid in the period. Totals stay per currency, such as `49.30 USD, 12.50 EUR`, and are never added together; `net_received` is what reached your payout wallet, in dollars, and `net_received_pending` counts paid payments whose amount isn't reported yet. It reads up to 500 payments; `complete` is `false` when there were more.

Example: Code by Zapier: yesterday's sales

```js
// Railbed → Zapier: add up the payments that became paid in the last day.
//
// Paste this whole file into a "Code by Zapier → Run JavaScript" step that follows
// "Schedule by Zapier → Every Day". Fill in the step's Input Data:
//
//   secret_key     a Railbed secret key; a Read only key is enough (rb_live_… or rb_test_…)
//   hours          optional; the period to add up, in whole hours (default 24)
//   lookback_days  optional; how far back to look for payments created earlier and paid in the
//                  period (default 2: checkout sessions stay open 24 hours; raise it if you use
//                  payment links that stay open longer)
//
// The period is whole hours in UTC and ends where the current hour began, so when the Zap
// runs at the same time every day, each period starts where the last one ended and a payment
// falls in one period only. Totals are kept per currency and never converted or added across
// currencies. Guide: https://railbed.com/docs/guides/zapier/

const API = 'https://pay.railbed.io/v1';
const MAX_PAGES = 5; // 500 payments; Code steps have a short time limit

const key = String(inputData.secret_key || '').trim();
const hours = inputData.hours ? Number(inputData.hours) : 24;
const lookbackDays = inputData.lookback_days ? Number(inputData.lookback_days) : 2;
if (!/^rb_(live|test)_[A-Za-z0-9_-]+$/.test(key)) {
  throw new Error('Add a Railbed secret key (it starts with rb_live_ or rb_test_) to this step\'s "secret_key" input.');
}
if (!Number.isInteger(hours) || hours <= 0 || !(lookbackDays > 0)) {
  throw new Error('hours must be a whole number of hours, such as 24, and lookback_days a positive number.');
}

const periodEnd = Math.floor(Date.now() / 1000 / 3600) * 3600; // where the current hour began
const periodStart = periodEnd - hours * 3600;
const createdSince = Math.min(periodStart, periodEnd - Math.round(lookbackDays * 86400));

// Decimal strings → whole units of the smallest digit, so nothing is rounded on the way.
const toUnits = (value, places) => {
  const [whole, fraction = ''] = String(value).split('.');
  return BigInt(whole + fraction.padEnd(places, '0').slice(0, places));
};
const fromUnits = (units, places) => {
  const text = units.toString().padStart(places + 1, '0');
  return `${text.slice(0, -places)}.${text.slice(-places)}`;
};

const sales = {}; // currency → cents
let paidCount = 0;
let netMicros = 0n; // dollars received after fees, six decimal places
let netMissing = 0;
let cursor = null;
let pages = 0;
let more = true;
while (more && pages < MAX_PAGES) {
  const params = new URLSearchParams({ status: 'paid', limit: '100', created_gte: String(createdSince) });
  if (cursor) params.set('starting_after', cursor);
  const response = await fetch(`${API}/payments?${params}`, { headers: { Authorization: `Bearer ${key}` } });
  const body = await response.json().catch(() => null);
  if (!response.ok) {
    const reason = body && body.error ? `${body.error.message} (${body.error.code})` : `HTTP ${response.status}`;
    throw new Error(`Railbed didn't return the payments: ${reason}`);
  }
  for (const payment of body.data) {
    // Paid in the period: from its start, up to but not including its end.
    if (!payment.paid_at || payment.paid_at < periodStart || payment.paid_at >= periodEnd) continue;
    paidCount += 1;
    sales[payment.currency] = (sales[payment.currency] || 0n) + toUnits(payment.amount, 2);
    const net = payment.settlement && payment.settlement.merchant_received;
    if (net) netMicros += toUnits(net, 6);
    else netMissing += 1;
  }
  pages += 1;
  more = body.has_more;
  cursor = body.next_cursor;
}

const currencies = Object.keys(sales).sort();
const summary = {
  period_start: new Date(periodStart * 1000).toISOString(),
  period_end: new Date(periodEnd * 1000).toISOString(),
  paid_count: paidCount,
  sales_summary: currencies.length ? currencies.map((c) => `${fromUnits(sales[c], 2)} ${c}`).join(', ') : 'No paid payments',
  net_received: fromUnits(netMicros, 6),
  net_received_pending: netMissing,
  complete: !more,
};
for (const c of currencies) summary[`sales_${c.toLowerCase()}`] = fromUnits(sales[c], 2);
return summary;
```

## Troubleshooting

| Symptom | Likely cause |
|---|---|
| The Code step says the signature doesn't match | The secret is from another endpoint or mode, or `raw_body` isn't mapped from **Raw Body**. Each endpoint has its own secret, and Test and Live differ |
| The Code step says there's no valid signature | The trigger is **Catch Hook** rather than **Catch Raw Hook**, or `signature` isn't mapped |
| The delivery was signed too long ago | Zapier ran the step more than a day (or `max_age_seconds`) after the event, usually because it held the run, at your plan's task limit or when many arrived at once, and it was replayed later. Check the payment in Railbed, or resend the delivery from Developers |
| Later steps ran with empty fields | The run was a `ping` or another event type. Add the Filter on **Event Type** after the Code step |
| A Custom Request returns `401` | The key was revoked, or the `Authorization` header doesn't read `Bearer` and the key |
| A Custom Request returns `403 insufficient_scope` | The key is Read only and the request changes something: use a Full access key in that step |
| A lookup returns `404` for a Test event | The event came from **Send test event**; its payment isn't real (`is_sample` is `true`) |
| Adding the endpoint fails | Each mode allows 10 endpoints: remove ones you no longer use in Developers |

- [Event types](https://railbed.com/docs/webhooks/events.md): Every event, what it means and the full payload
- [Fulfil orders safely](https://railbed.com/docs/guides/fulfilment.md): Deliver each order once, only for money that arrived
- [Webhook endpoints](https://railbed.com/docs/api/webhook-endpoints.md): Add and remove endpoints from code
