> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usetendr.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment links API

> Create, list, read, update and archive payment links, and the two public endpoints the pay page uses.

See [payment links](https://docs.usetendr.tech/payment-links/overview.md) for what a link is and how it behaves.

## Merchant endpoints

| Endpoint | Purpose |
| - | - |
| `POST /v1/payment_links` | Create a link. Returns `201`. |
| `GET /v1/payment_links` | List links, newest first. |
| `GET /v1/payment_links/:id` | Read one link. |
| `PATCH /v1/payment_links/:id` | Change a link. |
| `DELETE /v1/payment_links/:id` | Archive a link. Returns the link. |

Each call acts on the mode of the key or dashboard session that makes it.

### Who can call

* A secret key needs the `payment_links:read` scope to read and `payment_links:write` to create, change
  or archive.
* In the dashboard, reading needs `account:read` and writing needs `money:request`. No step-up is
  needed.
* Publishable keys and shopper sessions are refused.
* `POST` honours the `Idempotency-Key` header, see [idempotency](https://docs.usetendr.tech/concepts/idempotency.md).

## The link object

```json theme={null}
{
  "id": "plink_01J...",
  "object": "payment_link",
  "livemode": false,
  "title": "Logo design deposit",
  "description": "50% up front.",
  "slug": "logo-design-deposit",
  "url": "https://usetendr.tech/pay/acme/logo-design-deposit?mode=test",
  "qr_url": "https://usetendr.tech/pay/acme/logo-design-deposit/qr?mode=test",
  "amount_type": "fixed",
  "amount": "250.00",
  "currency": "USD",
  "min_amount": null,
  "note": "optional",
  "fee_payer": null,
  "gas_payer": null,
  "gas_max_usd": null,
  "status": "active",
  "archived": false,
  "availability": "available",
  "expires_at": null,
  "max_uses": 1,
  "use_count": 0,
  "total_paid": "0.00",
  "total_refunded": "0.00",
  "total_paid_usd": "0.00",
  "total_refunded_usd": "0.00",
  "created_at": "2026-10-30T09:00:00.000Z",
  "updated_at": "2026-10-30T09:00:00.000Z"
}
```

| Field | Meaning |
| - | - |
| `slug` | The link's address. Generated from the title when omitted. Unique per store and mode, immutable. |
| `url`, `qr_url` | Where to share it. Test links carry `?mode=test`. |
| `amount_type` | `fixed` or `open`. |
| `amount` | Fixed price, in the link's currency. Null for open links. |
| `currency` | `USD` or an ISO 4217 code. |
| `min_amount` | Open links only: a floor above the \$1.00 minimum. |
| `fee_payer`, `gas_payer` | `merchant`, `shopper`, or null for the store's default. |
| `status` | `active` or `inactive` (switched off). |
| `availability` | `available`, `inactive`, `archived`, `expired`, `used_up` or `merchant_not_ready`, computed on read. |
| `use_count`, `max_uses` | Settled payments, and the optional limit. |
| `total_*` | See [totals](https://docs.usetendr.tech/payment-links/overview.md#totals). Decimal strings in the currency's own decimals (USD figures have 2). |

## Create

`POST /v1/payment_links`. Unknown fields are rejected.

| Field | Required | Notes |
| - | - | - |
| `title` | yes | Up to 80 characters. |
| `amount_type` | yes | `fixed` or `open`. |
| `currency` | yes | `USD` or an ISO 4217 code. |
| `amount` | fixed only | Decimal string like `"25.10"`. Not allowed for open links. |
| `min_amount` | no | Open only. |
| `description` | no | Plain text, up to 500 characters. |
| `slug` | no | 3 to 60 characters: lower-case letters, digits and inner hyphens. Reserved words (such as `qr`, `new`, `edit`, `test`) are refused. |
| `note` | no | `optional` (default) or `off`. |
| `status` | no | `active` (default) or `inactive`. |
| `expires_at` | no | ISO 8601, in the future. |
| `max_uses` | no | Integer, at least 1. |
| `fee_payer`, `gas_payer` | no | `merchant` or `shopper`. |
| `gas_max_usd` | no | Decimal string, up to 6 decimals. |

```bash theme={null}
curl -X POST "$TENDR_API_URL/v1/payment_links" \
  -H "Authorization: Bearer $TENDR_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Logo design deposit",
    "amount_type": "fixed",
    "amount": "250.00",
    "currency": "USD",
    "max_uses": 1
  }'
```

## List

`GET /v1/payment_links?status=active&include_archived=false&limit=20&starting_after=plink_...`

Returns `{ "data": [link, ...], "has_more": boolean }`, newest first. `limit` is 1 to 100 (default 20).
Archived links are left out unless `include_archived=true`. Pass the last `id` as `starting_after` for
the next page.

## Update

`PATCH /v1/payment_links/:id` with any subset of the create fields, plus `archived` (boolean).

* `null` clears `expires_at`, `max_uses`, `description`, `min_amount`, `fee_payer`, `gas_payer` and
  `gas_max_usd`. An omitted key is unchanged.
* `slug` cannot be changed: the request is a `400`.
* Switching `amount_type` clears the fields that no longer apply.
* Changing `currency` on a fixed link needs the new `amount` in the same request (else `400`, field
  `amount`). On an open link it clears `min_amount` unless you send a new one.
* A change that touches the fixed amount or currency is re-checked against the \$1.00 minimum.

Payments already in progress keep the price they started with.

## Archive

`DELETE /v1/payment_links/:id` archives the link and returns it. Restore it with
`PATCH { "archived": false }`.

## Public endpoints

The pay page at `/pay/<store>/<link>` uses two endpoints that need no key. They are documented so you
know what the payer's page does; they are not an integration surface. Browsers can call them only from
Tendr's checkout origin (CORS), so a page on your own site cannot use them. To take a link's payment
from your own page, create a session with your secret key (`POST /v1/checkout/sessions` with
`payment_link_id`). Add `?mode=test` to address a test link.

### Read a link

`GET /v1/payment_links/public/:merchant/:link`

```json theme={null}
{
  "merchant": { "name": "Acme", "logo_url": "https://..." },
  "title": "Logo design deposit",
  "description": "50% up front.",
  "amount_type": "fixed",
  "amount": "250.00",
  "currency": "USD",
  "minor_units": 2,
  "min_amount": null,
  "usd_rate": null,
  "note": "optional",
  "fee_payer": "merchant",
  "availability": "available",
  "livemode": true
}
```

* `minor_units` is the currency's decimal places. Format and check amounts with it.
* `usd_rate` is `1 USD = X currency`, null for USD or when no rate is available.
* `min_amount` (open links) is the effective minimum, rounded up to the currency's decimals.
* An archived link reads as `inactive`. No ids, use counts or limits are returned.
* An unknown store and an unknown link give the same `404 NOT_FOUND`.

### Pay

`POST /v1/payment_links/public/:merchant/:link/sessions`

```json theme={null}
{ "amount": "25.00", "note": "Invoice 114" }
```

`amount` is required for open links and ignored for fixed ones. `note` is cleaned and capped at 280
characters, and ignored when the link has `note: "off"`. The answer is `201`:

```json theme={null}
{
  "url": "https://usetendr.tech/pay/ses_...#cs=ses_..._secret_...",
  "expires_at": "2026-10-30T12:05:00.000Z"
}
```

Send the payer to `url`. `expires_at` is the session's expiry at creation (5 minutes until its first
quote, then the normal session lifetime); the page uses it to offer "Continue your payment" to a payer
who comes back. The session has no `external_ref`. Responses are never cached.

## Limits

| Route | Limit |
| - | - |
| Public read | 120 per minute per payer IP |
| Pay | 10 per minute per payer IP, and 3 per payer IP per link per 5 minutes |
| `LINK_BUSY` | 200 open sessions with a quote on one link |

Past a rate limit the API answers `429 RATE_LIMITED`.

## Errors

| Code | Status | When |
| - | - | - |
| `NOT_FOUND` | 404 | Unknown link, or (public) unknown store. Identical for both. |
| `VALIDATION_ERROR` | 400 | A field is invalid. `details.field` names it. Includes changing `slug`, and an expired `expires_at`. |
| `AMOUNT_TOO_LOW` | 400 | Below the \$1.00 floor or the link's `min_amount`. `details.min_amount` is the minimum in the link's currency. |
| `FX_RATE_UNAVAILABLE` | 503 | No current rate for a non-USD link. Retry shortly. |
| `LINK_UNAVAILABLE` | 409 | The link cannot take a payment. `details.availability` is `inactive`, `expired`, `used_up` or `merchant_not_ready`; a session created with your key (`POST /v1/checkout/sessions` with `payment_link_id`) can also get `archived`. The public pay route reports an archived link as `inactive`. |
| `LINK_RESERVED` | 409 | Every remaining use is held by an open session. Try again shortly. |
| `LINK_NOT_SUPPORTED` | 409 | The store settles into its own contract and cannot have links. |
| `LINK_BUSY` | 429 | The link has 200 open sessions with a quote. |
| `RATE_LIMITED` | 429 | A per-IP limit above. |

## Creating a session for a link from your server

`POST /v1/checkout/sessions` accepts `payment_link_id`. The session takes the link's fee and gas
settings, counts towards its `max_uses`, and keeps the expiry and `external_ref` you give it. Every
session event then carries `payment_link_id`, see [webhooks](https://docs.usetendr.tech/api-reference/webhooks.md#payment-link-fields).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.