Docs
Dashboard
View as Markdown
On this page
  1. Keys
  2. Endpoints
  3. Catalog
  4. Place an order
  5. Fetch the keys
  6. Webhooks
  7. Limits, errors and audit
  8. Example

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" }
  • 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.
{
  "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: 429 with Retry-After.
  • Up to 5 active keys per reseller.
  • Errors are problem details: 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

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();
Was this page helpful?