---
title: "Payment links"
description: "Create a link that sells your Prices, share it, switch it off and read its payments."
url: https://docs.fluveo.com/payments/payment-links/
---

> Index of every page: https://docs.fluveo.com/llms.txt. Integrating Fluveo? Read https://docs.fluveo.com/agents/integrate.md first.

> Too long? This page with code in one language only: https://docs.fluveo.com/payments/payment-links.curl.md (cURL), https://docs.fluveo.com/payments/payment-links.node.md (Node.js), https://docs.fluveo.com/payments/payment-links.python.md (Python), https://docs.fluveo.com/payments/payment-links.php.md (PHP).

# Payment links

Create a link that sells your Prices, share it, switch it off and read its payments.

A payment link is a page address that sells a fixed set of your Prices, again and again, to anyone who opens it. You make it once with `POST /v1/payment_links`, then post the address in a message, a social profile, an email or a QR code. You need no website and no cart: each buyer who opens the link goes on to a hosted Checkout Session of their own and pays there.

Pick a link when many buyers pay for the same thing. When each order has its own cart, create a [Checkout Session](https://docs.fluveo.com/payments/checkout.md) per order instead. To make and share links without code, use the [Dashboard](https://docs.fluveo.com/dashboard/payment-links.md).

## Sell a Price through a link

### Step 1: Create the Price the link sells

A link sells Prices from your own [catalog](https://docs.fluveo.com/catalog/prices.md), never a bare amount. One call to `POST /v1/prices` with `product_data[name]` writes a new [Product](https://docs.fluveo.com/catalog/products.md) and its Price together. Amounts are whole numbers in the currency's smallest unit: `unit_amount=2500` with `currency=usd` is $25.00. The link sells in your store's currency, so give the Price that currency.

**Create a Price with its Product**

cURL:

```bash
curl https://api.fluveo.com/v1/prices \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: tee-price-1" \
  -d currency=usd \
  -d unit_amount=2500 \
  --data-urlencode "product_data[name]=Pro T-shirt"
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/prices", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "tee-price-1",
  },
  body: new URLSearchParams({
    currency: "usd",
    unit_amount: "2500",
    "product_data[name]": "Pro T-shirt",
  }),
});
const price = await response.json();
console.log(price.id);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/prices",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "tee-price-1",
    },
    data={
        "currency": "usd",
        "unit_amount": 2500,
        "product_data[name]": "Pro T-shirt",
    },
)
price = response.json()
print(price["id"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/prices");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: tee-price-1",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "currency" => "usd",
        "unit_amount" => 2500,
        "product_data" => [
            "name" => "Pro T-shirt",
        ],
    ]),
]);
$price = json_decode(curl_exec($ch), true);
echo $price["id"];
```

The answer is the Price. Keep its `id` for the link.

*Response (abridged)*

```json
{
  "id": "price_...",
  "object": "price",
  "active": true,
  "currency": "usd",
  "product": "prod_...",
  "type": "one_time",
  "unit_amount": 2500
}
```

### Step 2: Create the link

Name the Price in `line_items[0][price]` and how many units one payment buys in `line_items[0][quantity]`. Here the buyer lands on your thank-you page after paying, and the link keeps a message for the day you switch it off. Send an `Idempotency-Key`: a retry with the same key and body returns the same link, never a second one.

**Create a payment link**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_links \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: tee-link-1" \
  -d "line_items[0][price]=price_..." \
  -d "line_items[0][quantity]=1" \
  -d "after_completion[type]=redirect" \
  --data-urlencode "after_completion[redirect][url]=https://shop.example.com/thanks" \
  --data-urlencode "inactive_message=This sale has ended." \
  -d "metadata[campaign]=spring"
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_links", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "tee-link-1",
  },
  body: new URLSearchParams({
    "line_items[0][price]": "price_...",
    "line_items[0][quantity]": "1",
    "after_completion[type]": "redirect",
    "after_completion[redirect][url]": "https://shop.example.com/thanks",
    inactive_message: "This sale has ended.",
    "metadata[campaign]": "spring",
  }),
});
const link = await response.json();
console.log(link.url);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_links",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "tee-link-1",
    },
    data={
        "line_items[0][price]": "price_...",
        "line_items[0][quantity]": 1,
        "after_completion[type]": "redirect",
        "after_completion[redirect][url]": "https://shop.example.com/thanks",
        "inactive_message": "This sale has ended.",
        "metadata[campaign]": "spring",
    },
)
link = response.json()
print(link["url"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_links");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: tee-link-1",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "line_items" => [
            [
                "price" => "price_...",
                "quantity" => 1,
            ],
        ],
        "after_completion" => [
            "type" => "redirect",
            "redirect" => [
                "url" => "https://shop.example.com/thanks",
            ],
        ],
        "inactive_message" => "This sale has ended.",
        "metadata" => [
            "campaign" => "spring",
        ],
    ]),
]);
$link = json_decode(curl_exec($ch), true);
echo $link["url"];
```

The answer is a `payment_link` whose `id` starts with `plink_`. It is `active`, and `url` is the address to share.

*Response (abridged)*

```json
{
  "id": "plink_...",
  "object": "payment_link",
  "active": true,
  "currency": "usd",
  "url": "https://.../p/plink_...",
  "after_completion": {
    "type": "redirect",
    "redirect": { "url": "https://shop.example.com/thanks" }
  },
  "inactive_message": "This sale has ended.",
  "metadata": { "campaign": "spring" },
  "livemode": false
}
```

### Step 3: Share the url

Put `url` wherever your buyers are. Opening it shows the line items and their total. When the buyer goes on, Fluveo opens a new Checkout Session for that visit and sends the buyer to its page, where they choose how to pay. After payment the buyer goes where `after_completion` says.

A link holds up to 20 lines, each with an active one-time Price (`type` `one_time`) of an active Product, all in one currency. To sell something not in your catalog yet, send `line_items[0][price_data]` with `currency`, `unit_amount` and `product_data[name]` in place of `price`: Fluveo creates the Price first, then the link. A Price whose amount the buyer picks (`custom_unit_amount`) must be the link's only line, with `quantity` 1.

**Body parameters**

- `line_items` (array, required): What the link sells, at most 20 lines, all in your store's currency. Each line takes `quantity` and one of `price` or `price_data`.
  - `price` (string, optional): The `price_...` id of one of your active one-time Prices.
  - `price_data` (object, optional): A new Price to create for this line: `currency`, `unit_amount` in the smallest currency unit, and `product_data[name]`.
  - `quantity` (integer, required): Units of this line in one payment.
  - `adjustable_quantity` (object, optional): Lets the buyer pick this line's quantity on the link's page: `enabled`, with an optional `minimum` (from 0) and `maximum` (from 1 to 999999, 99 when unset). The line's `quantity` must sit between them.
- `after_completion` (object, optional): Where the buyer goes after paying: `type=hosted_confirmation`, with an optional `hosted_confirmation[custom_message]`, or `type=redirect` with `redirect[url]`.
- `inactive_message` (string, optional): The text the link's page shows once the link is inactive, up to 500 characters.
- `metadata` (object, optional): Up to 50 key-value pairs of your own, sent as `metadata[key]=value`. Keys starting with `fluveo_` are reserved.
- `payment_method_types` (array, optional): The methods the page offers: any of `card`, `pix` and `boleto` your store takes (see [Payment methods](https://docs.fluveo.com/payments.md#payment-methods)). Without it, the page offers the default methods of a Checkout Session.
- `restrictions` (object, optional): `restrictions[completed_sessions][limit]`: how many payments the link takes before it turns inactive by itself. A buyer whose checkout opened before the limit was reached cannot pay it afterwards: the page answers "This link can no longer accept payments. Contact the seller for more information."
- `submit_type` (string, optional): The word on the pay button: `auto`, `book`, `donate` or `pay`.
- `billing_address_collection` (string, optional): `auto` (the default) or `required`, to always ask the buyer for a full address.
- `shipping_address_collection` (object, optional): `shipping_address_collection[allowed_countries][]`: the two-letter countries the page takes a shipping address in.
- `phone_number_collection` (object, optional): `phone_number_collection[enabled]=true` asks the buyer for a phone number.
- `name_collection` (object, optional): `name_collection[individual]` and `name_collection[business]`, each with `enabled` and `optional`: whether the page asks for the buyer's name or the business name.
- `automatic_tax` (object, optional): `automatic_tax[enabled]`: only `false` is taken.
- `managed_payments` (object, optional): `managed_payments[enabled]`: only `false` is taken, and only on create.

The [Payment Links reference](https://docs.fluveo.com/api/payment_links.md#post-v1-payment-links) has every field of the object.

## Read a link back

Retrieve one link by its id with `GET /v1/payment_links/{payment_link}`.

**Retrieve a payment link**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_links/plink_... \
  -H "Authorization: Bearer sk_test_..."
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_links/plink_...", {
  headers: { Authorization: "Bearer sk_test_..." },
});
const link = await response.json();
console.log(link.active);
```

Python:

```python
import requests

response = requests.get(
    "https://api.fluveo.com/v1/payment_links/plink_...",
    headers={"Authorization": "Bearer sk_test_..."},
)
link = response.json()
print(link["active"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_links/plink_...");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$link = json_decode(curl_exec($ch), true);
echo $link["active"] ? "true" : "false";
```

Its line items are a list of their own. Each item has an `id` that starts with `li_`, the `price`, the `quantity`, the item's `amount_total` in the smallest currency unit, and a `description` taken from the Product's name.

**List a link's line items**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_links/plink_.../line_items \
  -H "Authorization: Bearer sk_test_..."
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_links/plink_.../line_items", {
  headers: { Authorization: "Bearer sk_test_..." },
});
const items = await response.json();
console.log(items.data[0].id);
```

Python:

```python
import requests

response = requests.get(
    "https://api.fluveo.com/v1/payment_links/plink_.../line_items",
    headers={"Authorization": "Bearer sk_test_..."},
)
items = response.json()
print(items["data"][0]["id"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_links/plink_.../line_items");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$items = json_decode(curl_exec($ch), true);
echo $items["data"][0]["id"];
```

*Response (abridged)*

```json
{
  "object": "list",
  "data": [
    {
      "id": "li_...",
      "object": "item",
      "description": "Pro T-shirt",
      "quantity": 1,
      "amount_total": 2500,
      "currency": "usd",
      "price": { "id": "price_...", "object": "price", "unit_amount": 2500 }
    }
  ],
  "has_more": false
}
```

`GET /v1/payment_links` lists your links, newest first. Filter it with `active`, and page it with `limit` (1 to 100, default 10), `starting_after` and `ending_before` (see [Pagination](https://docs.fluveo.com/start/pagination.md)).

**List the active payment links**

cURL:

```bash
curl -G https://api.fluveo.com/v1/payment_links \
  -H "Authorization: Bearer sk_test_..." \
  -d active=true \
  -d limit=3
```

Node.js:

```javascript
const query = new URLSearchParams({
  active: "true",
  limit: "3",
});
const response = await fetch("https://api.fluveo.com/v1/payment_links?" + query, {
  headers: { Authorization: "Bearer sk_test_..." },
});
const links = await response.json();
console.log(links.data.length);
```

Python:

```python
import requests

response = requests.get(
    "https://api.fluveo.com/v1/payment_links",
    headers={"Authorization": "Bearer sk_test_..."},
    params={
        "active": "true",
        "limit": 3,
    },
)
links = response.json()
print(len(links["data"]))
```

PHP:

```php
<?php
$query = http_build_query([
    "active" => "true",
    "limit" => 3,
]);
$ch = curl_init("https://api.fluveo.com/v1/payment_links?" . $query);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$links = json_decode(curl_exec($ch), true);
echo count($links["data"]);
```

## Change a link

`POST /v1/payment_links/{payment_link}` changes only what you send. It takes the create's fields except `managed_payments`, plus `active`. Lines are neither added nor removed: you edit an existing line by its `li_` id, with a new `quantity` or `adjustable_quantity`. To sell a different set of Prices, create a new link.

**Change a line's quantity**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_links/plink_... \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: tee-link-1-qty-2" \
  -d "line_items[0][id]=li_..." \
  -d "line_items[0][quantity]=2"
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_links/plink_...", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "tee-link-1-qty-2",
  },
  body: new URLSearchParams({
    "line_items[0][id]": "li_...",
    "line_items[0][quantity]": "2",
  }),
});
const link = await response.json();
console.log(link.id);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_links/plink_...",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "tee-link-1-qty-2",
    },
    data={
        "line_items[0][id]": "li_...",
        "line_items[0][quantity]": 2,
    },
)
link = response.json()
print(link["id"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_links/plink_...");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: tee-link-1-qty-2",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "line_items" => [
            [
                "id" => "li_...",
                "quantity" => 2,
            ],
        ],
    ]),
]);
$link = json_decode(curl_exec($ch), true);
echo $link["id"];
```

The answer is the whole link. An empty value removes an optional field: `inactive_message=` clears the message, and `metadata[campaign]=` removes one key.

## Switch a link off

Send `active=false`. The link keeps its id, its lines and its history, and `active=true` switches it back on.

**Switch the link off**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_links/plink_... \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: tee-link-1-off" \
  -d active=false
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_links/plink_...", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "tee-link-1-off",
  },
  body: new URLSearchParams({
    active: "false",
  }),
});
const link = await response.json();
console.log(link.active);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_links/plink_...",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "tee-link-1-off",
    },
    data={
        "active": "false",
    },
)
link = response.json()
print(link["active"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_links/plink_...");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: tee-link-1-off",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "active" => "false",
    ]),
]);
$link = json_decode(curl_exec($ch), true);
echo $link["active"] ? "true" : "false";
```

*Response (abridged)*

```json
{
  "id": "plink_...",
  "object": "payment_link",
  "active": false,
  "inactive_message": "This sale has ended.",
  "url": "https://.../p/plink_..."
}
```

A buyer who opens the `url` of a switched-off link gets a page titled "This payment link is no longer available", with HTTP status 404, and no way to pay. Under the title the page shows your `inactive_message`, or a line asking the buyer to contact the seller when the link has none. A link that took the number of payments in `restrictions[completed_sessions][limit]` turns inactive by itself, and its page says it reached its payment limit.

A link you switched off, or one that reached its payment limit, takes any change and stays inactive until you send `active=true`; a link at its limit switches back on only when the same update, or an earlier one, sets `restrictions[completed_sessions][limit]` above its count. An expired link takes no change but `active`.

## Payments made through a link

A link never charges anyone itself. Every buyer who goes on from its page gets a new Checkout Session, and the payment happens in that session, through its PaymentIntent, as on [Hosted checkout](https://docs.fluveo.com/payments/checkout.md). The session's `payment_link` field holds the link's id. To list the paid sessions of one link, filter the Checkout Sessions by `payment_link`:

**List the Checkout Sessions of a link**

cURL:

```bash
curl -G https://api.fluveo.com/v1/checkout/sessions \
  -H "Authorization: Bearer sk_test_..." \
  -d payment_link=plink_... \
  -d status=complete
```

Node.js:

```javascript
const query = new URLSearchParams({
  payment_link: "plink_...",
  status: "complete",
});
const response = await fetch("https://api.fluveo.com/v1/checkout/sessions?" + query, {
  headers: { Authorization: "Bearer sk_test_..." },
});
const sessions = await response.json();
console.log(sessions.data.length);
```

Python:

```python
import requests

response = requests.get(
    "https://api.fluveo.com/v1/checkout/sessions",
    headers={"Authorization": "Bearer sk_test_..."},
    params={
        "payment_link": "plink_...",
        "status": "complete",
    },
)
sessions = response.json()
print(len(sessions["data"]))
```

PHP:

```php
<?php
$query = http_build_query([
    "payment_link" => "plink_...",
    "status" => "complete",
]);
$ch = curl_init("https://api.fluveo.com/v1/checkout/sessions?" . $query);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$sessions = json_decode(curl_exec($ch), true);
echo count($sessions["data"]);
```

After a buyer paid the link at quantity 2, the list holds one session, and `amount_total` is in the smallest currency unit:

*Response (abridged)*

```json
{
  "object": "list",
  "data": [
    {
      "id": "cs_test_...",
      "object": "checkout.session",
      "payment_link": "plink_...",
      "status": "complete",
      "payment_status": "paid",
      "amount_total": 5000,
      "currency": "usd",
      "payment_intent": "pi_..."
    }
  ],
  "has_more": false
}
```

For card and Pix, fulfil the order from the `checkout.session.completed` event, never from the visit to your thank-you page. Read `data.object.payment_link` to know which link sold it. For a Boleto, wait for `checkout.session.async_payment_succeeded` and confirm `payment_status=paid`; an unpaid Boleto ends with `checkout.session.async_payment_failed`. A session that ends without issuing a Boleto emits `checkout.session.expired`.

## Link states and events

| `active` | What a buyer gets at `url` |
| --- | --- |
| `true` | The link's lines and total, and a way on to pay. |
| `false` | A 404 page with your `inactive_message`. You switched the link off, or it reached its payment limit. |

| Event | When |
| --- | --- |
| `payment_link.created` | You created a link. Sent once, even when the create is retried with its `Idempotency-Key`. |
| `payment_link.updated` | A field of the link changed, such as `active`. `previous_attributes` holds each changed field's old value. A change of a line's quantity sends none. |
| `checkout.session.completed` | A buyer paid through the link's session, or was issued a Boleto (then `payment_status` is `unpaid`). |
| `checkout.session.async_payment_succeeded` | The Boleto of a complete session was paid. |
| `checkout.session.async_payment_failed` | The Boleto of a complete session expired unpaid. |
| `checkout.session.expired` | A session opened from the link closed without payment. |

The session's PaymentIntent sends its own events, such as `payment_intent.succeeded`. See [Event types](https://docs.fluveo.com/webhooks/events.md#payment_link.created) for each body, and [Webhooks](https://docs.fluveo.com/webhooks.md) to receive them.

## Errors you may see

| Error | Why |
| --- | --- |
| 400 [`resource_missing`](https://docs.fluveo.com/errors.md#resource_missing) on `line_items[0][price]` | The Price is not one of your store's, in this mode. |
| 400 on `line_items[0][price]`, "Payment Links cannot include deleted or archived prices." ([no code](https://docs.fluveo.com/errors.md#no-code)) | The Price is archived. Use an active Price. |
| 400 on `line_items` ([no code](https://docs.fluveo.com/errors.md#no-code)) | A Price whose amount the buyer picks shares the link with other lines. |
| 400 [`parameter_unknown`](https://docs.fluveo.com/errors.md#parameter_unknown) | A parameter the link does not take. `param` names it. |
| 400 [`automatic_tax_unavailable`](https://docs.fluveo.com/errors.md#automatic_tax_unavailable) | `automatic_tax[enabled]=true`. Send `false` or leave it out. |
| 400 [`managed_payments_unavailable`](https://docs.fluveo.com/errors.md#managed_payments_unavailable) | `managed_payments[enabled]=true`. Send `false` or leave it out. |
| 400 [`payment_link_no_valid_payment_methods`](https://docs.fluveo.com/errors.md#payment_link_no_valid_payment_methods) | One payment through the link would be more than 99999999 in the currency's smallest unit. |
| 400 [`payment_link_inactive`](https://docs.fluveo.com/errors.md#payment_link_inactive) | You changed something other than `active` on a link that expired. A link you switched off, or one that reached its payment limit, takes any change; send `active=true` with it to switch the link back on in the same call. |
| 400 "You cannot reactivate a payment link if its restrictions have been met. Increase the limit for completed sessions." ([no code](https://docs.fluveo.com/errors.md#no-code)) | `active=true` on a link that reached its payment limit. Raise `restrictions[completed_sessions][limit]` above the link's count in the same call, or before it. A higher limit alone leaves the link switched off. |
| 400 `idempotency_error` ([no code](https://docs.fluveo.com/errors.md#no-code)) | You reused an `Idempotency-Key` with a different body. |
| 404 [`resource_missing`](https://docs.fluveo.com/errors.md#resource_missing) on `payment_link` | The `plink_` id is not one of your store's links in this mode. |

See [Errors](https://docs.fluveo.com/errors.md) for the shape of every error.

## Test it

1. With a `sk_test_` key, create the Price and the link as above.
2. Open the link's `url` in a browser. The page lists the Pro T-shirt at $25.00 (`2500` in cents).
3. Go on to pay and use the test card `4242 4242 4242 4242`, any future expiry date and any CVC. The browser lands on `https://shop.example.com/thanks`.
4. List the Checkout Sessions with `payment_link=plink_...`: one is `complete` and `paid`, and your endpoint got `checkout.session.completed` with the same `payment_link`.
5. Send the link create again with the same `Idempotency-Key` and body: the same `plink_` comes back with `Idempotent-Replayed: true`, and no second `payment_link.created` is sent.
6. Switch the link off and open its `url` again: the page shows "This sale has ended." and no way to pay. `GET /v1/events?type=payment_link.updated` shows the change, with `previous_attributes.active` `true`.
7. Create a link with `line_items[0][price]=price_000000000000000000000000`: it answers 400 `resource_missing` on `line_items[0][price]`.

See [Testing](https://docs.fluveo.com/start/testing.md) for the other test cards.

> **For AI agents:**
>
> Payment links for agents: first POST /v1/prices (currency, unit_amount in the smallest currency unit, product_data[name]) to get a price_ id, then POST /v1/payment_links with line_items[0][price]=price_..., line_items[0][quantity] and an Idempotency-Key. Share the url of the plink_ answer; never build the address yourself. A link sells only active one-time Prices in the store's currency, at most 20 lines. Switch a link off with active=false; never create a second link to stop the first. A link switched off takes any edit; send active=true in the same update to switch it back on with the edit. A link that reached its payment limit takes edits but stays off; to sell again, send active=true with a restrictions[completed_sessions][limit] above its count. A buyer whose checkout was opened before the link closed cannot pay it. An expired link takes no edit beside active (payment_link_inactive). Lines are edited by their li_ id and never added or removed: make a new link for a new set of Prices. For card and Pix, fulfil from checkout.session.completed and match the order by data.object.payment_link, never by the buyer's return to the redirect URL. For a Boleto, wait for checkout.session.async_payment_succeeded and payment_status=paid; do not fulfil on checkout.session.completed when it is unpaid. Find a link's payments with GET /v1/checkout/sessions?payment_link=plink_....

## Next

- [Hosted checkout](https://docs.fluveo.com/payments/checkout.md): Create a Checkout Session for each cart instead of one link for many buyers.
- [Prices](https://docs.fluveo.com/catalog/prices.md): Set the amounts your links sell.
- [Payment links in the Dashboard](https://docs.fluveo.com/dashboard/payment-links.md): Make, share and follow links without code.
- [Event types](https://docs.fluveo.com/webhooks/events.md): The body of each event a link and its sessions send.
- [Payment Links reference](https://docs.fluveo.com/api/payment_links.md): Every parameter and field of the payment link object.
