# AccKit Reseller API v1 — accounts & digital goods

> **Tiếng Việt (tóm tắt nhanh):** API bán tài khoản cho đại lý và phần mềm tự động. Địa chỉ gốc `https://acckit.com/api/v1`, xác thực bằng `api_key` lấy ở trang **Tài khoản** hoặc **Cài đặt API** (dùng chung cho API OTP và API Buff, chỉ dùng được trên chính website này). Tiền tệ **VND**. Luồng: `GET /categories` → lấy `id_product` → `POST /buy` → **lưu tài khoản ngay** (dữ liệu tài khoản tự xoá sau 15 ngày) → `POST /order` để xem lại. Giá trong danh mục là giá niêm yết; lúc mua hệ thống lấy giá bậc theo số lượng rồi trừ chiết khấu của tài khoản (không thấp hơn giá sàn) — số tiền thật nằm ở `price`/`total_price` của `/order` và `/orders`. `/buy` có thể trả `pending: true` (đã trừ tiền, tài khoản về sau) hoặc giao ít hơn số đặt. **Hết thời gian chờ hoặc lỗi 5xx khi gọi `/buy` thì KHÔNG gọi lại** — đợi khoảng 2 phút rồi tìm đơn trong `/orders` theo `id_product`. Luôn gửi header `Accept: application/json`. Có thể đưa nguyên tệp này cho AI/agent để tích hợp.

*Document revision 2026-09-30 · this copy was generated on 2026-10-31 for https://acckit.com*

## 1. Quick facts

| Item | Value |
|---|---|
| Base URL | `https://acckit.com/api/v1` |
| Authentication | `api_key` on every account-bound call: a form or JSON field for POST, a query parameter for GET. POST calls also accept it in the query string, but avoid that (URLs end up in logs) |
| Getting the key | Log in → **Account** page (`https://acckit.com/account`) → **API key** → *Generate* / *Regenerate*, or the **API settings** page (`https://acckit.com/setting_api`). One key per account; regenerating it invalidates the old key immediately. The same key works for the OTP API and the SMM API |
| Scope of a key | This website only (`https://acckit.com`). A key created on another website answers `Invalid api_key` here |
| Request encoding | `application/x-www-form-urlencoded` (recommended), or a JSON body sent with `Content-Type: application/json` (same field names) |
| Response encoding | JSON, UTF-8. Non-ASCII characters may arrive escaped (for example `\u00e1`); always use a JSON parser |
| Success and failure | Business failures are HTTP 200 `{"status": false, "msg": "<English message>"}`. Success shapes differ per endpoint (section 2). Detect failure with `status === false`; a missing `status` key is not a failure |
| Validation errors | HTTP 422 `{"message": "The given data was invalid.", "errors": {...}}` on `/buy`, `/order` and `/orders` when the request carries `Accept: application/json`. Without that header a script gets an HTTP 302 redirect (normally to the home page) instead. **Always send the header** |
| Currency | **VND**. Money values are JSON numbers; amounts charged are whole đồng. The website may display prices in another currency; the API always uses VND |
| Your price | Listings show list prices. `/buy` applies the volume tier, then your account discount, never below a floor price (section 3). The price actually charged is in `/order` and `/orders` |
| Delivery | Usually inside the `/buy` response. Some supplier-fulfilled orders answer `pending: true` and are filled in later, and a purchase can deliver fewer accounts than requested (section 6) |
| Retention | Delivered account data is deleted 15 days after the purchase. Save it as soon as you receive it (section 6.4) |
| Timeouts | A purchase can take up to about 2 minutes. Never repeat `/buy` after a timeout or a 5xx: reconcile through `/orders` (section 7) |
| Rate limit | 60 requests per minute per client IP, shared with the SMM API and the other non-OTP `/api/v1` endpoints (section 8.2) |
| Language | Names and descriptions in `vi`, `en`, `id`, `pt`, `zh` or `th`, chosen once on the API settings page (section 4) |
| Timestamps | `created_at` only (no endpoint returns `updated_at`): ISO-8601 in UTC, e.g. `2026-09-30T02:11:00.000000Z` |

## 2. Endpoint index

| Method | Path | `api_key` | Purpose |
|---|---|---|---|
| GET | `/categories` | optional (language only) | Every category currently for sale, with its products, list prices and stock |
| GET | `/category/{id}` | optional (language only) | The products of one category (`id` from `/categories`) |
| POST | `/balance` | required | Your wallet balance in VND |
| POST | `/buy` | required | Buy `quantity` units of one product |
| POST | `/order` | required | One of your orders, with its delivered accounts |
| POST | `/orders` | required | Your order history, newest first, 10 per page |
| POST | `/import` | administrator key | Shop administrators only: add or update one stock line |

Terminology: a **category** (`id`) groups **products** (`id_product`). `/buy` takes the **product** id. Never send a category id: the same number can be the id of an unrelated product.

Historical naming: in the `/buy` result, `data[].type` is the product **id**. In `/order` and `/orders`, `type` is the product **name** and the product id is in `id_product`.

Response shapes (all HTTP 200):

| Endpoint | Success | Failure |
|---|---|---|
| `/categories`, `/category/{id}` | `{"data": [...]}`, no `status` key | None. An unknown or hidden category gives `{"data": []}` |
| `/balance` | `{"status": true, "balance": 1250000}` | `{"status": false, "msg": "Invalid api_key"}` |
| `/buy` | `{"status": true, "order_id": 12345, "data": [...]}`, plus `"pending": true` and `msg` for a pending order | `{"status": false, "msg": "..."}` |
| `/order` | The order object itself, not wrapped: `{"id": 12345, ..., "data": [...], "id_product": 124}`, no `status` key | `{"status": false, "msg": "..."}` |
| `/orders` | `{"data": [...], "links": {...}, "meta": {...}}`, no `status` key | `{"status": false, "msg": "Invalid api_key"}` |
| `/import` | `{"status": true, "msg": "..."}` | `{"status": false, "msg": "..."}` |

Stability: the catalogue objects of `/categories` and `/category/{id}` are kept stable on purpose (no key is renamed or added). Order objects can gain keys at the end (`id_product` was added in September 2026), so ignore keys you do not recognise.

## 3. Pricing and your discount

`/categories` and `/category/{id}` return each product's **list price** (`price`), the same number for every customer. They never include volume tiers or your discount. Your unit price is computed at `/buy`, for your account and the `quantity` you send:

1. **Volume tier.** If the product has volume tiers, the lowest tier price among the tiers your `quantity` reaches replaces the list price (with normally ordered tiers, that is the tier with the highest threshold reached). Buying more is therefore never dearer per unit. A tier is never dearer than the list price (a tier priced above it counts as the list price), and a tier without a positive price is ignored.
2. **Your account discount.** Your account's discount percentage is taken off that price. The shop sets it for your account; you cannot change it. Without a discount you pay the tier or list price.
3. **Floor.** The discount stops at the product's floor price, if it has one (the shop's cost for that product; not published). On low-margin products your effective discount can therefore be smaller than your percentage, or zero. The floor never raises a price above the tier or list price.

`/buy` requires your balance to cover unit price × `quantity`, then charges unit price × the units billed (normally the accounts delivered, section 6.2).

Worked example: list price 10,000 VND, one volume tier "100 units or more: 8,000", your discount 10%. The first two rows assume the product's floor is lower than the discounted price.

| Quantity | Price before discount | After 10% | Unit price charged | Total charged |
|---|---|---|---|---|
| 99 | 10,000 (no tier reached) | 9,000 | 9,000 | 891,000 |
| 100 | 8,000 (tier 100+) | 7,200 | 7,200 | 720,000 |
| 100, product floor 7,500 | 8,000 (tier 100+) | 7,200, below the floor | 7,500 | 750,000 |

- The same calculation is used on the website and in the API, for the shop's own stock and for supplier-fulfilled products.
- To see your prices before buying, log in on the website with the same account: product pages show your unit price and the volume-tier table with your discount applied (in the website's display currency).
- After buying, `/order` and `/orders` return `price` (unit price charged) and `total_price` (amount charged). The `/buy` response itself carries no price.
- Your unit price is rounded **up** to a whole đồng (10,001 × 90% = 9,000.9 → 9,001), so an order's `total_price` is exactly `price` × `quantity`.
- Your discount also applies to purchases through the OTP API and the SMM API.

## 4. Language of names and descriptions

| Code | Language |
|---|---|
| `vi` | Vietnamese (the original text) |
| `en` | English |
| `id` | Bahasa Indonesia |
| `pt` | Portuguese (Brazil) |
| `zh` | Simplified Chinese |
| `th` | Thai |

- Choose it once on the **API settings** page (`https://acckit.com/setting_api`, logged in). It is stored on your account; there is no per-request language parameter.
- It applies whenever the request carries your `api_key`: category and product names and descriptions in `/categories` and `/category/{id}`, and product names (`type`) in `/order` and `/orders`.
- Without a key, or if you never chose a language, the shop's default API language is used (Vietnamese unless the shop has changed it).
- A missing translation falls back to English, then to the Vietnamese original.
- `msg` texts and validation messages are always English. Ids, prices, stock and delivered account data are never translated.

## 5. Endpoints

### 5.1 GET /categories

Public. Send `api_key` only to get names and descriptions in your API language; an unknown key is ignored.

```bash
curl "https://acckit.com/api/v1/categories?api_key=YOUR_API_KEY" -H "Accept: application/json"
```

Response (abbreviated to one category and one product):

```json
{
  "data": [
    {
      "id": 211,
      "name": "Social media accounts",
      "icon": null,
      "list_product": [
        {
          "id_product": 124,
          "name": "Account with 2FA and recovery mail",
          "desc": "Format: uid|pass|2fa|mail|mailpass",
          "price": 10000,
          "quantity": 89
        }
      ]
    }
  ]
}
```

| Field | Type | Meaning |
|---|---|---|
| `id` | integer | Category id. Use it only with `/category/{id}`, never with `/buy` |
| `name` | string | Category name, in your API language |
| `icon` | string or null | Absolute URL of the category image; `null` when the category has no image file |
| `list_product[].id_product` | integer | **Product id: the value to send as `id_product` to `/buy`** |
| `list_product[].name` | string | Product name, in your API language |
| `list_product[].desc` | string or null | Short description, in your API language (often the field layout of a delivered account, plus notes) |
| `list_product[].price` | number | List price per unit in VND, not your price (section 3). Parse it as a number: it may be written with a decimal point, e.g. `10000.0` |
| `list_product[].quantity` | integer | Units available. Exact for the shop's own stock. For supplier-fulfilled products it is refreshed about every 5 minutes (and lowered at once when units sell here), so treat it as approximate |

Only visible categories that contain at least one product for sale are returned, each with only its products for sale. This is exactly the set that `/buy` accepts. Categories and products come in the website's display order. A product whose stock reaches 0 is hidden automatically, usually within a minute.

### 5.2 GET /category/{id}

The products of one category. `{id}` is a category `id` from `/categories`. Same optional `api_key` for the language.

```bash
curl "https://acckit.com/api/v1/category/211?api_key=YOUR_API_KEY" -H "Accept: application/json"
```

```json
{"data": [{"id_product": 124, "name": "Account with 2FA and recovery mail", "desc": "Format: uid|pass|2fa|mail|mailpass", "price": 10000, "quantity": 89}]}
```

The product objects are the same as `list_product[]` above. An unknown, hidden or empty category returns `{"data": []}`.

### 5.3 POST /balance

| Param | Type | Required | Description |
|---|---|---|---|
| `api_key` | string | yes | Your API key |

```bash
curl -X POST "https://acckit.com/api/v1/balance" -H "Accept: application/json" -d "api_key=YOUR_API_KEY"
```

```json
{"status": true, "balance": 1250000}
```

`balance` is a number, in VND. A wrong, missing or blank key gives `{"status": false, "msg": "Invalid api_key"}` (this endpoint never answers 422).

### 5.4 POST /buy

Buys `quantity` units of one product. There is no idempotency key: every call that reaches the server is a new purchase (read section 7 before writing retry logic).

| Param | Type | Required | Description |
|---|---|---|---|
| `api_key` | string | yes | Your API key |
| `id_product` | integer | yes | Product id from `list_product[].id_product` |
| `quantity` | integer | yes | Units to buy, at least 1. Some products have a higher minimum that the listings do not show; `/buy` then answers `Minimum quantity is N` |

What happens, in this order:

1. The fields are validated: HTTP 422 if `api_key`, `id_product` or `quantity` is missing or malformed (`quantity` must be an integer).
2. The key is checked: `Invalid api_key`.
3. The product is checked: `Product is not for sale right now` if it is hidden (anything `/categories` does not list, including products withdrawn automatically while their supplier is failing), `Product ID does not exist` if the id is unknown.
4. The quantity is checked: `Minimum quantity must be at least 1`, `Minimum quantity is N`, `Quantity exceeds available stock`.
5. The unit price is computed (section 3): `Insufficient balance!` if your balance is below unit price × `quantity`.
6. The accounts are taken from the shop's own stock or ordered from a supplier. Your balance is charged, and the order created, in the same step: no order, no charge.

```bash
curl -X POST "https://acckit.com/api/v1/buy" -H "Accept: application/json" --max-time 120 \
  -d "api_key=YOUR_API_KEY" -d "id_product=124" -d "quantity=2"
```

Success:

```json
{
  "status": true,
  "order_id": 12345,
  "data": [
    {
      "buy_id": 12345,
      "uid": "100012345678901",
      "full_info": "100012345678901|Password123|ABCD EFGH IJKL MNOP|mail@example.com|MailPass",
      "type": 124
    },
    {
      "buy_id": 12345,
      "uid": "100012345678902",
      "full_info": "100012345678902|Password456|QRST UVWX YZAB CDEF|mail2@example.com|MailPass2",
      "type": 124
    }
  ]
}
```

| Field | Type | Meaning |
|---|---|---|
| `order_id` | integer | Your order number. Store it with the accounts |
| `data` | array | One element per delivered account. It can hold fewer elements than `quantity` (section 6.2), and is empty for a pending order |
| `data[].buy_id` | integer | Same as `order_id` |
| `data[].uid` | string | Account identifier, normally the first field of `full_info` |
| `data[].full_info` | string | The complete account line, fields separated by the pipe character. The field layout is described in the product's `desc` |
| `data[].type` | integer | The product id (historical name; see section 2) |
| `pending` | boolean | Only present, as `true`, for a pending order |
| `msg` | string | Only present with `pending` |

Pending order (a supplier accepted the order but has not handed over the accounts yet). You have been charged for the full `quantity`; see section 6.1 for what to do next:

```json
{
  "status": true,
  "order_id": 12346,
  "pending": true,
  "data": [],
  "msg": "Order placed. The supplier is still preparing the accounts — call /order with this order_id in a few minutes."
}
```

Failures (HTTP 200). None of these charge you or create an order:

| `msg` | Meaning and what to do |
|---|---|
| `Invalid api_key` | The key is wrong, or belongs to another website. (A missing key gives HTTP 422.) |
| `Product is not for sale right now` | The product or its category is hidden. Re-read `/categories` and buy only what it lists |
| `Product ID does not exist` | Unknown `id_product`. Check that you are not sending a category id |
| `Minimum quantity must be at least 1` | `quantity` is 0 or negative |
| `Minimum quantity is N` | This product requires at least N units per order |
| `Quantity exceeds available stock` | Not enough stock right now. Re-read `/categories`, then buy less or choose another product |
| `Insufficient balance!` | Top up your wallet |
| `Product temporarily unavailable, please try again later or contact support.` | The supplier refused the order or had no stock. The product is withdrawn from the catalogue until the next stock refresh (about 5 minutes) finds stock again. Retry only once `/categories` lists it again |
| `System error!` | Unexpected failure while delivering. Some products are also withdrawn from the catalogue until the shop has checked them. Re-read `/categories` before trying again |

### 5.5 POST /order

Returns one of your orders with its delivered accounts. The order object is the whole response body: it is **not** wrapped in a `data` object, and its own `data` key is the array of accounts.

| Param | Type | Required | Description |
|---|---|---|---|
| `api_key` | string | yes | Your API key |
| `order_id` | integer | yes | `order_id` from `/buy`, or `id` from `/orders` |

```bash
curl -X POST "https://acckit.com/api/v1/order" -H "Accept: application/json" \
  -d "api_key=YOUR_API_KEY" -d "order_id=12345"
```

```json
{
  "id": 12345,
  "quantity": 2,
  "price": 9000,
  "total_price": 18000,
  "type": "Account with 2FA and recovery mail",
  "created_at": "2026-09-30T02:11:00.000000Z",
  "data": [
    {
      "uid": "100012345678901",
      "full_info": "100012345678901|Password123|ABCD EFGH IJKL MNOP|mail@example.com|MailPass",
      "link_backup": "https://acckit.com/download_backup/12345/100012345678901"
    },
    {
      "uid": "100012345678902",
      "full_info": "100012345678902|Password456|QRST UVWX YZAB CDEF|mail2@example.com|MailPass2",
      "link_backup": "https://acckit.com/download_backup/12345/100012345678902"
    }
  ],
  "id_product": 124
}
```

| Field | Type | Meaning |
|---|---|---|
| `id` | integer | Order id |
| `quantity` | integer | Units billed (section 6.2) |
| `price` | number | Unit price charged, VND (section 3) |
| `total_price` | number | Amount charged, VND |
| `type` | string or null | Product **name** in your API language; `null` if the product no longer exists |
| `created_at` | string | Purchase time, ISO-8601 UTC |
| `data` | array | The delivered accounts. Empty while the order is pending (section 6.1) and after the retention period (section 6.4) |
| `data[].uid`, `data[].full_info` | string | As in `/buy` |
| `data[].link_backup` | string | Always present. A browser link to the account's backup file (an .html file), not an API endpoint: it works only in a browser logged in to the website with the same account (otherwise you are sent to the login page), and answers 404 when the product ships no backup file or after the retention period |
| `id_product` | integer | Product id, the same value as `id_product` in `/categories` |

Success is recognised by the presence of `id`. Failures (HTTP 200):

- `{"status": false, "msg": "Order not found"}`: unknown id, an order of another account, or a refunded order (section 6.3).
- `{"status": false, "msg": "Invalid api_key"}`: wrong key. A missing `api_key` or `order_id` gives HTTP 422.

### 5.6 POST /orders

Your order history, newest first, 10 orders per page. It includes the purchases made on the website with the same account. Refunded orders are not listed (section 6.3). OTP rentals and SMM orders are not part of this list.

| Param | Type | Required | Description |
|---|---|---|---|
| `api_key` | string | yes | Your API key |
| `page` | integer | no | Page number, default 1. Send it in the query string (`/orders?page=2`) or as a form field |

```bash
curl -X POST "https://acckit.com/api/v1/orders?page=1" -H "Accept: application/json" -d "api_key=YOUR_API_KEY"
```

Response (`data` abbreviated to one order; a full page holds up to 10):

```json
{
  "data": [
    {
      "id": 12345,
      "quantity": 2,
      "price": 9000,
      "total_price": 18000,
      "type": "Account with 2FA and recovery mail",
      "created_at": "2026-09-30T02:11:00.000000Z",
      "id_product": 124
    }
  ],
  "links": {
    "first": "https://acckit.com/api/v1/orders?page=1",
    "last": "https://acckit.com/api/v1/orders?page=2",
    "prev": null,
    "next": "https://acckit.com/api/v1/orders?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 2,
    "links": [
      {"url": null, "label": "&laquo; Previous", "active": false},
      {"url": "https://acckit.com/api/v1/orders?page=1", "label": 1, "active": true},
      {"url": "https://acckit.com/api/v1/orders?page=2", "label": 2, "active": false},
      {"url": "https://acckit.com/api/v1/orders?page=2", "label": "Next &raquo;", "active": false}
    ],
    "path": "https://acckit.com/api/v1/orders",
    "per_page": 10,
    "to": 10,
    "total": 14
  }
}
```

- Each item has exactly the fields of `/order` except the account list: `id`, `quantity`, `price`, `total_price`, `type` (product **name**), `created_at`, `id_product` (product **id**). Fetch the accounts with `/order`.
- The URLs in `links` and `meta.links` are plain page URLs. Call them with POST and your `api_key`; a GET answers HTTP 405. `meta.links` is a list of page buttons that you can ignore.
- A wrong key gives `{"status": false, "msg": "Invalid api_key"}`; a missing key gives HTTP 422.

### 5.7 POST /import (shop administrators only)

Not for customers. It requires the API key of a shop administrator account. It adds one account line to a product's own stock, or replaces the line whose first field (`uid`) already exists in that product.

| Param | Type | Required | Description |
|---|---|---|---|
| `api_key` | string | yes | Administrator's API key |
| `id_product` | integer | yes | Product id |
| `text` | string | yes | One account line. Everything before the first pipe character is its `uid` |

| Response (HTTP 200) | Meaning |
|---|---|
| `{"status": true, "msg": "Added successfully"}` | New line added. If the product was hidden, it is shown again |
| `{"status": true, "msg": "Updated successfully"}` | A line with the same `uid` existed in this product and was replaced |
| `{"status": false, "msg": "Invalid api_key or missing admin permission"}` | Key missing, wrong, or not an administrator's |
| `{"status": false, "msg": "Please provide the text field"}` | `text` is empty |
| `{"status": false, "msg": "Please provide the id_product field"}` | `id_product` is empty |
| `{"status": false, "msg": "id_product does not exist"}` | Unknown product |

## 6. After the purchase: pending orders, partial delivery, refunds, retention

### 6.1 Pending orders

`/buy` answers `status: true` with `pending: true` and an empty `data` when a supplier accepted the order but has not handed over the accounts yet.

- You have been charged for the full `quantity`, and the order already exists in `/order` and `/orders`.
- The shop asks the supplier again every minute and adds the accounts to the order as soon as they arrive. `/order` has no pending flag: an empty `data` on a recent order means it is still pending.
- Poll `/order` with the `order_id` every 1 to 2 minutes. Once `data` is not empty, store the accounts.
- Still empty after 30 minutes: contact support with the `order_id`. Automatic retrieval stops 24 hours after the purchase; after that the shop resolves the order by hand (delivery or refund).

### 6.2 Fewer accounts than requested

A purchase can deliver fewer accounts than `quantity`, typically when stock runs out during the purchase. The order always records what you paid for: `quantity` is the number of units billed and `total_price` the amount charged.

- Normally you are billed only for the accounts delivered: the order's `quantity` equals the number of accounts you received, and `total_price` is lower than for the full amount.
- You are never billed for more than you ordered. In the rare case a supplier sends extra accounts, you receive them too, but `quantity` and `total_price` stay at what you ordered.
- Supplier orders that were accepted as a whole (the kind that can answer `pending`) are billed for the full requested quantity when they are placed. If such an order ends up with fewer accounts in `data` than its `quantity`, contact support with the `order_id`.
- If nothing at all can be delivered, `/buy` fails (`status: false`) and nothing is charged. A pending order (section 6.1) is the only case where you are charged before receiving accounts.

Rule for your code: after each purchase, compare the number of accounts with `quantity` in `/order`. Fewer accounts than `quantity`, on an order that is neither pending nor past the retention period, means you should contact support.

### 6.3 Refunds

- The shop can refund a whole order (for example one that was never delivered). The full `total_price` goes back to your balance and the order is removed: it disappears from `/orders`, and `/order` answers `Order not found`. Keep your own record of `order_id`s so that a vanished order is not mistaken for an error.
- Warranty claims are handled on the website. A warranty refund for a single account credits that account's unit price to your balance and leaves the order unchanged in the API. Replacement accounts are shown on the website; they are not added to the order's `data`.

### 6.4 Delivered accounts are deleted after 15 days

Delivered account data is deleted from the server 15 days after the purchase (the shop's retention setting; the order page on the website states the current number of days). A daily clean-up removes it. After that:

- `/order` still returns the order, but `data` is `[]`;
- the order stays in `/orders`;
- `link_backup` links answer 404.

Store the accounts from the `/buy` response (or from your first successful `/order` call) immediately. "Download again later" only works within the retention period.

## 7. Timeouts, retries and reconciliation

`/buy` has no idempotency key, and a purchase that involves a supplier can take up to about 2 minutes. An intermediate proxy may give up earlier (HTTP 502 or 504, for example) while the server is still finishing, and the purchase can then complete and be charged without you receiving the answer.

Rules:

1. Use a client timeout of at least **120 seconds** for `/buy`.
2. **Never repeat a `/buy` call automatically** after a timeout, a network error, an HTTP 5xx, or a body that is empty or not JSON. The outcome is unknown: reconcile first.
3. Safe to repeat: HTTP 429 (the request was rejected before processing; wait `Retry-After` seconds), HTTP 422 (fix the fields first) and any HTTP 200 `{"status": false, ...}` answer (nothing was charged; fix the cause first).
4. The read-only endpoints (`/categories`, `/category/{id}`, `/balance`, `/order`, `/orders`) can be retried with exponential backoff.

Reconciliation after an unknown `/buy` outcome (the Python example in section 9 implements it):

1. Wait about 2 minutes.
2. POST `/orders` (page 1, and page 2 if you place many orders). A match is an order with the same `id_product`, a `created_at` (UTC) no earlier than about 2 minutes before you sent the request (the margin absorbs clock differences), and an `id` you have not already recorded for another purchase. Purchases made on the website with the same account appear in this list too.
3. Found: POST `/order` with that `id`, store the accounts and mark the purchase done. Its `quantity` and `total_price` tell you what was delivered and charged.
4. Not found: check again until at least 3 minutes have passed since you sent the request. Still not found then means the purchase did not happen and nothing was charged; you may buy again. If in doubt, compare `/balance` with the balance you had before the attempt.

While a purchase of a product is unresolved, do not send another `/buy` for the same product (and do not buy it on the website with the same account), so that the match stays unambiguous. `/orders` is the source of truth for what you bought.

## 8. Errors and rate limits

### 8.1 Error reference

| HTTP | Body | When | Repeat the request? |
|---|---|---|---|
| 200 | `{"status": false, "msg": "..."}` | Business errors (tables in section 5) | Yes, once the cause is fixed: nothing was charged |
| 422 | `{"message": "The given data was invalid.", "errors": {"quantity": ["The quantity must be an integer."]}}` | Missing or malformed field on `/buy`, `/order` or `/orders` (with `Accept: application/json`) | Yes, after fixing the field |
| 302 | Redirect, normally to the home page | A validation error on a request without `Accept: application/json` | Add the header |
| 405 | `{"message": "The GET method is not supported for this route. Supported methods: POST."}` | Wrong HTTP method | Use the documented method |
| 429 | `{"message": "Too Many Attempts."}` | Rate limit exceeded (section 8.2) | Yes, after `Retry-After` seconds |
| 500 | `{"message": "Server Error"}` | Unexpected server error | Read endpoints: yes, with backoff. `/buy`: no, reconcile (section 7) |
| Other 5xx, or no answer | Often an HTML page from a proxy | Server busy or restarting, or the answer took too long | Read endpoints: yes, with backoff. `/buy`: no, reconcile (section 7) |

The JSON bodies of 405, 429 and 500 are returned when the request carries `Accept: application/json`; a script without the header gets HTML pages for these errors.

### 8.2 Rate limits

- **60 requests per minute per client IP address.** The budget is shared by every non-OTP endpoint under `/api/v1`: this API, the SMM API (`/api/v1/smm/...`) and the other `/api/v1` endpoints called from the same IP. The OTP API (`/api/v1/otp/...`) has its own budget of 240 requests per minute per `api_key`.
- Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. A 429 also carries `Retry-After` (seconds) and `X-RateLimit-Reset` (Unix timestamp).
- Spend the budget carefully: cache `/categories` and refresh it every few minutes rather than before each purchase (supplier stock only refreshes about every 5 minutes anyway), poll pending orders every 1 to 2 minutes, and do not poll `/balance` in a loop.

## 9. Code examples

### cURL — full purchase flow

```bash
BASE="https://acckit.com/api/v1"; KEY="YOUR_API_KEY"; H="Accept: application/json"
curl -s -H "$H" "$BASE/categories?api_key=$KEY" | head -c 800; echo        # pick an id_product
curl -s -H "$H" -X POST "$BASE/balance" -d "api_key=$KEY"
curl -s -H "$H" --max-time 120 -X POST "$BASE/buy" -d "api_key=$KEY" -d "id_product=124" -d "quantity=1"
curl -s -H "$H" -X POST "$BASE/order" -d "api_key=$KEY" -d "order_id=12345"
curl -s -H "$H" -X POST "$BASE/orders?page=1" -d "api_key=$KEY"
```

### PHP

```php
<?php
const BASE = 'https://acckit.com/api/v1';
const KEY  = 'YOUR_API_KEY';

/** POST helper. Returns [httpStatus, decodedJsonOrNull]; httpStatus 0 = timeout or network error. */
function acckit(string $path, array $fields, int $timeout = 30): array
{
    $ch = curl_init(BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query(['api_key' => KEY] + $fields),
        CURLOPT_HTTPHEADER     => ['Accept: application/json'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
    ]);
    $body = curl_exec($ch);
    $http = $body === false ? 0 : (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $json = is_string($body) ? json_decode($body, true) : null;
    return [$http, is_array($json) ? $json : null];
}

[$http, $buy] = acckit('/buy', ['id_product' => 124, 'quantity' => 1], 120);

if ($http === 422 || $http === 429) {
    exit("Rejected before processing (HTTP $http): nothing was charged.\n");
}
if ($http !== 200 || $buy === null) {
    // Timeout, network error, 5xx or a non-JSON body: the purchase MAY have gone through.
    // Do NOT call /buy again. Wait about 2 minutes, then reconcile through /orders (section 7).
    exit("Unknown outcome: reconcile through /orders.\n");
}
if (($buy['status'] ?? null) === false) {
    exit('Failed, nothing was charged: ' . $buy['msg'] . "\n");
}

$orderId  = $buy['order_id'];     // store it right away, together with the accounts
$accounts = $buy['data'];
if (!empty($buy['pending'])) {    // charged; the accounts are added to the order later
    for ($i = 0; $i < 30 && !$accounts; $i++) {
        sleep(60);
        [$h, $order] = acckit('/order', ['order_id' => $orderId]);
        if ($h === 200 && isset($order['id'])) {
            $accounts = $order['data'];
        }
    }
}
foreach ($accounts as $acc) {
    echo $acc['full_info'], "\n";  // save them now: account data is deleted after 15 days
}
```

### Python

```python
import time
from datetime import datetime, timedelta, timezone

import requests

BASE = "https://acckit.com/api/v1"
KEY = "YOUR_API_KEY"
H = {"Accept": "application/json"}


def post(path, timeout=30, **fields):
    """POST helper. Returns (http_status, json_or_None); http_status 0 = timeout or network error."""
    try:
        r = requests.post(BASE + path, data={"api_key": KEY, **fields}, headers=H, timeout=timeout)
    except requests.RequestException:
        return 0, None
    try:
        return r.status_code, r.json()
    except ValueError:
        return r.status_code, None


def find_order(id_product, sent_at, known_ids):
    """Reconciliation after an unknown /buy outcome. sent_at = UTC time just before calling /buy."""
    for page in (1, 2):
        http, res = post("/orders", page=page)
        for o in (res or {}).get("data", []):
            created = datetime.fromisoformat(o["created_at"].replace("Z", "+00:00"))
            if (o["id_product"] == id_product and o["id"] not in known_ids
                    and created >= sent_at - timedelta(minutes=2)):
                return o
    return None


cats = requests.get(f"{BASE}/categories", params={"api_key": KEY}, headers=H, timeout=30).json()["data"]
product = cats[0]["list_product"][0]             # choose properly in real code
known_ids = set()                                # order ids you have already recorded

sent_at = datetime.now(timezone.utc)
http, buy = post("/buy", timeout=120, id_product=product["id_product"], quantity=1)
if http in (422, 429):
    raise SystemExit(f"rejected before processing (HTTP {http}); nothing was charged")
if http != 200 or buy is None:
    time.sleep(120)                              # never repeat /buy here: look for the order instead
    found = find_order(product["id_product"], sent_at, known_ids)
    raise SystemExit(f"order {found['id']} exists: fetch it with /order" if found
                     else "no order yet: check again in a few minutes before buying again")
if buy.get("status") is False:
    raise SystemExit("failed, nothing was charged: " + buy["msg"])

order_id, accounts = buy["order_id"], buy["data"]   # store both right away
known_ids.add(order_id)
for _ in range(30):                                  # pending: charged, accounts arrive later
    if accounts or not buy.get("pending"):
        break
    time.sleep(60)
    http, order = post("/order", order_id=order_id)
    if http == 200 and order and "id" in order:
        accounts = order["data"]
for acc in accounts:
    print(acc["full_info"])                          # save now: deleted after 15 days
```

### Node.js (18+)

```js
// Node.js 18+ (built-in fetch). Save as buy.js and run: node buy.js
const BASE = "https://acckit.com/api/v1", KEY = "YOUR_API_KEY";

async function post(path, fields, timeoutMs = 30000) {
  try {
    const r = await fetch(BASE + path, {
      method: "POST",
      headers: { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({ api_key: KEY, ...fields }),
      signal: AbortSignal.timeout(timeoutMs),
    });
    const json = await r.json().catch(() => null);
    return [r.status, json];
  } catch {
    return [0, null];                                   // timeout or network error
  }
}

(async () => {
  const [http, buy] = await post("/buy", { id_product: 124, quantity: 1 }, 120000);
  if (http === 422 || http === 429) return console.error(`rejected before processing (HTTP ${http}); nothing was charged`);
  if (http !== 200 || !buy) return console.error("unknown outcome: do NOT repeat /buy; wait about 2 minutes and reconcile through /orders");
  if (buy.status === false) return console.error("failed, nothing was charged:", buy.msg);

  let accounts = buy.data;                              // store buy.order_id and the accounts right away
  for (let i = 0; buy.pending && accounts.length === 0 && i < 30; i++) {
    await new Promise(resolve => setTimeout(resolve, 60000));
    const [h, order] = await post("/order", { order_id: buy.order_id });
    if (h === 200 && order && "id" in order) accounts = order.data;
  }
  accounts.forEach(a => console.log(a.full_info));      // save now: deleted after 15 days
})();
```

## 10. Integration checklist (developers and AI agents)

> Building a shop that resells these products? Read **acckit-storefront-integration.md**
> — catalogue mirroring, pricing, the checkout flow, timeout reconciliation and a
> paste-ready brief for an AI coding agent. Download it at https://acckit.com/api-docs.
> Where that guide and this reference disagree, this reference is correct.

- Send `Accept: application/json` on every request; otherwise validation failures come back as a redirect instead of JSON.
- Keep the key on your server. It works only on https://acckit.com, and regenerating it on the website invalidates the old key immediately.
- Take `id_product` from `list_product[]`; never send a category `id` to `/buy`.
- Treat catalogue prices as list prices. Your real charge follows section 3 and is returned by `/order` and `/orders` (`price`, `total_price`).
- Detect failures with `status === false`. A successful `/order` has no `status` key and is not wrapped in `data`; a successful `/orders` or `/categories` has no `status` key either.
- In `/order` and `/orders`, `type` is the product name; match on `id_product`.
- Save delivered accounts immediately: they are deleted after 15 days.
- Handle `pending: true` by polling `/order` (section 6.1), and compare the accounts received with `quantity` (section 6.2).
- Never repeat `/buy` after a timeout, a 5xx or an unreadable body: reconcile through `/orders` (section 7). Use a `/buy` timeout of at least 120 seconds.
- Expect refunded orders to disappear from `/orders` (section 6.3).
- Stock (`quantity`) changes constantly. Treat `Quantity exceeds available stock` as "re-read the catalogue and try a smaller amount or another product".
- Stay under 60 requests per minute per IP, a budget shared with the SMM API; cache `/categories` instead of polling it in a tight loop.
- Ignore response keys you do not recognise.
