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: true plus data and, for lists, meta.
  • 2 Error responses return ok: false and a machine-readable error.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
Request an API key from your customer panel or apply for a dealership account to get started. Get an API key
Response shapes
Success response
{
  "ok": true,
  "data": [ … ],
  "meta": { "page": 1, "total": 2104 }
}
Error response
{
  "ok": false,
  "error": {
    "code": "invalid_currency",
    "message": "Unsupported currency. Available: TRY, USD, EUR…"
  }
}
Typical integration flow
  1. 1 Generate an API key from your account. Get an API key
  2. 2 Send the key in every request as Authorization: Bearer <key>.
  3. 3 Call GET /products with your preferred lang and currency query parameters.
  4. 4 Follow meta.has_more and increment page until 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"
Your key is shown only once when it is created; we store it in an irreversible form. If you lose it you can generate a new one from your member panel — the old key stops working immediately. Never embed the key in browser-side code; use it only from your server.

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). Call GET /products, GET /categories, etc. — prices and stock reflect production catalog data.
  • 3 Call POST /orders on the test base. No wallet debit, no real pin assignment, no supplier top-up. The order number is prefixed with TEST- and each item returns a fake code such as TEST-A1B2C3D4.
  • 4 Configured webhooks still fire for test orders so you can verify your receiver. Responses include test: true / order numbers starting with TEST-.
Example request
bash
curl -X POST "https://playsultan.com/api/test/v1/orders?lang=en&currency=TRY" \
  -H "Authorization: Bearer pb_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"product_id":501,"quantity":1,"client_order_id":"sandbox-001"}'
A live key on /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 (::1 and 0:0:0:0:0:0:0:1 are 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-For yourself. 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

JSON
{
  "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.
Supported currencies
TRY CNY USD EUR GBP USDT AUD CAD CHF BRL INR BHD KWD PHP SAR NZD
Supported languages
TR EN DE
Prices are converted from the product’s stored currency at the current rate and rounded to two decimals. For easier reconciliation every product also returns its TRY equivalent in price_try.

Endpoints

All paths are appended to the base URL above. v1 accepts GET only; other methods return 405.

GET /ping
No key required

Checks that the service is up. Requires no authentication.

Example response
JSON
{
  "ok": true,
  "data": {
    "service": "Pinbasket",
    "version": "v1",
    "time": "2026-07-30T22:46:55+03:00"
  }
}
GET /me
Key required

Dealer details and wallet balances.

Example response
JSON
{
  "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 }
  }
}
GET /currencies
Key required

Supported currencies with their current TRY rates.

Example response
JSON
{
  "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" }
}
GET /categories
Key required

Lists active categories, either as a flat list or as a nested tree.

Query parameters — all optional
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.
Example response
JSON
{
  "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 }
}
GET /products
Key required

Lists active products with pagination. Prices are returned in the currency you request.

Query parameters — all optional
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.
Example response
JSON
{
  "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"
  }
}
GET /products/{id}
Key required

Returns a single product together with the fields required to order it and its delivery zones.

Query parameters — all optional
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.
Example response
JSON
{
  "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" }
}
POST /orders
Key required

Creates a wallet-paid order for a single product. Requires scope orders:write. Use client_order_id for idempotency.

JSON body: product_id, quantity, fields{}, delivery_zone_id?, currency?, client_order_id?
Query parameters — all optional
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.
Example response
JSON
{
  "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"
  }
}
GET /orders
Key required

Lists orders placed via this API key. Requires scope orders:read.

Query parameters — all optional
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.
Example response
JSON
{
  "ok": true,
  "data": [ { "id": 88421, "order_number": "ORD-20260803-A1B2C3", "status": "completed" } ],
  "meta": { "page": 1, "per_page": 50, "total": 12, "has_more": false }
}
GET /orders/{id}
Key required

Returns a single order with pin codes / delivery status. Requires scope orders:read.

Query parameters — all optional
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.
Example response
JSON
{
  "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.
Stock behaviour depends on the product type: for 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=…).

Events
order.created order.completed order.cancelled webhook.test
Example payload
JSON
{
  "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=200 and keep paging until meta.has_more is 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.
Syncing the full catalog
<?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++;
}
Place orders with POST /orders (wallet balance). Pass client_order_id to make retries safe. Delivery updates also arrive via webhook if configured in your member panel.