Skip to main content
The Reseller API lets an approved reseller browse a merchant’s wholesale catalog, check their balance, and buy stock programmatically — the same operations the Reseller tab of the customer portal exposes, as a machine interface. It is a separate API from the merchant Developer API. Different base path, different credential family, different key prefix. A merchant automating their own reseller program uses /dev/v1/resellers instead.
Wholesale orders are paid from your reseller balance, not with a card. Top up your balance in the portal before ordering — there is no payment step in this API.

Authentication

Send your key as a bearer token on every request:
Keys are created in the customer portal of the merchant’s store, under the Reseller tab. The full key is shown once at creation — Shoppex only stores its hash, so a lost key cannot be recovered, only replaced. Revoking a key is permanent. The key determines both the shop and the reseller identity. Passing a shop or reseller identifier in the request is not possible and any such parameter is ignored.
Returned for a missing or malformed Authorization header, an unrecognised key, and a revoked key. The message is always Invalid API key. — Shoppex does not distinguish the cases, so a revoked key cannot be used to probe which keys ever existed.
The reseller program of this shop is not active. when the merchant turned the program off. This reseller account is suspended. when the merchant suspended you. Any other non-active status returns This reseller account cannot use the API.
See Rate limits. Retry after the window in retry-after.

Errors

Every failure uses one envelope:
details appears only on VALIDATION_ERROR. error.balance appears only on INSUFFICIENT_BALANCE. Every response — success or failure — carries an x-request-id header. Quote it when reporting a problem to the merchant.
An order belonging to a different reseller returns ORDER_NOT_FOUND, not 403. Ownership is checked before existence, so the API never reveals that an order exists but is not yours.

Idempotency

POST /reseller/v1/orders requires an Idempotency-Key header. It is the only endpoint that requires one, and the only one that reads it.
Rules:
  • Same key, same payload → the stored response is replayed. No second order, no second charge. The response carries idempotency-status: cached, and data.order.reused is true.
  • Same key, different payload409 CONFLICT.
  • Same key while the first call is still running409 CONFLICT with idempotency-status: processing. Retry shortly.
  • Keys live 24 hours. After that the key is free again and a reuse creates a real order.
  • Maximum key length is 128 characters. X-Idempotency-Key is accepted as a synonym.
4xx responses are cached. A 402 INSUFFICIENT_BALANCE replayed under the same key returns the same 402 — even after you have topped up. Use a new Idempotency-Key after a top-up, otherwise the retry will keep failing for 24 hours.
Only 5xx and 429 responses are never cached, so a transient outage cannot permanently poison a key. Beyond the cache, the order itself carries an idempotency hash in the database, so a duplicate cannot be charged twice even if the cache is unavailable — and that backstop answers the same way the cache does: the original order for the same basket, 409 CONFLICT for a different one. The idempotency-status response header tells you which path ran: created, cached, processing, or bypassed.

Rate limits

A token bucket per key: 30 requests burst, refilling 10 tokens every 2 seconds. It applies uniformly to every endpoint, reads included. Rejected requests return 429 with code RATE_LIMITED. An invalid key never consumes a token.

GET /reseller/v1/me

Your identity, tier, and progress toward the next tier.
next_tier is null when no higher tier has an auto-advance threshold configured. Inside requirements, revenue and orders are independently nullable — the merchant may have configured only one of them. requires_both: false means either threshold alone advances you.

GET /reseller/v1/balance

available is balance - hold_balance and is the figure an order is checked against. A reseller who has never topped up reads all zeros.

GET /reseller/v1/products

The wholesale catalog: only products the merchant enrolled in the program, priced for your tier.
applied_override tells you which rung of the price cascade set your price: variant (a per-variant override), product (a per-product override), or tier (your tier’s flat discount). stock: -1 means unlimited. Check orderable, not stock, before you order a row. stock is the merchant’s own local stock, and when supplier_backed is true the merchant fulfils that product through their own supplier — the row carries no local units by design and its real quantity limit sits with that supplier, so this API does not report a number for it. A row with "orderable": false is the only one POST /reseller/v1/orders refuses as out of stock. On a product that has variants, the top-level orderable is a gate for the whole product: it is true when the product itself is orderable or at least one of its variants is. So filtering a listing on the top-level flag never hides something you could buy — but once a product is in, still check each variant’s own orderable to pick the option to order. supplier_backed is never aggregated: it always describes the exact row it sits on.

POST /reseller/v1/orders

Buys stock at wholesale price and pays from your balance in one step. Requires Idempotency-Key.
  • 1–100 items per order, each quantity an integer between 1 and 100000.
  • variant_id is optional; omit or send null for products without variants.
Returns 201:
Read the deliverables, not just the status. If the merchant ran short of stock, the order still completes for what could be delivered: the undeliverable lines come back with delivered: false and an empty serials array, partially_delivered becomes true, and refunded_amount carries the amount credited back to your balance. partially_delivered also becomes true when a line delivered fewer codes than it ordered, so compare serials against quantity per line rather than assuming an all-or-nothing line. refunded_amount is the total credited back to your balance for this order, whatever caused the refund. When your balance is too low, the call returns 402 and tells you the gap:
Top up in the portal, then retry with a new Idempotency-Key — the 402 is cached under the old one.

GET /reseller/v1/orders


GET /reseller/v1/orders/

One order with its delivered codes. This is the endpoint to poll or re-read when you need the serials again — they are the same codes a normal buyer would see.
serials is always an array, empty when nothing has been delivered for that line — never null. delivered is simply delivered_at !== null.

Conventions

  • Money is always a decimal string ("425.00"), never a float. Percentages are numbers.
  • Timestamps in data are ISO 8601 strings.
  • Lists return data plus pagination (page, per_page, total) — page-based, not cursor-based.
  • Field names are snake_case throughout.

Next Steps

Merchant reseller API

Automate the program from the merchant side.

Reseller webhooks

reseller:* events and their payloads.