# Reseller API

> Let approved resellers order at their tier price from their own bot or site: catalog, stock, wholesale orders, delivered keys and signed webhooks.

Source: https://shopfa.st/docs/guides/reseller-api

Resellers you approve in **Marketing → Resellers** buy in bulk at their tier price. The reseller API lets them do it
from their own Discord bot, Telegram shop or website instead of the wholesale portal: read the catalog with their
price and live stock, place an order, pay it on your normal checkout or from store credit, and fetch the delivered
keys. Every order is a normal order in your store: same fees, Fraud Shield, ledger and key delivery.

## Keys

Resellers create their own keys in their wholesale portal (**API and webhooks**). A key looks like
`rk_live_<id>_<secret>` and is shown once. It belongs to one reseller of one store, and only works on
`https://api.shopfa.st/v1/wholesale/…`. It cannot read anything else in your store, and it stops working when the
key is revoked (by the reseller or by you from the reseller's sheet) or when you suspend the reseller.

```http
GET /v1/wholesale/me
authorization: Bearer rk_live_…
```

| Scope | Lets the key |
| --- | --- |
| `catalog:read` | Read products at the reseller's price, with stock |
| `orders:read` | Read the reseller's own wholesale orders |
| `orders:write` | Place wholesale orders |
| `keys:read` | Fetch the keys delivered for a paid order |
| `webhooks:write` | Set the webhook endpoint for the reseller's orders |

`GET /v1/wholesale/me` works with any scope and returns the store, the reseller's tier and store credit balance, and
the key's own scopes.

## Endpoints

| Method | Path | Scope |
| --- | --- | --- |
| `GET` | `/v1/wholesale/me` | any |
| `GET` | `/v1/wholesale/catalog` | `catalog:read` |
| `GET` | `/v1/wholesale/orders?status=&limit=` | `orders:read` |
| `POST` | `/v1/wholesale/orders` | `orders:write` |
| `GET` | `/v1/wholesale/orders/{id}` | `orders:read` |
| `GET` | `/v1/wholesale/orders/{id}/keys` | `keys:read` |
| `PUT` | `/v1/wholesale/webhook` | `webhooks:write` |

### Catalog

```json
{
  "currency": "USD", "tierName": "Gold", "discountBps": 2000,
  "items": [
    { "productId": "prd_…", "name": "Ironveil Pro", "type": "license_key", "listPriceMinor": 5000, "wholesalePriceMinor": 4000, "stock": 12, "minQuantity": 2 }
  ]
}
```

Money is in minor units of the store's currency. `stock` is the number of available keys for keys and serials,
counted stock for limited products, and `null` when unlimited.

### Place an order

```http
POST /v1/wholesale/orders
authorization: Bearer rk_live_…
content-type: application/json

{ "productId": "prd_…", "quantity": 25, "payWith": "checkout", "reference": "po-1042" }
```

- `quantity` must reach the tier's minimum.
- `payWith: "checkout"` (default) returns the order with a `payUrl`: the store's normal checkout, valid for a day.
  Unpaid orders are cancelled after 7 days.
- `payWith: "store_credit"` pays at once from the reseller's store credit (`balance.storeCreditMinor` in
  `/me`). If the credit does not cover the whole order, nothing is created and you get `409 credit-insufficient`.
- `reference` is your own id (1–64 letters, digits, `.`, `:`, `-`, `_`). Sending the same reference again returns
  the same order instead of a second one, so retries are safe.

```json
{
  "id": "rso_…", "productId": "prd_…", "productName": "Ironveil Pro", "quantity": 25, "unitPriceMinor": 4000, "totalMinor": 100000,
  "currency": "USD", "status": "awaiting_payment", "payWith": "checkout", "payUrl": "https://lumenfold.shopfa.st/checkout/cs_…",
  "orderNumber": null, "keysDelivered": 0, "reference": "po-1042", "createdAt": "2026-10-21T14:02:11.000Z"
}
```

`status` moves `awaiting_payment` → `paid` → `fulfilled` (keys delivered), or `cancelled`.

### Fetch the keys

```http
GET /v1/wholesale/orders/rso_…/keys
authorization: Bearer rk_live_…
```

```json
{ "orderId": "rso_…", "keys": [{ "productName": "Ironveil Pro · 30 Days", "value": "IRVL-30D-8K2M-Q4XZ" }] }
```

Keys are opened for this answer only. A refunded order answers `409 refunded`; an order the store is reviewing
answers `409 held` until it is released.

## Webhooks

```http
PUT /v1/wholesale/webhook
authorization: Bearer rk_live_…
content-type: application/json

{ "url": "https://your-bot.example/shopfast", "events": ["wholesale_order.paid", "wholesale_order.fulfilled"], "rotateSecret": false }
```

The first save (and `rotateSecret: true`) returns a signing secret `whsec_…` once. Events:
`wholesale_order.created`, `wholesale_order.paid`, `wholesale_order.fulfilled`, `wholesale_order.cancelled`.
Each body is `{ "type", "timestamp", "data": <the order> }`, signed with
[Standard Webhooks](https://shopfa.st/docs/guides/webhooks) (`webhook-id`, `webhook-timestamp`, `webhook-signature`). Keys are never
sent in a webhook: fetch them with `keys:read`. Failed deliveries are retried after 30 s, 1, 2, 4 and 8 minutes.

## Limits, errors and audit

- 120 requests a minute per key. Over it: `429` with `Retry-After`.
- Up to 5 active keys per reseller.
- Errors are [problem details](https://shopfa.st/docs/guides/errors): `401 invalid-reseller-key`, `403` for a missing scope or a
  suspended account, `404` for another reseller's order, `409 below-minimum`, `409 out-of-stock`,
  `409 credit-insufficient`.
- Every call (accepted or refused) is kept for 30 days. Resellers see their recent calls in the portal; you see them,
  with every key and its last use, on the reseller's sheet in **Marketing → Resellers**, where you can revoke any key.

## Example

```ts
const BASE = 'https://api.shopfa.st/v1/wholesale';
const headers = { authorization: `Bearer ${process.env.SHOPFAST_RESELLER_KEY}`, 'content-type': 'application/json' };

const { items } = await (await fetch(`${BASE}/catalog`, { headers })).json();
const ironveil = items.find((p) => p.name === 'Ironveil Pro');
const order = await (await fetch(`${BASE}/orders`, {
  method: 'POST', headers, body: JSON.stringify({ productId: ironveil.productId, quantity: 10, payWith: 'store_credit', reference: `restock-${Date.now()}` }),
})).json();
// After the wholesale_order.fulfilled webhook:
const { keys } = await (await fetch(`${BASE}/orders/${order.id}/keys`, { headers })).json();
```
