# Products

Products are reusable catalogue items, each with a fixed price and its own hosted
checkout URL. List them to power the **"fixed product → share a URL"** flow: create the
product once in the dashboard, then hand its `url` to a shopper to take a payment. No
per-charge API call required. Storefronts that hold those products are
[Storefronts](./05b-stores.md).

```
GET    https://api.worthypayments.com/v1/products
```

Scope: `products:read`.

---

## List products

`GET /products`

### Query parameters

| Parameter | Type | Default | Notes |
|-----------|------|---------|-------|
| `limit` | integer | `10` | Page size. Capped at `100`. |
| `starting_after` | string | — | The opaque `next_cursor` from the previous page. Not a product id. |

Paging is cursor-based, so there is no `page` parameter and no `active` filter.
Unrecognized query parameters are silently ignored, so filtering client-side on the
`active` field is the only option.

### Example

```bash
curl "https://api.worthypayments.com/v1/products?limit=10" \
  -H "x-api-key: sk_live_xxx"
```

### Response

```json
{
  "object": "list",
  "data": [
    {
      "id": "4b1e77a0-2c93-45de-8f0a-6d5c39ab72e1",
      "object": "product",
      "name": "Pro Plan",
      "active": true,
      "url": "https://app.worthypayments.com/checkout/proplan"
    }
  ],
  "has_more": false,
  "next_cursor": "djJ8MjAyNi0wOC0yNFQxNDo0Njo1Nlp8NGIxZTc3YTA"
}
```

See [Pagination](./13-pagination.md) for the cursor list model.

| Field | Type | Notes |
|-------|------|-------|
| `id` | string | Product id. |
| `object` | string | Always `"product"`. |
| `name` | string | Display name. |
| `active` | boolean | Whether the product is purchasable. |
| `url` | string \| null | The product's hosted checkout page. Share this to take a payment. `null` when the product has no active checkout link. |

> **Price is not returned.** The list carries no `amount` or `currency`, so you cannot
> render a price from this endpoint. Read it from the dashboard or from the hosted
> checkout page itself.

---

## The fixed-product flow

1. Create a product in the dashboard (name, price, currency).
2. `GET /products` to retrieve its `url`.
3. Put the URL anywhere a shopper can click it: an email, an invoice, a button, a QR code.
4. The hosted page runs fraud, routing, and 3-D Secure, then collects payment. Nothing
   else to build.

This is the same hosted engine as a [checkout link](./04-checkout-links.md), with the
product carrying a fixed price and a stable URL. To bind a custom checkout link to a
product, pass `product_id` to `POST /checkout-links`.

---

## Tax rates

Your own store (WooCommerce, a custom cart) can use the tax rates you keep with us: create
the rates, attach them to products, and ask what each cart line owes at checkout.

```
POST   https://api.worthypayments.com/v1/tax_rates
GET    https://api.worthypayments.com/v1/tax_rates
GET    https://api.worthypayments.com/v1/tax_rates/{id}
POST   https://api.worthypayments.com/v1/tax_rates/{id}
DELETE https://api.worthypayments.com/v1/tax_rates/{id}
POST   https://api.worthypayments.com/v1/products/{id}/tax_rates
DELETE https://api.worthypayments.com/v1/products/{id}/tax_rates/{tax_rate}
POST   https://api.worthypayments.com/v1/tax/calculate
```

Scopes: `tax:read` (list, retrieve, calculate) and `tax:write` (everything else).
`tax:write` is server-only. Neither is on a new key by default: tick them in
**Dashboard → Developers → API keys**.

### 1. Create a rate

`percentage` is a **percent**: `7.25` means 7.25 %. It comes back as the string `"7.25"`.
Creating requires an `Idempotency-Key`, so a retry never makes a second rate.

```bash
curl https://api.worthypayments.com/v1/tax_rates \
  -H "x-api-key: sk_live_xxx" \
  -H "Idempotency-Key: 5d0c7a52-3f1e-4c8b-9d2a-6e4f1b7c8a90" \
  -H "Content-Type: application/json" \
  -d '{"display_name":"CA sales tax","percentage":7.25,"tax_type":"sales_tax","country":"US","state":"CA"}'
```

Updating (`POST /tax_rates/{id}`) changes only the fields you send. Deleting stops the rate
applying at once; the record is kept so past tax stays explained. To pause a rate, send
`active: false` instead. The default rate and global rates are set in the dashboard.

### 2. Attach it to a product

```bash
curl https://api.worthypayments.com/v1/products/4b1e77a0-2c93-45de-8f0a-6d5c39ab72e1/tax_rates \
  -H "x-api-key: sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"tax_rate":"6f1c2a4e-1d7b-4b8e-9b0a-2f5d3c9e7a11"}'
```

A product can carry several rates, and they stack. The product id is not checked against
the catalog: use the same id you will send as `line_items[].product` when you calculate.

### 3. Calculate at checkout

```bash
curl https://api.worthypayments.com/v1/tax/calculate \
  -H "x-api-key: sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"currency":"usd","address":{"country":"US","state":"CA"},
       "line_items":[{"product":"4b1e77a0-2c93-45de-8f0a-6d5c39ab72e1","amount":2000,"quantity":2,"reference":"cart-item-1"}]}'
```

`amount` is the line total in cents. The response gives each line its `amount_tax` and a
`tax_breakdown` per rate, plus `amount_subtotal`, `tax_amount_exclusive`,
`tax_amount_inclusive` and `amount_total`. The call writes nothing, so run it every time the
cart changes.

Each line picks its rates on its own, in this order: a global rate, then the rates
attached to its product, then your rates for the ship-to address, then your default rate.
A line with none of these pays no tax. Tax on an `inclusive` rate is already inside
`amount`, so only exclusive tax is added to `amount_total`. Shipping is not taxed by this
call. At most 100 lines per call.
