Base URL: https://nexcodesa.com/api/v1
Every response is JSON, and every response — success or failure — has the same shape:
{"success": true, "data": { ... }}
{"success": false, "error": {"code": "...", "message": "..."}}
Check success first, then error.code. message is written for a human to read and its wording can change; code is stable and safe to branch on.
That envelope describes what the API itself produces. Anything rejected before it gets that far — an unknown path, the wrong HTTP method, a non-numeric id in GET /products/{id} — comes back as {"message": "..."} on its own, with no success and no error.code. So treat any response without success as a failure of that kind: check the path and the method, and do not go looking in it for an error code that will not be there.
Sign in to see the state of your key and examples using your own product references.
Version v1 supports inventory voucher purchases and manual player ID top-ups fulfilled by administrators. Use /products for vouchers and /topup-products for ID top-ups. Customer webhooks and callbacks are not implemented; read current fulfilment through GET /orders/{reference}.
Issue a key from the API page in the merchant portal and configure allowed IPs if needed. Call from your server over HTTPS with Authorization: Bearer and Accept: application/json. Purchases also require Content-Type: application/json and Idempotency-Key. The curl examples use Bash; set TOKEN locally to your secret and never expose it in browser code or a repository.
Start with GET /ping and GET /balance. For vouchers, GET /products supplies id for product_id. For ID top-ups, GET /topup-products supplies id for system_product_id; send delivery_type=top_up and player_id. Price and stock can change between reading and purchasing. The purchase response confirms acceptance; GET the order for its current fulfilment state.
This is a live API, not a sandbox: a successful POST /orders debits the real wallet. IDs, codes and balances below are illustrative, not guaranteed products or redeemable codes. Replace product_id with an ID returned to your account and generate a new Idempotency-Key for every independent purchase.
merchant_reference is your mapped product reference, not your external order ID. It is at most 64 characters, starts with a letter or number, and permits letters, numbers, spaces, dots, hyphens, underscores and / : #. There is no external order reference field; store the mapping between your order ID and data.reference in your own system.
GET /products defaults per_page to 50; GET /orders defaults to 25. The server clamps per_page to 1..100; page starts at 1. Both return data.items and data.pagination; read through last_page. The order list currently has no status or date filters.
Successful reads return HTTP 200; accepted purchases return HTTP 201. Vouchers complete synchronously; ID top-ups start pending until fulfilled by operators. Stored replays return 201 with Idempotent-Replay: true and can retain the original state; use GET for current state. Voucher codes are strings; serial_number and pin_code are optional. created_at and server_time use ISO 8601. Money is numeric in currency; use decimal arithmetic in your ledger.
On 429 wait error.retry_after_seconds; a Retry-After header is not currently sent. On 409 inspect error.code: wait for request_in_progress, and retrieve the returned order reference for request_abandoned. On network failure or 5xx do not create a new purchase key until the outcome is reconciled. Gateway errors and unmatched routes may not use the normal JSON envelope; check HTTP status and Content-Type first.
Download OpenAPI 3.0.3 — import into Postman or integration toolsSend your key in the Authorization header on every request:
Authorization: Bearer nx_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Each key has a limit on requests per minute. Go past it and the response is 429, with retry_after_seconds telling you how many seconds to wait before trying again.
Confirms your key works. Start here, before anything with money in it.
curl -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" \
https://nexcodesa.com/api/v1/ping
The wallet balance you have available to buy with.
The products available to you, and how much stock each one has.
| Parameter | Description |
|---|---|
search | Search by name |
in_stock | 1 to keep only the products something can actually be bought from: codes on the product's own shelf, or on the shelf of one of its active variations. A product whose only codes sit under a retired variation is no longer kept, because no order can reach them. |
per_page | Items per page (up to 100) |
page | Page number |
available_stock is how many codes are on that shelf at that moment, counted from them rather than read off a stored figure. Codes, not sellable units: with quantity_step at 3 and available_stock at 4, only 3 of them can be bought, because the quantity has to be a multiple of the step. A variation's stock (variations) sits on a shelf of its own, separate from the product's, and is never added to it.
A product whose category is switched off is gone from GET /products altogether, and GET /products/{id} answers product_not_found with status 404 — the same thing a switched-off product has always done. Removal, not a flag: do not wait for sellable to warn you, because the row is not there at all.
| Field | Description |
|---|---|
price | The price of one sellable unit — the same price the merchant pays out of his wallet in the shop, not the cost price. It is null when the product has no sale price on file. |
quantity_step | How many codes make up one unit. It is 1 for the overwhelming majority, and the quantity you order must be a multiple of it. |
sellable | false means the order will be refused — do not try to buy it. But true on its own is not permission to buy: all it says is that the shelf has a price on file. Anything switched off — a product, a variation or a category — is not flagged false, it simply does not appear; and stock is a third question, answered by in_stock. |
in_stock | true when some shelf on this product holds codes an order could take: its own, or one belonging to an active variation. It is the same question the in_stock=1 parameter filters on, and it neither replaces available_stock nor contradicts it: true with available_stock at 0 means the stock all sits under a variation, so order the variation. |
merchant_reference | Your own name for this shelf if you have linked one, or null. It appears per shelf: a variation is a shelf of its own and may be linked on its own. |
One product with its details and its variations. The same fields as GET /products for a single item, and the way to check price and stock immediately before buying without pulling the whole list.
curl -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" \
https://nexcodesa.com/api/v1/products/12
The number in the path is our product_id, not your own reference. The reference is used for buying only; to read a shelf you have named yourself, look it up in GET /products, where merchant_reference comes back beside every shelf.
A product that does not exist, or is not active, answers product_not_found with status 404.
Voucher mode: the codes come back in the response itself, and the amount is taken from your own wallet balance — the same wallet and exactly the same price as buying from the shop.
| Field | Description |
|---|---|
product_id | Our number for the product. It has not changed and will not change, and every existing integration keeps working on it as it is. |
merchant_reference | Optional and additive. Your own name for the product, once you have linked it from the API page in your dashboard. It reaches the same shelf product_id reaches, so you do not have to keep our numbers in your code. |
variation_id | Optional with product_id. With merchant_reference there is no need for it at all: the variation is part of the link itself. |
quantity | The number of codes. A multiple of quantity_step, and up to 100 in a single request. |
product_id and merchant_reference together and we answer product_reference_ambiguous instead of picking one of them. The reason: if they disagree — you repointed the reference and forgot the number hard-coded in your own code — then every choice we could make is the wrong one, and a purchase is not the place to guess. An unknown reference comes back as product_reference_not_found; we do not buy you something approximate.
Idempotency-Key header is required.
Generate a unique ASCII UUID for each independent purchase and reuse it for retries under the same API client. A stored successful result is replayed without a second debit. Different request data under an existing key returns 409. The fingerprint uses parsed request inputs and the endpoint, not the raw HTTP bytes: JSON whitespace alone does not matter, but field order, value types and query parameters can matter. Persist the original JSON body and resend it unchanged to avoid accidental conflicts.
5xx error leaves the outcome unknown and the key in progress. Stored successes become eligible for scheduled cleanup seven days after the original request. After cleanup the same key can buy again; never reuse old keys. Read the saved order reference through GET /orders/{reference} to retrieve an existing purchase.
request_in_progress. After five minutes, an existing purchase returns request_abandoned with its reference in error.reference; read it via GET /orders/{reference}. If no purchase is found, one retry may claim the attempt and proceed, restarting the five-minute wait. Recovery works with either product_id or merchant_reference. Unfinished records become eligible for scheduled cleanup one day after their original creation. After cleanup a retry can buy again. Do not rely on an old key to reconcile an unknown outcome; inspect GET /orders and contact support before issuing another purchase.
curl -X POST https://nexcodesa.com/api/v1/orders \
-H "Accept: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: 9f1c0a7e-6b2f-4f0b-9a1d-2f3c4d5e6a7b" \
-H "Content-Type: application/json" \
-d '{"product_id": 12, "quantity": 3}'
Or by your own reference, once you have linked the product:
curl -X POST https://nexcodesa.com/api/v1/orders \
-H "Accept: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: b3072e7d-9d2a-46c9-98fd-d113cf52419a" \
-H "Content-Type: application/json" \
-d '{"merchant_reference": "PUBG-60", "quantity": 3}'
{
"success": true,
"data": {
"reference": "01J9Z6...",
"status": "completed",
"product_id": 12,
"variation_id": null,
"merchant_reference": "PUBG-60",
"quantity": 3,
"units": 3,
"units_per_sale": 1,
"unit_price": 10.00,
"total_price": 30.00,
"currency": "SAR",
"created_at": "2026-09-21T00:00:00+00:00",
"codes": [
{"code": "EXAMPLE-CODE-001"},
{"code": "EXAMPLE-CODE-002"},
{"code": "EXAMPLE-CODE-003"}
]
}
}
quantity is the number of codes, units the number of sellable units (quantity ÷ units_per_sale). unit_price is the price of one unit, and total_price is exactly the two multiplied. The units_per_sale here is the same number as quantity_step in GET /products — one figure under two names, one describing the product and the other describing what was sold. For an ordinary product (units_per_sale = 1) the numbers are what they have always been.
Every order in a response carries merchant_reference: your own name for the shelf the codes came off, or null if you have not linked it. It is read at the moment the order is shown rather than at the time of purchase, so you see the name you use today; the product_id beside it is the fixed record of what was actually sold.
Retrieve a past order with its codes. The scope is the key, not the account: you see what this key bought and nothing else, and a reference bought by another key — including a second key of your own on the same account — answers 404 as though it did not exist.
What this key has bought, newest first. The scope is the key, not the account, so your second key does not see the first one's orders.
/topup-products lists published manual ID top-up products. Send its id as system_product_id when purchasing, not a voucher product ID. Supports search, page and per_page (default 50, maximum 100). /topup-products/{id} returns one product or 404 when unavailable. Inactive products and products mapped to external providers are excluded.
POST /orders with delivery_type=top_up, system_product_id and player_id, plus the Idempotency-Key header. player_id must be a string of 1..64 ASCII letters, digits, hyphens or underscores; quote it to preserve leading zeroes. quantity is optional and must be 1: one package for one player per request. Do not combine top-up fields with product_id, variation_id or merchant_reference.
Some games require additional fields such as a server. Products expose fields with id, type, required and options. Send a fields object keyed by field IDs with string values, for example "fields":{"42":"EU"}. Use options.value for dropdowns. player_id is separate and must not also appear inside fields. Unknown, missing or invalid fields return validation_failed before debit.
An accepted request debits the package price once and returns HTTP 201. status=pending means accepted and awaiting fulfilment, not delivered. The order appears in administration and goes to Telegram when selected in bot settings and ready for execution. Poll GET /orders/{reference} using the same API client to read the outcome. Customer webhooks are not implemented; poll within your rate limit.
status becomes completed after delivery or failed after rejection/failure. fulfilment_status adds detail: pending, pending_review, processing, completed or failed. refunded=true confirms the wallet credit. failure_reason is wrong_id or no_stock for those Telegram rejections, otherwise fulfilment_failed. codes is always an empty array, including after completion.
Repeating POST with the same key and body replays the original creation response without another debit. That response can remain pending after completion; GET returns current state. Recovery, 5xx handling and key retention rules also apply to top-ups. No stock before acceptance or insufficient balance refuses the purchase without debit. A wholly unfulfilled failure after acceptance follows the refund path; uncertain or partially delivered outcomes need operator review.
curl -X POST https://nexcodesa.com/api/v1/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5cd789f0-2463-4da1-b15e-b11d8f07a553" \
-d '{"delivery_type":"top_up","system_product_id":1,"player_id":"001234567"}'
curl -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
https://nexcodesa.com/api/v1/orders/REPLACE_WITH_RETURNED_REFERENCE
The illustrative JSON examples below use the field names returned by the application. Append every endpoint below to the base URL shown above.
GET /ping · HTTP 200{
"success": true,
"data": {
"client": "Example client",
"server_time": "2026-09-21T00:00:00+00:00",
"rate_limit_per_minute": 60
}
}
GET /balance · HTTP 200{
"success": true,
"data": {
"balance": 100,
"currency": "SAR"
}
}
GET /products · HTTP 200{
"success": true,
"data": {
"items": [
{
"id": 12,
"name": "Example game voucher",
"type": "voucher",
"currency": "SAR",
"merchant_reference": null,
"available_stock": 3,
"in_stock": true,
"price": 10,
"quantity_step": 1,
"sellable": true,
"variations": []
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 1,
"last_page": 1
}
}
}
GET /products/{product} · HTTP 200{
"success": true,
"data": {
"id": 12,
"name": "Example game voucher",
"type": "voucher",
"currency": "SAR",
"merchant_reference": null,
"available_stock": 3,
"in_stock": true,
"price": 10,
"quantity_step": 1,
"sellable": true,
"variations": []
}
}
GET /orders · HTTP 200{
"success": true,
"data": {
"items": [
{
"reference": "01K5A6B7C8D9E0F1G2H3J4K5M6",
"status": "completed",
"product_id": 12,
"variation_id": null,
"merchant_reference": null,
"quantity": 3,
"units": 3,
"units_per_sale": 1,
"unit_price": 10,
"total_price": 30,
"currency": "SAR",
"created_at": "2026-09-21T00:00:00+00:00",
"codes": [
{
"code": "EXAMPLE-CODE-001"
},
{
"code": "EXAMPLE-CODE-002"
},
{
"code": "EXAMPLE-CODE-003"
}
]
}
],
"pagination": {
"page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
}
}
}
POST /orders · HTTP 201{
"success": true,
"data": {
"reference": "01K5A6B7C8D9E0F1G2H3J4K5M6",
"status": "completed",
"product_id": 12,
"variation_id": null,
"merchant_reference": null,
"quantity": 3,
"units": 3,
"units_per_sale": 1,
"unit_price": 10,
"total_price": 30,
"currency": "SAR",
"created_at": "2026-09-21T00:00:00+00:00",
"codes": [
{
"code": "EXAMPLE-CODE-001"
},
{
"code": "EXAMPLE-CODE-002"
},
{
"code": "EXAMPLE-CODE-003"
}
]
}
}
{
"success": true,
"data": {
"reference": "01K5A6B7C8D9E0F1G2H3J4K5M7",
"status": "pending",
"delivery_type": "top_up",
"system_product_id": 1,
"player_id": "001234567",
"quantity": 1,
"units": 1,
"units_per_sale": 1,
"unit_price": 25,
"total_price": 25,
"currency": "SAR",
"created_at": "2026-09-21T08:00:00+00:00",
"fulfilment_status": "processing",
"refunded": false,
"failure_reason": null,
"codes": []
}
}
GET /orders/{reference} · HTTP 200{
"success": true,
"data": {
"reference": "01K5A6B7C8D9E0F1G2H3J4K5M6",
"status": "completed",
"product_id": 12,
"variation_id": null,
"merchant_reference": null,
"quantity": 3,
"units": 3,
"units_per_sale": 1,
"unit_price": 10,
"total_price": 30,
"currency": "SAR",
"created_at": "2026-09-21T00:00:00+00:00",
"codes": [
{
"code": "EXAMPLE-CODE-001"
},
{
"code": "EXAMPLE-CODE-002"
},
{
"code": "EXAMPLE-CODE-003"
}
]
}
}
GET /topup-products · HTTP 200{
"success": true,
"data": {
"items": [
{
"id": 1,
"name": "Example ID top-up package",
"category": "Example game",
"delivery_type": "top_up",
"price": 25,
"currency": "SAR",
"available_stock": 3,
"sellable": true,
"quantity_step": 1,
"max_quantity": 1,
"fields": []
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 1,
"last_page": 1
}
}
}
GET /topup-products/{product} · HTTP 200{
"success": true,
"data": {
"id": 1,
"name": "Example ID top-up package",
"category": "Example game",
"delivery_type": "top_up",
"price": 25,
"currency": "SAR",
"available_stock": 3,
"sellable": true,
"quantity_step": 1,
"max_quantity": 1,
"fields": []
}
}
{"success": false, "error": {"code": "order_refused", "message": "Insufficient stock", "reason": "insufficient_stock"}}
| Code | HTTP | Meaning |
|---|---|---|
missing_token |
401 | No authentication header was sent |
invalid_token |
401 | The key is not valid |
client_disabled |
403 | The account is disabled |
ip_not_allowed |
403 | The address is not on the allowlist |
rate_limited |
429 | You went past the request limit |
client_without_wallet |
409 | The key is not attached to an account with a wallet. It comes back as a code of its own on GET /balance, and as a reason in error.reason under order_refused if you try to buy: one fault in two places. |
idempotency_key_required |
400 | A purchase with no idempotency key |
idempotency_key_too_long |
400 | The Idempotency-Key value is longer than 128 bytes. It is refused as it stands rather than truncated, because a truncated key could collide with another one and replay you an order that is not yours. |
idempotency_key_reused |
409 | The same key for different content |
request_in_progress |
409 | A request with the same key is still running |
request_abandoned |
409 | A request with the same key did not finish, and its order was created — read it at GET /orders/{reference}; the reference comes back to you in error.reference. Do not buy again. |
validation_failed |
422 | Data missing or not valid (details in error.fields) |
variation_mismatch |
422 | The variation (variation_id) does not belong to the product you asked for |
product_not_found |
404 | On GET /products/{id} only: the product does not exist, is switched off, or its category is. A purchase goes differently: a product number that does not exist fails validation and comes back as validation_failed with status 422, and a product that exists but is switched off comes back as order_refused with status 409 and reason: product_inactive. |
product_reference_not_found |
404 | merchant_reference is not linked to any product on your account. The reference that reached us comes back to you in error.merchant_reference, to compare against what you sent. |
product_reference_ambiguous |
422 | You named the product twice: product_id together with merchant_reference, or a variation_id that disagrees with the variation stored in the link. |
order_not_found |
404 | No order with that reference under this key. A wrong reference and a reference belonging to another key give the same answer, deliberately. |
order_refused |
409 | The purchase was refused. The error.reason field names the cause (see the table below), but it is not guaranteed: an unexpected refusal arrives with no error.reason at all. A missing one means the cause is unknown, not that it is temporary — do not retry on it automatically; read error.message and treat it as a fault. |
server_error |
500 | An unexpected error. Do not assume nothing was charged: retry with the same idempotency key, and if you get request_abandoned back, the order exists and its reference is in the response. |
error.reason alongside order_refused)| Reason | Retry? | Meaning |
|---|---|---|
insufficient_stock |
Yes, later | Stock is below what you asked for at this moment |
insufficient_balance |
After topping up | The wallet balance is not enough |
quantity_step |
Yes, with a different quantity | This product is sold in units of several codes; the quantity must be a multiple of quantity_step |
product_inactive |
No | The product, the variation or the category is switched off |
price_unavailable |
No | This shelf has no sale price on file. Do not retry; contact support. |
client_without_wallet |
No | The key is not attached to an account with a wallet |
No charge without codes.
The debit and the handover of the codes happen together, or neither happens: any failure before the transaction commits puts the balance and the stock back exactly as they were. One gap, stated plainly: a failure after the commit — while the response is being assembled — leaves the order raised and the wallet debited, and you get a 500. That is not a charge without codes, it is codes that did not reach you: recover them by retrying with the same idempotency key, which answers request_abandoned with the order reference, to read with its codes at GET /orders/{reference}.
No code for two customers. Codes are locked while a sale runs, so two simultaneous orders for the last code do not end up holding the same one — one of them gets it and the other gets a clear refusal.
NEXCODE · For technical support, contact your service provider.