Orders: create, track, and handle every ending
One endpoint creates orders, two read them back. The interesting part is not the call — it is the set of states an order can reach, and which of them you have to write code for.
Creating an order
POST /b2b/orders X-Api-Key: gc_live_… X-Idempotency-Key: 7f3c1a9e-2b44-4d1a-9d4e-1c0f9a2b3c4d Content-Type: application/json
{"items": [{ "productId": 34521, "quantity": 1, "deliveryData": { "gameUserId": "5123456789" } }],"externalOrderId": "MP-98765","callbackUrl": "https://api.yourshop.com/webhooks/gamecore"}
| Field | Required | Rules |
|---|---|---|
items | yes | 1 to 200 entries. Empty is a 400. |
items[].productId | yes | Numeric catalog id. |
items[].quantity | no | Defaults to 1; whole units, 1 to 10000. |
items[].deliveryData | yes | String-to-string map. Send {} when the product needs no fields — the key itself is not optional. |
externalOrderId | no | Your own reference. Comes back on both read endpoints and in the webhook — but not in the create response, which returns codes only. Record your own mapping at creation time. |
callbackUrl | no | Where the webhook goes. Validated at creation. |
X-Idempotency-Key is required; without it you get a 400. That check is the handler's first act, so it runs after your key is authenticated and after the body has been validated against the schema — an unauthenticated call is a 401 and a malformed body is a 422, whatever the header says. There is no price field: we compute the price at this moment, from the current rate and your volume tier, which is applied here and nowhere in the catalog.
Everything that follows the call is one database transaction: the charge against your balance, the payment record and the orders are committed together or not at all. There is no state in which your balance moved but the order does not exist.
One request, several orders
{"success": true,"data": {"paymentCode": "P-AB12CD","totalAmount": 2129.0,"orders": [{ "code": "ash-XY7K3M", "gameId": "pubg-mobile", "gameName": "PUBG Mobile", "total": 1064.5, "itemCount": 1 },{ "code": "ash-QW4N8P", "gameId": "free-fire", "gameName": "Free Fire", "total": 1064.5, "itemCount": 1 }]}}
Items are grouped by the supplier's game and by the supplier behind it — not by the game as you see it in the catalog — and each group becomes its own order with its own code and its own lifecycle. A basket spanning three games therefore produces at least three orders that succeed and fail independently, and a single catalog game can produce more than one when its SKUs come from more than one upstream source. Never assume one request means one order, not even for one game.
Two consequences worth designing for from the start: your data model needs a one-to-many relation between your own order and ours, and orders must be iterated even when you expect a single entry.
An order code is a short per-account prefix plus six characters from an alphabet with no O, 0, 1, I or L — ash-XY7K3M. It is safe to read aloud to a customer. Persist every code; it is the handle for every later call.
Paying for an order
Orders are paid from your B2B balance, in one of two modes agreed at onboarding:
- Prepaid — the charge must fit inside your balance.
- Overdraft — the balance may go negative down to your credit limit.
If it does not fit, nothing is created and the call returns 402 with the reason: Insufficient balance, or Exceeds credit limit (available: …) naming what is left. Both are business conditions rather than errors in your request: retrying the identical call after a top-up, with the same idempotency key, is exactly the right move.
A third message shares that status and means something else entirely. Concurrent balance modification says your balance was changed by something else — usually one of your own parallel order calls — while this one was committing; the transaction rolled back, so no money moved and no order exists. There is nothing to top up — pause briefly and repeat the identical request with the same key. Creating orders one at a time per account keeps it rare. Idempotency and errors has the full retry table.
Order statuses
| Status | Meaning | Terminal |
|---|---|---|
pending | Created, not yet charged. Lives for milliseconds inside the transaction — you will not normally observe it. | no |
processing | Charged and handed to the supplier. The normal answer right after creation. | no |
completed | Every item in the order was delivered. | yes |
failed | The order did not complete — including the case where only some items did. | yes |
Two more values, cancelled and refunded, exist in the underlying column but are not reachable through this API: there is no cancel endpoint and no partner-triggered refund. Handle them defensively if you like, but do not build a flow that waits for them.
Item statuses
Item status is separate from order status and has a wider vocabulary:
| Item status | Meaning |
|---|---|
pending | Accepted, not resolved yet. Also what you see while an item is queued for a retry. |
queued | Deferred to our retry queue because the supplier was unavailable. |
awaiting_code | Waiting on an out-of-band code. |
awaiting_screenshot | Waiting on manual confirmation. |
completed | Delivered. cdKeys is populated here, if the product has keys at all. |
failed | Not delivered. errorCode and errorMessage explain why. |
Treat any item status you do not recognise as "still working". Code that branches only on completed and failed waits forever on awaiting_code, and that wait is invisible until a buyer complains.
Reading one order
GET /b2b/orders/ash-XY7K3M X-Api-Key: gc_live_…
{"success": true,"data": {"code": "ash-XY7K3M","externalOrderId": "MP-98765","gameId": "pubg-mobile","gameName": "PUBG Mobile","status": "completed","totalAmount": 1064.5,"createdAt": "2026-08-18T18:30:58.114Z","completedAt": "2026-08-18T18:31:15.432Z","items": [{"id": 991204,"productName": "PUBG Mobile 660 UC","amount": 1,"price": 1064.5,"status": "completed","cdKeys": [],"errorCode": null,"errorMessage": null,"completedAt": "2026-08-18T18:31:15.432Z"}]}}
An unknown code returns 404 with { "success": false, "error": "Order not found" } — and so does another partner's code, since lookups are scoped to your account.
cdKeys is returned only for items in completed. On every other status it is [], which means "not yet", not "none". For an id_only product it stays [] forever, and that is the correct final state.
Listing orders
GET /b2b/orders?status=failed&page=1&limit=100
| Parameter | Default | Notes |
|---|---|---|
page | 1 | 1-based. |
limit | 20 | Clamped to 100. |
status | — | Exact match against the order status. An unknown value returns an empty page rather than an error. |
Sorting is fixed: newest first by creation time. The response carries pagination with page, limit and total, and each row is the order without its items — fetch the single order when you need the lines.
This endpoint is for reconciliation, not for delivery. It is the right tool for "show me everything that failed in the last hour"; it is the wrong tool for finding out whether one specific order is done.
Partial delivery
An order with several items can end with some delivered and some not. What happens then is the least intuitive part of this API, so it is spelled out:
- The order status becomes
failed, not "partially completed". There is no partial status. The delivered items are still delivered and still in the response withstatus: "completed". - No webhook is sent. Webhooks are emitted when every item completed, or when every item failed. A mixed outcome emits nothing — if you rely purely on webhooks, a partial order is silent.
- The failed lines are credited back to your balance automatically. The value of the delivered lines is not: you keep the goods and you paid for them.
The practical rule: a reconciliation sweep is not optional. Poll orders that are still processing after a few minutes, and always read the item array rather than trusting the order status alone — failed at the order level can still mean the buyer received most of the basket.
When an order does not move
Two different situations look the same from outside, and only one resolves on its own.
The supplier is unreachable. The item goes into a retry queue and is retried on a ladder of roughly one minute, five minutes, fifteen minutes, one hour and six hours, up to ten attempts. If the queue gives up, the item is marked failed, the order resolves, and your balance is credited back. You see nothing but processing in the meantime.
The supplier accepted the order and never resolves it. We poll them roughly every minute for as long as it takes. There is no wall-clock timeout that fails such an order for you: it stays processing indefinitely and your balance is not credited back automatically.
So an order that has been processing for hours is not something to keep polling. Alert on it — a threshold of an hour is reasonable for automated delivery types — and raise it with us with the order code. Manual delivery types legitimately take longer; set their threshold separately.
Polling or webhooks
Use webhooks as the primary channel and polling as the backstop. Neither alone is enough: webhooks are silent on partial orders, and polling at any sane interval is slower than a push.
A shape that works:
- Set
callbackUrlon every order and act on the event when it arrives. - Sweep every few minutes over your own orders that are still open and older than a couple of minutes, and read each one back.
- Do not poll a single order in a tight loop. Its status cannot change faster than our own supplier poll, roughly once a minute, and your budget is 500 requests per minute across your whole integration.
- Make the two paths idempotent against each other. The same order will frequently be resolved twice — once by the webhook, once by the sweep — and both must be safe.
Before you go live
- Your model maps one of your orders to many of ours, keyed by our
code. failedat the order level is inspected item by item before you tell a buyer anything.- Partial orders are detected by your sweep, because no webhook will announce them.
- Unknown item statuses are treated as in progress.
cdKeys: []on a completedid_onlyitem is a success path, not an error path.- An order stuck in
processingpast your threshold raises an alert rather than more polling. - Balance
402s are retried after a top-up with the same idempotency key, not with a new one.
Updated August 18, 2026
See also: Supply API overview · Pricing and tiers
Need an API key?
Tell us which games and regions you sell and we will issue a key, then walk the integration with you. Integration questions are answered by the same people who run the API.