Documentation

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"
}
FieldRequiredRules
itemsyes1 to 200 entries. Empty is a 400.
items[].productIdyesNumeric catalog id.
items[].quantitynoDefaults to 1; whole units, 1 to 10000.
items[].deliveryDatayesString-to-string map. Send {} when the product needs no fields — the key itself is not optional.
externalOrderIdnoYour 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.
callbackUrlnoWhere 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 Lash-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

StatusMeaningTerminal
pendingCreated, not yet charged. Lives for milliseconds inside the transaction — you will not normally observe it.no
processingCharged and handed to the supplier. The normal answer right after creation.no
completedEvery item in the order was delivered.yes
failedThe 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 statusMeaning
pendingAccepted, not resolved yet. Also what you see while an item is queued for a retry.
queuedDeferred to our retry queue because the supplier was unavailable.
awaiting_codeWaiting on an out-of-band code.
awaiting_screenshotWaiting on manual confirmation.
completedDelivered. cdKeys is populated here, if the product has keys at all.
failedNot 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
ParameterDefaultNotes
page11-based.
limit20Clamped to 100.
statusExact 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 with status: "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 callbackUrl on 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.
  • failed at 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 completed id_only item is a success path, not an error path.
  • An order stuck in processing past 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.

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.