- Recent changes
- Webhook callbacks
- API requests
- API methods
- Signature creation and validation
- Example webhook callback handlers
- OAuth + OpenID identity provider
- Reseller integration
- 22 Dec 2024
- Add native reseller integration docs
- 02 Dec 2024
- New api domain / endpoint
- 28 Jan 2024
- Added OAuth and OpenID info
- 08 Dec 2023
- Added
is-user-invited
method - Updated product list
- Added
- 27 Dec 2022
- It's now possible to use string unique request IDs
- 25 Apr 2022
- Added
get-balance
method - Webhooks are now have
kind
parameter,
so they can be distinguished easily when sent to single endpoint - Updated docs
- Added
- 04 Feb 2022
- Added
is-user-exists
method - Added
get-prices
method - Success/failure responses are now consistent between methods (without breaking changes)
- Added
Event notifications sent to your server as POST requests with json body. You can set webhook urls in market api settings
For signature verification refer to signatures section. You should always check signature validity and ignore any events with invalid signature
You can also check example webhook callback handler here
This event is sent when balance is transferred to your account
{
"kind": "transfer",
"amount": 1,
"username": "A49",
"unique_id": 22,
"signature": "51a6c1c281c4cd8c4e020f89cc3bdca6aa6c0747fc33f7112ef041064bd864a2"
}
Parameter | Description |
---|---|
kind |
Event type |
amount |
Amount received |
username |
Sender username |
unique_id |
Incrementing transfer id |
signature |
Event signature |
This event is sent when your item is purchased
{
"kind": "purchase",
"amount": 0.9,
"username": "A49",
"unique_id": 89968,
"item_id": "E3yugw",
"signature": "dc20a4d73447ac51689d6e03115aa135a8d734e610352dda818e830e70a60560"
}
Parameter | Description |
---|---|
kind |
Event type |
username |
Buyer |
unique_id |
Incrementing purchase id |
item_id |
Bought item code |
signature |
Event signature |
API domain is:
user-api.neverlose.cc
Marketplace API endpoint is:
user-api.neverlose.cc/api/market/<method>
API requests should be sent with POST method and Content-Type: application/json
Warning
Use of old domain/endpoint neverlose.cc/api/market
is still supported but discouraged.
Users are advised to migrate to new endpoint as soon as possible
Common parameters used in all types of actions (unless specified otherwise):
Parameter | Description | Additional |
---|---|---|
user_id |
Your user id | You can get it on market api settings page |
signature |
Request signature | Refer to signatures section |
id |
Unique request id | Used to prevent erroneous repetitive requests. Not needed for read-only requests |
id
parameter should be unique between requests. If you haven't received a successful response
for your request it's safe to try it again with the same id
parameter. If your action haven't
been completed it will be completed with second request, and if it was, and you failed to receive
the response previously for some unrelated reason you will receive Invalid id.
error and you will not
spend funds twice.
Unique id format
Parameter | Limitation |
---|---|
Type | integer or string ("id": 123 or "id": "abcde123" ) |
Max length | 80 characters (for both numbers and strings) |
Allowed characters | Lower and upper case latin alphabet, numbers, and .-_ are supported.Whitespace is not supported |
curl 'https://user-api.neverlose.cc/api/market/give-for-free' \
--data '{"user_id": 1, "signature": "..."}' \
-X POST --header "Content-Type: application/json"
Replace data and url in example above depending on action you need to do
{
"succ": true,
"success": true
}
Successful responses may contain additional fields, refer to methods documentation below.
succ
field is left for backwards compatibility with older api revisions and always have the same value as success
.
{
"succ": false,
"success": false,
"error": "Error message"
}
List of valid products you can specify in product
request field of methods below:
product name |
Game name |
---|---|
csgo |
CS:GO |
cs2 |
CS2 |
URL: /api/market/give-for-free
{
"user_id": 1,
"id": 1338,
"username": "darth",
"code": "E3yugw",
"signature": "c0e8a7fa9c9fafe16d21ad0be087a6372bb7a9256fab212ff106666a152c6e0a"
}
This request gives item E3yugw
to user darth
Parameter | Description |
---|---|
username |
Username that will receive this item |
code |
Market code of item you want to give |
This method is available for official resellers only
URL: /api/market/transfer-money
{
"user_id": 1,
"id": 1337,
"username": "a49",
"amount": 2.00,
"signature": "32208d45c593478eceb0e15aa0f8013a1259c1ef32f755edf2c49b9df2072aa2"
}
This request transfers 2 NLE
to user a49
Parameter | Description |
---|---|
username |
Username that will receive NLE |
amount |
Amount of NLE you want to transfer |
This method is available for official resellers only
URL: /api/market/gift-product
{
"user_id": 1,
"id": 2,
"username": "darth",
"product": "csgo",
"cnt": 0,
"signature": "32208d45c593478eceb0e15aa0f8013a1259c1ef32f755edf2c49b9df2072aa2"
}
This request gifts 30 days
for CS:GO to user darth
Parameter | Description |
---|---|
username |
Username that will receive product |
product |
Product name |
cnt |
Upgrade type (refer to table below) |
cnt
parameter:
cnt |
Price | Days |
---|---|---|
0 |
17.1 NLE | 30 |
1 |
44.1 NLE | 90 |
2 |
80.1 NLE | 180 |
3 |
134.1 NLE | 365 |
For converted RUB prices refer to get-prices
method below
This method is available for official resellers only
URL: /api/market/get-prices
{
"user_id": 1,
"product": "csgo",
"signature": "..."
}
Parameter | Description |
---|---|
product |
Product name |
Read-only method, id
parameter is not needed here
Response:
{
"succ": true,
"success": true,
"prices": {
"30": {
"cnt": 0,
"eur": 17.1,
"rub": 1368
},
"90": {
"cnt": 1,
"eur": 44.1,
"rub": 3529
},
"180": {
"cnt": 2,
"eur": 80.1,
"rub": 6410
},
"365": {
"cnt": 3,
"eur": 134.1,
"rub": 10731
}
}
}
This request will return current prices for selected product
This method is available for official resellers only
URL: /api/market/is-user-invited
{
"user_id": 1,
"username": "target_user",
"product": "cs2",
"signature": "..."
}
Parameter | Description |
---|---|
product |
Product name |
username |
Login of user to check |
Read-only method, id
parameter is not needed here
Response:
{
"succ": true,
"success": true,
"cheat_public": false,
"user_invited": true
}
- will return
cheat_public=true
if cheat does not require invite (user_invited
will always be true in this case) - when
cheat_public=false
,user_invited
determines whether this user is invited to this product or not. - You won't be able to gift subscription to non-invited user when cheat is not public!
This method is available for official resellers only
URL: /api/market/is-user-exists
{
"user_id": 1,
"username": "darth",
"signature": "..."
}
Parameter | Description |
---|---|
username |
Username to check |
Read-only method, id
parameter is not needed here
Response:
{
"success": true,
"succ": true,
"user_exists": true
}
user_exists
field will be true
if user darth
exists, false
otherwise
URL: /api/market/get-balance
{
"user_id": 1,
"signature": "..."
}
Read-only method, id
parameter is not needed here
Response:
{
"succ": true,
"success": true,
"balance": 62.6
}
#!/usr/bin/python3
from hashlib import sha256
def market_api_generate_signature(j, secret):
str_to_hash = ("".join([i + str(j[i]) for i in sorted(j)]) + secret).encode()
# print(str_to_hash)
hashed = sha256(str_to_hash).hexdigest()
return hashed
def market_api_validate_signature(j, secret):
nl_sig = j["signature"]
del j["signature"]
our_sig = market_api_generate_signature(j, secret)
return nl_sig == our_sig
# Validation
event_data = {
"amount": 0.9,
"username": "A49",
"unique_id": 89968,
"item_id": "E3yugw",
"signature": "dc20a4d73447ac51689d6e03115aa135a8d734e610352dda818e830e70a60560"
}
assert (market_api_validate_signature(event_data, "key") == True)
# Generation
request_data = {
"user_id": 1,
"id": 1337,
"username": "a49",
"amount": 2.00,
}
request_data.update(signature=market_api_generate_signature(request_data, "key"))
"use strict";
let crypto = require("crypto");
const sort_obj = function(obj) {
return Object.keys(obj).sort().reduce(function (result, key) {
result[key] = obj[key];
return result;
}, {});
}
const obj_to_string = function (obj) {
obj = sort_obj(obj);
let str = '';
for (let p in obj) {
if (obj.hasOwnProperty(p)) {
str += p + obj[p];
}
}
return str;
}
const market_api_generate_signature = function(j, secret){
const str_to_hash = obj_to_string(j) + secret
const hashed = crypto.createHash('sha256').update(str_to_hash).digest('hex');
return hashed
}
const market_api_validate_signature = function(j, secret){
const nl_sig = j["signature"]
delete j["signature"]
const our_sig = market_api_generate_signature(j, secret)
return nl_sig === our_sig
}
// Validation
let data = {
"amount": 0.9,
"username": "A49",
"unique_id": 89968,
"item_id": "E3yugw",
"signature": "dc20a4d73447ac51689d6e03115aa135a8d734e610352dda818e830e70a60560"
}
let sign_valid = market_api_validate_signature(data, "key")
console.log(sign_valid)
composer require rainedot/php-nl-market
require("vendor/autoload.php");
$api = new \Rainedot\PhpNlMarket\MarketAPI('YOUR_API_KEY', 1);
$api->validateRequest(array $request); // Returns true if request is valid
#!/usr/bin/python3
from bottle import run, request, post
import bottle
from hashlib import sha256
SECRET_KEY = "key"
def market_api_generate_signature(j, secret):
str_to_hash = ("".join([i + str(j[i]) for i in sorted(j)]) + secret).encode()
hashed = sha256(str_to_hash).hexdigest()
return hashed
def market_api_validate_signature(j, secret):
nl_sig = j["signature"]
del j["signature"]
our_sig = market_api_generate_signature(j, secret)
return nl_sig == our_sig
@post("/on_purchase")
def on_purchase():
data = request.json
if not market_api_validate_signature(data, SECRET_KEY):
return "invalid signature"
res = data["username"] + " bought item https://neverlose.cc/market/item?id=" + data["item_id"]
print(res, flush=True)
return res
app = application = bottle.Bottle()
run(host='0.0.0.0', port=8080)
const express = require('express')
const app = express()
const crypto = require("crypto")
const secret_key = "key"
const sort_obj = function(obj) {
return Object.keys(obj).sort().reduce(function (result, key) {
result[key] = obj[key]
return result
}, {})
}
const obj_to_string = function (obj) {
obj = sort_obj(obj)
let str = ''
for (let p in obj) {
if (obj.hasOwnProperty(p)) {
str += p + obj[p]
}
}
return str
}
const market_api_generate_signature = function(j, secret){
const str_to_hash = obj_to_string(j) + secret
const hashed = crypto.createHash('sha256').update(str_to_hash).digest('hex')
return hashed
}
const market_api_validate_signature = function(j, secret){
const nl_sig = j["signature"]
delete j["signature"]
const our_sig = market_api_generate_signature(j, secret)
return nl_sig === our_sig
}
app.post('/on_purchase', (req, res) => {
if (!market_api_validate_signature(req.body.data, secret_key))
res.send("invalid signature")
let response = req.body.data.username + " bought item https://neverlose.cc/market/item?id=" + req.body.data.item_id
console.log(response)
res.send(response)
})
app.listen(8080)
Neverlose now supports OAuth authorization and being an OpenID idP.
Supported api versions and their docs:
- OpenID Connect Core 1.0 (OIDC)
- OAuth 2.0
Client IDs are issued manually.
If you need one, please open a new support ticket and describe your use-case,
we will gladly register your app if it meets our guidelines.
Endpoint | URL |
---|---|
Auth URI | https://auth2.neverlose.cc/oauth/authorize |
Token URI | https://auth2.neverlose.cc/oauth/token |
OIDC Userinfo | https://auth2.neverlose.cc/oauth/oidc_userinfo |
- Supported response types:
code
- Supported grant types:
authorization_code
- Refresh tokens are NEVER issued, access_token expire time can be adjusted at request
state
parameter is required unlessno_state
is in scope- PKCE (
code_challenge
) is required unless you have good reason to not implement it.no_pkce
will not work unless you are permitted to use it - PKCE S256 is the only supported challenge method (
plain
is not supported)
Scope | Description |
---|---|
profile |
Read access to user's login and profile picture |
email |
Read access to user's email |
openid |
Required to access OIDC endpoints |
no_state |
Allows to omit state parameter during auth flow |
no_pkce |
Allows to skip/omit PKCE during auth flow (requires special permission) |
Field | Content | Scope needed |
---|---|---|
sub |
Numeric user id (presented as string per OIDC spec) | openid |
preferred_username |
User's login | profile |
name |
User's login | profile |
profile |
URL to user's profile (forum) | profile |
picture |
URL to user's profile picture (PNG) | profile |
email |
User's email | email |
Reseller integration provides a way for you to integrate your service
into our payment/checkout UI.
You can set up your integration at API settings page. If you don't see
"Reseller integration" settings section, and you're willing to use it,
please contact us through tickets section.
When user clicks on your payment method in our checkout UI, they will be
redirected to your website (provided in Redirect URL in market API settings),
and redirect URL will contain a query argument nl_purchase
that hold JWT token with information
about purchase. For example:
Redirect URL in settings: https://test.local/checkout
User will be redirected to: https://test.local/checkout?nl_purchase=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoicGF5bG9hZCIsImZvbyI6ImJhciIsImlzcyI6Ik5MUkkiLCJleHAiOjB9.yNvxOjAE0K8ns4YiMI7oV8XV3R8qGpf2my0HHQGDO_I
JWT payload format is explained in latter section
After creating your integration and specifying valid Redirect URL, your payment method will be present in method dropdown on product purchase page.
Please note that until you enable Public checkbox, your method is visible only to you. You can use this private method to test your integration and set it to public as soon as integration is ready.
User will also see your actual prices in our checkout UI if you provide them.
You can provide your prices using this market api method:
URL: /api/market/set-reseller-prices
Prices object:
{
"cs2-30": {"EUR": ["30.50", "31.30"], "USD": ["31.82", "32.65"]},
"csgo-180": {"EUR": "14.10", "USD": "14.71", "XMR": "0.162"},
"marketplace": {"EUR": "1.1", "USD": "1.15"}
}
Request:
{
"user_id": 1,
"integration_id": 100,
"prices": "<prices json>",
"signature": "..."
}
Parameter | Description |
---|---|
integration_id |
ID of your reseller integration (check on api settings page) |
prices |
Product-Currency-Price object in JSON string (explained below) |
Response will be simple success: true|false
response
Note that you need to first compose prices object, serialize it to json, then
put it as string to "prices"
key of request object. This is necessary to keep signature
algorithm compatible. Example pseudo-Javascript code:
const request = {
user_id: 1,
integration_id: 100,
prices: JSON.stringify({
"cs2-30": {...},
"csgo-180": {...}
})
}
request.signature = generateSignature(request)
const requestJson = JSON.stringify(request)
fetch("...", {method: "POST", body: requestJson, ...})
Price set using this method will automatically expire after 24 hours, so you should
update them periodically using automatic script, otherwise prices will disappear from checkout UI.
You can check remaining time until expiration on API settings page. We recommend you to update
prices each hour if they are changed frequently and/or dynamically. Otherwise, if your prices
are set manually and mostly static, we recommend you to update them each 12 hours.
Prices object:
Example describes the following prices:
- You sell CS2 (30 days) for:
- from 30.50 to 31.30 EUR
- from 31.82 to 32.65 USD
- This means that you have multiple payment methods for this product that fit specified price range
- You sell CS:GO (180 days) for:
- 14.10 EUR
- 14.71 USD
- 0.162 XMR
- In this case there's exactly one price for all methods available for this product
- You sell 1 NLE for:
- 1.1 EUR
- 1.15 USD
Rules:
- First level key should be in format
<product_name>-<days>
- Product names are same as in
gift-product
method days
should be existing plan presented as days count (30, 90, 180, etc.)- If you don't specify plan in key it will default to 30 days (i.e.
"cs2"
is same as"cs2-30"
)
- If you don't specify plan in key it will default to 30 days (i.e.
- Special product
marketplace
sets price for 1 NLE market topup - 2nd level key is currency name
- Currency name should be exactly 3 uppercase latin letters. Any other names will result in invalid format error.
- This object should contain up to 3 currencies. Specifying more currencies will result in an error
- 2nd level value is price or price range for this product in this currency
- Decimal values should be passed as strings to avoid rounding errors
- To specify a range you should pass array with exactly 2 elements
- First price in a range should be less than second one
- To specify singular price you should pass number string as value without array
- Product names are same as in
JWT token passed to your redirect URL (see "How it works?" section above) is constructed as following:
- Algorithm:
HS256
- Secret key: your Neverlose API key
- Claims:
iss
(Issuer) - alwaysNLRI
iat
(Issued at) - order creation timestampexp
(Expire) - timestamp at which this token is not valid anymore (currently iat + 1 hour)
Mainstream JWT libraries should check these fields automatically (you'll only need to provide valid Issuer).
Caution
We strongly encourage you to use proper JWT library for your language that properly checks token signature.
This is important to avoid phishing attacks and scam attempts.
Payload fields:
Field | Description |
---|---|
integration_id |
ID of your reseller integration |
login |
Login of user that should receive the order |
email |
Email of receiving user |
product |
Product name that user ordered (same names as in gift-product ); marketplace if market top-up |
market |
true if NLE (market top-up) order, false if product order |
cnt |
Plan number requested by user (same as in gift-product ), null for market |
days (product only) |
Number of days to gift to user (provided for convenience, always in sync with cnt field) |
nle (market only) |
NLE amount requested by user (Decimal number presented as string, with . as decimal separator) |
After receiving this redirect you should direct user directly to payment method selection, or directly to order confirmation if you have only one method available for customers of this product.
After confirming user's payment, you should gift the product or transfer funds to login
that was previously specified in JWT token using usual methods gift-product
and transfer-money
Full payload example:
For product:
{
"iss": "NLRI",
"iat": 1734899417,
"exp": 1734903017,
"integration_id": 100,
"product": "cs2",
"cnt": 0,
"login": "a47",
"email": "foo@bar.baz",
"market": false,
"days": 30
}
For market:
{
"iss": "NLRI",
"iat": 1734899417,
"exp": 1734903017,
"integration_id": 100,
"product": "marketplace",
"cnt": null,
"login": "a47",
"email": "foo@bar.baz",
"market": true,
"nle": "13.37"
}