# Payment links

The simplest way to get paid online: create a link for an amount, share it anywhere, and BaxPay
hosts the payment page.

A payment link is a single-use charge with a hosted page. No invoice, no regulation-specific
fields, no checkout integration — one API call, one URL. The payer pays by **card** (no BaxPay
account needed) or with **Pay with BaxPay** (they sign in to their own account; SCA included).

If the money you're collecting belongs to a regulated French e-invoice, use
[invoice payment links](https://developers.baxpay.com/einvoicing-paylinks) instead — same rails, but the invoice's legal
lifecycle updates itself.

## Create a link

- `POST /payment-links`

Requires an `Idempotency-Key` header.

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/payment-links \
  -H "Authorization: Bearer bx_test_9f2a4c8e1d…" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Coaching session — July",
    "amount": 120.50,
    "reference": "COACH-2026-118",
    "businessWide": true
  }'
```

```js
const res = await fetch('https://sandbox-api.baxpay.com/v1/payment-links', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BAXPAY_API_KEY}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    description: 'Coaching session — July',
    amount: 120.5,
    reference: 'COACH-2026-118',
    businessWide: true,
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(JSON.stringify(body));
console.log(JSON.stringify(body, null, 2)); // share body.data.url
```

```json
{
  "success": true,
  "data": {
    "id": "1c9f4a2e-…",
    "token": "plk_4f1f0c4c4e1a…",
    "url": "https://pay.baxpay.com/plk_4f1f0c4c4e1a…",
    "description": "Coaching session — July",
    "reference": "COACH-2026-118",
    "amount": 120.5,
    "feeAmount": 3.62,
    "chargeAmount": 120.5,
    "netAmount": 116.88,
    "currency": "EUR",
    "feePayer": "RECEIVER",
    "mode": "ONE_TIME",
    "status": "OPEN",
    "storeId": null,
    "expiresAt": null,
    "livemode": false,
    "createdAt": "2026-07-16T14:02:10.000Z"
  }
}
```

Amounts are **decimal euros** (`120.50`, at most 2 decimals, at least `0.01`).

**Where the money is booked (one is required):**

| Field | Meaning |
| --- | --- |
| `storeId` | One of your locations; the payment is reported under it |
| `businessWide: true` | The business as a whole, no location |

**Optional fields:**

| Field | Meaning |
| --- | --- |
| `currency` | `EUR` (default), `GBP`, `CHF` or `USD` |
| `reference` | Your own id, up to 100 characters, echoed in the webhook |
| `clientId` | A [client](https://developers.baxpay.com/clients) from your book. Optional for one-time links; **required** for recurring ones, and the client needs an email |
| `expiresAt` | ISO time after which the link stops accepting payment |
| `feePayer` | `RECEIVER` (default): you absorb BaxPay's fee. `SENDER`: the payer pays `amount + fee`. See `chargeAmount` / `netAmount` in the response |
| `mode`, `interval` | `ONE_TIME` (default) or `RECURRING` with `interval` `WEEKLY`, `MONTHLY` or `YEARLY` |
| `items` | Catalog lines (`productId`, `quantity`, optional `variantId`, `modifierIds`), so stock and reporting follow the sale |
| `payerBusinessProfileId` | Ask another BaxPay business to pay: the request appears in its app |

Recurring links set up the subscription when the payer opens checkout. After the first payment the
link status becomes `ACTIVE` (subscription running), and each later cycle is charged and recorded
automatically.

When a link is tied to a client with an email, BaxPay emails the link on create. Unpaid links also
get reminder emails on days 1, 3 and 7.

## Manage links

- `GET /payment-links?status=OPEN&page=1&limit=25` — list (see pagination)
- `GET /payment-links/{id}` — retrieve
- `POST /payment-links/{id}/cancel` — cancel an open link

```bash
curl "https://sandbox-api.baxpay.com/v1/payment-links?status=OPEN&limit=25" \
  -H "Authorization: Bearer $BAXPAY_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "data": [{ "id": "1c9f4a2e-…", "status": "OPEN", "amount": 120.5, "url": "https://pay.baxpay.com/plk_…" }],
    "total": 1,
    "page": 1,
    "limit": 25
  }
}
```

Filter with `status` and `storeId`; lists follow the [pagination rules](https://developers.baxpay.com/conventions#lists-and-pagination).

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/payment-links/1c9f4a2e-…/cancel \
  -H "Authorization: Bearer $BAXPAY_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

```json
{ "success": true, "data": { "id": "1c9f4a2e-…", "status": "CANCELED" } }
```

Statuses: `OPEN` → `PAID` (one-time) or `ACTIVE` (recurring), or `CANCELED` / `EXPIRED`.
Canceling an `ACTIVE` recurring link also cancels the subscription. A paid one-time link can't be
canceled, and a canceled or expired link can't be paid.

Resending the link email is a dashboard action (**Payment links → Resend**); it isn't available with
an API key.

## What the payer sees

`url` opens a hosted page showing your business name, the description, and the amount — nothing
else. The page wears **your brand**: set your logo and colors once in the Business dashboard
(**Payment links → Personnalisation**; the logo comes from your brand settings) and every hosted
page — payment links and invoice pay pages alike — uses them. Two methods:

- **Card** — guest payment, processed by the configured acquirer. In live mode without an
  acquirer contracted the method is hidden and the endpoint answers `503`; BaxPay never fakes a
  charge.
- **Pay with BaxPay** — the payer signs in to their own BaxPay account and pays with their
  wallet, card or bank account, Strong Customer Authentication included. BaxPay verifies the
  captured transaction matches this exact link (merchant, amount) before anything flips to paid.

## What happens on payment

1. The link flips to `PAID` (one-time) or `ACTIVE` (recurring) atomically — a double payment
   race is structurally impossible.
2. A `Transaction` is recorded for reconciliation (`PLK-…` reference; `PLK-TEST-…` in sandbox).
3. Your webhooks receive `payment_link.paid`:

```json
{
  "id": "f017738f-…",
  "type": "payment_link.paid",
  "livemode": true,
  "data": {
    "paymentLinkId": "1c9f4a2e-…",
    "description": "Coaching session — July",
    "reference": "COACH-2026-118",
    "clientId": null,
    "amount": 120.5,
    "currency": "EUR",
    "mode": "ONE_TIME",
    "interval": null,
    "method": "CARD_NOT_PRESENT",
    "transactionRef": "be294a82-…"
  },
  "createdAt": "2026-07-16T14:03:21.530Z"
}
```

`transactionRef` is the id of the recorded transaction. `method` says how the payer paid:
`CARD_NOT_PRESENT` (guest card), `BAXPAY_ACCOUNT` (their BaxPay account), or a wallet such as
`APPLE_PAY`, `GOOGLE_PAY` or `REVOLUT_PAY`.

## Sandbox

With a `bx_test_` key everything works end to end, and nothing reaches a card network or a bank. Pay with
a [test card](https://developers.baxpay.com/sandbox#test-cards): `4242 4242 4242 4242` succeeds, **`4000 0000 0000 0002` always
declines**, and others play insufficient funds, fraud or 3D Secure. The test pay page can fill one
in for you, and offers the business's other active methods (iDEAL, Apple Pay, …) as
[simulated payments](https://developers.baxpay.com/sandbox#test-payment-methods). Test payments only fire test-mode webhooks.
The [quickstart](https://developers.baxpay.com/quickstart) walks this exact flow.

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/pay/link/plk_4f1f0c4c4e1a…/card \
  -H "Content-Type: application/json" \
  -d '{ "number": "4000 0000 0000 0002", "expMonth": 12, "expYear": 2030, "cvc": "123", "holder": "MARIE DUPONT" }'
```

```json
{ "statusCode": 400, "message": "Your card was declined.", "error": "Bad Request", "code": "card_declined" }
```
