---
updatedAt: 2026-09-30T17:50:34.235Z
---

Fetch the complete documentation index at: https://docs.avvio.xyz/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Create a checkout link

`POST https://api.avvio.xyz/business/api/v1/checkout/organizations/{orgId}/links`

Creates a link a buyer can pay. Send either a
`productId` or `currency` + `items`. Every link carries `shareUrl`, the
page to send your buyer to; with `publish: true` it is live on return.

Without `publish` the link is a `draft`: not publicly readable, editable
with `PATCH`, deletable. `POST /links/{linkId}/publish` makes it live.

`methods` omitted means `[{ "kind": "card" }]`, the hosted card page
(card, wallets and PayPal). `methods: []` is a link nothing can pay.
Bank and crypto rails need the account or address to pay into.

**When publish is refused inside a `publish: true` create, the call
still answers `201`** with the kept draft: the normal link body with
`status: "draft"` and a `publishError: { status, type, message }`
saying why (a business not yet set up for cards, a currency the
processor cannot price). Branch on `status !== "sent"`, then read
`publishError`. Fix the cause and `POST /links/{linkId}/publish`, or
`DELETE` the draft. It answers `201` rather than throwing because a
stored idempotent response is only kept for a success: an error would
release the key, and a client's automatic retry would create a second
draft.

The exception is the card processor failing to answer at all (a
timeout or a 5xx while the hosted card page is minted). That answers
`500 INTERNAL`, **and the draft is kept**. Do not retry the create:
list your drafts (`GET /links?status=draft`) and publish or delete the
one already made.

`expiresAt` gives the link a deadline. From
that instant the page answers `410` and the link reads `expired`;
the merchant's own booking or order logic decides what a payment that
lands after it means. Omitted, the link runs until paused.

`Idempotency-Key` is **required** on an API key (`400
IDEMPOTENCY_KEY_REQUIRED` without it): a retry under the same key
returns the same link instead of a second one.

## Parameters

- `orgId` (path, required) — The opaque organization id issued to you, normally CUID-shaped (for example `cmsx…`). It is not an `org_`-prefixed alias. Pass it unchanged in every organization-scoped path.
- `Idempotency-Key` (header, required) — A unique value per logical operation, 1-255 chars of `A-Z a-z 0-9 _ . : -`. Reuse it to retry. Same key with the same body replays the stored response; same key with a *different* body is a `409`, because answering with the first call's result would hand you a receipt for a payout you did not request. A `4xx` releases the key, so you can fix the body and reuse it. **Reuse it; do not generate one per attempt.** A key minted per attempt defeats replay entirely: every retry looks like a new request, so every retry pays. We also watch for an identical body arriving under a *different* key within 15 minutes and refuse it with `DUPLICATE_REQUEST_DETECTED`. Records are kept for 7 days. That is a retention window only: there is no path where an expired key is re-executed.

## Example

```bash
IDEMPOTENCY_KEY=$(uuidgen)  # new key per call; reuse it only to retry this exact request
curl -s -X POST "$AVVIO_BASE_URL/checkout/organizations/$AVVIO_ORG_ID/links" \
  -H "x-api-key: $AVVIO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "content-type: application/json" \
  -d '{
        "productId": "2a7b8c9d-1e2f-4a5b-8c9d-0e1f2a3b4c5d",
        "successUrl": "https://example.com/thanks",
        "cancelUrl": "https://example.com/pricing",
        "clientReferenceId": "order_1042",
        "metadata": {
          "orderId": "1042"
        },
        "publish": true
      }'
```

## Responses

- `201` — The link. `status` is `sent` after a successful `publish: true`; `draft` otherwise, with `publishError` set when a publish was refused.
- `400` — `VALIDATION_ERROR` (a field is malformed, or a `token`, `chain` or `currency` we do not support; `errors` names each), `BAD_REQUEST` (both `productId` and `items`; an archived product; a currency the card processor cannot price, such as `CNY`; a bank rail with no account), or `IDEMPOTENCY_KEY_REQUIRED` / `IDEMPOTENCY_KEY_INVALID` (no header, or a malformed one). A refused publish is not an error here: the draft comes back `201` with `publishError`, whose `type` is `LINK_EXPIRED` when the draft's `expiresAt` has already passed.
- `401` — The key was refused. Nothing ran. - `UNAUTHORIZED`: missing, invalid or revoked, or a key on a route that does not accept one. - `KEY_EXPIRED`: the key passed the expiry it was issued with. Issue a new one; an expired key cannot be rotated. - `KEY_IP_NOT_ALLOWED`: the key is pinned to source addresses and this request came from another.
- `403` — A valid key that may not make this write. Nothing was changed. - `FORBIDDEN`: the key belongs to a different organization, **or** your business has not completed verification to accept payments; `detail` says which. - `ACCOUNT_BLOCKED`: API access for your organization is suspended. - `LIVE_KEY_ORG_NOT_APPROVED`: a live key, before we have approved your business verification. Use a test key until then. - `INSUFFICIENT_SCOPE`: a read-only key.
- `404` — `NOT_FOUND`: the `productId` is not a product in this organization.
- `409` — Either the key was reused with a different body (`IDEMPOTENCY_KEY_CONFLICT`: use a new key), or the first request with this key is still running (`IDEMPOTENCY_KEY_REQUEST_IN_PROGRESS`: back off and retry the same key).
- `429` — Too many requests. The default ceiling is **100 requests per minute per API credential** on a 60-second window. High-volume payout and reconciliation routes declare a 600/minute override, and batch submission a 30/minute ceiling. A separate 2,000/minute per-source-IP abuse ceiling always applies. Obey `Retry-After`; it is in seconds and is authoritative. A 429 means the request was refused before the handler ran. Retry reads normally; retry an idempotent mutation with its same `Idempotency-Key`.
- `500` — `INTERNAL`: with `publish: true`, the card processor did not answer while the card page was being set up. The draft **was kept**. Do not retry the create; find the draft and publish or delete it.

Machine contract: [partner-checkout.openapi.yaml](/partner-checkout.openapi.yaml), operation `createCheckoutLink`.
