Nexus Shop APIv1.5.0

Nexus Shop API

This document describes how your shop communicates securely with the Nexus platform.

Sandbox

Nexus Sandbox is a separate environment for integrating your shop with the API. It uses virtual USD and simulated deliveries. No purchase reaches a real provider, credits a game account, or produces a redeemable code. Obtain the sandbox credentials from your Nexus contact. The portal and API share https://nexus-sandbox.amadeustech.dev (API prefix: /api/v1); availability depends on the sandbox being provisioned.

Connect your shop

  1. Sign in to the sandbox portal with your sandbox login and password. The portal displays Nexus Sandbox. In My shops, copy the shop token and configure a webhook URL belonging to your test integration.
  2. Set a separate API base URL and Authorization: Shop <id:secret> token in your application. Use separate local storage for sandbox orders and balances. Production credentials, sessions and funds are not shared with this environment.
  3. Check GET /api/v1/sandbox. It requires no token and returns {"mode":"sandbox","realMoney":false,"providers":["test"]}. Shop API responses include X-Nexus-Environment: sandbox.
  4. Read /api/v1/games, categories and products using the usual API described below. Render each product's deliveryData form and honor its amountType. Use fictitious account identifiers: the simulator logs the supplied fields.

Virtual balance

A shop provisioned with the sandbox onboarding command starts with 1,000 test USD and a default 3% commission. Read GET /api/v1/sandbox/balance with shop authorization:

{ "sandbox": true, "balance": 1000 }

Use +1,000 test USD in My shops or POST /api/v1/sandbox/topup with the same token (no request body required). A successful top-up returns HTTP 201:

{ "balance": 2000, "credited": 1000, "sandbox": true }

Self-service top-ups add 1,000 USD at a time, at most once per minute and only if the resulting available balance is at most 10,000 USD. A top-up within a minute of shop creation or the previous deposit returns 429; exceeding the balance cap returns 409. Both return {"error":"..."}. No payment is needed. These endpoints exist only in sandbox; they do not change your real balance.

Purchases apply the normal shop commission. Completed and pending orders consume available funds; failed orders release their reservation. Always use the API's actual product prices and order totalPrice. Top-ups preserve order history and externalId idempotency; they do not reset the shop.

Predictable purchase scenarios

The sandbox exposes two synthetic services:

ServiceCategoriesBehavior
test-1test-1:det-topup, test-1:det-codesResult chosen with deliveryData.outcome
test-2test-2:rnd-topup, test-2:rnd-codesRandom success, failure or delayed result

Each category contains p1 through p5; for example, test-1:det-topup:p1 and test-1:det-codes:p1. There are 20 products in total. p1 and p2 accept only amount 1. The other products exercise multiple-amount rules: p3 accepts 1–100; p4 accepts 10–1000 in steps of 10; p5 accepts 1–10 in steps of 1. Check the returned amountType when constructing requests.

Set account to any fictitious value. For test-1, select one of these outcome values (HTTP statuses refer to a new, valid order):

OutcomeInitial responseFinal result
success201, status: completedVirtual purchase succeeds
fail503, errorCode: PROVIDER_ERRORFailed; no funds consumed
fail_credentials400, errorCode: INVALID_CREDENTIALSFailed; no funds consumed
pending_success201, status: pendingCompleted after about 15 seconds
pending_fail201, status: pendingFailed after about 15 seconds; funds released

The operator can configure the pending delay. Restarting the sandbox API does not discard pending orders: they are recovered from the database. Cdkey products return synthetic TEST-... codes when completed. They have no redemption value.

export NEXUS_SANDBOX_API='https://nexus-sandbox.amadeustech.dev'
export NEXUS_SANDBOX_TOKEN='<sandbox-shop-id>:<sandbox-secret>'

curl --fail-with-body "$NEXUS_SANDBOX_API/api/v1/orders" \
  -H "Authorization: Shop $NEXUS_SANDBOX_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{
    "product": "test-1:det-topup:p1",
    "amount": 1,
    "externalId": "sandbox-example-001",
    "deliveryData": { "account": "demo-account", "outcome": "pending_success" }
  }'

Repeat the identical request to check idempotency: the same order is returned with HTTP 200 and no second charge. Use a new externalId for each different scenario; reusing one with different order data returns 409. Poll GET /api/v1/orders/<id> or GET /api/v1/orders/external/<externalId> using your token. Orders belonging to another shop are not accessible. The standard order endpoint rate limit also applies (currently 30 order requests per minute per client IP).

Webhooks and lookup helpers

Sandbox sends the normal statusChange webhooks directly to your configured test receiver, including deferred completions and failures. Payload and Sign verification are the same as in Webhooks: HMAC-SHA256 of the raw body using the complete sandbox id:secret token. Keep your test receiver separate from production processing, return a successful response promptly, and handle duplicates. Localhost/private receiver URLs are rejected by Nexus; use a public HTTPS endpoint or your own tunnel for local development.

Lookup endpoints return fixtures, without contacting the real game/provider:

EndpointSandbox fixtures
/utils/pubg/player/:idPositive ID → Sandbox-<id>; 1 → null username; 2 → HTTP 502
/utils/fireloot/player/:uidNumeric UID → simulated CIS player; 0 → 404 not_found; 1 → 404 region_unsupported; 2 → 502
/utils/genshin/player/:server/:playerValid server and positive player → simulated username; player 1 → null
/utils/roblox/get-user/:usernameFixed user ID 10000001 and supplied username
/utils/roblox/get-places/:idOne simulated place
/utils/roblox/get-passes/:idOne purchasable and one unavailable simulated pass

All paths in this table start with /api/v1 and require shop authorization. Superpass listings are empty, superpass stock is unavailable, and employee / logpass verification lookups return 404. Real provider-specific delivery flows and the production catalog are not simulated in this version.

Before switching your integration to production, check success and both failure paths, delayed outcomes, webhook signatures/duplicates, invalid form data, amount validation, insufficient virtual funds, and repeated externalId. Production onboarding supplies a separate URL/token and real funding; no sandbox orders, codes or balances are transferred.

What's new in 1.5.0

Free Fire orders can be checked before the buyer pays. GET /api/v1/utils/fireloot/player/:uid resolves a Free Fire player id to a nickname and — more importantly — to the market the account belongs to:

  • Free Fire is sold as a separate product line per market (free-fire:diamonds-cis, free-fire:pass-cis, …), and an order whose product belongs to a different market than the account is rejected on delivery. Use region to offer the right nominals up front instead of letting the buyer discover the mismatch at checkout;
  • a valid id can still come back without a nickname — check nameChecked before showing one (see Free Fire).

The endpoint itself has been live since the 1.3.0 line; this release documents it. Nothing else in the contract changed.

What's new in 1.4.0

A new product line needs a change on your side. Products under roblox:robux-login (Robux credited by signing in to the buyer's own Roblox account) cannot be delivered unless you show the buyer a verification link:

  • handle the new robloxLogpassVerification webhook (see Webhooks), or read the link back from GET /api/v1/utils/roblox/logpass-order/:orderId/verification;
  • these orders stay pending until the buyer signs in, and are never cancelled by timeout — an order whose link the buyer never saw waits forever and freezes the money;
  • the delivery form gained a secret field type for the optional password and recovery code (see Secret type). Collecting them is optional but shortens the buyer's journey; render them masked and never store them.

Also in this release: every product carries an instruction for this line — check haveInstruction and show it at checkout (see Form instructions).

Authentication

All requests to the Nexus Shop API must be authenticated.

Set the Authorization header using the following format:

Authorization: Shop <token>

Example:

Authorization: Shop 111:aaabbbccc

You can obtain (or rotate) your Shop token in the Control Panel. Treat the token as a secret and never expose it in client-side code or logs.

If the header is missing or invalid the API will respond with an authentication error.

API Endpoints

Data format

type Game = {
	id: string;
	name: string;
	icon: string;
	tags: string[]; // tag keys; see GET /api/v1/tags for localized labels
};

type Category = {
	id: string;
	name: string;
};

type Product = {
	id: string;
	name: string;
	icon: string;
	price: number;
	deliveryData: FormField[];
	type: "topup" | "cdkey" | "gift_card" | "subscription";
	// Activation platform of the code / gift card; null when not applicable.
	platform: Platform | null;
	// Whether GET /api/v1/instructions/<id> has an instruction for this
	// product. Only call that endpoint when true — probing blindly is wasted
	// traffic (the endpoint answers 404 for most products). A missing key
	// means the platform predates this field; treat it as "unknown, may probe".
	haveInstruction: boolean;
	amountType: ProductAmountType;
	description?: string;
	// Descriptive metadata about the product. Present ONLY when the request asks
	// for it with `?include=metadata`; `null` means we hold nothing for this
	// product. See "Product metadata".
	metadata?: ProductMetadata | null;
};

type ProductMetadata = {
	// --- light half: returned by ?include=metadata on list endpoints ---
	releaseDate?: string; // ISO-8601 date
	isReleased?: boolean; // false = announced but not yet released (pre-order)
	developer?: string;
	publisher?: string;
	genres?: string[];
	languages?: string[]; // ISO-639-1 codes supported by the product itself
	ageRating?: { system: "pegi"; age?: number; descriptors: string[] };
	region?: {
		regions: string[]; // macro-regions, e.g. ["EU"]
		countries: string[]; // ISO-3166-1 alpha-2
		excludedCountries: string[]; // carve-outs, e.g. ["BY", "RU"]
		global: boolean;
	};
	countryPolicyKind?: "worldwide" | "allow" | "deny";

	// --- rich half: only on /products/:id and /catalog/metadata ---
	about?: ContentBlock[];
	requirements?: {
		minimal?: RequirementsSpec;
		recommended?: RequirementsSpec;
	};
	countryPolicy?:
		| { kind: "worldwide" }
		| { kind: "allow"; countries: string[] }
		| { kind: "deny"; countries: string[] };
	activationNotice?: string;
	media?: { images?: string[]; video?: { provider: "youtube"; id: string } };
	redeemUrl?: string;
	notes?: string[];
	contents?: { name: string; quantity?: number; rarity?: string }[];
	tags?: string[];
};

// Product copy, pre-parsed into blocks. Plain text only — never HTML — so it can
// be rendered without `dangerouslySetInnerHTML`.
type ContentBlock = {
	type: "p" | "h" | "li" | "quote";
	text: string;
};

type RequirementsSpec = {
	os?: string;
	processor?: string;
	memory?: string;
	graphics?: string;
	storage?: string;
	other?: string;
};

type Platform =
	| "steam"
	| "xbox"
	| "psn"
	| "nintendo"
	| "gog"
	| "epic"
	| "ea"
	| "ubisoft"
	| "google_play"
	| "apple"
	| "other";

type FormField =
	| {
			type: "text";
			id: string;
			label: string;
			required: boolean;
			regex?: string;
	  }
	| {
			type: "number";
			id: string;
			label: string;
			required: boolean;
			step?: number;
			min?: number;
			max?: number;
	  }
	| {
			type: "select";
			id: string;
			label: string;
			required: boolean;
			options: {
				id: string;
				label: string;
			}[];
	  }
	| {
			type: "secret";
			id: string;
			label: string;
			required: boolean;
			regex?: string;
	  };

type ProductAmountType =
	| {
			type: "one";
	  }
	| {
			type: "multiple";
			min?: number | undefined;
			max?: number | undefined;
			step?: number | undefined;
	  };

Full Catalog

GET /api/v1/catalog retrieve all games, categories and products in a single request.

GET /api/v1/catalog
Authorization: Shop 111:aaabbbccc
{
	"games": [
		{
			"id": "highrise",
			"name": "Highrise",
			"icon": "https://cdn.example.com/icons/highrise.png"
		}
	],
	"categories": [
		{ "id": "highrise:gold_pack", "name": "Gold Packs" },
		{ "id": "highrise:season_pass", "name": "Season Pass" }
	],
	"products": [
		{
			"id": "highrise:gold_pack:999_v2",
			"name": "Gold Pack — 999",
			"icon": "https://cdn.example.com/products/gold_999.png",
			"price": 9.99,
			"deliveryData": [
				{
					"type": "text",
					"id": "username",
					"label": "Username",
					"required": true,
					"regex": "\\S+"
				}
			],
			"type": "topup",
			"platform": null,
			"haveInstruction": true,
			"amountType": { "type": "one" }
		}
	]
}

The response contains three flat arrays. Relationships are encoded in IDs:

  • category.id starts with the parent game.id (e.g. highrise:gold_pack belongs to game highrise)
  • product.id starts with the parent category.id (e.g. highrise:gold_pack:999_v2 belongs to category highrise:gold_pack)

Products include the same fee and delivery data transformations as GET /api/v1/products. The response is cached server-side for up to 5 minutes.

Games

GET /api/v1/games/ retrieve a list of all available games in Game[] format.

GET /api/v1/games/
Authorization: Shop 111:aaabbbccc
[
	{
		"id": "highrise",
		"name": "Highrise",
		"icon": "https://cdn.example.com/icons/highrise.png"
	},
	{
		"id": "sandbox",
		"name": "Sandbox",
		"icon": "https://cdn.example.com/icons/sandbox.png"
	}
]

GET /api/v1/games/<id> retrieve a specific game in Game format.

GET /api/v1/games/highrise
Authorization: Shop 111:aaabbbccc
{
	"id": "highrise",
	"name": "Highrise",
	"icon": "https://cdn.example.com/icons/highrise.png"
}

Explanation for type Game

FieldDescription
iconA URL for icon file

Tags

GET /api/v1/tags returns the platform's service tags. Tag name is localized via Accept-Language (see Localization); key is a stable identifier used for filtering and in each game's tags array.

GET /api/v1/tags
Authorization: Shop 111:aaabbbccc
Accept-Language: ru
[
	{ "key": "games", "name": "Игры" },
	{ "key": "gift-cards", "name": "Подарочные карты" }
]

GET /api/v1/games?tag=<key> returns only the games attached to that tag (still subject to the shop's catalog visibility). Each Game carries its own tags array of tag keys.

GET /api/v1/games?tag=games
Authorization: Shop 111:aaabbbccc
[
	{ "id": "highrise", "name": "Highrise", "icon": "https://cdn.example.com/icons/highrise.png", "tags": ["games"] }
]

Categories

GET /api/v1/categories?gameId=<id> retrieve a list of all available categories in Category[] format for a specific game. gameId search param is required.

GET /api/v1/categories?gameId=highrise
Authorization: Shop 111:aaabbbccc
[
	{ "id": "gold_pack", "name": "Gold Packs" },
	{ "id": "season_pass", "name": "Season Pass" }
]

GET /api/v1/categories/<id> retrieve a specific category in Category format.

GET /api/v1/categories/gold_pack
Authorization: Shop 111:aaabbbccc
{
	"id": "gold_pack",
	"name": "Gold Packs"
}

Products

GET /api/v1/products?categoryId=<id> retrieve a list of all available products in Product[] format for a specific category. categoryId search param is required.

GET /api/v1/products?categoryId=gold_pack
Authorization: Shop 111:aaabbbccc
[
	{
		"id": "highrise:gold_pack:999_v2",
		"name": "Gold Pack — 999",
		"icon": "https://cdn.example.com/products/gold_999.png",
		"price": 9.99,
		"deliveryData": [
			{
				"type": "text",
				"id": "username",
				"label": "Username",
				"required": true,
				"regex": "\S+"
			},
			{
				"type": "select",
				"id": "serverId",
				"label": "Server",
				"required": true,
				"options": [
					{ "id": "eu", "label": "Europe" },
					{ "id": "us", "label": "USA" },
					{ "id": "ru", "label": "Russia" }
				]
			}
		],
		"type": "topup",
		"platform": null,
		"amountType": { "type": "one" }
	},
	{
		"id": "highrise:gold_pack:bulk_100",
		"name": "Gold Pack — Bulk",
		"icon": "https://cdn.example.com/products/gold_bulk.png",
		"price": 0.19,
		"deliveryData": [
			{
				"type": "text",
				"id": "username",
				"label": "Username",
				"required": true,
				"regex": "\S+"
			}
		],
		"type": "cdkey",
		"platform": "steam",
		"amountType": { "type": "multiple", "min": 50, "step": 1 }
	}
]

GET /api/v1/products/<id> retrieve a specific product in Product format.

GET /api/v1/products/highrise:gold_pack:999_v2
Authorization: Shop 111:aaabbbccc
{
	"id": "highrise:gold_pack:999_v2",
	"name": "Gold Pack — 999",
	"icon": "https://cdn.example.com/products/gold_999.png",
	"price": 9.99,
	"deliveryData": [
		{
			"type": "text",
			"id": "username",
			"label": "Username",
			"required": true,
			"regex": "\\S+"
		},
		{
			"type": "select",
			"id": "serverId",
			"label": "Server",
			"required": true,
			"options": [
				{ "id": "eu", "label": "Europe" },
				{ "id": "us", "label": "USA" },
				{ "id": "ru", "label": "Russia" }
			]
		}
	],
	"type": "topup",
	"platform": null,
	"amountType": { "type": "one" }
}

Product type

FieldDescription
iconA URL for icon file
pricePrice in USD for 1 unit of product
typeType of product
platformActivation platform of the code / gift card, or null — see below
deliveryDataSpecifies which data is required from the customer to complete the order
amountTypeSpecifies the rules for the amount field that apply to this product
metadataDescriptive product metadata; opt-in via ?include=metadata — see below

Product.platform

The platform the customer redeems the product on. Intended for storefront platform filters and Steam/Xbox/PSN badges, and therefore most relevant for type: "cdkey" and type: "gift_card".

The value is one of a fixed set of machine slugs:

Field valueDescription
steamSteam (Valve)
xboxXbox / Microsoft Store / Game Pass
psnPlayStation Network
nintendoNintendo eShop / Switch Online
gogGOG.com
epicEpic Games Store
eaEA app / EA Play / Origin
ubisoftUbisoft Connect
google_playGoogle Play
appleApp Store / iTunes
otherRedeemed somewhere else (e.g. a streaming-service gift card)

platform is null when no activation platform applies:

  • always for type: "topup" and type: "subscription" — these are activated by us directly on the buyer's account, so there is no store to send anyone to;
  • for a cdkey / gift_card product not yet classified on our side.

The key is always present in the product object, so an explicit null means "not applicable / unknown", while a missing key means the Nexus instance predates this field.

The value is resolved per product with the most specific classification winning: product → its category → its game. A product's platform therefore does not depend on which provider currently serves it, and a broad classification never leaks onto a top-up that happens to sit in the same category as a key.

Product.type

Field valueDescription
topupDirect delivery to account
cdkeyYou will receive a CD key for product in webhook
gift_cardYou will receive a CD key for product in webhook (semantically a gift card; same delivery channel as cdkey)
subscriptionDirect delivery to account (semantically a period subscription; same delivery channel as topup)

Product.deliveryData

deliveryData field in the Product type is an array of fields that must be filled in by the customer to purchase this product.

The following properties are common for all field types

PropDescription
idUnique ID of field. Can be used as name in html form
labelLabel of field for customer in english
requiredIndicates whether this field is mandatory or not
typeIndicates the field type
Text type

A simple text field. Can have a regex field that applies rules to the input.

Number type

Number field. Can have min, max and step configuration identical to HTML attributes for an input of type number (<input type="number"/>).

Select type

A select field that provides a list of options from which the user must choose one. Options are an array of objects with id and label. The label is the English name of the option displayed to the customer. The id is the actual value that must be sent during order creation.

Secret type

A credential belonging to the customer — today a Roblox account password or a Microsoft recovery code. On the wire it behaves like a text field and can have a regex, but it carries obligations a text field does not:

  • Render it masked (<input type="password"/>) and never pre-fill it.
  • Do not store it. It exists only to be forwarded to the supplier for this one order. We do not keep it either: it is erased from our order record as soon as the supplier accepts the order, and every response and webhook that echoes deliveryData back to you shows it as "[hidden]".
  • A secret field is never required on a product where the customer has another way through — for logpass they can type the credential on the supplier's verification page instead. Leaving it out is always a valid order.

Never send an empty string for an optional secret field; omit the key instead.

Usage on order creation

When creating an order, you must collect the required data from the customer and send it in the deliveryData field as a dictionary. For example, for a product with the following delivery data

{
	...
	"deliveryData": [
		{
			"type": "text",
			"id": "username",
			"label": "Username",
			"required": true,
			"regex": "\S+"
		},
		{
			"type": "select",
			"id": "serverId",
			"label": "Server",
			"required": true,
			"options": [
				{
					"id": "eu",
					"label": "Europe",
				},
				{
					"id": "us",
					"label": "USA",
				},
				{
					"id": "ru",
					"label": "Russia",
				}
			]
		},
	],
	...
}

In a new order request, you must send something like this

{
	...
	"deliveryData": {
		"username": "foo",
		"serverId": "eu"
	},
	...
}

Product.amountType

If field has type one this means the product can only be purchased once per order. And amount in order request must be equal 1. For example

{
	"type": "one"
}

If field has type multiple this means the product can have multiple quantities in each order. This type can also declare minimal and maximal value for amount. If not declared, there are no limits. Also it can have step field which indicates the minimum step between values.

For example "step": 1 means that amount must be multiple of 1 and must be an integer value like 1, 2, 3 and can't be 1.5. For step equal 0.01 amount can be 1.99, 2.5 but not a 1.001.

In other words, for a valid amount, the following equation must be satisfied amount % step = 0, where % is remainder operator.

Example configuration

{
	"type": "multiple",
	"min": 50,
	"step": 1
}

Product descriptions

Products may include an optional description field with a human-readable description of the product. Products that have no description configured omit the field from the response — there is no placeholder.

When a request includes Accept-Language and resolves to an enabled language, the description follows the same fallback chain as name:

  1. If a translation exists for the resolved language and product slug, the translated description is returned.
  2. Otherwise, if a default description is configured for the product slug, the default description is returned.
  3. Otherwise, description is omitted from the response.

The fallback applies independently per product — a product without a translation but with a default description still returns the default text; a product without either remains untranslated and undescribed.

description is curated by us and is independent of metadata.about, which is harvested from upstream sources. A product can have either, both, or neither.

Product metadata

Descriptive facts about the product itself — copy, screenshots, trailer, genres, system requirements, age rating, activation restrictions. Everything here is a property of the product, not of any particular supply route, and it does not change when the product's price or availability does.

Metadata is opt-in: without ?include= every response is byte-identical to what it was before this feature existed.

RequestReturns
GET /api/v1/products?categoryId=…&include=metadatathe light half on every product
GET /api/v1/catalog?include=metadatathe light half on every product
GET /api/v1/products/<id>?include=metadatathe full object
GET /api/v1/catalog/metadatathe full object for many products — the recommended integration

The split exists for size. The light half is ~150–250 bytes per product; the full object can carry ~3 KB, and the whole catalog is tens of thousands of products, so full metadata is never inlined into a listing. List endpoints return the light half regardless of what include asks for — use /products/<id> or /catalog/metadata to get the rest.

metadata is null when we hold nothing for that product. An absent key means metadata was not requested (or the deployment predates the feature) — the same convention as platform.

Bulk sync

GET /api/v1/catalog/metadata walks the whole metadata set, newest changes last, and is designed to be mirrored into your own storage.

GET /api/v1/catalog/metadata?updatedSince=2026-07-01T00:00:00Z&limit=200
Authorization: Shop 111:aaabbbccc
{
	"items": [
		{
			"productId": "gta-v:keys:standard",
			"version": "9f2c…",
			"updatedAt": "2026-07-26T19:41:03.117Z",
			"metadata": { "developer": "Rockstar North", "…": "…" }
		}
	],
	"nextCursor": "MTc4NTEwMjkxMzA4NXxndGEtdjprZXlzOnN0YW5kYXJk",
	"syncedAt": "2026-07-27T08:12:44.001Z"
}
ParamMeaning
updatedSinceISO-8601 timestamp or epoch milliseconds. Omit for a full walk
cursorOpaque; pass back nextCursor to continue. Do not construct one
limit1–1000, default 200

Loop until nextCursor is null, then store syncedAt and pass it as the next run's updatedSince. syncedAt is captured before the page is read, so a product changed mid-walk lands in your next sync rather than being skipped.

version is an opaque content hash: equal version means the metadata is byte-identical, so you can skip re-processing without diffing. It changes only when the content actually changes, which is also why a re-sync of an unchanged catalog returns nothing.

Only products visible to your shop are returned. Pages may therefore come back shorter than limit (or empty) while the cursor still advances — that is normal, keep following nextCursor and stop only when it is null.

Responds 503 while the feature is disabled for the deployment.

Notes on individual fields

  • about is pre-parsed into typed blocks of plain text rather than HTML, so it can be rendered without dangerouslySetInnerHTML. Inline emphasis from the original copy is not preserved.
  • countryPolicy describes where the product can be activated. {"kind":"worldwide"} is an explicit statement that there is no restriction — it is not the same as the field being absent, which means we hold no information. countryPolicyKind mirrors just the discriminator into the light half so a listing can badge "region locked" without carrying country arrays.
  • region.excludedCountries carries carve-outs (a "Europe except BY/RU" key). Treat countryPolicy as authoritative for eligibility; region is a display hint.
  • isReleased: false means the product is announced but not yet released — a pre-order. releaseDate may be present in that case.
  • media.images are always served from this API's own domain. media.video is an identifier, not a URL: build the embed yourself from provider + id.
  • ageRating.descriptors are PEGI content descriptors (violence, bad_language, gambling, online, …). An empty array means rated with nothing flagged; an absent ageRating means unrated or unknown.
  • languages are the languages the product itself supports, not the languages of this API's responses.

Fields are omitted rather than sent as null, and coverage varies widely by product — treat every field as optional.

Form instructions

GET /api/v1/instructions/<id> returns a localizable rich-text (Markdown) instruction that explains how to fill in a product's deliveryData form and where to obtain the required values. <id> is a product id in the gameSlug:categorySlug:productSlug format (the same id used by GET /api/v1/products/<id>).

Check haveInstruction first. Every product in GET /api/v1/catalog and GET /api/v1/products carries haveInstruction: boolean — request an instruction only when it is true. The endpoint answers 404 for products without one, and blind per-product probing has real cost on both sides (it was a third of all API traffic before this flag existed).

GET /api/v1/instructions/highrise:gold_pack:999_v2
Authorization: Shop 111:aaabbbccc
Accept-Language: ru
{
	"productId": "highrise:gold_pack:999_v2",
	"level": "product",
	"content": "## Как заполнить форму\n\nВведите **Username** из профиля игры…\n\n![где найти](https://api.example.com/static/icons/instructions/ab12.webp)"
}
FieldDescription
productIdThe product id from the request
levelHow specific the returned instruction is: product, category, or game
contentThe instruction body in Markdown (may contain images as absolute URLs)

The server returns the single most specific instruction available, checked in this order (a more specific level overrides a more general one):

  1. a general instruction on this product (level: "product")
  2. a general instruction on this product's category (level: "category")
  3. a general instruction on the game (level: "game")

A product may additionally carry a variant of its product-level instruction that applies only to the way it is currently fulfilled. That variant wins over the general product instruction when it exists, and is still reported as level: "product" — which fulfilment path is in use is not part of the Shop API contract and can change without notice.

If none of these exist, the endpoint responds with 404. It also responds with 404 when the product does not exist or is not visible to your shop, and with 400 when the id is not a valid gameSlug:categorySlug:productSlug.

content is localized via Accept-Language (see Localization): when the header resolves to an enabled language and a translation exists for the resolved instruction, the translated Markdown is returned; otherwise the default content is returned. The instruction level is resolved first (by existence), and the language is applied to that resolved instruction.

Localization

The Shop API supports response-name translation via the standard Accept-Language request header. Translation applies to game, category, and product name fields, to product description fields, and to the metadata.about / metadata.activationNotice prose — IDs, slugs, icons, prices, and delivery data never change.

Upstream coverage of translated metadata is thin and language-dependent; when no translation exists for the resolved language the original copy is returned.

Examples:

Accept-Language: pt-BR Accept-Language: pt-BR,pt;q=0.9,en;q=0.5 Accept-Language: ja, en;q=0.5

Resolution rules:

  1. The header is parsed by RFC 9110 quality values; equal weights keep request order.
  2. Each candidate code is matched against the active languages list. If no exact match exists, the base subtag is tried (e.g. pt-BRpt).
  3. If no enabled language matches, the response uses the original English names — IDs and other fields are unchanged.

Affected endpoints:

GET /api/v1/catalog GET /api/v1/games GET /api/v1/games/:id GET /api/v1/categories GET /api/v1/categories/:id GET /api/v1/products GET /api/v1/products/:id GET /api/v1/instructions/:id GET /api/v1/tags GET /api/v1/catalog/metadata GET /api/v2/shops/my/hot-products

Orders

POST /api/v1/orders to create a new order.

POST /api/v1/orders
Authorization: Shop 111:aaabbbccc
Content-Type: application/json

{
	"product": "highrise:gold_pack:999_v2",
	"deliveryData": {
		"username": "foobar"
	},
	"externalId": "f92051c0-3450-49ce-8454-2f69425fd824",
	"amount": 100
}
{
	"id": 42,
	"product": "highrise:gold_pack:999_v2",
	"externalId": "ORDER-12345",
	"amount": 1,
	"totalPrice": 9.99,
	"status": "completed",
	"deliveryData": { "username": "foobar" },
	"cdKeys": [
		{
			"code": "ABCD-EFGH-IJKL",
			"expireAt": "2026-09-18T00:00:00.000Z"
		}
	],
	"createdAt": "2025-09-18T12:34:56.000Z",
	"updatedAt": "2025-09-18T12:35:10.000Z"
}

Order creation fail

A failed order create returns status code 400, 409, 500 or 503 with the following response body.

type OrderErrorCode =
	| "INVALID_CREDENTIALS"
	| "INVALID_AMOUNT"
	| "PRODUCT_NOT_FOUND"
	| "PRODUCT_ALREADY_BOUGHT"
	| "SERVER_ERROR"
	| "UNKNOWN_ERROR"
	| "PROVIDER_ERROR"
	| "PROVIDER_CANCELLED"
	| "PARTIAL_FULFILLMENT"
	| "DELIVERY_UNCONFIRMED"
	| "DUPLICATE_EXTERNAL_ID"
	| "ROBLOX_NOT_IN_GAME"
	| "ROBLOX_NEVER_PLAY"
	| "ROBLOX_GAME_ERROR"
	| "ROBLOX_SUPERPASS_UNAVAILABLE"
	| "USER_REACH_LIMIT"
	| "REGION_RESTRICTED"
	| "NOT_ENOUGH_SHOP_BALANCE"
	| "NOT_ENOGHT_SHOP_BALANCE"; // @deprecated Use NOT_ENOUGH_SHOP_BALANCE

type OrderFailedResponse = {
	errorCode: OrderErrorCode;
	errorMessage: string; // Human-readable, derived from errorCode — do not parse
	errorId?: string; // UUID v7 for tracking order execution errors
};

Branch on errorCode, never on errorMessage. errorMessage is a fixed English sentence per code (a few codes append the offending values, e.g. your balance or the allowed amount range), it is not localized, and its wording may change at any time. It never reproduces an upstream system's own text.

OrderErrorCode is one type for both channels, but not every code can reach both. An order that is rejected while your request is still open answers on that request; an order that is accepted and fails later reports through the statusChange webhook. Codes marked "webhook only" below never appear in an HTTP response, and DUPLICATE_EXTERNAL_ID is the reverse — it is a request rejection and never arrives by webhook.

Contract change — accept two new errorCode values. PROVIDER_CANCELLED and DELIVERY_UNCONFIRMED are new. Both can appear in the POST /api/v1/orders response and in the statusChange webhook. Clients that strictly validate the error enum must accept them first.

They exist because a supplier that reports every failure with one opaque code makes UNKNOWN_ERROR mean two different things — "the supplier flapped, retry" and "we cannot tell whether the goods landed". Branch as follows: treat PROVIDER_CANCELLED like PROVIDER_ERROR (retryable, not the customer's fault), and treat DELIVERY_UNCONFIRMED as needing a human — the order was not charged, but re-ordering the same account risks a double delivery.

Nothing else changes: existing codes keep their meanings, and a client that lumps unrecognised codes with UNKNOWN_ERROR stays correct, just less precise.

Error Response Details

Status Codes:

  • 400 - Client errors (invalid input, insufficient balance, user limits)
    • INVALID_CREDENTIALS, INVALID_AMOUNT, PRODUCT_NOT_FOUND, NOT_ENOUGH_SHOP_BALANCE, USER_REACH_LIMIT, REGION_RESTRICTED
  • 409 - Conflict
    • DUPLICATE_EXTERNAL_ID — the externalId was already used for a different order payload
  • 500 - Server errors (internal failures)
    • SERVER_ERROR, UNKNOWN_ERROR, DELIVERY_UNCONFIRMED
  • 503 - Service temporarily unavailable (retry later)
    • PROVIDER_ERROR, PROVIDER_CANCELLED, ROBLOX_SUPERPASS_UNAVAILABLE

What each code means:

CodeMeaningWhat to do
INVALID_CREDENTIALSThe player identifier (UID, server, username) was rejected by the game or the providerAsk the customer to correct the data and re-order
INVALID_AMOUNTThe requested quantity or denomination was rejectedRe-read the product's amountType and re-order
PRODUCT_NOT_FOUNDThe product is unknown, disabled or out of stock upstreamRefresh the catalog; do not retry the same product
PRODUCT_ALREADY_BOUGHTThe player already owns this one-time purchase (webhook only)Nothing to retry
USER_REACH_LIMITThe player hit a purchase limit imposed by the gameRetry later
REGION_RESTRICTEDThe product cannot be delivered to this customer's game region or account countryDo not retry with the same account; check the product's regional availability
NOT_ENOUGH_SHOP_BALANCEYour shop balance cannot cover the orderTop up and re-order
PROVIDER_ERRORThe upstream provider is unavailable, misconfigured or out of funds — never the customer's faultRetry later
PROVIDER_CANCELLEDThe order was cancelled upstream before delivery and was not charged. Distinct from PROVIDER_ERROR: the order was accepted and then taken back, not refusedRetry later
DELIVERY_UNCONFIRMEDNobody can say whether the goods were delivered. The order was not charged, but the target account may already have received themDo not re-order the same account before support has checked; report the errorId
ROBLOX_NOT_IN_GAME, ROBLOX_NEVER_PLAY, ROBLOX_GAME_ERRORRoblox-specific delivery failures (webhook only)See the Roblox section
ROBLOX_SUPERPASS_UNAVAILABLENo SuperPass worker is available to process the orderRetry later
PARTIAL_FULFILLMENTA bundle delivered fewer units than ordered (webhook only, with partially_completed)Only the delivered units are charged
DUPLICATE_EXTERNAL_IDThe externalId is already bound to a different order payload (HTTP only)Use a fresh externalId, or re-send the identical payload to replay idempotently
SERVER_ERRORA fault on our sideRetry later; report with the errorId
UNKNOWN_ERRORThe order failed and the cause could not be attributedReport with the errorId

errorCode on the order object. Until now the cause of a failure travelled only on the statusChange webhook, which meant a shop that missed one — a restart, a deploy, a brief outage — had no second source: GET /orders/:id answered failed and nothing else. The order object now carries errorCode too, so the cause is recoverable by polling.

It is present only when we actually know the cause; a failure the supplier did not explain still arrives without it, and the field is absent rather than UNKNOWN_ERROR, so "we do not know" stays distinguishable from "the supplier said UNKNOWN_ERROR". The webhook's own top-level errorCode is unchanged — prefer it when you have it, and treat the order field as the fallback.

Error ID (errorId): The errorId field is included for order execution errors (errors that occur during the order fulfillment process). It's a UUID v7 that can be used to correlate errors with server logs when contacting support. The errorId is present for:

  • INVALID_CREDENTIALS - When order execution fails due to invalid player credentials
  • USER_REACH_LIMIT - When player has reached purchase limit
  • REGION_RESTRICTED - When the provider rejected the delivery because of the customer's game region or account country
  • NOT_ENOUGH_SHOP_BALANCE - When shop balance is insufficient
  • ROBLOX_SUPERPASS_UNAVAILABLE - When SuperPass service has no available workers to process the order
  • PRODUCT_NOT_FOUND, INVALID_AMOUNT, PROVIDER_ERROR, PROVIDER_CANCELLED - When the upstream provider rejected the order and named the reason
  • DELIVERY_UNCONFIRMED - When the outcome of the delivery could not be established either way
  • UNKNOWN_ERROR - For other execution failures

Validation failures rejected before execution (a malformed body, an unknown product, a DUPLICATE_EXTERNAL_ID conflict) carry no errorId.

Example error response:

{
	"errorCode": "USER_REACH_LIMIT",
	"errorMessage": "User reached a limit of purchase in game",
	"errorId": "019473a2-7c3f-7000-8000-000000000001"
}

GET /api/v1/orders to get all your orders.

Query paramDescription
offsetOptional offset for list
limitOptional limit for list
GET /api/v1/orders?offset=20&limit=10
Authorization: Shop 111:aaabbbccc
[{
	"id": 42,
	"product": "highrise:gold_pack:999_v2",
	"externalId": "ORDER-12345",
	"amount": 1,
	"totalPrice": 9.99,
	"status": "completed",
	"deliveryData": { "username": "foobar" },
	"cdKeys": [
		{
			"code": "ABCD-EFGH-IJKL",
			"expireAt": "2026-09-18T00:00:00.000Z"
		}
	],
	"createdAt": "2025-09-18T12:34:56.000Z",
	"updatedAt": "2025-09-18T12:35:10.000Z"
}]

GET /api/v1/orders/:id to get certain order.

GET /api/v1/orders/42
Authorization: Shop 111:aaabbbccc
{
	"id": 42,
	"product": "highrise:gold_pack:999_v2",
	"externalId": "ORDER-12345",
	"amount": 1,
	"totalPrice": 9.99,
	"status": "completed",
	"deliveryData": { "username": "foobar" },
	"cdKeys": [
		{
			"code": "ABCD-EFGH-IJKL",
			"expireAt": "2026-09-18T00:00:00.000Z"
		}
	],
	"createdAt": "2025-09-18T12:34:56.000Z",
	"updatedAt": "2025-09-18T12:35:10.000Z"
}

Bundles & partial fulfillment

A bundle is an ordinary catalog product whose purchase places several units of one underlying component product (e.g. a "32000 UC" bundle that buys 4 × 8000 UC). You order it exactly like any other product — amount must be 1, and its catalog price is the sum of those units.

Because each unit is fulfilled independently, a bundle order can be partially fulfilled. Two extra fields appear on bundle orders (and are null on every non-bundle order):

FieldDescription
requestedQuantityUnits the bundle was meant to deliver (N)
deliveredQuantityUnits actually delivered (M ≤ N)

A new order status — partially_completed — is reported when 0 < M < N. In that case totalPrice is reduced to the price of the delivered units only: you are never charged for undelivered units — the difference is restored to your balance automatically, exactly as for a failed order. For code-based products, cdKeys then contains only the delivered keys.

Contract change — action required before selling bundles. partially_completed is a new value of status, and PARTIAL_FULFILLMENT is a new errorCode. They can appear in the POST /api/v1/orders response, GET /api/v1/orders, GET /api/v1/orders/:id, and the statusChange webhook. Clients that strictly validate the status/error enums must accept these values first. Treat partially_completed as terminal success for the delivered units, and use deliveredQuantity / cdKeys for the exact amount delivered. Only bundle orders ever carry this status — if you do not sell bundles, you will never receive it.

Balance

GET /api/v2/shops/my/balance retrieve your current shop balance in USD.

GET /api/v2/shops/my/balance
Authorization: Shop 111:aaabbbccc
{
	"balance": 123.45
}

The balance represents the available funds in your shop account (deposits minus usage). All orders placed through the API will deduct from this balance. If your balance is insufficient for an order, the API will return a NOT_ENOUGH_SHOP_BALANCE error.

Hot Products

GET /api/v2/shops/my/hot-products retrieve the curated list of hot (featured) products, ordered by the admin-defined priority.

GET /api/v2/shops/my/hot-products
Authorization: Shop 111:aaabbbccc
[
	{
		"id": "highrise:gold_pack:999_v2",
		"name": "Gold Pack — 999",
		"icon": "https://cdn.example.com/products/gold_999.png",
		"price": 9.99,
		"deliveryData": [
			{
				"type": "text",
				"id": "username",
				"label": "Username",
				"required": true,
				"regex": "\\S+"
			}
		],
		"type": "topup",
		"amountType": { "type": "one" },
		"categoryName": "Gold Packs",
		"gameName": "Highrise",
		"gameIcon": "https://cdn.example.com/icons/highrise.png"
	}
]

Each product in the response is identical to the Product type described above, with three additional fields:

FieldDescription
categoryNameName of the product's category
gameNameName of the product's game
gameIconURL for the game's icon

Products are returned in the order set by the admin. Products that are temporarily unavailable are silently omitted from the response.

Utils

This section describe special game-specific utils API.

Roblox

For products Robux via pass you need to provide pass id from user. To make it easy to help user get this id you can use next API.

You need to get username from user and then fetch user id, fetch user places and show it to customer. When customer choose place you need to fetch passes for this place and show it to customer.

And then you just need to create an order with Nexus API providing choosen universe_id and price.

Get user's id

GET /api/v1/utils/roblox/get-user/<username>

{
	"id": "5642960000",
	"username": "someuser"
}
Get user's places

GET /api/v1/utils/roblox/get-places/<user_id>

[
	{
		"universe_id": "9063010000",
		"name": "Some place"
	},
	{
		"universe_id": "5728448000",
		"name": "someuser's Place",
		"icon": "https://t4.rbxcdn.com/180DAY-545d29ed783b5cb8d3b8cd65ff22ffff"
	}
]

Response can have not an icon field.

Get user's passes in certain place

GET /api/v1/utils/roblox/get-passes/<place_id>

{
	"passes": [
		{
			"id": "1580665000",
			"name": "pass 32323",
			"is_for_sale": true,
			"price": 29
		},
		{
			"id": "1581555000",
			"name": "pass123",
			"is_for_sale": true,
			"price": 72
		},
		{
			"id": "1565427000",
			"name": "Pass 1",
			"is_for_sale": false,
			"price": 0
		}
	]
}
Get games which support SuperPasses purchase

GET /api/v1/utils/roblox/super-games

[
	{
		"id": "69203a7f0fdd2147559b5b97",
		"name": "[🔥UPD] Гиперстрельба",
		"name_en": "[🔥UPD] Hypershot",
		"uri": "https://www.roblox.com/games/17516596118",
		"image_uri": "https://storage.yandexcloud.net/roblox/1763719805017818977_6376.jpg",
		"description": "✨Добро пожаловать в Hypershot!✨\nПрыгайте в нашу интенсивную и быструю стрельбу от первого лица, где мастерство, стиль и хаос преобладают!\n\n🏆Доминируйте в интенсивных классических режимах игры, таких как командный матч смерти, захват флага, бесплатная игра для всех и игра с оружием (Арсенал).\n💥 Объединитесь с друзьями, чтобы бросить вызов соперникам в дуэлях 1 на 1 или 2 на 2\n👑Поднимитесь по рангам, чтобы достичь таблицы лидеров!\n🌀Овладейте своими способностями, чтобы перехитрить и перехитрить своих врагов!\n🔥 Зарабатывайте монеты и драгоценные камни, чтобы разблокировать новое оружие, скины, способности и многое другое!\n🎮Доступно на ПК, телефонах, планшетах и консолях\n\n❤️Сделано с любовью Frosted Studios\n\n⚠️МОШЕННИЧЕСТВО В ЛЮБОЙ ФОРМЕ ПРИВЕДЕТ К ЗАПРЕТУ НА ВСЕ УЧЕТНЫЕ ЗАПИСИ⚠️",
		"rate": 95,
		"online": 25792,
		"in_game": false,
		"market_enabled": false,
		"superpass_enabled": true,
		"tags": null,
		"market_orders": 0,
		"superpass_orders": 0,
		"universe_id": "5995470825"
	},
	{
		"id": "692041ef0fdd2147559b5cc9",
		"name": "[👮] Диспетч: Полицейский симулятор",
		"name_en": "[👮] Dispatch: Police Simulator",
		"uri": "https://www.roblox.com/games/98100179608264",
		"image_uri": "https://storage.yandexcloud.net/roblox/1763721708509406099_19076.jpg",
		"description": "🚨 Полицейский симулятор:\nСтаньте настоящим офицером в высокооплачиваемом полицейском симуляторе на базе ИИ. Отвечайте на динамичные чрезвычайные ситуации, преследуйте бегущих подозреваемых, рассеивайте напряженные ситуации и восстанавливайте порядок - все это в живом, реактивном городе, которому нужна ваша помощь.\n\n👮 Как играть:\n- Присоединяйтесь к игре и выберите свое снаряжение и оружие из шкафчика.\nДождитесь появления активных вызовов в меню и ответьте на них.\n- Подойдите к месту происшествия и попробуйте справиться с подозреваемыми умным способом.\n\n⭐ Ожидайте новых обновлений каждую неделю!\n\n🎉 Присоединяйтесь к нашей группе и добавьте \"Нравится\" к игре, чтобы разблокировать $350 + 100XP.\n",
		"rate": 92,
		"online": 1027,
		"in_game": true,
		"market_enabled": false,
		"superpass_enabled": true,
		"tags": null,
		"market_orders": 0,
		"superpass_orders": 0,
		"universe_id": "8104504677"
	}
]
FieldDescription
rate0-100 rating
in_gamewhether client is required to join the game during the trade for this game rules
superpass_enabledwhether the game is available for superpass method
superpass_orderscurrent orders count for superpass method in this game
Get SuperPasses for certain game

GET /api/v1/utils/roblox/super-games/<game-id>/passes

[
	{
		"id": "roblox:passes:692041450fdd2147559b5ca8",
		"amountType": {
			"type": "one"
		},
		"deliveryData": [
			{
				"type": "text",
				"id": "username",
				"label": "Username",
				"required": true
			}
		],
		"icon": "https://storage.yandexcloud.net/roblox/1763721538644369518_50793.jpg",
		"name": "Machine gems",
		"type": "topup",
		"price": 11.3943
	},
	{
		"id": "roblox:passes:692041880fdd2147559b5cb6",
		"amountType": {
			"type": "one"
		},
		"deliveryData": [
			{
				"type": "text",
				"id": "username",
				"label": "Username",
				"required": true
			}
		],
		"icon": "https://storage.yandexcloud.net/roblox/1763721606119493422_41507.jpg",
		"name": "Sparkling Gems",
		"type": "topup",
		"price": 22.7943
	}
]

If you try to use default GET /api/v1/products?categoryId=roblox:passes it will return empty array.

But GET /api/v1/products/roblox:passes:692041880fdd2147559b5cb6 still works fine and return product data.

Important: For superpass orders, the buyer may be required to join a Roblox game and connect to an employee for the trade. When this is needed, your webhook URL will receive a robloxSuperpassInGame event with the employee's Roblox username and profile link. Show this information to the buyer so they can join the employee's game room.

Important: Products under roblox:robux-login (Robux credited by signing into the buyer's own account) cannot complete without buyer action. Right after the order is created your webhook URL receives a robloxLogpassVerification event carrying a verification.url. The buyer must open that page and sign into their Roblox account (including 2FA) there. Until they do, the order stays pending — it is never failed by timeout — so an order whose link was never shown to the buyer hangs indefinitely. If you missed the webhook, read the link back from GET /api/v1/utils/roblox/logpass-order/:orderId/verification.

The password and recovery_code delivery-data fields are type: "secret". They are optional (omit them and the buyer types them on that page instead). If you do collect them: render them masked, send them over HTTPS, and do not store them — we forward them to the supplier and erase our copy as soon as the order is accepted, and every place we echo an order back to you shows them as "[hidden]".

Check SuperPass availability

GET /api/v1/utils/roblox/superpass-stock

Returns whether superpass orders can currently be placed.

{
	"available": true
}

When available is false, any attempt to create a superpass order will fail with 503 ROBLOX_SUPERPASS_UNAVAILABLE. Wait and retry later.

Get SuperPass Order Employee Data

GET /api/v1/utils/roblox/superpass-order/:orderId/employee

Fallback endpoint to retrieve employee data for a superpass order if the robloxSuperpassInGame webhook was missed. Only works for superpass orders in pending status.

{
	"employee": {
		"username": "worker123",
		"profileUrl": "https://www.roblox.com/users/5087011877/profile"
	}
}

Returns 404 if order not found, not a superpass order, not in pending status, or employee data is not yet available.

Get Logpass Order Verification URL

GET /api/v1/utils/roblox/logpass-order/:orderId/verification

Fallback endpoint to retrieve the verification page for a roblox:robux-login order if the robloxLogpassVerification webhook was missed. Only works for logpass orders in pending status.

{
	"verification": {
		"url": "https://.../v/KldB0zRd"
	}
}

Returns 404 if the order is not found, is not a logpass order, is not in pending status, or the supplier has not issued the link yet (it is issued within seconds of order creation, but not always before the create call returns — retry shortly rather than creating a new order).

PUBG

Check user ID

You can check if user with certain id exists in game using next endpoint

GET /api/v1/utils/pubg/player/<id>

If user exists you will receive next response

{
	"username": "someuser"
}

Otherwise you will receive

{
	"playerName": null
}

Free Fire

Check player ID and account market

Free Fire is sold as a separate product line per market — the CIS line, the Brazil line and so on are different categories (free-fire:diamonds-cis, free-fire:packs-cis, free-fire:pass-cis, …). The market is a property of the player's account, never of the delivery form: the form asks for the player id and nothing else. An order whose product belongs to a different market than the account is rejected at delivery, so resolve the market first and show the buyer only the nominals they can actually receive.

GET /api/v1/utils/fireloot/player/<uid>, where <uid> is the numeric Free Fire player id (1 to 20 digits)

If the player exists you will receive

{
	"username": "someuser",
	"nameChecked": true,
	"region": "CIS"
}
FieldDescription
usernameThe player's in-game nickname, or null when the game did not answer it
nameCheckedfalse means the nickname is unknown — the id is still valid, but do not show a name
regionThe market the account belongs to; see the table below

region is the market code as the supplier spells it. The catalog spells the same market as a slug suffix on the category:

regionCategory suffix
CIS-cis
BANGLADESH-bangladesh
BRAZIL-brazil
EU-europe
INDONESIA-indonesia
LATAM-latam
MENA-mena
PAKISTAN-pakistan
SG-singapore
TAIWAN-taiwan
VIETNAM-vietnam

New markets can appear without notice, so never hard-code this list: an unknown region means "no matching line for this player", not an error. Only the markets present in your catalog response are on sale — a market missing from the catalog cannot be served even when the lookup returns it.

If the player id does not exist you will receive 404

{
	"error": "Player not found",
	"code": "invalid_uid"
}

If the account exists but plays in a market that is not for sale you will receive 404 with a different code

{
	"error": "Player's game region is not supported",
	"code": "region_unsupported"
}

region_unsupported is not a bad id — the player is real and other Free Fire lines in the catalog may still be able to serve them.

Other responses:

StatusBodyMeaning
400Invalid id (plain text)The id is not a 1 to 20 digit number
502{ "error": "Player lookup failed" }The lookup itself failed — the verdict is unknown, retry rather than block the buyer
503{ "error": "Lookup is not available" }Free Fire lookup is not configured on this installation

A verdict is cached for 5 minutes per player id, so a nickname changed in-game can take that long to show up.

Genshin

Check user ID for server

You can check if user with certain id exists in game using next endpoint

GET /api/v1/utils/genshin/player/<server>/<id>, where server can be one of os_usa, os_euro, os_asia, os_cht

If user exists you will receive next response

{
	"username": "someuser"
}

Otherwise you will receive

{
	"playerName": null
}

Webhooks

Nexus sends webhooks to notify your system about asynchronous events (e.g. order status changes). Configure your webhook URL in the Control Panel.

Every webhook request:

  • Uses the POST method
  • Has Content-Type: application/json
  • Includes a cryptographic signature in the Sign header
  • Contains a JSON body with the event payload

You must verify the signature before trusting the payload.

Signature Verification

The Sign header is an HMAC-SHA256 digest (hex-encoded) of the exact raw request body using your shop token as the secret.

Verification steps:

  1. Read the raw request body as a string (do not JSON.parse before hashing).
  2. Compute HMAC_SHA256(body, shopToken) and hex-encode the result.
  3. Compare it with the value of the Sign header.
  4. Reject the request (e.g. HTTP 401) if the signatures differ or the header is missing.

TypeScript example:

import { createHmac } from "node:crypto";

async function verifyWebhook(
	req: Request,
	shopToken: string,
): Promise<boolean> {
	const body = await req.text();
	const receivedSign = c.req.header("sign");

	if (!receivedSign) return false;

	const sign = createHmac("sha256", shopToken).update(body).digest("hex");

	return sign === receivedSign;
}

Recommendations:

  • Respond with 200 quickly (within 5s). Perform longer processing asynchronously.
  • Log signature failures with correlation identifiers but never log the raw token.

Event Payload Schema

type OrderStatus =
	| "pending"
	| "completed"
	| "failed"
	| "partially_completed"; // bundle orders only — see "Bundles & partial fulfillment"

type Order = {
	id: number; // Order ID in Nexus
	product: string; // Product ID
	externalId: string | null; // Your internal order ID (if supplied)
	amount: number; // Quantity purchased (always 1 for a bundle order)
	requestedQuantity: number | null; // Bundle: units intended (N); null for non-bundle orders
	deliveredQuantity: number | null; // Bundle: units delivered (M ≤ N); null for non-bundle orders
	totalPrice: number; // Total price paid (USD); for partially_completed, only delivered units
	status: OrderStatus; // Current status
	deliveryData: Record<string, string | number>; // Provided delivery info
	cdKeys: CDKey[] | null; // CD keys array if product type is `cdkey`
	errorCode?: OrderErrorCode; // Why a `failed` order failed, when we know. See below.
	createdAt: string; // ISO 8601 timestamp
	updatedAt: string; // ISO 8601 timestamp
};

// Codes a webhook can carry. Meanings are tabulated under "Order creation
// fail"; DUPLICATE_EXTERNAL_ID is the one code that never arrives this way.
type OrderErrorCode =
	| "INVALID_CREDENTIALS"
	| "INVALID_AMOUNT"
	| "PRODUCT_NOT_FOUND"
	| "PRODUCT_ALREADY_BOUGHT"
	| "SERVER_ERROR"
	| "UNKNOWN_ERROR"
	| "PROVIDER_ERROR"
	| "PROVIDER_CANCELLED"
	| "DELIVERY_UNCONFIRMED"
	| "USER_REACH_LIMIT"
	| "NOT_ENOUGH_SHOP_BALANCE"
	| "ROBLOX_NOT_IN_GAME"
	| "ROBLOX_NEVER_PLAY"
	| "ROBLOX_GAME_ERROR"
	| "PARTIAL_FULFILLMENT"; // accompanies a partially_completed bundle order

type CDKey = {
	code: string; // Redeemable key
	cardNo?: string; // Gift card / card number (if applicable)
	url?: string; // Redemption URL (if applicable)
	expireAt?: string; // ISO 8601 expiry (if applicable)
};

type WebhookPayload =
	| {
			type: "statusChange";
			order: Order;
			errorCode?: OrderErrorCode; // Present if order.status is 'failed' or 'partially_completed'
			errorId?: string; // UUID v7 for tracking (present for execution errors)
	  }
	| {
			type: "robloxSuperpassInGame";
			order: Order; // order.status will be "pending"
			employee: {
				username: string; // Roblox username of the employee
				profileUrl: string; // Roblox profile URL of the employee
			};
	  }
	| {
			type: "robloxLogpassVerification";
			order: Order; // order.status will be "pending"
			verification: {
				url: string; // Page the buyer must open to sign in and finish the purchase
			};
	  };

Example JSON Body

Successful Order

{
	"type": "statusChange",
	"order": {
		"id": 42,
		"product": "highrise:gold_pack:999_v2",
		"externalId": "ORDER-12345",
		"amount": 1,
		"totalPrice": 9.99,
		"status": "completed",
		"deliveryData": { "username": "foobar" },
		"cdKeys": [
			{
				"code": "ABCD-EFGH-IJKL",
				"expireAt": "2026-09-18T00:00:00.000Z"
			}
		],
		"createdAt": "2025-09-18T12:34:56.000Z",
		"updatedAt": "2025-09-18T12:35:10.000Z"
	}
}

Failed Order

{
	"type": "statusChange",
	"order": {
		"id": 43,
		"product": "highrise:gold_pack:999_v2",
		"externalId": "ORDER-12346",
		"amount": 1,
		"totalPrice": 9.99,
		"status": "failed",
		"deliveryData": { "username": "invalid_user" },
		"cdKeys": null,
		"createdAt": "2025-09-18T12:40:00.000Z",
		"updatedAt": "2025-09-18T12:40:05.000Z"
	},
	"errorCode": "INVALID_CREDENTIALS",
	"errorId": "019473a2-7c3f-7000-8000-000000000001"
}

Partially Completed Order (bundle)

{
	"type": "statusChange",
	"order": {
		"id": 45,
		"product": "pubg-mobile:uc:32000-bundle",
		"externalId": "ORDER-12348",
		"amount": 1,
		"requestedQuantity": 4,
		"deliveredQuantity": 3,
		"totalPrice": 7.50,
		"status": "partially_completed",
		"deliveryData": { "player_id": "5123456789" },
		"cdKeys": null,
		"createdAt": "2025-09-18T12:50:00.000Z",
		"updatedAt": "2025-09-18T12:50:08.000Z"
	},
	"errorCode": "PARTIAL_FULFILLMENT"
}

3 of the 4 units were delivered. totalPrice reflects only those 3 delivered units, and the undelivered unit's cost was restored to your shop balance automatically. For a code-based bundle, cdKeys would contain exactly the 3 delivered keys.

Roblox SuperPass In-Game Action

{
	"type": "robloxSuperpassInGame",
	"order": {
		"id": 44,
		"product": "roblox:passes:682387148499439cf26703a3",
		"externalId": "ORDER-12347",
		"amount": 1,
		"totalPrice": 11.39,
		"status": "pending",
		"deliveryData": { "username": "CoolPlayer" },
		"cdKeys": null,
		"createdAt": "2025-09-18T12:34:56.000Z",
		"updatedAt": "2025-09-18T12:34:56.000Z"
	},
	"employee": {
		"username": "worker123",
		"profileUrl": "https://www.roblox.com/users/5087011877/profile"
	}
}

This webhook is sent when a Roblox superpass order requires the buyer to join a game for the trade. The buyer should open the employee's profile and join their game room to complete the exchange.

  • order.status will be "pending" — the order is not yet complete
  • Display the employee's username and profileUrl to the buyer
  • The final result (completed / failed) will arrive later via a regular statusChange webhook

Roblox Logpass Verification

{
	"type": "robloxLogpassVerification",
	"order": {
		"id": 46,
		"product": "roblox:robux-login:1000",
		"externalId": "ORDER-12349",
		"amount": 1,
		"totalPrice": 9.75,
		"status": "pending",
		"deliveryData": { "username": "CoolPlayer", "password": "[hidden]" },
		"cdKeys": null,
		"createdAt": "2026-08-29T12:34:56.000Z",
		"updatedAt": "2026-08-29T12:34:56.000Z"
	},
	"verification": {
		"url": "https://.../v/KldB0zRd"
	}
}

This webhook is sent when a Roblox logpass order needs the buyer to sign into their own Roblox account. The order cannot progress until they do.

  • order.status will be "pending" — the order is not yet complete
  • Send the buyer to verification.url; they sign in and pass 2FA on that page
  • There is no timeout: an order whose buyer never completes verification stays pending indefinitely, so treat a missed delivery of this webhook as a stuck order and read the link back from GET /api/v1/utils/roblox/logpass-order/:orderId/verification
  • Any credential fields in order.deliveryData read "[hidden]" — we never echo them back
  • The final result (completed / failed) arrives later via a regular statusChange webhook

Security Best Practices

  • Enforce HTTPS and reject plain HTTP.
  • Use a distinct, unguessable webhook path (e.g. /webhooks/nexus/<uuid>).
  • Validate Content-Type is application/json.
  • Rate-limit the endpoint to mitigate abuse.
  • Store only required fields; avoid persisting entire raw payloads unless needed for audit.

For AI agents & programmatic access

Download this document as raw Markdown, or request GET /docs/api with the header Accept: text/markdown to receive the same source.

Download nexus-shop-api.md