A payment link is a hosted checkout page for one price. Share its url by text, email, DM or QR code, put it behind a button on your site, or create one per order from your backend and send the customer straight to it. Customers pay by card, Apple Pay or Google Pay, and by bank account or stablecoin where you have turned them on. Links made for invoices and subscription plans are managed with those and aren't listed here.
| Topic | Detail |
|---|---|
| Endpoints | `POST /api/v1/payment-links`, `GET /api/v1/payment-links`, `GET /api/v1/payment-links/{id}`, `POST /api/v1/payment-links/{id}`, `POST /api/v1/payment-links/{id}/deactivate` |
| Key permissions | payment_links:write, payment_links:read |
| Samples | curl; Node.js 18+ with the built-in fetch (in an ES module); the Node SDK (preview: not on npm yet); Python with requests. |
| Every request can also answer | 401 UNAUTHORIZED, 401 API_KEY_EXPIRED, 403 NOT_ENROLLED, 403 PERMISSION_DENIED, 403 IP_NOT_ALLOWED, 429 RATE_LIMITED, 500 INTERNAL_ERROR. See Error codes. |
The payment link object
{
"object": "payment_link",
"livemode": true,
"id": "pl_4kTz8QwN2xYb6RcD1mPa",
"url": "https://anytime-capital.com/pay/business/pl_4kTz8QwN2xYb6RcD1mPa",
"title": "Consulting — October",
"description": "Two strategy sessions",
"amountCents": 12500,
"currency": "usd",
"active": true,
"singleUse": false,
"shippingCents": 0,
"collectShipping": false,
"allowDiscountCodes": false,
"successUrl": null,
"confirmationMessage": "Thanks! We'll email your session times within a day.",
"requireTermsConsent": false,
"metadata": {
"campaign": "fall-launch"
},
"items": [],
"createdAt": "2026-10-01T09:30:00.000Z",
"deactivatedAt": null,
"allowQuantity": false,
"maxQuantity": null,
"customFields": [],
"tipsEnabled": false,
"payWhatYouWant": false,
"minAmountCents": null
}| Field | Type | Description |
|---|---|---|
object | string | The type of object. Always payment_link. |
livemode | boolean | False for test-mode data (an ak_test_ key); true for live data. |
id | string | pl_ followed by 20 letters and digits. |
url | string (uri) | The pay page. Share it, redirect to it or embed it. |
title | string | What the customer pays for. |
description | string or null | Shown under the title. |
amountCents | integer | The price in cents. With payWhatYouWant, the suggested amount. |
currency | string | usd. |
active | boolean | Taking payments. |
singleUse | boolean | Closes after its first payment. |
shippingCents | integer | Flat shipping. Integer cents. |
collectShipping | boolean | Asks for a shipping address. |
allowDiscountCodes | boolean | Accepts discount codes. |
successUrl | string or null | Where customers go after paying. A link has at most one of successUrl and confirmationMessage. |
requireTermsConsent | boolean | Customers must agree to your terms. |
confirmationMessage | string or null | Shown after paying. |
metadata | object | Your own key/value data. |
items | object[] | Catalog products, for links made in the dashboard from products. Empty for single-price links. |
createdAt | string (date-time) | Created. |
deactivatedAt | string (date-time) or null | Deactivated. |
allowQuantity | boolean | Customers choose how many. |
maxQuantity | integer or null | Most per payment. |
customFields | object[] | |
customFields[].id | string | Your id. |
customFields[].label | string | The question. |
customFields[].type | string | text, long_text, phone, dropdown. |
customFields[].options | string[] | |
customFields[].required | boolean | Must be answered. |
tipsEnabled | boolean | Offers a tip. |
payWhatYouWant | boolean | Customers choose the amount. |
minAmountCents | integer or null | Smallest amount with payWhatYouWant. Integer cents. |
Endpoints
Create a payment link
/api/v1/payment-linkspayment_links:writeSupports Idempotency-Key.A hosted payment page for one price. Catalog products (items) can't be sent through the API yet: send amountCents for a single price. Links built from products in the dashboard still list and retrieve with their items.
Headers
Idempotency-Keystring- Recommended. A unique key such as a UUID: a retry with the same key within 24 hours gets the first response back instead of acting twice. See Idempotency.
Body
titlestringrequired- What the customer is paying for. Up to 120 characters.
amountCentsintegerrequired- The price in cents. With payWhatYouWant, the suggested amount. From 100 ($1.00) to 1,000,000 ($10,000.00).
descriptionstring- Shown under the title. Up to 500 characters.
singleUseboolean- Close the link after its first successful payment. Default false.
shippingCentsinteger- Flat shipping added to each payment. Default 0. From 0 ($0.00) to 100,000 ($1,000.00).
collectShippingboolean- Ask for a shipping address. Default false.
allowDiscountCodesboolean- Let customers enter one of your discount codes. Default false.
successUrlstring (uri)- An https:// page on your site customers go to after paying. Not with confirmationMessage. Up to 500 characters.
confirmationMessagestring- Shown on the confirmation page after paying, instead of a successUrl. Up to 500 characters.
requireTermsConsentboolean- Customers must tick “I agree to the terms” before paying. Needs a terms link in Settings → Payments (
409 TERMS_URL_REQUIREDotherwise). Defaultfalse. metadataobject- Up to 20 of your own key/value pairs, e.g.
{"orderId": "1042"}: keys up to 40 letters, numbers,-and_; values text up to 500 characters. Copied onto each payment made on the link; customers never see it. allowQuantityboolean- Let customers choose how many. Default false.
maxQuantityinteger- With allowQuantity: the most per payment (default 10). From 1 to 99.
tipsEnabledboolean- Offer a tip. Default false.
payWhatYouWantboolean- Customers choose the amount. Not with quantity, tips or discount codes. Default false.
minAmountCentsinteger- With payWhatYouWant: the smallest amount. From 100 ($1.00) to 1,000,000 ($10,000.00).
customFieldsobject[]- Up to 3 questions asked at checkout. Up to 3 items.
customFields[].idstringrequired- Your id for the question; answers are keyed by it.
customFields[].labelstringrequired- Up to 60 characters.
customFields[].typestringrequired- One of
text,long_text,phone,dropdown. customFields[].optionsstring[]- Dropdown only: 2–10 different choices. Up to 10 items.
customFields[].requiredboolean
curl -X POST https://anytime-capital.com/api/v1/payment-links \
-H "Authorization: Bearer $ANYTIME_API_KEY" \
-H "Idempotency-Key: order-1042-link" \
-H "Content-Type: application/json" \
-d '{
"title": "Consulting — October",
"description": "Two strategy sessions",
"amountCents": 12500,
"singleUse": true,
"metadata": {
"orderId": "1042"
}
}'const res = await fetch("https://anytime-capital.com/api/v1/payment-links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "order-1042-link",
},
body: JSON.stringify({
title: "Consulting — October",
description: "Two strategy sessions",
amountCents: 12500,
singleUse: true,
metadata: { orderId: "1042" },
}),
});
const link = await res.json();
if (!res.ok) throw new Error(`${link.error.code}: ${link.error.message}`);import { AnytimeCapital } from "@anytimecapital/node";
const client = new AnytimeCapital({ apiKey: process.env.ANYTIME_API_KEY! });
const link = await client.paymentLinks.create(
{
title: "Consulting — October",
description: "Two strategy sessions",
amountCents: 12500,
singleUse: true,
metadata: { orderId: "1042" },
},
{ idempotencyKey: "order-1042-link" },
);import os
import requests
res = requests.post(
"https://anytime-capital.com/api/v1/payment-links",
headers={
"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}",
"Idempotency-Key": "order-1042-link",
},
json={
"title": "Consulting — October",
"description": "Two strategy sessions",
"amountCents": 12500,
"singleUse": True,
"metadata": {"orderId": "1042"},
},
timeout=30,
)
link = res.json()
if not res.ok:
raise RuntimeError(f"{link['error']['code']}: {link['error']['message']}"){
"object": "payment_link",
"livemode": true,
"id": "pl_4kTz8QwN2xYb6RcD1mPa",
"url": "https://anytime-capital.com/pay/business/pl_4kTz8QwN2xYb6RcD1mPa",
"title": "Consulting — October",
"description": "Two strategy sessions",
"amountCents": 12500,
"currency": "usd",
"active": true,
"singleUse": true,
"shippingCents": 0,
"collectShipping": false,
"allowDiscountCodes": false,
"successUrl": null,
"confirmationMessage": null,
"requireTermsConsent": false,
"metadata": {
"orderId": "1042"
},
"items": [],
"createdAt": "2026-10-01T09:30:00.000Z",
"deactivatedAt": null,
"allowQuantity": false,
"maxQuantity": null,
"customFields": [],
"tipsEnabled": false,
"payWhatYouWant": false,
"minAmountCents": null
}Errors
- 400
INVALID_JSON - The body isn't valid JSON, or couldn't be read. An empty body reads as
{}. SendContent-Type: application/jsonand a JSON object. - 400
INVALID_INPUT - A field, filter,
limitorcursoris missing or invalid. The message starts with the field's name, as inamountCents: …, or readsInvalid cursor.Fix the named field. Don't retry the same request unchanged. - 400
INVALID_IDEMPOTENCY_KEY - The
Idempotency-Keyheader is empty, longer than 255 characters, or has characters other than printable ASCII. Use a UUID. See Idempotency. - 409
TERMS_URL_REQUIRED requireTermsConsentneeds a link to your terms, and the business hasn't set one. Add your terms link in Settings → Payments, then retry.- 422
IDEMPOTENCY_KEY_REUSED - The
Idempotency-Keywas used in the last 24 hours with a different body or endpoint. Use a new key for a new request; reuse a key only to retry the same request.
List payment links
/api/v1/payment-linkspayment_links:readPayment links with their pay page url and buyer choices. Links behind invoices and plans aren't listed.
Query
activeboolean- true for live objects, false for deactivated or archived ones.
limitinteger- How many objects per page, 1–100. Default 25. See Pagination.
cursorstring- The previous page's
nextCursor, for the page after it.
curl "https://anytime-capital.com/api/v1/payment-links?active=true&limit=10" \
-H "Authorization: Bearer $ANYTIME_API_KEY"const res = await fetch("https://anytime-capital.com/api/v1/payment-links?active=true&limit=10", {
headers: { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` },
});
const page = await res.json();
if (!res.ok) throw new Error(`${page.error.code}: ${page.error.message}`);import { AnytimeCapital } from "@anytimecapital/node";
const client = new AnytimeCapital({ apiKey: process.env.ANYTIME_API_KEY! });
const page = await client.paymentLinks.list({ active: true, limit: 10 });import os
import requests
res = requests.get(
"https://anytime-capital.com/api/v1/payment-links",
headers={"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"},
params={"active": "true", "limit": "10"},
timeout=30,
)
page = res.json()
if not res.ok:
raise RuntimeError(f"{page['error']['code']}: {page['error']['message']}"){
"object": "list",
"data": [
{
"object": "payment_link",
"livemode": true,
"id": "pl_4kTz8QwN2xYb6RcD1mPa",
"url": "https://anytime-capital.com/pay/business/pl_4kTz8QwN2xYb6RcD1mPa",
"title": "Consulting — October",
"description": "Two strategy sessions",
"amountCents": 12500,
"currency": "usd",
"active": true,
"singleUse": false,
"shippingCents": 0,
"collectShipping": false,
"allowDiscountCodes": false,
"successUrl": null,
"confirmationMessage": "Thanks! We'll email your session times within a day.",
"requireTermsConsent": false,
"metadata": {
"campaign": "fall-launch"
},
"items": [],
"createdAt": "2026-10-01T09:30:00.000Z",
"deactivatedAt": null,
"allowQuantity": false,
"maxQuantity": null,
"customFields": [],
"tipsEnabled": false,
"payWhatYouWant": false,
"minAmountCents": null
}
],
"hasMore": false,
"nextCursor": null
}Errors
- 400
INVALID_INPUT - A field, filter,
limitorcursoris missing or invalid. The message starts with the field's name, as inamountCents: …, or readsInvalid cursor.Fix the named field. Don't retry the same request unchanged.
Get a payment link
/api/v1/payment-links/{id}payment_links:readOne payment link by id.
Path
idstringrequiredpl_…
curl https://anytime-capital.com/api/v1/payment-links/pl_4kTz8QwN2xYb6RcD1mPa \
-H "Authorization: Bearer $ANYTIME_API_KEY"const res = await fetch("https://anytime-capital.com/api/v1/payment-links/pl_4kTz8QwN2xYb6RcD1mPa", {
headers: { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` },
});
const link = await res.json();
if (!res.ok) throw new Error(`${link.error.code}: ${link.error.message}`);import { AnytimeCapital } from "@anytimecapital/node";
const client = new AnytimeCapital({ apiKey: process.env.ANYTIME_API_KEY! });
const link = await client.paymentLinks.retrieve("pl_4kTz8QwN2xYb6RcD1mPa");import os
import requests
res = requests.get(
"https://anytime-capital.com/api/v1/payment-links/pl_4kTz8QwN2xYb6RcD1mPa",
headers={"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"},
timeout=30,
)
link = res.json()
if not res.ok:
raise RuntimeError(f"{link['error']['code']}: {link['error']['message']}"){
"object": "payment_link",
"livemode": true,
"id": "pl_4kTz8QwN2xYb6RcD1mPa",
"url": "https://anytime-capital.com/pay/business/pl_4kTz8QwN2xYb6RcD1mPa",
"title": "Consulting — October",
"description": "Two strategy sessions",
"amountCents": 12500,
"currency": "usd",
"active": true,
"singleUse": false,
"shippingCents": 0,
"collectShipping": false,
"allowDiscountCodes": false,
"successUrl": null,
"confirmationMessage": "Thanks! We'll email your session times within a day.",
"requireTermsConsent": false,
"metadata": {
"campaign": "fall-launch"
},
"items": [],
"createdAt": "2026-10-01T09:30:00.000Z",
"deactivatedAt": null,
"allowQuantity": false,
"maxQuantity": null,
"customFields": [],
"tipsEnabled": false,
"payWhatYouWant": false,
"minAmountCents": null
}Errors
- 404
NOT_FOUND - No such object on your business (an id from another business reads the same), or no such endpoint under
/api/v1, which still answers in JSON. Check the id and the path. Ids are case-sensitive.
Update a payment link
/api/v1/payment-links/{id}payment_links:writeSupports Idempotency-Key.Send only what changes. The price is fixed: 400 NOT_EDITABLE names a field that can't change. Changes apply straight away; payments already made keep what they were charged. The price and how it is priced are fixed: amountCents, items, payWhatYouWant, minAmountCents, shippingCents, allowDiscountCodes and singleUse answer 400 NOT_EDITABLE.
Path
idstringrequiredpl_…
Headers
Idempotency-Keystring- Recommended. A unique key such as a UUID: a retry with the same key within 24 hours gets the first response back instead of acting twice. See Idempotency.
Body
titlestring- Up to 120 characters.
descriptionstring or null- New description; null removes it. Up to 500 characters.
successUrlstring (uri) or null- An https:// page to send customers to after paying; setting it clears confirmationMessage, null clears it. Up to 500 characters.
confirmationMessagestring or null- Message after paying; setting it clears successUrl, null clears it. Up to 500 characters.
requireTermsConsentbooleanmetadataobject or null- Replaces all of the link's metadata; null clears it. Up to 20 entries.
customFieldsobject[]- Up to 3 questions asked at checkout. Up to 3 items.
customFields[].idstringrequired- Your id for the question; answers are keyed by it.
customFields[].labelstringrequired- Up to 60 characters.
customFields[].typestringrequired- One of
text,long_text,phone,dropdown. customFields[].optionsstring[]- Dropdown only: 2–10 different choices. Up to 10 items.
customFields[].requiredbooleantipsEnabledbooleanallowQuantitybooleanmaxQuantityinteger or null- With allowQuantity: the most per payment. From 1 to 99.
collectShippingboolean
curl -X POST https://anytime-capital.com/api/v1/payment-links/pl_4kTz8QwN2xYb6RcD1mPa \
-H "Authorization: Bearer $ANYTIME_API_KEY" \
-H "Idempotency-Key: update-pl_4kTz8Q-title" \
-H "Content-Type: application/json" \
-d '{
"title": "Consulting — November",
"metadata": {
"campaign": "winter"
}
}'const res = await fetch("https://anytime-capital.com/api/v1/payment-links/pl_4kTz8QwN2xYb6RcD1mPa", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "update-pl_4kTz8Q-title",
},
body: JSON.stringify({ title: "Consulting — November", metadata: { campaign: "winter" } }),
});
const link = await res.json();
if (!res.ok) throw new Error(`${link.error.code}: ${link.error.message}`);import { AnytimeCapital } from "@anytimecapital/node";
const client = new AnytimeCapital({ apiKey: process.env.ANYTIME_API_KEY! });
const link = await client.paymentLinks.update(
"pl_4kTz8QwN2xYb6RcD1mPa",
{ title: "Consulting — November", metadata: { campaign: "winter" } },
{ idempotencyKey: "update-pl_4kTz8Q-title" },
);import os
import requests
res = requests.post(
"https://anytime-capital.com/api/v1/payment-links/pl_4kTz8QwN2xYb6RcD1mPa",
headers={
"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}",
"Idempotency-Key": "update-pl_4kTz8Q-title",
},
json={"title": "Consulting — November", "metadata": {"campaign": "winter"}},
timeout=30,
)
link = res.json()
if not res.ok:
raise RuntimeError(f"{link['error']['code']}: {link['error']['message']}"){
"object": "payment_link",
"livemode": true,
"id": "pl_4kTz8QwN2xYb6RcD1mPa",
"url": "https://anytime-capital.com/pay/business/pl_4kTz8QwN2xYb6RcD1mPa",
"title": "Consulting — November",
"description": "Two strategy sessions",
"amountCents": 12500,
"currency": "usd",
"active": true,
"singleUse": false,
"shippingCents": 0,
"collectShipping": false,
"allowDiscountCodes": false,
"successUrl": null,
"confirmationMessage": "Thanks! We'll email your session times within a day.",
"requireTermsConsent": false,
"metadata": {
"campaign": "winter"
},
"items": [],
"createdAt": "2026-10-01T09:30:00.000Z",
"deactivatedAt": null,
"allowQuantity": false,
"maxQuantity": null,
"customFields": [],
"tipsEnabled": false,
"payWhatYouWant": false,
"minAmountCents": null
}Errors
- 400
INVALID_JSON - The body isn't valid JSON, or couldn't be read. An empty body reads as
{}. SendContent-Type: application/jsonand a JSON object. - 400
INVALID_INPUT - A field, filter,
limitorcursoris missing or invalid. The message starts with the field's name, as inamountCents: …, or readsInvalid cursor.Fix the named field. Don't retry the same request unchanged. - 400
INVALID_IDEMPOTENCY_KEY - The
Idempotency-Keyheader is empty, longer than 255 characters, or has characters other than printable ASCII. Use a UUID. See Idempotency. - 400
NOT_EDITABLE - The update names a field that is fixed once a link exists:
amountCents,items,payWhatYouWant,minAmountCents,shippingCents,allowDiscountCodesorsingleUse. The message names it. Create a new link at the new price and deactivate the old one. - 404
NOT_FOUND - No such object on your business (an id from another business reads the same), or no such endpoint under
/api/v1, which still answers in JSON. Check the id and the path. Ids are case-sensitive. - 409
TERMS_URL_REQUIRED requireTermsConsentneeds a link to your terms, and the business hasn't set one. Add your terms link in Settings → Payments, then retry.- 422
IDEMPOTENCY_KEY_REUSED - The
Idempotency-Keywas used in the last 24 hours with a different body or endpoint. Use a new key for a new request; reuse a key only to retry the same request.
Deactivate a payment link
/api/v1/payment-links/{id}/deactivatepayment_links:writeSupports Idempotency-Key.Stop a link taking payments. Safe to repeat. Anyone who opens the link sees that it's closed. There's no API to turn it back on.
Path
idstringrequiredpl_…
Headers
Idempotency-Keystring- Recommended. A unique key such as a UUID: a retry with the same key within 24 hours gets the first response back instead of acting twice. See Idempotency.
curl -X POST https://anytime-capital.com/api/v1/payment-links/pl_4kTz8QwN2xYb6RcD1mPa/deactivate \
-H "Authorization: Bearer $ANYTIME_API_KEY" \
-H "Idempotency-Key: deactivate-pl_4kTz8Q"const res = await fetch("https://anytime-capital.com/api/v1/payment-links/pl_4kTz8QwN2xYb6RcD1mPa/deactivate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
"Idempotency-Key": "deactivate-pl_4kTz8Q",
},
});
const link = await res.json();
if (!res.ok) throw new Error(`${link.error.code}: ${link.error.message}`);import { AnytimeCapital } from "@anytimecapital/node";
const client = new AnytimeCapital({ apiKey: process.env.ANYTIME_API_KEY! });
const link = await client.paymentLinks.deactivate(
"pl_4kTz8QwN2xYb6RcD1mPa",
{ idempotencyKey: "deactivate-pl_4kTz8Q" },
);import os
import requests
res = requests.post(
"https://anytime-capital.com/api/v1/payment-links/pl_4kTz8QwN2xYb6RcD1mPa/deactivate",
headers={
"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}",
"Idempotency-Key": "deactivate-pl_4kTz8Q",
},
timeout=30,
)
link = res.json()
if not res.ok:
raise RuntimeError(f"{link['error']['code']}: {link['error']['message']}"){
"object": "payment_link",
"livemode": true,
"id": "pl_4kTz8QwN2xYb6RcD1mPa",
"url": "https://anytime-capital.com/pay/business/pl_4kTz8QwN2xYb6RcD1mPa",
"title": "Consulting — October",
"description": "Two strategy sessions",
"amountCents": 12500,
"currency": "usd",
"active": false,
"singleUse": false,
"shippingCents": 0,
"collectShipping": false,
"allowDiscountCodes": false,
"successUrl": null,
"confirmationMessage": "Thanks! We'll email your session times within a day.",
"requireTermsConsent": false,
"metadata": {
"campaign": "fall-launch"
},
"items": [],
"createdAt": "2026-10-01T09:30:00.000Z",
"deactivatedAt": "2026-10-16T10:00:00.000Z",
"allowQuantity": false,
"maxQuantity": null,
"customFields": [],
"tipsEnabled": false,
"payWhatYouWant": false,
"minAmountCents": null
}Errors
- 400
INVALID_IDEMPOTENCY_KEY - The
Idempotency-Keyheader is empty, longer than 255 characters, or has characters other than printable ASCII. Use a UUID. See Idempotency. - 404
NOT_FOUND - No such object on your business (an id from another business reads the same), or no such endpoint under
/api/v1, which still answers in JSON. Check the id and the path. Ids are case-sensitive. - 422
IDEMPOTENCY_KEY_REUSED - The
Idempotency-Keywas used in the last 24 hours with a different body or endpoint. Use a new key for a new request; reuse a key only to retry the same request.
Prefill checkout
Fill in details for your customer by adding parameters to a link's url. A value that isn't valid is ignored, never an error.
| Parameter | What it does |
|---|---|
prefilled_email | The customer's email for their receipt. They can still change it. |
code | One of your discount codes, applied when checkout opens, on links that accept codes. |
ref | Your own reference for the payment, up to 200 letters, numbers, spaces and _ . : / # @ + -. Stored on the payment as clientReference and returned by the API and in webhooks. |
https://anytime-capital.com/pay/business/pl_4kTz8QwN2xYb6RcD1mPa?prefilled_email=jordan%40example.com&code=FALL20&ref=order_1042On your own website, the Pay button and the inline checkout take the same values as data-email, data-code and data-ref. See Checkout.
One link per order
For a cart or an order whose total you work out yourself, create a singleUse link per order with the order id in metadata, send the customer to its url, and mark the order paid when the payment.succeeded webhook arrives: data.metadata carries your order id, data.paymentLinkId the link id and data.customer.email the buyer. Use a stored Idempotency-Key so a retry never makes a second link. For a fixed price you sell again and again, one reusable link with ?ref= per customer does the same job. The Next.js guide builds this end to end.