Getting Started
With the Pinbasket dealer API you can pull our category and product catalog into your own system, with live prices in your own currency. The API is a read-only REST interface that returns JSON. Every request uses GET and is authenticated with your API key. The version is part of the URL: we will not make breaking changes within v1, though new fields may be added over time.
-
1
Successful responses contain
ok: trueplusdataand, for lists,meta. -
2
Error responses return
ok: falseand a machine-readableerror.code. -
3
All responses are UTF-8 encoded
application/json.
- Base URL
https://playsultan.com/api/v1- Auth Bearer
Authorization: Bearer pb_live_…- Rate limit
- 120 / 60s
- Supported languages
- TR, EN, DE
- Supported currencies
- 16 currencies
{
"ok": true,
"data": [ … ],
"meta": { "page": 1, "total": 2104 }
}
{
"ok": false,
"error": {
"code": "invalid_currency",
"message": "Unsupported currency. Available: TRY, USD, EUR…"
}
}
- 1 Generate an API key from your account. Get an API key
-
2
Send the key in every request as
Authorization: Bearer <key>. -
3
Call
GET /productswith your preferredlangandcurrencyquery parameters. -
4
Follow
meta.has_moreand incrementpageuntil the full catalog is synced.
Authentication
Send your key as a Bearer token in the Authorization header on every request. If your environment cannot forward that header, X-Api-Key is accepted as an alternative.
# Recommendedcurl "https://playsultan.com/api/v1/me" \
-H "Authorization: Bearer pb_live_xxxxxxxxxxxxxxxx"
# Alternativecurl "https://playsultan.com/api/v1/me" \
-H "X-Api-Key: pb_live_xxxxxxxxxxxxxxxx"
Test API
The Test API lives on a separate Base URL: /api/test/v1. Create a pb_test_… key in your member panel. Catalog endpoints return live data; POST /orders creates a sandbox order with fake TEST-… codes and test: true — no wallet debit, no pin stock.
- Base URL
https://playsultan.com/api/test/v1- Auth Bearer
Authorization: Bearer pb_test_…
-
1
Open the API page in your customer panel and create a Test API key (
pb_test_…). Live and test keys can exist at the same time. -
2
Point your client at
/api/test/v1(not/api/v1). CallGET /products,GET /categories, etc. — prices and stock reflect production catalog data. -
3
Call
POST /orderson the test base. No wallet debit, no real pin assignment, no supplier top-up. The order number is prefixed withTEST-and each item returns a fake code such asTEST-A1B2C3D4. -
4
Configured webhooks still fire for test orders so you can verify your receiver. Responses include
test: true/ order numbers starting withTEST-.
curl -X POST "https://playsultan.com/api/test/v1/orders?lang=en¤cy=TRY" \
-H "Authorization: Bearer pb_test_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"product_id":501,"quantity":1,"client_order_id":"sandbox-001"}'
/api/test/v1 (or a test key on /api/v1) returns 403 wrong_key_mode. IP allowlist and webhook URL are shared. GET /me returns mode and base for the endpoint you called.IP Protection
You must lock your key to at least one server address. Configure up to 4 IP addresses on the API page of your member panel; requests from any other address are rejected with 403 ip_not_allowed.
- Both IPv4 and IPv6 are supported, and matching ignores formatting (
::1and0:0:0:0:0:0:0:1are treated as the same address). - The client address is read only from the trusted proxy chain; you cannot influence the restriction by sending headers such as
X-Forwarded-Foryourself. Behind NAT or a proxy, add your outbound IP to the list. - Changing the list does not invalidate the key; the new restriction takes effect on the next request.
Request from an address that is not on the list
{
"ok": false,
"error": {
"code": "ip_not_allowed",
"message": "Requests from this IP address are not allowed for this key."
}
}
Common Parameters
Every catalog endpoint supports language and currency selection. If you omit these two parameters, the defaults configured for your key are used; you can see them in your member panel.
| Parameter | Type | Values | Description |
|---|---|---|---|
lang |
string | tr | en | de | Sets the language of names, slugs and URLs in the response. Falls back to the key’s default language. |
currency |
string | TRY | CNY | USD | EUR | GBP | USDT … | Currency the prices are returned in. Falls back to the key’s default currency. |
price_try.Endpoints
All paths are appended to the base URL above. v1 accepts GET only; other methods return 405.
Checks that the service is up. Requires no authentication.
{
"ok": true,
"data": {
"service": "Pinbasket",
"version": "v1",
"time": "2026-07-30T22:46:55+03:00"
}
}
Dealer details and wallet balances.
{
"ok": true,
"data": {
"user_id": 133,
"name": "Example Dealer",
"email": "[email protected]",
"key_label": "Dealer API",
"key": "pb_live_9076…f3e9",
"scopes": ["catalog:read", "orders:read", "orders:write"],
"balances": [
{ "currency": "TRY", "balance": 12500.50 },
{ "currency": "EUR", "balance": 320.00 }
],
"webhook": { "url": "https://example.com/hooks/pinbasket", "events": ["order.completed", "order.cancelled"], "configured": true }
}
}
Supported currencies with their current TRY rates.
{
"ok": true,
"data": [
{ "code": "TRY", "name": "Turkish Lira", "symbol": "₺", "try_rate": 1 },
{ "code": "EUR", "name": "Euro", "symbol": "€", "try_rate": 53.853771 }
],
"meta": { "count": 16, "base": "TRY" }
}
Lists active categories, either as a flat list or as a nested tree.
| Parameter | Type | Default | Description |
|---|---|---|---|
lang |
string | tr | en | de | Sets the language of names, slugs and URLs in the response. Falls back to the key’s default language. |
parent_id |
integer | — | Returns only the direct children of this category. Pass 0 or leave empty for top-level categories. |
tree |
boolean | 0 | When 1, categories are returned as a nested tree via the children field. |
with_counts |
boolean | 0 | When 1, a product_count field is added to every category. |
{
"ok": true,
"data": [
{
"id": 5742,
"parent_id": null,
"name": "Telegram",
"slug": "telegram",
"type": "pin",
"image": "https://pinbasket.com/assets/images/categories/telegram.png?v=1784723055",
"url": "https://pinbasket.com/en/category/telegram",
"sort_order": 0,
"product_count": 12,
"children": []
}
],
"meta": { "lang": "en", "count": 38, "tree": true }
}
Lists active products with pagination. Prices are returned in the currency you request.
| Parameter | Type | Default | Description |
|---|---|---|---|
lang |
string | tr | en | de | Sets the language of names, slugs and URLs in the response. Falls back to the key’s default language. |
currency |
string | TRY | CNY | USD | EUR | GBP | USDT … | Currency the prices are returned in. Falls back to the key’s default currency. |
category_id |
integer | — | Filters products in this category and all of its subcategories. |
q |
string | — | Searches product names, including translations in all languages. |
in_stock |
boolean | 0 | When 1, only purchasable products (in stock or unlimited) are returned. |
page |
integer | 1 | Page number. |
per_page |
integer | 50 | Records per page. Maximum 200. |
{
"ok": true,
"data": [
{
"id": 445,
"category_id": 5695,
"category_name": "PlayStation United States",
"name": "PlayStation $1 USD PSN United States",
"slug": "playstation-1-usd-psn-united-states",
"type": "pin",
"price": 0.82,
"currency": "EUR",
"price_try": 44.25,
"old_price": null,
"unit": null,
"min_quantity": 1,
"max_quantity": 10,
"stock": 42,
"in_stock": true,
"unlimited_stock": false,
"image": "https://pinbasket.com/assets/images/categories/psn.png?v=1784721829",
"url": "https://pinbasket.com/en/product/playstation-1-usd-psn-united-states"
}
],
"meta": {
"page": 1,
"per_page": 50,
"total": 2104,
"total_pages": 43,
"has_more": true,
"lang": "en",
"currency": "EUR"
}
}
Returns a single product together with the fields required to order it and its delivery zones.
| Parameter | Type | Default | Description |
|---|---|---|---|
lang |
string | tr | en | de | Sets the language of names, slugs and URLs in the response. Falls back to the key’s default language. |
currency |
string | TRY | CNY | USD | EUR | GBP | USDT … | Currency the prices are returned in. Falls back to the key’s default currency. |
{
"ok": true,
"data": {
"id": 501,
"name": "Valorant 1700 VP Turkey",
"price": 6.50,
"currency": "EUR",
"stock": 42,
"in_stock": true,
"required_fields": [
{
"key": "player_id",
"label": "Player ID",
"type": "text",
"required": true,
"placeholder": "player#1234"
}
],
"delivery_zones": [
{ "id": 7, "name": "Europe" }
]
},
"meta": { "lang": "en", "currency": "EUR" }
}
Creates a wallet-paid order for a single product. Requires scope orders:write. Use client_order_id for idempotency.
| Parameter | Type | Default | Description |
|---|---|---|---|
lang |
string | tr | en | de | Sets the language of names, slugs and URLs in the response. Falls back to the key’s default language. |
currency |
string | TRY | CNY | USD | EUR | GBP | USDT … | Currency the prices are returned in. Falls back to the key’s default currency. |
{
"ok": true,
"data": {
"id": 88421,
"order_number": "ORD-20260803-A1B2C3",
"status": "completed",
"product_id": 501,
"quantity": 1,
"price": 6.50,
"currency": "EUR",
"price_try": 350.00,
"charged": 6.50,
"wallet_currency": "EUR",
"client_order_id": "my-ref-001",
"test": false,
"items": [
{ "id": 1, "product_id": 501, "epin_code": "XXXX-YYYY", "delivered": true }
],
"created_at": "2026-08-03T19:00:00+03:00"
}
}
Lists orders placed via this API key. Requires scope orders:read.
| Parameter | Type | Default | Description |
|---|---|---|---|
lang |
string | tr | en | de | Sets the language of names, slugs and URLs in the response. Falls back to the key’s default language. |
currency |
string | TRY | CNY | USD | EUR | GBP | USDT … | Currency the prices are returned in. Falls back to the key’s default currency. |
status |
string | — | Filter by status: pending, completed, cancelled, … |
page |
integer | 1 | Page number. |
per_page |
integer | 50 | Records per page. Maximum 200. |
{
"ok": true,
"data": [ { "id": 88421, "order_number": "ORD-20260803-A1B2C3", "status": "completed" } ],
"meta": { "page": 1, "per_page": 50, "total": 12, "has_more": false }
}
Returns a single order with pin codes / delivery status. Requires scope orders:read.
| Parameter | Type | Default | Description |
|---|---|---|---|
lang |
string | tr | en | de | Sets the language of names, slugs and URLs in the response. Falls back to the key’s default language. |
currency |
string | TRY | CNY | USD | EUR | GBP | USDT … | Currency the prices are returned in. Falls back to the key’s default currency. |
{
"ok": true,
"data": {
"id": 88421,
"status": "pending",
"items": [
{ "id": 1, "product_id": 501, "epin_code": null, "delivered": false }
]
}
}
Product Object
The product list and product detail return the same object; the detail additionally includes order fields and delivery zones.
| Field | Type | Description |
|---|---|---|
id |
integer | Permanent product identifier. Use this value when ordering. |
category_id |
integer | Identifier of the category the product belongs to. |
category_name |
string | Category name in the requested language. |
name |
string | Product name in the requested language. |
slug |
string | The product’s URL slug in the requested language. |
type |
string | pin (e-pin code), topup (direct account top-up) or currency (in-game currency). |
price |
decimal | Sale price in the requested currency. null when the product has no price. |
currency |
string | Currency of the price field. |
price_try |
decimal | The same price in TRY. Useful for reconciliation. |
old_price |
decimal | Pre-discount price. null when there is no discount. |
unit |
string | Unit name for currency-type products (e.g. yang). null otherwise. |
min_quantity |
integer | Minimum orderable quantity. |
max_quantity |
integer | Maximum orderable quantity. null when unlimited. |
stock |
integer | Number of ready pin codes. Populated for pin type only, null otherwise. |
in_stock |
boolean | Whether the product can be purchased right now. |
unlimited_stock |
boolean | When true, the stock count does not impose a limit. |
image |
string | Full URL of the product image. Falls back to the category image when the product has none. |
url |
string | The product’s storefront page in the requested language. |
pin, stock is the number of ready codes; topup and currency products keep no stock, so they return stock null and unlimited_stock true. Always check in_stock to decide whether a product can be sold.Webhooks
Configure an HTTPS URL in your member panel. For every API order event (created, completed, cancelled) we POST a signed JSON payload. Verify with the X-Pinbasket-Signature header: HMAC-SHA256 of t.body using your webhook secret (format t=…,v1=…).
{
"ok": true,
"event": "order.completed",
"created_at": "2026-08-03T19:05:00+03:00",
"data": { "id": 88421, "order_number": "ORD-20260803-A1B2C3", "status": "completed" }
}
Error Codes
Handle errors by error.code rather than the HTTP status: the codes are stable, the message texts may change.
| Code | HTTP | Description |
|---|---|---|
unauthorized |
401 | No key was sent, or the key is invalid. |
key_revoked |
403 | The key has been revoked. |
account_disabled |
403 | The account owning the key is closed. |
wrong_key_mode |
403 | Live key used on /api/test/v1, or test key used on /api/v1. |
ip_not_allowed |
403 | The request came from an address that is not on the key’s IP allowlist. |
insufficient_scope |
403 | The key is missing a required scope (e.g. orders:write). |
invalid_lang |
400 | Unsupported language code. |
invalid_currency |
400 | Unsupported currency code. |
invalid_param |
400 | A parameter is not in the expected format. |
validation_error |
400 | Order fields, quantity, or delivery zone failed validation. |
insufficient_balance |
402 | Wallet balance is too low for this order. |
product_not_found |
404 | Product does not exist or is not available via the API. |
not_found |
404 | Endpoint or record not found. |
out_of_stock |
409 | No pin stock and oversell is disabled for this product. |
method_not_allowed |
405 | v1 accepts GET requests only. |
rate_limited |
429 | Rate limit exceeded. Wait the number of seconds given in the Retry-After header. |
webhook_failed |
502 | Could not deliver the test webhook to your URL. |
server_error |
500 | Unexpected server error. Let us know if it persists. |
Rate Limits
Each key may make up to 120 requests per 60 seconds. Exceeding the limit returns 429, and the Retry-After header tells you how many seconds to wait.
- When pulling the full catalog use
per_page=200and keep paging untilmeta.has_moreis false. - Prices move with exchange rates; running your catalog sync every 5–15 minutes is enough.
- If your requests come from a fixed server, you can ask us to lock your key to that IP address.
<?php
$key = 'pb_live_xxxxxxxxxxxxxxxx';
$page = 1;
do {
$url = 'https://playsultan.com/api/v1/products?currency=EUR&per_page=200&page=' . $page;
$res = file_get_contents($url, false, stream_context_create([
'http' => ['header' => "Authorization: Bearer $key\r\n"],
]));
$body = json_decode($res, true);
foreach ($body['data'] as $product) {
// write to your own database
}
$page++;
} while (!empty($body['meta']['has_more']));
const key = 'pb_live_xxxxxxxxxxxxxxxx';
let page = 1, hasMore = true;
while (hasMore) {
const res = await fetch(
`https://playsultan.com/api/v1/products?currency=EUR&per_page=200&page=${page}`,
{ headers: { Authorization: `Bearer ${key}` } }
);
const { data, meta } = await res.json();
for (const product of data) {
// write to your own database
}
hasMore = meta.has_more;
page++;
}
POST /orders (wallet balance). Pass client_order_id to make retries safe. Delivery updates also arrive via webhook if configured in your member panel.