/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. This API has no payment step.
Authentication
Send your key as a bearer token on every request:Open
https://<shop-domain>/customer-portal on the merchant’s storefront, sign in with your
customer account, and select the Reseller tab. You can create API keys and top up your
balance there — see the reseller panel guide. If needed, use
https://portal.shoppex.io as the central portal fallback.401 — invalid, unknown, or revoked key
Returned for a missing or malformedAuthorization 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.
403 — program disabled or account not active
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.
429 — rate limited
See Rate limits. Retry after the window inretry-after.
Errors
Every failure uses one envelope:details appears only on VALIDATION_ERROR. error.balance appears only on
INSUFFICIENT_BALANCE.
Every response carries an
x-request-id header, whether it succeeds or fails. 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.
- Same key, same payload → the stored response is replayed. No second order, no second
charge. The response carries
idempotency-status: cached, anddata.order.reusedistrue. - Same key, different payload →
409 CONFLICT. - Same key while the first call is still running →
409 CONFLICTwithidempotency-status: processing. Retry shortly. - A key that placed an order never expires. The cached response lives 24 hours. After that
the order itself still answers the key: the same basket returns the original order with
reused: true, a different basket returns409 RESELLER_ORDER_IDEMPOTENCY_CONFLICT. Use a new key for every new order. - Maximum key length is 128 characters.
X-Idempotency-Keyis accepted as a synonym.
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.
A duplicate cannot be charged twice even if the cache is unavailable. That backstop answers
the same way the cache does: the original order for the same basket, or 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 can configure
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
Each product includesdescription exactly as stored by the merchant, including HTML when present.
An unset description is null; an empty description remains "".
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. It is 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 local
stock. 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 can
buy. But once a product is in, still check each variant’s own orderable to pick the option
to order.
custom_fields lists the questions the merchant asks for this product, for example a Discord
server invite the delivery needs. Every order line for the product answers them in
items[].custom_fields (see below). All variants of a product share the same fields. The list
is empty when the product has none.
POST /reseller/v1/orders
Buys stock at wholesale price and pays from your balance in one step. RequiresIdempotency-Key.
- 1–100 items per order, each
quantityan integer between 1 and 100000. variant_idis optional. Omit or sendnullfor products without variants.custom_fieldsanswers the product’scustom_fieldsfromGET /reseller/v1/products, keyed byname. One set of answers covers the whole line, whatever the quantity. Omit it for a product without fields. Keys the product does not define are ignored. A missing required answer or an invalid one returns422withRESELLER_ORDER_CUSTOM_FIELDS_INVALID, and nothing is charged.
201:
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.
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:
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 sees.serials is always an array. It is empty when nothing has been delivered for that line, and
never null. delivered is delivered_at !== null.
Dynamic delivery URL
A personal delivery URL lets you sell a supplier’s serial keys in your own shop without holding stock. Your shop’s Dynamic Delivery calls the URL when a buyer pays; the call places a wholesale order at your tier price, paid from your reseller balance, and answers with the keys your buyer receives. The supplier switches the channel on (Partners → Resellers → Settings → Dynamic delivery, ordelivery_url_enabled on the Developer API reseller program). You create the URL in the
customer portal, Reseller tab, Dynamic delivery. The portal catalog lists the URL of every
product, with today’s wholesale price as max_unit_price, so a later price rise refuses the sale
before your balance is charged. The catalog stays readable while the supplier has portal
purchasing off. Only serial products
can be delivered this way.
Connect a Shoppex shop
In your own Shoppex dashboard, open Products → ⋯ → Import from Supplier and paste the Connect key from the portal. The supplier’s serial products arrive unlisted, priced at the supplier’s list price, with the delivery URLs and signing secret filled in. Each URL carries today’s wholesale price asmax_unit_price, so a later supplier price rise refuses the sale
before your balance is charged; edit the URL to accept the new price. Check the prices, then
publish. Running the import again only adds products that are new.
To set up a product by hand instead: set its type to Dynamic, paste the catalog row’s delivery
URL into Dynamic Delivery URL and the signing secret into Signing secret. A variant gets
its own URL with &variant_id=.
A supplier’s serial products that resellers can buy hold keys of at most 500 characters. The
supplier cannot add or save a longer key on such a product.
A Shoppex checkout sells at most 348 units of a product per order through a delivery URL, imported
or set by hand, whatever maximum quantity the product has. All keys of a line come back in one
answer, and 348 keys of 500 characters still fit the 1 MiB answer limit.
Call it from your own system
Any caller that follows the Dynamic Delivery request contract works:- Sign the exact body bytes you send:
HMAC-SHA256(secret, "<delivery id>.<timestamp>.<body>"), hex. The timestamp must be within 5 minutes of the server clock. - The body needs
quantity, a whole number of at least 1 (Dynamic Delivery sends the line’s quantity). String values incustom_fieldsanswer the supplier product’s fields of the same name; other keys are dropped. max_unit_priceis a ceiling per unit in the supplier’s currency. A higher price is refused before anything is charged.
Retries and failures
- Same delivery id → the same keys, charged once. Two calls at the same time place one order.
- Low balance →
402 INSUFFICIENT_BALANCEwith thebalanceblock, nothing charged. Top up and call again with the same delivery id; in a Shoppex shop that is Retry Delivery on the invoice line. - Out of stock or above
max_unit_price→409, nothing charged. - A key sold elsewhere between price check and delivery → the order is refunded to your
reseller balance and the call answers
409. A retry with the same delivery id after the supplier restocks orders again. - Revoked or replaced URL, wrong signature or old timestamp →
401, nothing charged.
Conventions
- Money is always a decimal string (
"425.00"), never a float. Percentages are numbers. - Timestamps in
dataare ISO 8601 strings. - Lists return
datapluspagination(page,per_page,total). Pagination is page-based, not cursor-based. - Field names are
snake_casethroughout.