# Colortune Seller API — Full Reference

> Programmatic access to key generation and product status for authorized sellers.
> This page is designed to be readable both by humans and by AI assistants
> (LLMs) that generate integration code. Feed the URL directly to your AI.

Live endpoint base: `https://colortune.ru`
API version: v1 (stable)
Format: JSON only

---

## Authentication

All requests are authenticated by a per-seller **API key**, sent one of two ways:

1. **Body/query parameter** `api_key` (default; works with GET and POST)
2. **Bearer header** is NOT supported on this endpoint — this API predates our v2 auth stack. Use `api_key` param.

Get your key from the panel: `Seller API` page → `Regenerate API key`.
Keep it secret; anyone with it can spend your balance.

---

## Endpoints

### POST /api/seller_api

Purchase keys for a product.

**Request** (`Content-Type: application/json` — also accepts `application/x-www-form-urlencoded`):

| Field | Type | Required | Description |
|---|---|---|---|
| `api_key` | string | yes | Your seller API key. |
| `product` | integer | yes | Product ID (cheat_id). See `GET /seller_api/status` for the list you can sell. |
| `days` | integer | yes | Subscription length in days. Must match one of the product's configured `ProductDuration` rows. |
| `qty` | integer | no | Number of keys. Default `1`, max `50` per request. |
| `idempotency_key` | string | no | Alternative to the `Idempotency-Key` header. Length 8..128. |

**Headers**:

| Header | Description |
|---|---|
| `Content-Type: application/json` | required for JSON body |
| `Idempotency-Key: <uuid or unique string>` | strongly recommended — see [Idempotency](#idempotency) below |
| `X-Idempotency-Key` | alternative name for the same header |

**Response 200** (success):

```json
{
  "success": true,
  "keys": ["ABCDEF1234567890ABCDEF1234567890", "..."],
  "balance_remaining": 43.50
}
```

- `keys` — array of freshly-generated 32-char hex keys, activatable through the client loader.
- `balance_remaining` — seller's `users.money` USD balance after this purchase.

Additional response header on retry:

- `Idempotent-Replay: true` — present only when the response was served from the idempotency cache. Absent on first request.

**Response 400/401/403/429/500** (error):

```json
{
  "success": false,
  "error": "insufficient_balance",
  "message": "Not enough balance. Required: $2.00, Available: $0.50"
}
```

Error codes:

| Code | HTTP | Meaning |
|---|---|---|
| `invalid_api_key` | 401 | Missing or wrong `api_key`. |
| `account_banned` | 403 | Seller account has been banned by an admin. `message` includes the reason. |
| `invalid_idempotency_key` | 400 | Idempotency-Key length outside 8..128 chars. |
| `invalid_params` | 400 | Missing `product` / `days` / bad type. |
| `duration_not_available` | 400 | The `product`+`days` combo isn't configured or you can't sell that product. |
| `insufficient_balance` | 400 | `users.money < price × qty`. |
| `rate_limit_exceeded` | 429 | More than 30 successful requests per minute per api_key. |
| `internal_error` | 500 | Anything unexpected. Retry with the same `Idempotency-Key` — you'll get the same result if the first attempt actually succeeded. |

**Rate limit**: 30 successful requests per minute per api_key.

---

### GET /api/seller_api/status

Read current status of products you can sell.

**Request** (query string):

| Field | Type | Required | Description |
|---|---|---|---|
| `api_key` | string | yes | Your seller API key. |
| `product` | integer | no | Return only this product. If omitted → all sellable products. |

**Response 200**:

```json
{
  "success": true,
  "products": [
    {
      "id": 541,
      "name": "Apex Legends",
      "status": "Undetected",
      "status_reason": null,
      "status_reason_note": null,
      "project_id": 1,
      "project_name": "Memez"
    },
    {
      "id": 551,
      "name": "RustExternal",
      "status": "freezed",
      "status_reason": "maintenance",
      "status_reason_note": null,
      "project_id": 1,
      "project_name": "Memez"
    }
  ]
}
```

- `status` values: `Undetected`, `freezed`, `detected`, `updating`, `Maintenance`, and other admin-set strings.
- `status_reason` (only set when `status = freezed`): `maintenance`, `testing`, `detected`, `updating`, `bypass_check`, `other`, or `null`.
- `status_reason_note` — free-form text set when `status_reason = other`.
- Rate limit: shares the 30/min bucket with the buy endpoint.

---

## Idempotency

The buy endpoint uses **Stripe-style idempotency**. To make network retries safe:

1. Generate a unique string per **logical** purchase (e.g. UUIDv4 or a customer-order-id).
2. Send it as the `Idempotency-Key` header on every retry of the same purchase.
3. If the server has seen this key within the last **48 hours**, it returns the **exact same response body** with the same generated keys, without charging your balance again or generating new keys.
4. `Idempotent-Replay: true` header is added on cached responses.

**Rules:**

- Use a fresh key for every different purchase — reusing a key returns the cached response, not a new purchase.
- Two different sellers can safely use the same client-side idempotency strings — keys are internally scoped by `api_key`.
- Cache TTL is 48h; after that a stale key is treated as new and generates real keys.
- If your first request truly succeeded but the network dropped the response, replay with the same key. You get the same `keys[]`.

**Examples:**

cURL:
```bash
IDEMP=$(uuidgen)
curl -X POST https://colortune.ru/api/seller_api \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMP" \
  -d '{"api_key":"YOUR_API_KEY","product":541,"days":30,"qty":1}'
```

Python:
```python
import uuid, requests

idemp = str(uuid.uuid4())
r = requests.post(
    "https://colortune.ru/api/seller_api",
    headers={"Idempotency-Key": idemp, "Content-Type": "application/json"},
    json={"api_key": "YOUR_API_KEY", "product": 541, "days": 30, "qty": 1},
    timeout=(5, 30),
)
data = r.json()
if data["success"]:
    print("Keys:", data["keys"], "· replay:", r.headers.get("Idempotent-Replay") == "true")
```

Node.js (fetch):
```js
const idemp = crypto.randomUUID();
const res = await fetch("https://colortune.ru/api/seller_api", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Idempotency-Key": idemp,
    },
    body: JSON.stringify({
        api_key: "YOUR_API_KEY",
        product: 541,
        days: 30,
        qty: 1,
    }),
});
const data = await res.json();
```

PHP:
```php
$idemp = bin2hex(random_bytes(16));
$ch = curl_init("https://colortune.ru/api/seller_api");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Content-Type: application/json",
        "Idempotency-Key: $idemp",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        "api_key" => "YOUR_API_KEY",
        "product" => 541,
        "days"    => 30,
        "qty"     => 1,
    ]),
    CURLOPT_TIMEOUT        => 30,
]);
$data = json_decode(curl_exec($ch), true);
```

---

## Webhooks

Sellers can subscribe to platform events per project via the panel `Seller API → Webhooks` tab.

**Delivery:**

- HTTP POST to your registered URL
- `Content-Type: application/json`
- `X-Signature: sha256=<hex>` — HMAC-SHA256 of the raw body using your webhook secret
- `X-Event: <event name>`
- `X-Webhook-Id: <numeric id>`
- Timeout: 3 seconds, one in-process retry, then failure is recorded.
- After 20 consecutive failures the webhook is auto-disabled — re-enable from the panel.

**Verify the signature** (PHP):
```php
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $YOUR_SECRET);
if (!hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'] ?? '')) {
    http_response_code(403); exit;
}
$data = json_decode($body, true);
```

### Events

| Event | When | Extra `data` fields |
|---|---|---|
| `product.frozen` | Product status changed to `freezed` (or admin explicit freeze). | `reason` (string or null), `reason_note` (string or null) |
| `product.unfrozen` | Product left `freezed` (unfreeze or auto-promote of a fresh build). | `hours_added` (int; 0 if no time compensation was given) |
| `webhook.test` | Manual test-ping fired from the panel. Must be treated as no-op by your handler. | `test: true`, `note` |

**Common `data` fields on every event:**

```json
{
  "product_id":   541,
  "product_name": "Apex Legends",
  "project_id":   1,
  "old_status":   "Undetected",
  "new_status":   "freezed"
}
```

Full example (`product.frozen`):
```json
{
  "event": "product.frozen",
  "timestamp": 1781824308,
  "data": {
    "product_id": 541,
    "product_name": "Apex Legends",
    "project_id": 1,
    "old_status": "Undetected",
    "new_status": "freezed",
    "reason": "maintenance",
    "reason_note": null
  }
}
```

Full example (`product.unfrozen` + compensation):
```json
{
  "event": "product.unfrozen",
  "timestamp": 1781825000,
  "data": {
    "product_id": 541,
    "product_name": "Apex Legends",
    "project_id": 1,
    "old_status": "freezed",
    "new_status": "Undetected",
    "reason": null,
    "reason_note": null,
    "hours_added": 24
  }
}
```

**Reason values** (only appear on `product.frozen`):

- `maintenance` — Scheduled maintenance
- `testing` — Testing an update
- `detected` — Cheat got detected — urgent
- `updating` — Build update in progress
- `bypass_check` — Checking AC bypass
- `other` — Free-form — see `reason_note`
- `null` — Frozen without an explicit reason

---

## Product IDs

Product IDs are stable; the list is dynamic. Query `GET /seller_api/status` (no `product` arg) at runtime for the up-to-date set your account can sell. Do not hard-code product IDs — new ones get added and old ones can be renamed.

---

## Best practices for integrations

1. **Always** send `Idempotency-Key` on `POST /seller_api`. Loss-of-response is the #1 cause of duplicate charges.
2. Store the returned `keys[]` immediately in your DB with the client purchase reference before showing the customer.
3. On network error (timeout, 5xx, connection reset), retry with the SAME `Idempotency-Key`. Don't spin up a new one — that's a new purchase.
4. Cache the product catalog from `GET /seller_api/status` and refresh at most once per minute. The endpoint has a 30/min rate limit shared with the buy endpoint.
5. Watch `status` on every product; when it flips to `freezed`, pause purchase attempts and inform customers. Also subscribe to `product.frozen` webhooks so you don't need to poll.
6. HMAC-verify every webhook. Anyone with your webhook URL can pretend to be Colortune.
7. Treat `webhook.test` as a no-op — never trigger customer-facing actions from it.

---

## Contact

Panel: [https://colortune.ru](https://colortune.ru)
Admin support via project TG chat.

Docs source in the repo: `resources/docs/api-reference.md`. This page is public and stable; feel free to point AI assistants directly here for integration help.
