Create an order

POST/api/v1/orders
Required scope:orders:write

Buy one product and pay for it out of your wallet. cdkey (a game key) and account1 (a never-used game account) are delivered straight away, inside this request. account2 (a pre-owned account) is delivered later, over your callback or by reading the order again, because the account has to be checked before we buy it and that takes far longer than one request can wait, so this call only takes the order and queues it. Every reply carries an order_id — always keep it, because it is the only way back to the order. If a request times out or you are unsure it completed, send the identical request again with the same request_id, never a fresh one.

The same request_id returns the same answer, never a second order

Replaying the same request_id with the same request body returns 200 with the existing order; nothing is ordered a second time · Replaying the same request_id with a different type, product_id, item_id or expected_price returns 409 idempotency_conflict, and the existing order is not touched at all · Those four fields are the whole fingerprint; no other field in the body affects it · When a response is lost in transit, replay the identical request. That is the entire reason this field exists — you will either get the existing order back, or the order will be created for the first time, never both · Changing request_id in order to try again places a second order, which means paying twice whenever the first one had actually succeeded.

How much an order was charged is answered in one place: charged

The same error code can reach you through paths with different payment histories, and the HTTP status does not tell you which one you got — a price_changed answered live at ordering time happens before any money moves, while the same code on an account2 order the queue closed as failed comes from a path that had already passed the reservation step; supplier_failed on a pre-owned account usually happens at the account-check step, which is before any charge at all · Do not infer it from the code or from the status: reconcile from that order's charged only — it is null while the order carries no record of a charge, and an object once it does.

queued carries no timing guarantee, and the figures below describe the whole system

We buy pre-owned accounts one order at a time, in the order they arrive, from a single queue shared by every reseller — not several at once · A typical order has an answer in about two minutes, because the first account check of an order takes longer than one pass of that queue can finish · The realistic ceiling is therefore around 24 orders per hour for the entire system, not per reseller, and not a best-case figure · The queue gives nobody a reserved share and has no length limit: submitting many account2 orders at once slows down everybody's orders, including your own · An order whose check or purchase does not succeed is retried for you with a growing gap between attempts, at most 5 times, and is then closed as failed · An attempt only counts when we actually work on that order; time spent waiting for its turn does not count, so five attempts is not an upper bound on how long an order can wait.

The authentication refusals reach this endpoint too

The five refusals at the authentication gate — invalid_key (401) · key_revoked (401) · insufficient_scope (403) · quota_exceeded (429) · too_many_inflight (429) — can happen on every endpoint, this one included, because they are thrown before the request reaches a single line of this endpoint's own logic (the scope it needs is the one declared at the top of this page). The response tabs above therefore list only what this endpoint itself answers; that is not a claim that those five cannot happen here. All of the detail lives in one place, on the identity endpoint page at /developers/access/me: the HTTP status of each one, which of them spend your daily quota, which headers come back, and why the two 429s need opposite handling. It is deliberately not copied onto every page, because the second copy is the one nobody remembers to update the day the rules change.

Request parameters

Request body

NameTypeHow to use it
typeRequiredcdkey · account1 · account2
Which of the three product types you are buying, and the single field that decides when you get the goods: cdkey and account1 are bought inside this request, so they answer 201 (finished) or 202 (charged, goods seconds behind), while account2 has to be checked before we buy it, so it always answers 202 with status set to queued and is delivered later
Details and caveats
  • it is also the only type that needs an item_id
  • Anything outside those three values is rejected outright
  • Every string value in the request is trimmed before it is validated, and a value that is empty after trimming counts as missing.
product_idRequiredstring, at most 200 characters, only A-Z a-z 0-9 _ -
The product id taken from the catalogue, the id field of a product · A value that breaks the format is refused with bad_request before anything touches the database.
item_idOptionalsame format as product_id
Required for account2 and for nothing else: the one pre-owned account you picked from GET /api/v1/products/{product_id}/items
Details and caveats
  • one account2 product has many individual accounts for sale, and the price and the availability live on the individual account rather than on the product, so you have to say which one you want
  • Sent alongside cdkey or account1 it is ignored, not rejected, and it does not take part in the request fingerprint either.
expected_priceRequirednumber, or a numeric string of at most 40 characters
The exact price in THB you are willing to pay, copied from the catalogue response for this same product or listing; the server re-prices on every request and compares the two exactly, and if they differ it refuses with price_changed instead of charging you a different amount
Details and caveats
  • That is what binds the price you quoted to your own buyer to the price you are actually charged
  • Take it from GET /api/v1/products/{product_id} for cdkey and account1, and from the individual listing for account2
  • the list endpoint skips the per-product price floor, so its price can be too low.
request_idRequiredstring, at most 200 characters
Your own idempotency key, one unique value per real order (a UUID, say), stored on your side: sending it again returns the order you already have rather than buying a second one
Details and caveats
  • The value never expires, and it is scoped to the API key that sent it, not to your account, so the same value sent from a different key becomes a different order.

Header

NameTypeHow to use it
AuthorizationRequiredstring
Your key as Bearer <key>, required on every request · A key holding only orders:write can place orders but cannot follow them up afterwards, so ask for orders:read as well.
Content-TypeRequiredstring
Must be application/json, because a body declared as anything else is read as plain text or as a form and the request fails with 400 bad_request even when the JSON itself is valid to the letter
Details and caveats
  • Watch out for curl -d, which declares a form content type of its own accord unless you tell it otherwise.

Response fields

NameTypeHow to use it
order_idstring or null
The order id: ord_ followed by 24 hexadecimal characters
Details and caveats
  • Derived deterministically from the key and the request_id, so replaying the same request always yields the same value
  • Store it: it is the only way back to this order directly.
statusstring or null
Where the order stands: queued = accepted, waiting for us to buy it (account2 only)
Details and caveats
  • processing = in progress, goods not here yet
  • completed = delivered, read delivery
  • failed = unsuccessful, read error.code
  • refunded = the order was refunded afterwards
  • These five are the values that exist today, not a closed set, so keep a default branch for a value you do not recognise and never treat an unfamiliar one as either success or failure
  • Each value is described in full, including the ones that can be permanent, on the single-order page (/developers/orders/get).
typestring or null
The type you ordered — this is what tells you which shape delivery takes.
product_idstring or null
The product you ordered.
pricenumber or null
The agreed price in THB, equal to the expected_price you sent.
chargedobject or null
{ amount, currency }, what was actually taken from your wallet, and the only answer to the question of whether this order was paid for: it is null while the order carries no record of a charge and an object once it does
Details and caveats
  • currency is your wallet's currency while price is always THB, so the two differ whenever your wallet is not in baht
  • do not reconcile by comparing these two fields directly
  • The field only appears once both the amount and the currency were recorded; half of a record counts as none.
deliveryobject or null
The goods, present only while status is completed, and it comes in two shapes that share no field at all: cdkey and account1 give you { keys: [{ serial }] }, while account2 gives you login, password, email, email_password and extra
Details and caveats
  • Branch on the type field of this same order, never on the contents
  • branching the wrong way reads undefined across the board, which looks exactly like the goods not having arrived
  • Both shapes are described field by field on the single-order page (/developers/orders/get).
errorobject or null
Why it did not succeed.
Details and caveats
  • Present only while status is failed, carrying { code, message }, and its code must always be read together with charged, because the same code can reach you at different moments
  • at some of them no money has moved, at others it moved and came back
  • The vocabulary is the same one the status table below uses.
created_atISO 8601 string (UTC) or null
When the order was created.
updated_atISO 8601 string (UTC) or null
When the order was last modified.

Possible statuses

StatusCodeMeaning
200
Replay — the order you already have, not a new purchase
You sent the identical request again (after a lost response, say), so you get the existing order back exactly as it stands right now
Details and caveats
  • in this example an account2 order we finished buying in the meantime
  • No second purchase happened, and order_id is always the same value.
201
Finished inside this request — nothing to follow up
The purchase already ran to completion, so you can read the result straight off this response: no callback to wait for and no need to read the order again
Details and caveats
  • status is completed (delivered, read delivery), failed (unsuccessful, read error.code) or refunded
  • Only cdkey and account1 can answer this way, because they are the two types we can deliver immediately.
202
Charged, goods not here yet — read the order again
The order has a charged but delivery is still null: we took the money and placed the order, but the key did not arrive in time for this response, which is normal, not a failure
Details and caveats
  • What to do is call GET /api/v1/orders/{order_id} periodically until status is completed, then read delivery
  • and do not place a new order
  • ⚠️ Reading it again is not a guarantee that it will move: a cdkey or account1 order can sit at processing and never move on its own, even though you have paid and the goods may already have reached us
  • If that happens, contact us with the order_id, exactly as you would for an order that is completed with an empty delivery.keys, and still never place a new order, because a new order means paying twice.
202
Non-baht wallet — price is THB, charged is your wallet's currency
Same situation as the row above, shown for a wallet that is not in baht
Details and caveats
  • price is the agreed price and is always in THB whatever your wallet holds, while charged is what actually left the wallet and therefore carries your wallet's currency
  • In this example price is 450 THB and charged is 12.86 USD: the same money in two units, not two charges and not a discrepancy
  • Reconcile from charged alone and never subtract one of these fields from the other.
202
Queued — we will buy the pre-owned account and tell you afterwards
account2 always answers this way with status set to queued, because a pre-owned account has to be checked before we buy it (we sign in and confirm the account really works), and that takes far longer than one request can wait
Details and caveats
  • There is nothing more for you to do than keep the order_id and wait for the result, either by setting up a callback so we tell you when it finishes (/developers/webhooks/callback) or by calling GET /api/v1/orders/{order_id} periodically
  • Once status is completed, the account details are in delivery
  • The charge for this type happens when we actually buy, not in this request, so read charged on the order for the answer
  • Do not place a new order while you wait, and do not build your side around waiting synchronously.
400bad_request
The body did not pass validation
Fix the request and send it again under the same request_id
Details and caveats
  • The message changes with the field at fault and has many forms, among them Request body must be a JSON object
  • request_id is required
  • type must be one of: cdkey, account1, account2
  • product_id is required
  • product_id has an invalid format
  • item_id has an invalid format
  • expected_price is required and must be a number
  • branch on code only
  • The code unsupported_type shares this vocabulary but does not come from the ordering path: it comes from the market-listing endpoint when the product is not an account2
  • bad_request can also reach you as error.code on an order that closed as failed, by a different route than the live 400 you see here
  • the live one is refused before an order exists, while the one on an order belongs to an account2 that was already queued and whose stored values the background job could not read, or which was refused with a 400 while the job worked on it
  • Those orders arrive from several stages with different payment histories, so read charged on the order for the answer, never the code.
409idempotency_conflict
The same request_id with a different body
The existing order is not touched at all, so read it before deciding whether to place a new order under a fresh request_id · This code never shows up on an order.
404product_not_found
No such product, or it is not on sale
Refresh from the catalogue and pick another one
Details and caveats
  • A product that was withdrawn and a product that never existed answer identically, on purpose
  • This code can also show up as error.code on an order that closed as failed.
404item_not_found
The account2 listing you picked has gone
That one was bought by someone else or withdrawn, so fetch the listings again and pick another
Details and caveats
  • This code never shows up on an order, because it is refused before any order is created.
409price_changed
The price no longer matches what you sent
The fresh price is in data: price is the value to send as expected_price on a new order, which needs a new request_id, because a new price is a different order
Details and caveats
  • The same code can also appear as error.code on an account2 order the queue closed as failed, which is a different path entirely
  • read that order's charged to see how far the money got.
409out_of_stock
Sold out, or someone else took it first
Pick another one · This code can also show up as error.code on an order that closed as failed.
409price_unavailable
We cannot price it
The product exists but cannot be sold through the API, so skip it and tell us
Details and caveats
  • do not drop it from your own catalogue the way you would for product_not_found
  • An account2 order whose chosen listing cannot be priced answers with the same body but the message Item is not available through the API
  • This code never lands on an order: pricing runs before an order is ever created, so there is nothing to record it on, and it is not among the codes that can be recorded on an order in the first place.
409agreed_price_invalid
The agreed price failed validation at charging time
Get a fresh quote from the catalogue and order again under a new request_id
Details and caveats
  • The sentence in message describes the path this code takes most often
  • When the same code reaches you as error.code on an order that has already closed, treat that order's charged field as the answer, not that sentence.
404wallet_not_found
No wallet was found for this account
Contact us · This code can also show up as error.code on an order that closed as failed.
400insufficient_points
The wallet balance does not cover this order
Top up and order again
Details and caveats
  • Your current balance is readable from GET /api/v1/me
  • This code can also appear as error.code on an order the queue closed as failed; read that order's charged alongside it, every time.
400unsupported_currency
Your wallet currency cannot be used for this order
Contact us · This code can also show up as error.code on an order that closed as failed.
502supplier_failed
The order could not be placed with our distributor
Always read charged on the order before deciding to retry
Details and caveats
  • This code can also show up as error.code on an order that closed as failed, and it reaches you both through paths where money moved and paths where it never did.
500order_save_failed
We bought it but failed to record it — the one code you must never retry
We charged the wallet and placed the order, but the step that records the result on the order failed
Details and caveats
  • This is the one code on this page you must never retry, 5xx or not, so a client configured to retry every 5xx automatically needs an exception for it, and the way out is to contact us with the order_id
  • The same code can reach you as error.code on an order that closed as failed, and the never-retry rule applies there word for word
  • this request can run for tens of seconds, so its 500 can be lost in transit and the same code then reaches you when you read the order or when a callback arrives
  • Applying the rule to the synchronous failure alone is not enough: doing that means re-submitting an order that really was paid for and really was placed
  • And these orders are the one exception where charged cannot answer the money question for you
  • recording is precisely what failed, so the order can carry null there while the money has left and the goods have been bought (the message for this code says so outright)
  • Contact us with the order_id and wait for our answer rather than reconciling from this order yourself.
500unknown
Closed for a reason we do not classify
Stop and let a human look
Details and caveats
  • an order may already exist, so read it with the single-order endpoint, or find it through the order list if you cannot work out the order_id, and look at charged before you decide anything
  • This code can also reach you as error.code on an order, and it is what we record in place of any code that cannot be recorded: an order that closed as failed for a reason we do not classify carries this one, so it does not mean nothing happened and it does not mean no money was taken
  • The message that accompanies any code is English prose we write ourselves, so never branch on the message text, only on code
  • wording changes while codes stay put
  • Codes outside this table are always possible in future: treat an unrecognised code the way you treat unknown, rather than retrying automatically.
Last updated 2026-09-07