# Payment links

> Create, list, read and cancel one-off payment links from your own code. The same links you make in the dashboard, with the same rules, each one payment for one payer.

Source: https://railbed.com/docs/api/payment-links/ · Updated: 2026-10-14 · 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

## The payment link object

A payment link asks one payer for one amount: an invoice, a deposit, a one-off order. It's the same link you can make under [Payment links](https://app.railbed.io/payment-links) in the dashboard, and it shows up there. A link is one payment, so its id is the payment's id: read its settlement with [Retrieve a payment](https://railbed.com/docs/api/payments.md#retrieve-a-payment). For a price many buyers can pay, use a [checkout](https://railbed.com/docs/api/checkouts.md) instead.

| Field | Type | Description |
|---|---|---|
| `id` | string | The link's id, `pay_…`. The same id identifies its [payment](https://railbed.com/docs/api/payments.md) |
| `object` | string | `"payment_link"` |
| `url` | string | The payer's page. Send this to them |
| `tracking_url` | string or null | A page that shows only the link's status, without the payer's details. Safe to share with anyone who should follow it |
| `status` | string | `open`, `pending`, `paid`, `held`, `failed` or `expired`. See [statuses](https://railbed.com/docs/how-it-works.md#statuses) |
| `livemode` | boolean | `false` for Test mode, `true` for Live mode |
| `amount` | string | The amount asked for: `"250.00"` |
| `currency` | string | `USD`, `EUR`, `GBP`, `CAD` or `AUD` |
| `description` | string | What the payment is for, shown to the payer |
| `reference` | string or null | Your own id, such as an invoice number |
| `customer_name` | string or null | Who the link is for |
| `customer_email` | string or null | Their email, filled in on their page, lowercased |
| `memo` | string or null | Your note to the payer |
| `success_url` | string or null | Where the payer lands once paid |
| `collect` | object or null | What the payer's page asks for besides their email, or `null` for nothing more. See [The collect field](https://railbed.com/docs/api/checkout-sessions.md#the-collect-field) |
| `order_id` | string or null | The [order](https://railbed.com/docs/api/orders.md) the link belongs to, `ord_…` |
| `created` | integer | When it was created, in Unix seconds |
| `expires_at` | integer | When it stops accepting new payments |
| `started_at` | integer or null | When the payer started paying |
| `paid_at` | integer or null | When it was paid |
| `canceled_at` | integer or null | When you canceled it, in Unix seconds |

Example: A payment link · response 200 OK

```json
{
  "id": "pay_Lk4mQ8vR2xTn7WcZs1Pb",
  "object": "payment_link",
  "url": "https://pay.railbed.io/p/pay_Lk4mQ8vR2xTn7WcZs1Pb",
  "tracking_url": "https://pay.railbed.io/t/Tq3nV8xK2mRz7LpW4cYb5sDh",
  "status": "open",
  "livemode": false,
  "amount": "250.00",
  "currency": "USD",
  "description": "Consulting, September",
  "reference": "INV-0042",
  "customer_name": "Ada Lovelace",
  "customer_email": "ada@example.com",
  "memo": "Thanks for your business.",
  "success_url": null,
  "collect": null,
  "order_id": "ord_Hn6pW2zQ8rKt3VmXc9La",
  "created": 1790380525,
  "expires_at": 1791590125,
  "started_at": null,
  "paid_at": null,
  "canceled_at": null
}
```

## Create a payment link

`POST /v1/payment_links`

Creates a link in the key's mode. Nothing is charged and no provider is contacted until the payer starts paying, so a long expiry costs nothing.

| Parameter | Type | Description |
|---|---|---|
| `amount` | string · required | A decimal string from `"1.00"` to `"100000.00"`, at most two decimals |
| `currency` | string · required | `USD`, `EUR`, `GBP`, `CAD` or `AUD`, in any letter case |
| `description` | string · required | What it's for, up to 120 characters |
| `customer_name` | string | Who it's for, up to 80 characters |
| `customer_email` | string | Their email, filled in on their page |
| `customer_phone` | string | Their phone number, up to 40 characters, kept on the order |
| `shipping_address`, `billing_address` | object | Their addresses, if you have them: `line1`, `line2`, `city`, `region`, `postal_code` and `country` (two letters). Kept on the order and filled in for the payer, under `customer_name`. `region` is a code for the US, Canada and Australia (`NY`) |
| `reference` | string | Your own id, up to 64 characters |
| `memo` | string | A note to the payer, up to 500 characters |
| `expires_in_days` | integer | 1–30. Default 7 |
| `success_url` | string | Where the payer lands once paid. Live mode needs `https://` |
| `collect` | object | What the payer's page asks for besides their email, as on a [checkout session](https://railbed.com/docs/api/checkout-sessions.md#the-collect-field) |

Example: cURL

```bash
curl https://pay.railbed.io/v1/payment_links \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "250.00",
    "currency": "USD",
    "description": "Consulting, September",
    "customer_name": "Ada Lovelace",
    "customer_email": "ada@example.com",
    "reference": "INV-0042",
    "memo": "Thanks for your business.",
    "expires_in_days": 14
  }'
```

Example: Node\.js

```js
const link = await fetch('https://pay.railbed.io/v1/payment_links', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: '250.00',
    currency: 'USD',
    description: 'Consulting, September',
    customer_name: 'Ada Lovelace',
    customer_email: 'ada@example.com',
    reference: 'INV-0042',
    expires_in_days: 14,
  }),
}).then((r) => r.json());
// Send link.url to the payer.
```

Example: Python

```python
link = requests.post(
    "https://pay.railbed.io/v1/payment_links",
    headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"},
    json={
        "amount": "250.00",
        "currency": "USD",
        "description": "Consulting, September",
        "customer_name": "Ada Lovelace",
        "customer_email": "ada@example.com",
        "reference": "INV-0042",
        "expires_in_days": 14,
    },
    timeout=15,
).json()
# Send link["url"] to the payer.
```

Example: Response · response 201 Created

```json
{
  "id": "pay_Lk4mQ8vR2xTn7WcZs1Pb",
  "object": "payment_link",
  "url": "https://pay.railbed.io/p/pay_Lk4mQ8vR2xTn7WcZs1Pb",
  "tracking_url": "https://pay.railbed.io/t/Tq3nV8xK2mRz7LpW4cYb5sDh",
  "status": "open",
  "livemode": false,
  "amount": "250.00",
  "currency": "USD",
  "description": "Consulting, September",
  "reference": "INV-0042",
  "customer_name": "Ada Lovelace",
  "customer_email": "ada@example.com",
  "memo": "Thanks for your business.",
  "success_url": null,
  "collect": null,
  "order_id": "ord_Hn6pW2zQ8rKt3VmXc9La",
  "created": 1790380525,
  "expires_at": 1791590125,
  "started_at": null,
  "paid_at": null,
  "canceled_at": null
}
```

The link follows the dashboard's rules, and an error's `field` names the parameter you sent, such as `expires_in_days` or `shipping_address.postal_code`. This endpoint ignores `Idempotency-Key`: after a timeout, [list your links](#list-payment-links) by `reference` before creating another.

| Error | Cause |
|---|---|
| `400 invalid_amount` | `amount` isn't a decimal string from `"1.00"` to `"100000.00"` |
| `400 invalid_currency` | `currency` isn't one of the five |
| `400 missing_description`, `400 too_long` | `description` is empty, or a text field is over its limit |
| `400 invalid_customer_name`, `400 invalid_customer_phone`, `400 invalid_reference`, `400 invalid_memo` | That field isn't a string |
| `400 invalid_email` | `customer_email` isn't an email address (send `null`, or leave it out, for none) |
| `400 invalid_address` | An address is missing a line, city, region or postal code its country needs. `field` names it |
| `400 invalid_expiry` | `expires_in_days` isn't a whole number from 1 to 30 (a JSON number, not a string) |
| `400 invalid_url` | `success_url` isn't a full address, has a username or password, or isn't `https://` in Live mode |
| `400 invalid_collect` | `collect` asks for something it can't. `field` names the part |
| `409 no_payout_wallet` | Live mode, and the account has no payout wallet yet |
| `409 commerce_paused` | The account has paused taking payments |
| `403 integration_disabled` | Payment links, or the API, are turned off for the account |
| `403 merchant_read_only` | The account is suspended or archived |

## Retrieve a payment link

`GET /v1/payment_links/:id`

Returns one of your links in the key's mode. Another account's, one in the other mode, or a payment that isn't a link is `404 not_found`. A link past its `expires_at` reads as `expired`.

Example: cURL

```bash
curl https://pay.railbed.io/v1/payment_links/pay_Lk4mQ8vR2xTn7WcZs1Pb \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY"
```

## List payment links

`GET /v1/payment_links`

Returns your links in the key's mode, newest first. The parameters are those of [List payments](https://railbed.com/docs/api/payments.md#list-payments).

| Parameter | Type | Description |
|---|---|---|
| `limit` | integer | 1–100. Default 20 |
| `starting_after` | string | A link id from the previous page's `next_cursor` |
| `status` | string | `open`, `pending`, `paid`, `held`, `failed` or `expired` (a canceled link is `expired`) |
| `reference` | string | Only links with exactly this reference |
| `created_gte`, `created_lte` | integer | Only links created in this range, in Unix seconds, inclusive |

Example: cURL

```bash
curl "https://pay.railbed.io/v1/payment_links?status=open" \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY"
```

Example: Response · response 200 OK

```json
{
  "data": [
    {
      "id": "pay_Lk4mQ8vR2xTn7WcZs1Pb",
      "object": "payment_link",
      "url": "https://pay.railbed.io/p/pay_Lk4mQ8vR2xTn7WcZs1Pb",
      "tracking_url": "https://pay.railbed.io/t/Tq3nV8xK2mRz7LpW4cYb5sDh",
      "status": "open",
      "livemode": false,
      "amount": "250.00",
      "currency": "USD",
      "description": "Consulting, September",
      "reference": "INV-0042",
      "customer_name": "Ada Lovelace",
      "customer_email": "ada@example.com",
      "memo": "Thanks for your business.",
      "success_url": null,
      "collect": null,
      "order_id": "ord_Hn6pW2zQ8rKt3VmXc9La",
      "created": 1790380525,
      "expires_at": 1791590125,
      "started_at": null,
      "paid_at": null,
      "canceled_at": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## Cancel a payment link

`POST /v1/payment_links/:id/cancel`

Closes a link nobody has started paying, as **Cancel link** does in the dashboard. No body is needed.

Example: cURL

```bash
curl -X POST https://pay.railbed.io/v1/payment_links/pay_Lk4mQ8vR2xTn7WcZs1Pb/cancel \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY"
```

Example: Response · response 200 OK

```json
{
  "id": "pay_Lk4mQ8vR2xTn7WcZs1Pb",
  "object": "payment_link",
  "url": "https://pay.railbed.io/p/pay_Lk4mQ8vR2xTn7WcZs1Pb",
  "tracking_url": "https://pay.railbed.io/t/Tq3nV8xK2mRz7LpW4cYb5sDh",
  "status": "expired",
  "livemode": false,
  "amount": "250.00",
  "currency": "USD",
  "description": "Consulting, September",
  "reference": "INV-0042",
  "customer_name": "Ada Lovelace",
  "customer_email": "ada@example.com",
  "memo": "Thanks for your business.",
  "success_url": null,
  "collect": null,
  "order_id": "ord_Hn6pW2zQ8rKt3VmXc9La",
  "created": 1790380525,
  "expires_at": 1791590125,
  "started_at": null,
  "paid_at": null,
  "canceled_at": 1790384125
}
```

It follows the rules of [Cancel a checkout session](https://railbed.com/docs/api/checkout-sessions.md#cancel-a-checkout-session): the link becomes `expired` with `canceled_at` set and sends `payment.canceled`; canceling again returns the same link; once the payer has gone to a card provider it answers `409 not_cancelable`, because their payment may be on its way. Money that arrives after a cancel still settles the link.

## Access

Retrieving and listing need the `payments:read` [scope](https://railbed.com/docs/api.md#scopes), which **Read only** keys have. Creating and canceling need `checkouts:write`, which **Full access** keys and agents with full access have.
