# Storefront API

URL: https://zerodev.ca/docs/prism/api

> Read your catalog, start checkouts and manage products and licenses over HTTP.



Prism's store site uses the same public API you can call yourself. Every endpoint lives under `/api/v1/` on your API address, and on your site address, which forwards `/api/v1/` to the API.

```text
https://shop.example.com/api/v1/products
```

## Authentication

| Caller    | How                               | Can use                                          |
| --------- | --------------------------------- | ------------------------------------------------ |
| Anyone    | No header                         | Catalog, reviews, updates, checkout and coupons. |
| API token | `Authorization: Bearer prism_...` | Everything its scopes allow.                     |

Create tokens under **System > API Tokens**. Pick the scopes the token needs, such as `products.view` or `licenses.create`, or `*` for everything. The full token is shown once. A token can have an expiry date, and the page shows when each one was last used. Calls with an unknown token are written to the audit log.

## Signed Responses

Storefront responses carry an `x-prism-signature` header: an Ed25519 signature of the raw response body, in base64. Check it against the key from `GET /api/v1/public-key` so a client knows the answer came from your shop.

```json
{
    "algorithm": "ed25519",
    "publicKey": "-----BEGIN PUBLIC KEY-----\n...",
    "raw": "base64-encoded-32-byte-key"
}
```

```js
import { createPublicKey, verify } from "node:crypto";

const response = await fetch("https://shop.example.com/api/v1/products");
const body = await response.text();
const valid = verify(null, Buffer.from(body), createPublicKey(publicKeyPem), Buffer.from(response.headers.get("x-prism-signature"), "base64"));
```

## Catalog

| Endpoint                             | Returns                                                             |
| ------------------------------------ | ------------------------------------------------------------------- |
| `GET /api/v1/products`               | `{ success, total, products }`. Published products, featured first. |
| `GET /api/v1/products/:slug`         | `{ success, product }`. One published product by slug or ID.        |
| `GET /api/v1/products/:slug/reviews` | `{ success, items, total, average, distribution }`.                 |
| `GET /api/v1/products/:slug/updates` | `{ success, updates }`. Newest first.                               |

`GET /api/v1/products` takes `search`, `limit`, `offset`, `featured=true` and `slugs=a,b,c`. Each is limited to 120 requests a minute per IP.

```json
{
    "id": 1,
    "slug": "my-product",
    "name": "My Product",
    "price": 9.99,
    "sale": null,
    "currency": "USD",
    "points_earned": 0,
    "points_price": 0,
    "summary": "One short line.",
    "description": "[b]BBCode[/b] body",
    "cover": "https://...",
    "addons": [{ "name": "Extra", "icon": "Puzzle", "price": 4.99, "originalPrice": null, "sale": false, "download": true }],
    "links": [{ "label": "Docs", "url": "https://...", "icon": "BookOpen" }],
    "featured": true,
    "download": true,
    "created_at": "2026-10-01T12:00:00.000Z",
    "updated_at": "2026-10-01T12:00:00.000Z"
}
```

Public responses never include download links. During a sale, `price` is the sale price and `sale` holds `{ name, discount, originalPrice, price }`.

A token with `products.view` gets every product, drafts included, with all fields.

## Checkout

| Endpoint                                 | What it does                                                                   |
| ---------------------------------------- | ------------------------------------------------------------------------------ |
| `GET /api/v1/checkout/config`            | Currency, cart, terms, sale, points and which gateways are on.                 |
| `POST /api/v1/checkout/coupon`           | Checks a coupon. Body: `{ code, items }`.                                      |
| `POST /api/v1/checkout`                  | Starts a checkout.                                                             |
| `POST /api/v1/checkout/webhook/:gateway` | Where gateways report payments. See [Checkout](/docs/prism/checkout#gateways). |

```json title="POST /api/v1/checkout"
{
    "items": [{ "productId": 1, "addons": [0] }],
    "provider": "stripe",
    "couponCode": "LAUNCH",
    "points": 0,
    "customer": { "email": "customer@example.com" }
}
```

`addons` are indexes into the product's `addons`. `provider` defaults to the primary gateway. The response holds `provider`, `mode` (`embedded` or `hosted`), and a `url` to send the customer to or a `clientSecret` for an embedded checkout. A free order returns `{ success, free: true, url }`.

## Management

These need an API token with the matching scope. Responses use `{ success, ... }` and errors are `{ error }` with an HTTP status.

| Endpoint                                      | Scope                                        |
| --------------------------------------------- | -------------------------------------------- |
| `POST /api/v1/products`                       | `products.create`                            |
| `GET`, `PATCH`, `DELETE /api/v1/products/:id` | `products.view`, `.edit`, `.delete`          |
| `GET`, `POST /api/v1/products/:id/updates`    | `products.view`, `.edit`                     |
| `GET`, `POST /api/v1/licenses`                | `licenses.view`, `.create`                   |
| `GET`, `PATCH`, `DELETE /api/v1/licenses/:id` | `licenses.view`, `.edit`, `.delete`          |
| `POST /api/v1/licenses/:id/activate`          | `licenses.edit`                              |
| `POST /api/v1/licenses/:id/deactivate`        | `licenses.edit`                              |
| `GET /api/v1/licenses/key`                    | `licenses.create`. Returns a new unused key. |
| `GET`, `POST /api/v1/users`                   | `team.view`, `.create`                       |
| `GET`, `PATCH`, `DELETE /api/v1/users/:id`    | `team.view`, `.edit`, `.delete`              |
| `GET`, `POST /api/v1/roles`                   | `team.view`, `.create`                       |
| `GET`, `PATCH`, `DELETE /api/v1/roles/:id`    | `team.view`, `.edit`, `.delete`              |

`/api/v1/customers` is the same as `/api/v1/users`. Lists take `search`, `limit` (up to 500) and `offset`.

```bash
curl -X POST https://shop.example.com/api/v1/licenses \
  -H "Authorization: Bearer prism_..." \
  -H "Content-Type: application/json" \
  -d '{ "product_id": 1, "user_id": 4, "addons": ["Extra"], "expires_at": null }'
```

## Health

| Endpoint                          | Returns                                                                                                              |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `GET /api/health` on the API port | `200` when Prism is running with the database up and no failed modules, otherwise `503`. Point uptime monitors here. |
