Connect e-pin and top-up sales to your own system
Pull the catalog and your dealer prices, validate the player account, place the order and receive the code within seconds. Balance and ledger live in the same API. Price, stock and delivery changes reach you as signed webhooks.
The dealer panel and the API share the SAME core: the two channels never show a different price, stock or error. What you see in the panel is what the API returns.
Quick start · First order in five minutes
- In the dealer panel (Integration > API keys) create a TEST key first. Orders placed with a test key spend no money and return TEST- prefixed dummy codes.
- Call GET /bayi-api/v1/me to confirm the key belongs to the right account and to see its scopes.
- Call GET /bayi-api/v1/catalog/products for product ids, prices and the required-field schema. Never invent ids or field names, always read them here.
- Place a test order with POST /bayi-api/v1/orders (do not forget the Idempotency-Key header). Retry with the same key: the same response comes back and no second order is created.
- When the flow works, create a LIVE key in the panel and swap the token. The code stays the same; the only difference is that money now moves.
- Stop polling: register your address with PUT /bayi-api/v1/webhooks, store the signing secret and verify the link with POST /bayi-api/v1/webhooks/test.
curl "https://oyuneks.com/bayi-api/v1/me" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
<?php
$ch = curl_init("https://oyuneks.com/bayi-api/v1/me");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"],
]);
$yanit = json_decode(curl_exec($ch), true);
$kod = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($kod >= 400) {
// Dallanmayi HER ZAMAN error.code uzerinden yap; message degisebilir.
throw new \RuntimeException($yanit["error"]["code"] . ": " . $yanit["error"]["message"]);
}
print_r($yanit["data"]);
const yanit = await fetch("https://oyuneks.com/bayi-api/v1/me", {
method: "GET",
headers: {
"Authorization": "Bearer oyxb_test_KENDI_ANAHTARIN"
}
});
const govde = await yanit.json();
if (!yanit.ok) {
// error.code sabittir; message insan icindir.
throw new Error(govde.error.code + ": " + govde.error.message);
}
console.log(govde.data);
curl -X POST "https://oyuneks.com/bayi-api/v1/orders" \
-H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: SIP-2026-0001-1" \
-d '{"externalRef":"SIP-2026-0001","waitForStock":true,"items":[{"productId":6,"quantity":2,"expectedPrice":"98.70"},{"productId":1421,"quantity":1,"fields":{"playerId":"5123456789"}}]}'
<?php
$ch = curl_init("https://oyuneks.com/bayi-api/v1/orders");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer oyxb_test_KENDI_ANAHTARIN", "Content-Type: application/json", "Idempotency-Key: SIP-2026-0001-1"],
CURLOPT_POSTFIELDS => json_encode([
"externalRef" => "SIP-2026-0001",
"waitForStock" => true,
"items" => [
[
"productId" => 6,
"quantity" => 2,
"expectedPrice" => "98.70"
],
[
"productId" => 1421,
"quantity" => 1,
"fields" => [
"playerId" => "5123456789"
]
]
]
]),
]);
$yanit = json_decode(curl_exec($ch), true);
$kod = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($kod >= 400) {
// Dallanmayi HER ZAMAN error.code uzerinden yap; message degisebilir.
throw new \RuntimeException($yanit["error"]["code"] . ": " . $yanit["error"]["message"]);
}
print_r($yanit["data"]);
const yanit = await fetch("https://oyuneks.com/bayi-api/v1/orders", {
method: "POST",
headers: {
"Authorization": "Bearer oyxb_test_KENDI_ANAHTARIN",
"Content-Type": "application/json",
"Idempotency-Key": "SIP-2026-0001-1"
},
body: JSON.stringify({"externalRef":"SIP-2026-0001","waitForStock":true,"items":[{"productId":6,"quantity":2,"expectedPrice":"98.70"},{"productId":1421,"quantity":1,"fields":{"playerId":"5123456789"}}]})
});
const govde = await yanit.json();
if (!yanit.ok) {
// error.code sabittir; message insan icindir.
throw new Error(govde.error.code + ": " + govde.error.message);
}
console.log(govde.data);
Authentication
Send Authorization: Bearer <key> on every request. Keys are created in the dealer panel and shown ONCE at creation time; only a SHA-256 digest is stored. Scopes, IP restrictions and rate limits are chosen per key.
If a key leaks, revoke it in the panel: it dies instantly, requests with the old key get 401 YETKISIZ and the attempt is written to the dealer audit log. Scopes CANNOT be added later; create a new key instead.
The key prefix states the mode: oyxb_live_ is live, oyxb_test_ is test. The mode is baked into the token and cannot be overridden by the request body.
| Scope | What it allows |
|---|---|
| catalog:read | Read catalog and prices |
| orders:read | Read orders and delivered codes |
| orders:write | Create and cancel orders (spends balance) |
| balance:read | Read balance and ledger |
| webhooks:manage | Manage the webhook subscription |
Test environment
There is NO separate host: the same base URL runs in test mode with an oyxb_test_ key. Catalog and prices are real and the flow is real, but Basket / Orders / Stocks / Customers are never touched, no balance is spent, codes come back prefixed with TEST- and no top-up is credited.
Test orders have ids like "test_12" and are read from GET /orders/{orderId}. Webhooks are REALLY sent in test mode as well; the payload says "mode":"test" so you can branch on it.
A GB (type=goldbar) test order MIMICS THE REAL FLOW: the response is processing with no code and no receipt, and about a minute later GET /orders/{orderId} shows it delivered (receipt TEST-GB-...). The order.delivered webhook arrives at that moment too. In production an operator delivers it in-game, so exercise the waiting state with a test key before going live.
Going live is a one-line change: swap the token.
Conventions
- The success envelope is identical everywhere: {"data": ..., "mode": "live|test", "requestId": "..."}. The error envelope: {"error":{"code","message","retryable","requestId","details"}}.
- Money fields are STRINGS with a dot decimal: "411.50". A JSON float becomes 411.5 and shows up as 411.49999 in some clients, which breaks accounting matches. The currency is always TRY.
- Dates are ISO-8601 in Turkish time: "2026-08-28T14:05:00+03:00".
- Lists are CURSOR paged: send the nextCursor value back as ?cursor=. limit is at most 200 (100 for the order list). There is no OFFSET paging; it skips or repeats rows when new records arrive.
- Every response carries an X-Request-Id header; quote it in support tickets and the request is found with a single query.
- Catalog endpoints return an ETag; send If-None-Match and you get a 304 when the body did not change.
- Required fields (player id, server, character...) go into the fields object per the product requiredFields schema. The KEY NAME matters, not the order.
- An unknown field is ignored; an unknown EVENT name returns 400 (dropping it silently would create a false sense of subscription).
Request headers
| Header | Meaning |
|---|---|
| Authorization | Bearer oyxb_live_... ya da oyxb_test_... (ZORUNLU) |
| Idempotency-Key | POST /orders ucunda ZORUNLU; en fazla 64 karakter. Aynı anahtar + aynı gövde = aynı sipariş (iş tekrar çalışmaz), aynı anahtar + farklı gövde = 409. Anahtar 24 saat hatırlanır; bu süreden SONRA aynı anahtarla gelen istek YENİ sipariş oluşturur. |
| If-None-Match | Katalog uçlarında: gövde değişmediyse 304 döner, kota harcanmaz. |
| Content-Type | application/json (form-data da kabul edilir). |
Response headers
| Header | Meaning |
|---|---|
| X-Request-Id | İsteğin kimliği (26 karakter). Destek kaydında bu numarayı verin, istek tek sorguyla bulunur. |
| X-RateLimit-Limit | Bu dakika için geçerli tavan (okuma ve yazma AYRI pencere). |
| X-RateLimit-Remaining | Bu dakika içinde kalan istek hakkı. |
| X-RateLimit-Reset | Pencerenin sıfırlanacağı zaman (unix saniye). |
| Retry-After | Yalnız 429 yanıtında: kaç saniye beklenmeli. |
| ETag | Katalog uçlarında: sonraki istekte If-None-Match ile geri gönderin. |
| Idempotent-Replayed | true ise bu yanıt ÖNCEKİ isteğin saklanan sonucudur; yeni sipariş OLUŞMADI. |
Errors
The same envelope everywhere. Branch on code: message is for humans and may change, code is stable. With retryable=true the same request may be retried later; with false, retrying without fixing the request returns the same result.
The details field carries the machine readable specifics (required/available amount, current price, offending fields). It is {} when empty.
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Bakiye yetersiz.",
"retryable": false,
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE",
"details": {
"required": "608.90",
"available": "120.00"
}
}
}
| HTTP | code | Meaning and what to do | Retry |
|---|---|---|---|
| 503 | API_KAPALI | Dealer API is temporarily switched off (maintenance). Retry after a while; no client change needed. | yes |
| 401 | YETKISIZ | Key missing, invalid, revoked, IP not allowed, or the account is not a dealer. Check the key and its IP list in the panel. If the key leaked, revoke it and create a new one. | no |
| 403 | BAYI_ASKIDA | The dealership is suspended. Contact support; retrying will not help. | no |
| 403 | API_ERISIMI_KAPALI | Dealership is active but API access is disabled for the account. Ask your dealer representative; access is enabled by our team. | no |
| 403 | KAPSAM_YOK | The key is valid but lacks the scope this endpoint requires. Create a new key that includes the scope (scopes cannot be added later). | no |
| 429 | RATE_LIMITED | Rate limit exceeded. Wait the number of seconds in Retry-After. Read and write windows are separate. | yes |
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
| 404 | NOT_FOUND | Record or endpoint not found. Verify the id and the path. Another dealer's order also returns "not found" (we do not leak existence). | no |
| 405 | METHOD_NOT_ALLOWED | The endpoint does not accept this HTTP method. The correct method is listed in the endpoint reference on this page. | no |
| 422 | INSUFFICIENT_BALANCE | Insufficient balance. details.required and details.available are in the response. Top up and retry. | no |
| 422 | PRICE_CHANGED | The expectedPrice you sent differs from the current price; the order was NOT created. details.current holds the current price. Refresh the catalog and retry with it. | no |
| 422 | OUT_OF_STOCK | Not enough stock (waitForStock=false was sent). Retry later, or send waitForStock=true to let the order wait in queue. | yes |
| 422 | INVALID_FIELDS | Required fields are missing or violate the length constraint. details.fields lists the offending fields. Read the schema from GET /catalog/products/{productId}. | no |
| 422 | INVALID_PLAYER_ID | The player information could not be validated by the supplier. Check with POST /orders/validate before ordering; ask the customer for the correct id. | no |
| 422 | LIMIT_EXCEEDED | Daily purchase limit or per-item quantity cap exceeded. See the limits object in GET /me; the daily limit resets at midnight. | no |
| 422 | PRODUCT_DISABLED | The product is not on sale or has no price defined. Refresh the catalog; you will get a product.enabled event when it is back. | no |
| 422 | PRODUCT_NOT_ALLOWED | The product cannot be sold through the dealer channel. Such products are not listed by the catalog endpoints either; drop them from the body. | no |
| 422 | NOT_CANCELLABLE | A goldbar item cannot be cancelled through this endpoint; it is delivered by hand. Contact support to cancel. E-pin/top-up items in the same order are cancelled as usual. | no |
| 422 | MIN_QTY | Below the product minimum order quantity. Use limits.minQty from the catalog. | no |
| 409 | IDEMPOTENCY_MISMATCH | The same Idempotency-Key was previously used with a DIFFERENT body. Generate a new key per new order (your order number plus an attempt counter works well). | no |
| 409 | ORDER_IN_PROGRESS | An order with the same key is being processed right now. Wait a few seconds and read the result with GET /orders/by-ref/{externalRef}. | yes |
| 501 | NOT_IMPLEMENTED | The endpoint is announced but not working yet. No endpoint currently returns this code; it is listed for historical completeness. | no |
| 500 | INTERNAL | Unexpected server error. Retry; if it persists open a support ticket with the X-Request-Id. | yes |
Rate limits
Per key, per minute: 300 reads and 60 writes (orders included). READ AND WRITE ARE SEPARATE WINDOWS: bulk catalog pulls do not eat your ordering budget. HEAD counts as a read.
Every response returns X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. On overflow you get 429 plus Retry-After seconds; wait and continue.
Do not pull the whole catalog every minute: take 304s with ETag/If-None-Match and read price moves from the price.changed event or the GET /catalog/prices?since= delta.
Account & balance
Which account the key belongs to, limits and money.
Key health check #
Dealer id, company name, tier, available balance, daily limit and the scopes of the key. The mode field returns "live" or "test"; this should be the first call of any integration. Requires NO scope: every valid key can verify itself.
curl "https://oyuneks.com/bayi-api/v1/me" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"dealerId": 385841,
"company": "ÖRNEK BİLİŞİM LTD. ŞTİ.",
"tier": "Bronz",
"tierId": 1,
"balance": "48250.00",
"currency": "TRY",
"limits": {
"dailyPurchase": "250000.00",
"dailyUsed": "1200.00",
"maxQtyPerOrder": 500
},
"apiEnabled": true,
"key": {
"name": "Canlı anahtar",
"masked": "oyxb_live_a91f...3c0e",
"scopes": [
"catalog:read",
"orders:read",
"orders:write",
"balance:read"
],
"rateReadPerMin": 300,
"rateWritePerMin": 60
},
"apiVersion": "v1",
"serverTime": "2026-08-28T14:05:00+03:00"
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Available balance #
Available balance, blocked amount, pending bank transfers and the breakdown (balance + refundable + bonus). The available value is the very same number the order gate reads.
curl "https://oyuneks.com/bayi-api/v1/balance" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"available": "48250.00",
"blocked": "0.00",
"breakdown": {
"balance": "46250.00",
"refundable": "1500.00",
"bonus": "500.00"
},
"pendingDeposits": "5000.00",
"currency": "TRY",
"asOf": "2026-08-28T14:05:00+03:00"
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Ledger entries (cursor paged) #
The ledger. Order rows return a NEGATIVE amount and carry orderId + externalRef; use balanceAfter for reconciliation. Cursor paging: send the nextCursor value back as ?cursor=.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| from | query | string | no | Start date (inclusive). An unparsable date is ignored. |
| to | query | string | no | End date (inclusive). |
| type | query |
string
order | refund | deposit | adjustment | other |
no | Entry type filter. Unknown types return "other" and typeId carries the raw value. |
| cursor | query | string | no | The nextCursor from the previous response. Digits only. |
| limit | query |
integer
default: 50 |
no | Page size (1-200). |
curl "https://oyuneks.com/bayi-api/v1/balance/transactions?from=2026-08-01&to=2026-08-28" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"items": [
{
"id": 3012402,
"type": "order",
"typeId": 1,
"amount": "-197.40",
"balanceAfter": "48052.60",
"orderId": 184213,
"externalRef": "SIP-2026-0001",
"description": "Sipariş ödemesi",
"createdAt": "2026-08-28T14:05:00+03:00"
},
{
"id": 3012301,
"type": "deposit",
"typeId": 2,
"amount": "25000.00",
"balanceAfter": "48250.00",
"orderId": null,
"externalRef": null,
"description": "Havale",
"createdAt": "2026-08-28T09:12:00+03:00"
}
],
"nextCursor": "3012301",
"currency": "TRY"
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
Catalog & price
Product ids, your dealer price, stock and the required-field schema. Never invent ids or field names, always read them here.
Game list #
Games (top categories) that have products available to dealers, their sub-categories (categories) and product counts. Pass categories[].category to the category filter of the product list. Pass the slug from here to the game filter of the product list, not the category id: ids may change when categories are merged, slugs cannot.
Returns an ETag. Send it back as If-None-Match and you get 304 with no body when nothing changed.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| If-None-Match | header | string | no | The ETag of the previous response. Returns 304 when the body did not change. |
curl "https://oyuneks.com/bayi-api/v1/catalog/games" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"items": [
{
"game": "pubg-mobile",
"name": "PUBG Mobile",
"categoryId": 25,
"productCount": 34,
"categories": [
{
"category": "pubg-mobile-uc",
"name": "PUBG Mobile UC",
"categoryId": 251,
"productCount": 34
}
]
},
{
"game": "knight-online",
"name": "Knight Online",
"categoryId": 12,
"productCount": 18,
"categories": []
}
]
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Product list (cursor paged) #
Dealer price, list price, stock state, quantity bounds, required-field schema and delivery estimate. price is YOUR price (tier plus any dealer-specific override), listPrice is the retail price on the site. An ETag is returned: send it back with If-None-Match and you get a 304 when nothing changed.
Returns an ETag. Send it back as If-None-Match and you get 304 with no body when nothing changed.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| type | query |
string
epin | topup | goldbar |
no | epin = a code is delivered, topup = the player account is credited (no code, a receipt instead), goldbar = game currency (GB) delivered in-game by hand to the character name, no code. Any other value returns 400. |
| game | query | string | no | Game slug (from GET /catalog/games). |
| category | query | string | no | Sub-category slug (categories[].category in GET /catalog/games). Works alone or together with game. |
| q | query | string | no | Search in the product, game or category name. |
| inStock | query | boolean | no | Send 1 to return only in-stock products ("0", "false", "no", "off" and empty count as off). |
| updatedSince | query | string | no | NOT APPLIED YET: products carry no update stamp, so the full list is returned and the note field says so. Use ETag or GET /catalog/prices?since= for incremental sync. |
| cursor | query | string | no | The nextCursor from the previous response (last product id). |
| limit | query |
integer
default: 50 |
no | Page size (1-200). |
| If-None-Match | header | string | no | The ETag of the previous response. |
curl "https://oyuneks.com/bayi-api/v1/catalog/products?type=epin&game=pubg-mobile" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"items": [
{
"productId": 6,
"name": "Knight Online 1020 Cash",
"type": "epin",
"game": "knight-online",
"category": "knight-online-cash",
"region": null,
"listPrice": "105.00",
"price": "98.70",
"currency": "TRY",
"vatIncluded": true,
"stock": {
"mode": "count",
"count": 420
},
"limits": {
"minQty": 1,
"maxQty": 500
},
"requiredFields": [],
"delivery": {
"mode": "instant",
"etaSeconds": 5
},
"enabled": true,
"updatedAt": null
},
{
"productId": 1421,
"name": "PUBG Mobile 660 UC",
"type": "topup",
"game": "pubg-mobile",
"category": "pubg-mobile-uc",
"region": null,
"listPrice": "428.90",
"price": "411.50",
"currency": "TRY",
"vatIncluded": true,
"stock": {
"mode": "instant"
},
"limits": {
"minQty": 1,
"maxQty": 500
},
"requiredFields": [
{
"key": "playerId",
"label": "Oyuncu ID",
"type": "string",
"minLength": 8,
"maxLength": 12,
"required": true,
"pattern": "^.{8,12}$",
"placeholder": null
}
],
"delivery": {
"mode": "instant",
"etaSeconds": 30
},
"enabled": true,
"updatedAt": null
}
],
"nextCursor": "1421",
"count": 2
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
Single product #
The full view of one product. The required-field SCHEMA is published here: field keys (playerId, server, zoneId, character...) are derived from the product labels. Do not invent them, read them here; the KEY NAME matters, not the order.
Returns an ETag. Send it back as If-None-Match and you get 304 with no body when nothing changed.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| productId | path | integer | yes | Product id (digits only). |
| If-None-Match | header | string | no | The ETag of the previous response. |
curl "https://oyuneks.com/bayi-api/v1/catalog/products/1421" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"productId": 1421,
"name": "PUBG Mobile 660 UC",
"type": "topup",
"game": "pubg-mobile",
"category": "pubg-mobile-uc",
"region": null,
"listPrice": "428.90",
"price": "411.50",
"currency": "TRY",
"vatIncluded": true,
"stock": {
"mode": "instant"
},
"limits": {
"minQty": 1,
"maxQty": 500
},
"requiredFields": [
{
"key": "playerId",
"label": "Oyuncu ID",
"type": "string",
"minLength": 8,
"maxLength": 12,
"required": true,
"pattern": "^.{8,12}$",
"placeholder": null
}
],
"delivery": {
"mode": "instant",
"etaSeconds": 30
},
"enabled": true,
"updatedAt": null
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 404 | NOT_FOUND | Record or endpoint not found. Verify the id and the path. Another dealer's order also returns "not found" (we do not leak existence). | no |
Price/stock dump and delta #
The light version of the product list: only productId, price, stock and enabled. Built for price-sync bots. With ?since you get a REAL DELTA (products whose price/stock/state changed after that moment, each row carrying a real updatedAt). If the snapshot for the account does not exist yet the filter is NOT applied: the full list plus a warning note is returned, because an empty list would make the bot believe nothing changed.
Returns an ETag. Send it back as If-None-Match and you get 304 with no body when nothing changed.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| since | query | string | no | Products changed after this moment. An unparsable value returns the full list plus a note. |
| cursor | query | string | no | The nextCursor from the previous response. |
| limit | query |
integer
default: 200 |
no | Page size (1-200). |
| If-None-Match | header | string | no | The ETag of the previous response. |
curl "https://oyuneks.com/bayi-api/v1/catalog/prices?since=2026-08-28T09%3A00%3A00%2B03%3A00&limit=200" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"items": [
{
"productId": 6,
"listPrice": "105.00",
"price": "96.20",
"currency": "TRY",
"stock": {
"mode": "count",
"count": 418
},
"enabled": true,
"updatedAt": "2026-08-28T14:30:00+03:00"
}
],
"nextCursor": null,
"count": 1,
"asOf": "2026-08-28T14:35:00+03:00"
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Account validation
For top-up products, check the player information BEFORE crediting.
Pre-validate player information #
Asks the supplier about the player information BEFORE ordering; no money moves. supported=false means the product has no pre-validation (it does not block ordering). valid=null means "could not check right now": a technical failure never blocks a sale, the same check runs again in the core at order time. The response also returns the requiredFields schema.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| productId | integer | yes | Product id. |
| fields | object | no | Keys and values from the product requiredFields schema. |
curl -X POST "https://oyuneks.com/bayi-api/v1/orders/validate" \
-H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN" \
-H "Content-Type: application/json" \
-d '{"productId":1421,"fields":{"playerId":"5123456789","server":"7012"}}'
{
"data": {
"supported": true,
"valid": true,
"nickname": "OyuncuAdi",
"validUntil": "2026-08-28T14:10:00+03:00",
"requiredFields": [
{
"key": "playerId",
"label": "Oyuncu ID",
"type": "string",
"minLength": 8,
"maxLength": 12,
"validate": true
}
],
"message": null
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
| 404 | NOT_FOUND | Record or endpoint not found. Verify the id and the path. Another dealer's order also returns "not found" (we do not leak existence). | no |
Orders
Create, read and cancel pending items. The panel and the API share the SAME core, so the two channels never show a different price, stock or error.
Create an order #
Creates an order and debits your balance. The Idempotency-Key header is REQUIRED. Up to 50 items per request. If you send expectedPrice and the price moved, the order is NOT created (PRICE_CHANGED). With waitForStock=false the order never starts when stock is short. The response is 201 (final) or 202 (delivery in progress). Repeating with the same key returns the same body plus the Idempotent-Replayed: true header and creates NO new order, so retrying after a network error is safe.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | yes | Your unique reference, max 64 characters. Generate a new one for every NEW order. A key is remembered for 24 hours. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| externalRef | string | no | Your own order number. It comes back in reports, the ledger and webhook payloads. |
| waitForStock |
boolean
default: true |
no | true: queue the order and deliver when stock arrives. false: do not start at all when stock is short (OUT_OF_STOCK). |
| items | array | yes | Items (1 to 50). Each item: productId (required), quantity (required), fields (per schema for top-up), expectedPrice (optional price lock). |
curl -X POST "https://oyuneks.com/bayi-api/v1/orders" \
-H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: SIP-2026-0001-1" \
-d '{"externalRef":"SIP-2026-0001","waitForStock":true,"items":[{"productId":6,"quantity":2,"expectedPrice":"98.70"},{"productId":1421,"quantity":1,"fields":{"playerId":"5123456789"}}]}'
{
"data": {
"orderId": 184213,
"externalRef": "SIP-2026-0001",
"channel": "dealer-api",
"status": "delivered",
"mode": "live",
"total": "608.90",
"balanceAfter": "47641.10",
"createdAt": "2026-08-28T14:05:00+03:00",
"items": [
{
"itemId": "184213-1",
"productId": 6,
"name": "Knight Online 1020 Cash",
"quantity": 2,
"unitPrice": "98.70",
"status": "delivered",
"codes": [
{
"code": "KO10-2XQ4-9WPA",
"serial": null,
"expiresAt": null
},
{
"code": "KO10-7MTB-3VZC",
"serial": null,
"expiresAt": null
}
],
"receipt": null,
"failCode": null,
"failReason": null
},
{
"itemId": "184213-2",
"productId": 1421,
"name": "PUBG Mobile 660 UC",
"quantity": 1,
"unitPrice": "411.50",
"status": "delivered",
"codes": [],
"receipt": "TOPUP-9F31A2",
"failCode": null,
"failReason": null
}
]
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
HTTP 202 - At least one item is waiting for stock or being pulled from the supplier (status=processing). Learn the result from the webhook or GET /orders/{orderId}.
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
| 422 | INSUFFICIENT_BALANCE | Insufficient balance. details.required and details.available are in the response. Top up and retry. | no |
| 422 | PRICE_CHANGED | The expectedPrice you sent differs from the current price; the order was NOT created. details.current holds the current price. Refresh the catalog and retry with it. | no |
| 422 | OUT_OF_STOCK | Not enough stock (waitForStock=false was sent). Retry later, or send waitForStock=true to let the order wait in queue. | yes |
| 422 | INVALID_FIELDS | Required fields are missing or violate the length constraint. details.fields lists the offending fields. Read the schema from GET /catalog/products/{productId}. | no |
| 422 | INVALID_PLAYER_ID | The player information could not be validated by the supplier. Check with POST /orders/validate before ordering; ask the customer for the correct id. | no |
| 422 | LIMIT_EXCEEDED | Daily purchase limit or per-item quantity cap exceeded. See the limits object in GET /me; the daily limit resets at midnight. | no |
| 422 | PRODUCT_DISABLED | The product is not on sale or has no price defined. Refresh the catalog; you will get a product.enabled event when it is back. | no |
| 422 | PRODUCT_NOT_ALLOWED | The product cannot be sold through the dealer channel. Such products are not listed by the catalog endpoints either; drop them from the body. | no |
| 422 | MIN_QTY | Below the product minimum order quantity. Use limits.minQty from the catalog. | no |
| 409 | IDEMPOTENCY_MISMATCH | The same Idempotency-Key was previously used with a DIFFERENT body. Generate a new key per new order (your order number plus an attempt counter works well). | no |
| 409 | ORDER_IN_PROGRESS | An order with the same key is being processed right now. Wait a few seconds and read the result with GET /orders/by-ref/{externalRef}. | yes |
| 422 | PRODUCT_NOT_ALLOWED | The product cannot be sold through the dealer channel. Such products are not listed by the catalog endpoints either; drop them from the body. | no |
Order detail and codes #
Order detail plus delivered codes. Codes are returned ONLY here and in the order response; they are never sent inside a webhook payload. Test-mode orders have ids like "test_12" and are read from the same endpoint. Another dealer's order returns 404.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| orderId | path | string | yes | Order number, or a test order id (test_12). |
curl "https://oyuneks.com/bayi-api/v1/orders/184213" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"orderId": 184213,
"externalRef": "SIP-2026-0001",
"channel": "dealer-api",
"status": "partially_delivered",
"mode": "live",
"total": "608.90",
"balanceAfter": "47641.10",
"createdAt": "2026-08-28T14:05:00+03:00",
"items": [
{
"itemId": "184213-1",
"productId": 6,
"name": "Knight Online 1020 Cash",
"quantity": 2,
"unitPrice": "98.70",
"status": "delivered",
"codes": [
{
"code": "KO10-2XQ4-9WPA",
"serial": null,
"expiresAt": null
}
],
"receipt": null,
"failCode": null,
"failReason": null
},
{
"itemId": "184213-2",
"productId": 1421,
"name": "PUBG Mobile 660 UC",
"quantity": 1,
"unitPrice": "411.50",
"status": "failed",
"codes": [],
"receipt": null,
"failCode": "TIMEOUT",
"failReason": "ZAMAN_ASIMI: stok 15 dk icinde saglanamadi"
}
]
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 404 | NOT_FOUND | Record or endpoint not found. Verify the id and the path. Another dealer's order also returns "not found" (we do not leak existence). | no |
Find an order by your own reference #
Finds an order by your own reference (the LATEST record if the reference repeats). This is the fastest answer to "did my order go through" after a network error. Test orders are found here too.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| externalRef | path | string | yes | The externalRef you sent in the order body (1-64 chars, no slash). |
curl "https://oyuneks.com/bayi-api/v1/orders/by-ref/SIP-2026-0001" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"orderId": 184213,
"externalRef": "SIP-2026-0001",
"channel": "dealer-api",
"status": "delivered",
"mode": "live",
"total": "608.90",
"balanceAfter": "47641.10",
"createdAt": "2026-08-28T14:05:00+03:00",
"items": []
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 404 | NOT_FOUND | Record or endpoint not found. Verify the id and the path. Another dealer's order also returns "not found" (we do not leak existence). | no |
Order list (cursor paged) #
Your orders; panel and API orders live in the same list (same ledger) and can be split with channel. The productId filter is applied ONLY to the returned page, because order contents live inside a JSON snapshot and cannot be queried in SQL. In that case the response carries a note field, so the bot does not conclude "no such product".
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| from | query | string | no | Start date (inclusive). |
| to | query | string | no | End date (inclusive). |
| status | query |
string
processing | delivered | partially_delivered | cancelled | failed |
no | Status filter. An invalid value returns 400 (it is not silently ignored). |
| channel | query |
string
dealer-api | dealer-panel |
no | Channel filter. Left empty, both channels are returned. |
| productId | query | integer | no | Filters within the returned page only; continue with nextCursor for further pages. |
| cursor | query | string | no | The nextCursor from the previous response (last order id). |
| limit | query |
integer
default: 25 |
no | Page size (1-100). |
curl "https://oyuneks.com/bayi-api/v1/orders?from=2026-08-01&to=2026-08-28" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"items": [
{
"orderId": 184213,
"externalRef": "SIP-2026-0001",
"channel": "dealer-api",
"status": "delivered",
"mode": "live",
"total": "608.90",
"balanceAfter": "47641.10",
"createdAt": "2026-08-28T14:05:00+03:00",
"items": [
{
"itemId": "184213-1",
"productId": 6,
"name": "Knight Online 1020 Cash",
"quantity": 2,
"unitPrice": "98.70",
"status": "delivered",
"codes": [],
"receipt": null,
"failCode": null,
"failReason": null
}
]
}
],
"nextCursor": "184213",
"count": 1
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
Cancel pending items #
Cancels only PENDING (processing) items and refunds them. Delivered items are untouched: you already hold the code, refunding an unused code is a support decision. The refund goes through the canonical delivery path; this endpoint does not open a second refund route.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| orderId | path | integer | yes | Order number (digits only; test orders cannot be cancelled). |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| reason | string | no | Cancellation reason (stored in the audit trail, not shown to customers). |
curl -X POST "https://oyuneks.com/bayi-api/v1/orders/184219/cancel" \
-H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN" \
-H "Content-Type: application/json" \
-d '{"reason":"Müşteri vazgeçti"}'
{
"data": {
"orderId": 184219,
"externalRef": "SIP-2026-0007",
"channel": "dealer-api",
"status": "cancelled",
"mode": "live",
"total": "41.90",
"balanceAfter": "48250.00",
"createdAt": "2026-08-28T14:20:00+03:00",
"items": [
{
"itemId": "184219-1",
"productId": 1421,
"name": "PUBG Mobile 660 UC",
"quantity": 1,
"unitPrice": "41.90",
"status": "cancelled",
"codes": [],
"receipt": null,
"failCode": "CANCELLED",
"failReason": "Müşteri vazgeçti"
}
]
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
| 404 | NOT_FOUND | Record or endpoint not found. Verify the id and the path. Another dealer's order also returns "not found" (we do not leak existence). | no |
| 422 | NOT_CANCELLABLE | A goldbar item cannot be cancelled through this endpoint; it is delivered by hand. Contact support to cancel. E-pin/top-up items in the same order are cancelled as usual. | no |
Reports
Statement and invoices. The panel statement, the API statement and the invoice show the SAME numbers.
Period statement (json/csv) #
Opening balance, deposit/order/refund totals, closing balance and the entry rows. format=csv returns a file you can hand to an accounting tool (with BOM, semicolon separated, no envelope). If the 5000-row cap is hit a note appears; narrow the range.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| from | query |
string
default: ayın 1'i |
no | Start date. Defaults to the first day of the current month. |
| to | query |
string
default: bugün |
no | End date. Returns 400 when from > to. |
| format | query |
string
json | csv default: json |
no | With csv you get a file download (text/csv). |
curl "https://oyuneks.com/bayi-api/v1/reports/statement?from=2026-08-01&to=2026-08-28" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"from": "2026-08-01",
"to": "2026-08-28",
"currency": "TRY",
"openingBalance": "12500.00",
"deposits": "75000.00",
"orders": "39250.00",
"refunds": "0.00",
"other": "0.00",
"closingBalance": "48250.00",
"count": 2,
"items": [
{
"id": 3012301,
"date": "2026-08-28T09:12:00+03:00",
"typeId": 2,
"amount": "25000.00",
"balanceAfter": "48250.00",
"orderId": null,
"description": "Havale"
},
{
"id": 3012402,
"date": "2026-08-28T14:05:00+03:00",
"typeId": 1,
"amount": "-608.90",
"balanceAfter": "47641.10",
"orderId": 184213,
"description": "Sipariş ödemesi"
}
]
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
Invoice list #
Issued invoices: number, date, total, VAT and the panel URL. A PDF/UBL download link is NOT exposed through the API yet (pdfUrl is null): the on-site invoice view is session based, and minting signed short-lived links would open a new authorization surface that is out of scope for this version. Use panelUrl to view the document.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| from | query | string | no | Start date (inclusive). |
| to | query | string | no | End date (inclusive). |
curl "https://oyuneks.com/bayi-api/v1/invoices?from=2026-08-01&to=2026-08-28" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"items": [
{
"invoiceId": 90124,
"number": "OYN2026000012345",
"numbers": [
"OYN2026000012345"
],
"date": "2026-08-01T10:00:00+03:00",
"type": "satis",
"orderNo": "184213",
"total": "608.90",
"vat": "0.00",
"currency": "TRY",
"pdfUrl": null,
"panelUrl": "/faturalarim/90124"
}
],
"count": 1,
"note": "PDF indirme baglantisi henuz API uzerinden verilmiyor; belge panelUrl adresinden (oturumlu) goruntulenir."
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Webhooks
Stop polling: order results, price changes and balance credits arrive as signed events. There is ONE address per dealer, so PUT /webhooks is an upsert.
Subscription status #
Address, subscribed events, status (active|suspended), last success/failure stamps, consecutive failure counter and the MASKED signing secret. The raw secret is never returned here: if whoever can read the token could also read the secret, the signature would be pointless. The response also carries the catalogue of subscribable events.
curl "https://oyuneks.com/bayi-api/v1/webhooks" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"configured": true,
"webhook": {
"id": 214,
"url": "https://bayi.example.com/oyuneks-hook",
"events": [
"order.delivered",
"order.failed",
"price.changed",
"balance.low"
],
"status": "active",
"secretMasked": "whsec_a91f3c********9d0e",
"failStreak": 0,
"lastSuccessAt": "2026-08-28T14:05:00+03:00",
"lastFailureAt": null
},
"availableEvents": {
"order.delivered": "Siparis teslim edildi (kismi teslim de bu olayla gelir)"
}
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Delivery log (cursor paged) #
Event name, channel (webhook|email), status (pending|sent|dead), attempt count, HTTP code, duration and error text. The event BODY is not returned. Re-sending is currently only available from the dealer panel. The retry table is included in the response.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| cursor | query | string | no | The nextCursor from the previous response. |
| limit | query |
integer
default: 50 |
no | Page size (1-200). |
curl "https://oyuneks.com/bayi-api/v1/webhooks/deliveries?limit=50" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"items": [
{
"id": 9214,
"eventId": "evt_o184213_d",
"type": "order.delivered",
"channel": "webhook",
"mode": "live",
"status": "sent",
"attempts": 1,
"httpCode": 200,
"durationMs": 184,
"error": null,
"createdAt": "2026-08-28T14:05:01+03:00",
"nextAttemptAt": null,
"sentAt": "2026-08-28T14:05:02+03:00"
}
],
"nextCursor": "9214",
"count": 1,
"retry": {
"maxAttempts": 8,
"backoffMinutes": [
2,
4,
8,
16,
32,
64,
128
],
"note": "Tavan 720 dakikadir. 8 denemede cevap alinamazsa olay olu sayilir."
}
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Create or update the subscription #
One address per dealer, so this is an upsert. https only; localhost and private ranges are rejected (SSRF). On the FIRST save the signing secret is returned ONCE and never shown again. Changing the address lifts an existing suspension. An unknown event name is not dropped silently but reported with 400. Sending an empty events list applies the default set. POST /webhooks maps to the same handler.
Aliases (same handler):
POST /bayi-api/v1/webhooks
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | yes | An https URL. Redirects (3xx) are NOT followed, write the final address. |
| events | array | no | Event names. Empty applies the default set (order.delivered, order.failed, price.changed, balance.low). |
curl -X PUT "https://oyuneks.com/bayi-api/v1/webhooks" \
-H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN" \
-H "Content-Type: application/json" \
-d '{"url":"https://bayi.example.com/oyuneks-hook","events":["order.delivered","order.failed","price.changed","balance.low"]}'
{
"data": {
"webhook": {
"id": 214,
"url": "https://bayi.example.com/oyuneks-hook",
"events": [
"order.delivered",
"order.failed",
"price.changed",
"balance.low"
],
"status": "active",
"secretMasked": "whsec_a91f3c********9d0e",
"failStreak": 0,
"lastSuccessAt": null,
"lastFailureAt": null
},
"signature": {
"header": "X-Oyuneks-Signature",
"format": "t=<unix>,v1=<hex>",
"algorithm": "HMAC-SHA256"
},
"secret": "whsec_a91f3c7d2b8e4f16a05c9d0e",
"secretNote": "Bu sir yalniz bu yanitta doner."
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
Rotate the signing secret #
Generates a new signing secret and returns it ONCE. The old secret dies immediately: keeping two valid secrets would make revoking a leaked one pointless. Events arriving before you deploy the new secret cannot be verified, so roll it out right away.
curl -X POST "https://oyuneks.com/bayi-api/v1/webhooks/rotate-secret" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"secret": "whsec_2f7c81a94de0b3c65a1f8d42",
"secretNote": "Eski sir ANINDA gecersizdir.",
"webhook": {
"id": 214,
"url": "https://bayi.example.com/oyuneks-hook",
"events": [
"order.delivered"
],
"status": "active",
"secretMasked": "whsec_2f7c81********8d42"
},
"signature": {
"header": "X-Oyuneks-Signature",
"format": "t=<unix>,v1=<hex>",
"algorithm": "HMAC-SHA256"
}
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
Send a test event (ping) #
Sends the "ping" event IMMEDIATELY (not queued, not retried) and returns the HTTP result. If your address does not answer the response is STILL 200 and delivered=false in the body: the request was handled correctly, the delivery failed. Returning 502 would make the bot think its own request was wrong. POST /webhooks/{webhookId}/test maps to the same handler.
Aliases (same handler):
POST /bayi-api/v1/webhooks/{webhookId}/test
curl -X POST "https://oyuneks.com/bayi-api/v1/webhooks/test" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"delivered": true,
"eventId": "evt_01K3QF6ZC2M9V0X7NB4TDHR8YE",
"httpCode": 200,
"durationMs": 184,
"error": null
},
"mode": "test",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request: missing field, invalid value, or no Idempotency-Key. Read the message and fix the body; repeating the same request returns the same error. | no |
Delete the subscription #
Deletes the subscription; queued events are marked dead. DELETE /webhooks/{webhookId} is accepted too; an id owned by another dealer returns 404. Narrowing the event list is usually better than deleting: without an address you also lose delivery notifications.
Aliases (same handler):
DELETE /bayi-api/v1/webhooks/{webhookId}
curl -X DELETE "https://oyuneks.com/bayi-api/v1/webhooks" \ -H "Authorization: Bearer oyxb_test_KENDI_ANAHTARIN"
{
"data": {
"deleted": true
},
"mode": "live",
"requestId": "01K3QF6ZC2M9V0X7NB4TDHR8YE"
}
Endpoint specific errors
| HTTP | code | Meaning | Retry |
|---|---|---|---|
| 404 | NOT_FOUND | Record or endpoint not found. Verify the id and the path. Another dealer's order also returns "not found" (we do not leak existence). | no |
Connect via MCP
MCP (Model Context Protocol) is the open protocol AI clients use to connect to a system through tools: you connect your Oyuneks dealer account to a client like Claude and get work done without writing code.
One address: /bayi-api/mcp (JSON-RPC 2.0, stateless). Authentication uses the SAME key as /bayi-api/v1; the mode comes from the key prefix (oyxb_test_ / oyxb_live_) and every tool result carries a "mode" field. Tools are a thin translator on top of this API: each call goes to the real /bayi-api/v1 endpoint with the same scopes, rate limits and audit trail.
The model cannot invent prices: product ids, prices and required fields are read from the catalog tools. The two money tools (place order, cancel order) are TWO-STEP: the first call does nothing and returns a summary (items, quantity, unit and total price, balance after, mode) plus a five minute confirmation token; the tool is called again with that token only after you approve.
Delivered codes are MASKED in tool results (last four characters) and revealed only on request. Repeat protection uses an Idempotency-Key derived from the confirmation token: repeating the same confirmation returns the same order, never a second one.
claude mcp add --transport http oyuneks-bayi https://oyuneks.com/bayi-api/mcp --header "Authorization: Bearer <anahtar>"
| Tool | Endpoint | Scope | Two-step | What it does |
|---|---|---|---|---|
| bayi_bilgim | GET /bayi-api/v1/me | (herhangi) + balance:read | no | Dealer identity, company, tier, available balance, daily limit and the key mode (live/test). |
| katalog_ara | GET /bayi-api/v1/catalog/products | catalog:read | no | Searches products: productId, dealer price, stock, required-field schema and quantity limits. |
| fiyat_degisiklikleri | GET /bayi-api/v1/catalog/prices | catalog:read | no | Products whose price, stock or availability changed since a given time (price sync). |
| hesap_dogrula | POST /bayi-api/v1/orders/validate | orders:write | no | Validates the player account with the supplier for top-up products and returns the nickname. |
| siparis_ver | POST /bayi-api/v1/orders | orders:write | yes | Places an order. TWO-STEP: a summary plus a confirmation token first, the order only after approval. |
| siparislerim | GET /bayi-api/v1/orders | orders:read | no | Lists orders (status, channel, date filters). Codes are always masked in this list. |
| siparis_detayi | GET /bayi-api/v1/orders/{siparisNo} | orders:read | no | One order in detail, by order number or your own reference. Codes can be revealed on request. |
| siparis_iptal | POST /bayi-api/v1/orders/{siparisNo}/cancel | orders:write | yes | Cancels pending items and refunds them to the balance. Requires the TWO-STEP confirmation. |
| ekstre | GET /bayi-api/v1/reports/statement | balance:read | no | Period statement: opening/closing balance, deposits, orders, refunds and the ledger rows. |
| webhook_ayarim | GET /bayi-api/v1/webhooks | webhooks:manage | no | Shows the webhook subscription (address, events, state). Changing it is done in the panel. |
| webhook_test | POST /bayi-api/v1/webhooks/test | webhooks:manage | no | Sends a signed ping event to the registered webhook address and returns the result. |
- claude.ai custom connector (web and mobile): enter https://oyuneks.com/bayi-api/mcp as the connector URL and follow the "sign in with OAuth" flow. You are redirected to Oyuneks where you pick the scopes and whether a LIVE or TEST key is issued. Test is the default.
- OAuth requires TWO-FACTOR authentication on your account (Google Authenticator, SMS or e-mail code). Without it the consent screen will not grant access.
- An OAuth grant shows up in the panel as an ordinary API key (named "OAuth: <app>"). Revoking that key disconnects the client.
- Connect with a TEST key first: the flow is real, no money moves and codes are TEST- prefixed. Going live is only a key swap.
- If you are writing software you do not need MCP: calling /bayi-api/v1 directly is faster and cheaper. MCP is for the case where a human works through an AI client.
State machine
Item state and order state are separate: delivery and cancellation happen per item, the order state is derived from them. A pending item that cannot be delivered within 15 minutes is automatically failed and refunded (failCode=TIMEOUT).
| Order | Item | Meaning | Final? |
|---|---|---|---|
| processing | processing | Paid, delivery in progress: waiting for stock or being pulled from the supplier. Returned with HTTP 202. | no |
| delivered | delivered | All items delivered (a code for e-pin, a receipt for top-up). | yes |
| partially_delivered | — | Some items delivered, others cancelled/refunded. The refund appears as a refund row in the ledger. | yes |
| cancelled | cancelled | Pending items were cancelled and refunded (by you or by support). | yes |
| failed | failed | Timed out: stock could not be supplied in time, the amount was refunded. Item carries failCode=TIMEOUT. | yes |
Event catalogue
Register an https address and an event list (from the panel or with the webhooks:manage scope). Every event is POSTed to you with a signature.
| Event | When |
|---|---|
| order.delivered | Order delivered (a partial delivery arrives with this event too) |
| order.failed | Order failed or cancelled: the amount was refunded to your balance |
| price.changed | Your dealer price changed |
| stock.changed | The stock state of a product changed |
| product.enabled | The product is on sale again |
| product.disabled | The product went off sale |
| balance.low | Your balance dropped below your threshold |
| balance.credited | Your balance was credited |
| ping | Sent when you press "test" in the panel or call the test endpoint. No subscription needed, not queued, not retried. The mode field is "test". |
Headers we send
| Header | Value |
|---|---|
| X-Oyuneks-Event | Olay adı (order.delivered, price.changed, ping...). |
| X-Oyuneks-Event-Id | Olay kimliği. AYNI KİMLİK İKİ KEZ GELEBİLİR (ağ hatası sonrası yeniden deneme); ikinci gelişi yok sayın. |
| X-Oyuneks-Signature | t=<unix saniye>,v1=<hex HMAC-SHA256> |
| User-Agent | Oyuneks-Bayi-Webhook/1.0 |
| Content-Type | application/json |
{
"id": "evt_o184213_d",
"type": "order.delivered",
"mode": "live",
"createdAt": "2026-08-28T14:05:02+03:00",
"data": {
"orderId": 184213,
"externalRef": "SIP-2026-0001",
"channel": "dealer-api",
"status": "delivered",
"total": "197.40",
"currency": "TRY",
"createdAt": "2026-08-28T14:05:00+03:00",
"items": [
{
"itemId": "184213-1",
"productId": 6,
"name": "1020 Cash",
"quantity": 2,
"status": "delivered",
"unitPrice": "98.70",
"codeCount": 2
}
]
}
}
Signature
- Algorithm: HMAC-SHA256
- Signed payload:
t + "." + HAM istek gövdesi - Key: Panelden ya da rotate-secret ucundan alınan sır (whsec_ ile başlar).
- Tolerance: 300 saniyeden eski t değerini reddedin (tekrar oynatma koruması).
- JSON'i çözüp yeniden serileştirirseniz imza TUTMAZ; ham gövdeyi kullanın (PHP: file_get_contents("php://input")).
<?php
// HAM govde sart: json_decode + json_encode ile imza TUTMAZ.
$ham = file_get_contents("php://input");
$imza = $_SERVER["HTTP_X_OYUNEKS_SIGNATURE"] ?? "";
$t = null; $v1 = null;
foreach (explode(",", $imza) as $parca) {
list($ad, $deger) = array_pad(explode("=", trim($parca), 2), 2, null);
if ($ad === "t") { $t = $deger; }
if ($ad === "v1") { $v1 = $deger; }
}
// 5 dakikadan eski istegi reddet (tekrar oynatma korumasi).
if ($t === null || abs(time() - (int) $t) > 300) { http_response_code(400); exit; }
$beklenen = hash_hmac("sha256", $t . "." . $ham, "whsec_SIZIN_SIRRINIZ");
if (!hash_equals($beklenen, (string) $v1)) { http_response_code(401); exit; }
$olay = json_decode($ham, true);
// Ayni olay iki kez gelebilir: id ile tekillestir, isi ASENKRON yap.
kuyrugaAl($olay["id"], $olay["type"], $olay["data"]);
http_response_code(200);
import crypto from "node:crypto";
// express: app.post("/oyuneks-hook", express.raw({ type: "application/json" }), ...)
function dogrula(hamGovde, imzaBasligi, sir) {
const parcalar = Object.fromEntries(
String(imzaBasligi).split(",").map((p) => p.trim().split("="))
);
const t = Number(parcalar.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const beklenen = crypto
.createHmac("sha256", sir)
.update(t + "." + hamGovde)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(beklenen), Buffer.from(parcalar.v1));
}
Your response and retries
- 2xx. Gövde okunmaz, boş yanıt yeterlidir. Bağlantı 3 saniye, toplam 5 saniye. İşi ASENKRON yapın: olayı kaydedip hemen 200 dönün.
- 3xx izlenmez (302 ile iç ağa yönlendirme girişimlerine kapalı). Nihai adresi yazın.
-
Retry intervals (minutes):
2, 4, 8, 16, 32, 64, 128. 8 denemede 2xx alınamazsa olay "ölü" sayılır ve gönderim kaydında öyle görünür. - Üst üste 8 ölü olayda adres ASKIYA alınır: gönderim durur, bayiye e-posta + site içi bildirim gider, panelden "Yeniden etkinleştir" gerekir.
Reconciliation
Webhooks are a helper, NOT the source of truth. At end of day pull the ledger with GET /bayi-api/v1/balance/transactions: every order row carries an orderId and, when set, your externalRef, and balanceAfter lets you verify the balance chain.
The same event may arrive more than once (retry after a network error): deduplicate by the id field. Ordering is not guaranteed; read the FINAL state of an order from GET /bayi-api/v1/orders/{orderId}.
The panel statement, the API statement and the invoice show the same numbers. If you see a one-kurus difference, open a support ticket with the X-Request-Id.
Versioning & changelog
The version lives in the URL (/v1). Adding a field is NOT a breaking change: write your client so unknown fields are ignored. Removing a field or changing its meaning happens in a new version, and the old one lives at least six months.
Changes are announced here with dates; machines can follow the info.version field of openapi.json.
28.08.2026 · v1 · Dealer API is live
- Catalog, dealer price, stock and required-field schema endpoints.
- Order create (Idempotency-Key required), read, lookup by your own reference, cancel pending items.
- Balance, ledger, period statement (json/csv) and invoice list.
- Signed webhook subscription: 8 events plus a ping test, delivery log, secret rotation.
- Test key (oyxb_test_): real catalog, real flow, no money moves, codes come prefixed with TEST-.
- This page, the OpenAPI 3.1 file and the Postman collection are generated from a single source.
- MCP server (/bayi-api/mcp): 11 tools for AI clients. Money tools require a two-step confirmation.
This document is public: oyuneks.com/bayi-api/docs. Machine readable: openapi.json, postman.json, llms.txt.