# Checkout Links

A checkout link is a WorthyPayments-hosted payment page. Create one server-side, redirect
the shopper to its URL, and the hosted page collects the card and runs the full engine
(fraud screening, routing, 3-D Secure) with **no card data ever touching your code**.
This is the redirect integration, and the lowest-PCI way to take a payment.

```
POST   https://api.worthypayments.com/v1/checkout-links
GET    https://api.worthypayments.com/v1/checkout-links
GET    https://api.worthypayments.com/v1/checkout-links/{id}
```

Scopes: `checkout_links:create` to make one, `checkout_links:read` to list or fetch them
back. Both are **server-only**. A publishable key (`pk_`) cannot reach either; use a
secret key (`sk_`). See [Authentication](./02-authentication.md).

There is no update or delete endpoint. See [Changing the price](#changing-the-price).

---

## The redirect flow

```mermaid
sequenceDiagram
    autonumber
    participant S as Your server
    participant W as WorthyPayments
    participant B as Shopper's browser
    S->>W: POST /checkout-links
    W-->>S: { url }
    S->>B: Redirect the shopper to url
    B->>W: Enters card, completes 3DS
    Note over W: Charge runs: fraud, routing, 3DS
    W-->>B: Hosted page completes
    B->>S: Shopper returns after payment
```

---

## Create a checkout link

`POST /checkout-links`

### Request body

| Field | Type | Required | Notes |
|-------|------|:---:|-------|
| `name` | string | Yes* | Shown on the hosted page (e.g. `"Order #1234"`). *Or provide `description`. One of the two must yield a non-empty name. |
| `amount` | integer | one | **Cents.** For `FIXED` you almost always set this (minimum `50` in practice). Omit for shopper-driven `DONATION`. |
| `currency` | string | No | ISO 4217, e.g. `"usd"`. Defaults to `usd`. |
| `link_type` | string | No | `"FIXED"` (default), `"DONATION"`, or `"SUBSCRIPTION"`. |
| `plan` | string | SUBSCRIPTION | `plan_…` id. Required when `link_type` is `SUBSCRIPTION` (`400 parameter_missing` without it). Echoed on the **201 create body** and on GET, persist either. |
| `description` | string | No | Optional blurb; also used as the name fallback when `name` is empty. |

There is no `success_url` / `cancel_url` on this create body today. The hosted page owns
the post-pay experience; reconcile via [webhooks](./16-webhooks.md).

> The field is **`link_type`**, not `type`. Values are **`FIXED` | `DONATION` |
> `SUBSCRIPTION`**. Not `CUSTOM` / `PRODUCT`. Unknown values fall back to `FIXED`.

### Example: a fixed one-off amount

```bash
curl https://api.worthypayments.com/v1/checkout-links \
  -H "x-api-key: sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order #1234",
    "amount": 4999,
    "currency": "usd",
    "link_type": "FIXED"
  }'
```

### Response

```json
{
  "id": "9f2c1b84-77ad-4e0c-9c31-0b6e5d2a4413",
  "object": "checkout_link",
  "url": "https://app.worthypayments.com/checkout/abc123",
  "code": "abc123",
  "active": true
}
```

Redirect the shopper to `url`. It is absolute and points at our hosted checkout, so it is
safe to hand straight to a redirect. `code` is the short slug at the end of it, use that
if you need to build the link yourself.

---

## Link types

| `link_type` | Use it for |
|-------------|------------|
| `FIXED` | A one-off charge for a specific `amount` you set at creation. **Default.** |
| `DONATION` | A donation-style page where the shopper can enter their own amount. |
| `SUBSCRIPTION` | A recurring / subscription-oriented link. |

For a catalogue product’s own share URL, see [Products](./05-products.md). Product
binding is not a field on this create body.

---

## Read your links back

```
GET https://api.worthypayments.com/v1/checkout-links
GET https://api.worthypayments.com/v1/checkout-links/{id}
```

Scope: **`checkout_links:read`**. A separate grant from `checkout_links:create`. Tick it
on the key in **Settings → Developers → API keys**; a key that only creates links gets
`403 permission_denied` here.

The list is cursor-paged the same way as [`GET /charges`](./13-pagination.md), and
`next_cursor` is **omitted** rather than `null` when there is no next page.

| Query | Notes |
|-------|-------|
| `limit` | Default `10`, max `100`. |
| `starting_after` | The previous page's `next_cursor`. |
| `active` | Optional filter. Absent lists both active and inactive links. |
| `link_type` | Optional filter: `FIXED` \| `DONATION` \| `SUBSCRIPTION`. |

```json
{
  "object": "list",
  "data": [
    {
      "object": "checkout_link",
      "id": "df62a7a3-e2de-4f47-8635-e3c9adfebddf",
      "url": "https://app.worthypayments.com/checkout/lKHx64iW",
      "code": "lKHx64iW",
      "name": "Order #1234",
      "description": "",
      "link_type": "FIXED",
      "currency": "USD",
      "amount": 4999,
      "min_amount": null,
      "max_amount": null,
      "max_uses": null,
      "times_used": 0,
      "active": true,
      "product_id": null,
      "store_id": null,
      "expires_at": null,
      "created": 1788100472,
      "updated": 1788100472
    }
  ],
  "has_more": false
}
```

`link_type` reads back as the same value you sent, `FIXED`, not the internal
`PAYMENT_LINK_TYPE_FIXED`. Timestamps are Unix seconds. `amount` is minor units, and is
`null` on a `DONATION` link with no fixed amount.

---

## Changing the price

**A link's `amount` is fixed once created. There is no update endpoint**, `PATCH` and
`PUT` on `/checkout-links/{id}` are not served and answer `404`. Two ways forward:

- **Create a new link** and redirect to that one instead. Old links keep working, so
  deactivate the previous one in the dashboard if it must stop taking money.
- **Edit it in the dashboard** (**Payment Links → the link → Edit**), which can change
  the amount in place and keeps the same `code`, so a URL you have already sent out stays
  valid at the new price.

> **Re-pricing a cart mid-checkout is a different problem, and it has a real answer.** If
> the shopper is on your page rather than ours, use the
> [Payment Element](./08b-payment-element.md) and
> [`PATCH /v1/payment_intents/{id}`](./08b-payment-element.md): it rewrites the amount on
> an intent that is already mounted, the `client_secret` does not change, and nothing has
> to be re-rendered. Checkout links have no equivalent because the shopper is on our page
> by then, and the page has already been handed a price.

---

## After payment

The hosted page runs the entire payment lifecycle, including any 3-D Secure step-up. You
do not handle `requires_action` yourself in the redirect flow. Reconcile the order on
your side via webhooks or by reading the charge once you have its id. See
[Webhooks](./16-webhooks.md).

> Prefer to keep the buyer on your domain instead of redirecting? Use the
> [Payment Element](./08b-payment-element.md). For a pure server-to-server charge after
> your own tokenization, see [`POST /charges`](./03-charges.md).
