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.
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.
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
{
"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
POST /v1/wholesale/orders
authorization: Bearer rk_live_…
content-type: application/json
{ "productId": "prd_…", "quantity": 25, "payWith": "checkout", "reference": "po-1042" }
quantitymust reach the tier’s minimum.payWith: "checkout"(default) returns the order with apayUrl: 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.storeCreditMinorin/me). If the credit does not cover the whole order, nothing is created and you get409 credit-insufficient.referenceis 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.
{
"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
GET /v1/wholesale/orders/rso_…/keys
authorization: Bearer rk_live_…
{ "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
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 (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:
429withRetry-After. - Up to 5 active keys per reseller.
- Errors are problem details:
401 invalid-reseller-key,403for a missing scope or a suspended account,404for 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
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();