NEXCODE — API

Version v1 · Vouchers and player ID top-ups

العربية

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.

Your key

Sign in to see the state of your key and examples using your own product references.

Before you integrate

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 tools

Authentication

Send your key in the Authorization header on every request:

Authorization: Bearer nx_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The key is shown once and only when you create it. We store its fingerprint, not the key itself, so there is nothing to hand back — if you lose it, ask for a rotation. And if you have set an IP allowlist on your account, requests from any other address are refused even with the right key.

Rate limits

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.

Endpoints

GET /ping

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

GET /balance

The wallet balance you have available to buy with.

GET /products

The products available to you, and how much stock each one has.

ParameterDescription
searchSearch by name
in_stock1 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_pageItems per page (up to 100)
pagePage 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.

FieldDescription
priceThe 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_stepHow many codes make up one unit. It is 1 for the overwhelming majority, and the quantity you order must be a multiple of it.
sellablefalse 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_stocktrue 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_referenceYour 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.

GET /products/{id}

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.

POST /orders

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.

Naming the product: two ways, one per request

FieldDescription
product_idOur number for the product. It has not changed and will not change, and every existing integration keeps working on it as it is.
merchant_referenceOptional 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_idOptional with product_id. With merchant_reference there is no need for it at all: the variation is part of the link itself.
quantityThe number of codes. A multiple of quantity_step, and up to 100 in a single request.
Name the product one way, not two. Send 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.
The 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.

Only successful results are stored. A purchase refused before execution (stock, balance or validation) releases its key so you can retry after fixing the cause. A 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.

On a lost response, retry the same key and request. An unfinished attempt updated less than five minutes ago returns 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.

GET /orders/{reference}

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.

GET /orders

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.

Player ID top-ups

GET /topup-products

GET /topup-products/{id}

/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

Complete endpoint response examples

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": []
    }
}

Example purchase refusal before execution — HTTP 409

{"success": false, "error": {"code": "order_refused", "message": "Insufficient stock", "reason": "insufficient_stock"}}

Error codes

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.

Reasons for refusal (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

Voucher delivery guarantees

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.