Documentation

Catalog: games, categories and products

The catalog is five GET endpoints under /b2b/catalog. All of them are reads and all of them require your X-Api-Key. What they do not share is one notion of "visible to you": each applies a different subset of your account's rules, and that difference decides which endpoint you can base a buy decision on.

EndpointHidden gameProduct hideDelivery-type blockRegion allowlistYour SKU allow-listImage-upload SKU
GET /gamesremovedcount onlycount onlycount onlynocount only
GET /games/{slug}404nonononono
GET /games/{slug}/productsempty 200yesyesyesyesexcluded
GET /products/{id}404yesyesnono404
GET /searchremovednonononono

"Count only" means the rule is applied to productCount — and therefore to inStockOnly — but to nothing else in that response. The SKU allow-list column matters only if your account is configured with an explicit per-account SKU list; most are not, and for them that column is moot.

The hidden-game column is the one rule that answers in three different shapes, so read it literally rather than as "hidden means 404". A game hidden for your account is removed from /games and from /search; it is a 404 on /games/{slug} and a 404 on /products/{id} for any SKU under it; but on /games/{slug}/products it is a 200 with an empty data array. That endpoint has no game-level gate at all — its only 404 is an unrecognised or merged slug — and the hide reaches it by filtering the product list, which for a hidden game filters every row away. An empty data there is therefore ambiguous by construction: "hidden for you" and "this game genuinely has nothing you can order" arrive as the same bytes. When you need to tell them apart, ask /games/{slug}: a 404 means hidden, a 200 means visible and simply empty for you.

Read the table as one sentence: only the products endpoint tells you what you can actually order. The game list comes closest — a game whose entire stock is blocked for you counts 0 and drops out of inStockOnly — but a count is not a SKU. The game detail applies exactly one rule, the hidden-game check: its category list is not filtered for you at all, so a category listed there can be empty for your account. Search applies that same single rule. A hit in either means "this game exists", not "you can sell it".

The three levels

game            PUBG Mobile                 slug: pubg-mobile
└── category    UC (Global)                 id: 8812
    └── product PUBG Mobile 660 UC          id: 34521
LevelIdentified byWhat it is
Gamenumeric id and a stable slugThe title your buyer searches for. Deduplicated across suppliers: one game, whatever number of upstream sources sit behind it.
Categorynumeric idA grouping inside one game — usually a region or a denomination family. Returned only inside the game detail.
Productnumeric idThe SKU you actually order: one denomination, one price, one delivery type.

Games are addressed by slug in URLs; products by id. Categories are not addressable on their own — they exist to let you filter a game's products.

Listing games

GET /b2b/catalog/games?page=1&limit=200
X-Api-Key: gc_live_…
ParameterDefaultNotes
page11-based.
limit50Clamped to 200.
qCase-insensitive substring over name and slug.
inStockOnlytrueOnly the exact string false turns it off.
sortby stocksort=name sorts alphabetically; anything else sorts by product count, descending.
typeSee the trap below.
{
"success": true,
"data": [
{ "id": 4412, "slug": "pubg-mobile", "name": "PUBG Mobile", "icon": "https://…", "type": "game", "productCount": 84 }
],
"pagination": { "page": 1, "limit": 200, "total": 1893 }
}

The type parameter and the type field are not the same vocabulary. type=topup and type=cdkey select by fulfilment family — how the SKU is delivered upstream. Any other value is matched against the game's own kind, the one you see in the response: game, gift_card, software, dlc, currency, subscription, topup. So type=gift_card filters on the field, while type=cdkey does not.

productCount is computed for your account, and inStockOnly uses it — a game whose entire stock is outside your regions or hidden for you reports zero and drops out of the list entirely.

One game and its categories

GET /b2b/catalog/games/pubg-mobile
{
"success": true,
"data": {
"id": 4412,
"slug": "pubg-mobile",
"name": "PUBG Mobile",
"icon": "https://…",
"description": "…",
"type": "game",
"categories": [
{ "id": 8812, "name": "UC (Global)", "slug": "uc-global" }
]
}
}

An unknown slug returns 404 with { "success": false, "error": "Game not found" }. So does the slug of a game that has since been merged into another one — there is no redirect, by design: a silent 301 would let your catalog drift onto a title you never chose to sell. Treat a 404 on a slug you used to sync as "re-resolve this game", not as "retry later".

Categories are deduplicated by slug across the sources behind the game, so the id you get back is one of possibly several equivalent upstream categories. Use it only as a filter value, never as a durable key of your own.

They are also not filtered for your account. A 200 here means the game is not hidden from you and nothing more: the category list is the game's full active set, so a category can be listed for you and hold nothing you may order. Do not build a buyable-category menu from this response — build it from the distinct categories of the products endpoint below.

The products of a game

GET /b2b/catalog/games/pubg-mobile/products?deliveryType=id_only&categoryId=8812
{
"success": true,
"data": [
{
"id": 34521,
"name": "PUBG Mobile 660 UC",
"slug": "pubg-mobile-660-uc",
"wholesalePrice": 1064.5,
"currency": "RUB",
"deliveryType": "id_only",
"region": "global",
"platform": null,
"amountType": { "type": "fixed", "value": 660 },
"inStock": true,
"deliveryDataSchema": [
{ "id": "gameUserId", "type": "text", "label": "Player ID", "required": true }
]
}
]
}

Three things this endpoint does not do, each of which has surprised somebody:

  • It does not paginate. There is no page, no limit and no pagination block — you get the whole (already filtered) list for that game. Size your parser accordingly.
  • It does not fuzzy-match filters. categoryId and deliveryType are exact equality, and deliveryType is case-sensitive. deliveryType=ID_ONLY matches nothing.
  • It does not show you everything that exists. SKUs whose delivery form asks for an image or screenshot are excluded from the B2B catalog entirely — there is no file channel on this API, and an order for one is rejected. Your account's region allowlist, delivery-type blocks and product-level hides are applied here too.

On deliveryType, one asymmetry matters: a product that has not been classified yet carries deliveryType: null, and ?deliveryType=other does not return it — an unclassified SKU is reachable only in an unfiltered request. Direct top-up API explains the consequence for your own types; Delivery types lists the five values.

Two fields read differently than they look:

  • wholesalePrice is your account's wholesale price in rubles before your volume tier. The tier discount is applied only when an order is created, to each line of it. Two things happen between this response and that moment, and they pull in opposite directions: the tier discount lowers the price, and the price itself is recomputed from scratch at order time, with the exchange rate and any supplier change since. So this number is not a ceiling. Against an unchanged price the tier can only take the unit price down; against a price that has moved up, the amount charged can be above the number you fetched. Show your buyer your own price; use this one for margin, and re-read the single-product endpoint just before ordering when the gap matters. If a price cannot be computed for a SKU it is returned as 0 in this list — treat 0 as "not orderable right now" rather than as free, because an order for it is rejected with ZERO_PRICE_ITEM.
  • inStock is always true here. The endpoint only returns available products, so the field is a constant, not a signal. Absence from the list is the real stock signal.

A single product

GET /b2b/catalog/products/34521

Returns the same object as one element of the list above, priced live for your account — and, like the list, before the volume tier. This is the call to make right before you create an order: prices move with the exchange rate and with supplier changes, and the price we charge is the one computed at order time, not the one you cached.

A non-numeric id is a 400 (Invalid product ID). An id that does not exist, is inactive, is an image-upload SKU, is hidden for your account or carries a delivery type you have blocked is a 404 (Product not found) — deliberately the same answer, so a direct id probe cannot map what your account is not allowed to see.

One rule this endpoint does not apply is your region allowlist. It is enforced at order time instead, so a product that returns 200 here can still be refused when you order it, with region_not_allowed. Where region restricts you, take products from the game's product list, which does filter on it.

GET /b2b/catalog/search?q=pubg&limit=10

q must be at least 2 characters (400 below that); limit defaults to 10 and is clamped to 20. This searches games, not products, and is meant for a "which game is this" lookup rather than for synchronisation.

Search and the game detail share one limit, as the table at the top of this page shows: both apply only the hidden-game check. Your region allowlist, delivery-type blocks and product-level hides are not consulted here, because no product is being returned. A hit means "this game exists", not "you can sell it" — read the game's products before you show it to a buyer, since that is the only endpoint that answers the second question.

Parse this response defensively. In normal operation data is an array of games. If the search backend is unavailable the endpoint degrades to a database lookup that returns data as an object with a games array instead. A client that assumes one shape will break exactly during a degradation.

What is fresh and what is cached

No catalog response carries an HTTP cache header, so nothing is cached in front of you. Behind us:

DataFreshness
The game listServed from a snapshot rebuilt every 30 minutes; under load a stale snapshot may be served for up to 40 minutes while it refreshes.
Products of a game, single productRead live per request.
PricesComputed per request, rounded to two decimals.
Your account's visibility rules and keyCached for 60 seconds to 5 minutes after a change on our side.

Practically: a game we publish shows up for you within half an hour, a new SKU inside an existing game shows up immediately, and a price you fetched an hour ago is a price you should not trust at checkout.

What to store on your side

Store ids. Names get retranslated, prices move with the rate, icons get re-hosted, categories get merged — ids do not.

A shape that survives production:

  • Keep game-level metadata locally (id, slug, name, icon, productCount) and refresh it every 15 to 30 minutes. It is one small paginated call.
  • Read a game's products lazily, when a buyer opens that game, with a short cache — 10 to 15 minutes is plenty.
  • Re-read the single product immediately before POST /b2b/orders, and take the price from there.
  • Never persist deliveryDataSchema as a global per-game constant: it is per product, and suppliers change field names between regions and denominations.

Walking every game's products on a schedule is possible but wasteful — it is thousands of calls against a 500-per-minute budget, for data that mostly did not change.

Before you go live

  • Product ids are your join key; names and prices are display data.
  • deliveryType is nullable in your own type, and your filter code knows that other does not match null.
  • A 404 on a known game slug triggers re-resolution, not a retry loop.
  • wholesalePrice: 0 is treated as unavailable.
  • The search response is parsed for both shapes.
  • Sellability is decided by the products endpoint — never by a search hit, a game detail or a category list.
  • The price used at checkout was fetched in the same minute, not from a nightly dump, and your buyer's price is not derived from wholesalePrice as if it were a ceiling.

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.