AccKit API Documentation v1

Reseller API (accounts) · OTP API (phone numbers) · SMM API (likes, followers, views). One api_key for all three.

Download all docs (.md) View all.md

Tiếng Việt: Mỗi API có tài liệu riêng bên dưới. Nút Tải .md lưu tệp Markdown về máy, Xem .md mở tệp gốc trong tab mới, Sao chép chép toàn bộ nội dung vào clipboard để dán cho ChatGPT / Claude / công cụ AI của bạn. Nội dung trang web và tệp .md sinh từ cùng một nguồn nên luôn giống nhau. Tệp Download all gộp cả ba API để nạp một lần cho AI.
Your API key

Every account gets a personal key at registration. Log in to see yours right here, or open the Account page → section API. No account yet? Register free.

1. Reseller API — accounts & digital goods

Buy accounts and stock from your own shop or software: catalogue, balance, buy, order history.

Tải .md / Download Xem .md / View
File: acckit-reseller-api.md · raw URL for your AI agent: https://acckit.com/api-docs/general.md

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.

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

Response (abbreviated to one category and one product):

{
  "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.

curl "https://acckit.com/api/v1/category/211?api_key=YOUR_API_KEY" -H "Accept: application/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
curl -X POST "https://acckit.com/api/v1/balance" -H "Accept: application/json" -d "api_key=YOUR_API_KEY"
{"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.
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:

{
  "status": true,
  "order_id": 12345,
  "data": [
    {
      "buy_id": 12345,
      "uid": "100012345678901",
      "full_info": "100012345678901|Password123|ABCD EFGH IJKL MNOP|[email protected]|MailPass",
      "type": 124
    },
    {
      "buy_id": 12345,
      "uid": "100012345678902",
      "full_info": "100012345678902|Password456|QRST UVWX YZAB CDEF|[email protected]|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:

{
  "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
curl -X POST "https://acckit.com/api/v1/order" -H "Accept: application/json" \
  -d "api_key=YOUR_API_KEY" -d "order_id=12345"
{
  "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|[email protected]|MailPass",
      "link_backup": "https://acckit.com/download_backup/12345/100012345678901"
    },
    {
      "uid": "100012345678902",
      "full_info": "100012345678902|Password456|QRST UVWX YZAB CDEF|[email protected]|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
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):

{
  "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_ids 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

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
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

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+)

// 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.

2. OTP API — rent phone numbers, receive SMS codes

Rent a real number in 190+ countries, read the SMS verification code, automatic refund when no SMS arrives.

Tải .md / Download Xem .md / View
File: acckit-otp-api.md · raw URL for your AI agent: https://acckit.com/api-docs/otp.md

AccKit OTP API v1 — rent a phone number, receive the SMS code

Tiếng Việt (tóm tắt nhanh): API thuê số điện thoại nhận mã OTP cho tool. Địa chỉ gốc https://acckit.com/api/v1/otp, dùng chung api_key với API bán hàng, tiền tệ VND. Gửi kèm api_key ở /services và /servers để thấy giá của chính tài khoản bạn (đã trừ chiết khấu) — /buy trừ đúng giá đó. Luồng: GET /servers → chọn một dòng → POST /buy với đúng operator (hoặc server: vip1, vip2, vip3, vip4, hoặc mã SV thường) của dòng đó → hỏi GET /check mỗi 3–5 giây tới khi status là RECEIVED (có mã) hoặc REFUNDED (đã hoàn tiền). Mỗi số giữ tối đa 15 phút (hạn thật là expires_ts của đơn); không có mã thì tự hoàn 100%. /buy có thể mất tới ~90 giây: đặt timeout ≥ 120 giây và không tự gửi lại /buy khi bị timeout. Chi tiết bằng tiếng Anh bên dưới; có thể đưa nguyên file này cho AI / agent của bạn.

Document version 2026-09-30 — checked against the source code on 2026-09-30. This copy: https://acckit.com, 2026-10-31.

1. Quick facts

Item Value
Base URL https://acckit.com/api/v1/otp
Authentication The same api_key as the Reseller API (Account page → section API), sent as a query parameter, a form field or a JSON body field. Required on /buy, /check, /cancel, /balance. Optional on /services and /servers, where it switches the prices to yours
Currency VND, integers
Your price List price minus your account discount, never below the shop's cost, rounded up to a whole VND. Send api_key to /services and /servers to see it (section 2.1)
Charging /buy debits your wallet, then requests the number. If no number is issued, the money is back before /buy answers
Hold time At most 15 minutes after the number is issued. The real deadline of each order is its expires_ts
Refund Automatic and full (order.price) when no code arrives before the deadline or the rental ends early upstream. Immediate when you cancel before a code arrives
One order, one code Each order delivers one SMS code. Need another code → buy again
Rate limit 240 requests per minute per api_key, shared by all OTP endpoints (per IP for calls without a key). HTTP 429 with a Retry-After header when exceeded
Slow calls /buy can take up to about 90 s; /check and /cancel 30 s or more. Use client timeouts of 120 s and 60 s. Never repeat a /buy automatically after a timeout (section 2.2)
Response shape {"status": true, ...} or {"status": false, "msg": "..."} with HTTP 200
Validation errors HTTP 422 JSON when you send Accept: application/json; without it the server answers HTTP 302. Always send the header
Messages Fixed English texts. The language chosen for the Reseller API does not apply here. Branch on the fields (status, sold_out, need_deposit, thieu, order.status), not on msg
Module switched off If the shop switches the OTP service off, every endpoint except /balance answers HTTP 404
Time zone expires_at is Vietnam time (UTC+7). Prefer expires_ts (unix seconds) or expires_at_iso (ISO-8601 with offset)

2. Flow

  1. GET /countries → pick a country slug (e.g. philippines).
  2. GET /services?country=…&api_key=… → pick a service slug (e.g. facebook). This list is only an overview.
  3. GET /servers?country=…&service=…&api_key=… → the rows you can buy right now, each with your price. Take one row's server or operator (vip1, vip2, vip3, vip4, or standard server code).
  4. POST /buy with api_key, country, service, operator (or server) → you get order.id, order.phone and order.expires_ts. Keep the id: no endpoint lists your orders.
  5. Submit order.phone to the app you are verifying.
  6. Poll GET /check?id=… every 3–5 s until order.status is RECEIVED (read order.code) or REFUNDED (no code, the money is back).
  7. Optional: POST /cancel while the order is still WAITING to give up and get the refund at once.

2.1 Pricing and your discount

  • Every row has a list price. Your account can have a discount: a percentage the shop sets on your account (the same reseller discount as in the Reseller API). You cannot change it through the API.
  • Your price = list price minus your discount, rounded up to a whole VND. The discount never takes a price below the shop's cost for that number, so on some rows your price is higher than the plain percentage would give (it is never above the list price).
  • Send your api_key on /services and /servers to see your prices. Without a key, or with a wrong key, both endpoints answer with list prices; a wrong key is not reported as an error there. When you are logged in, the website shows you the same prices.
  • /buy charges exactly the price that a keyed /servers call shows for that row at that moment: both use the same rule. order.price is the amount charged, and a refund returns exactly order.price.
  • Prices follow the upstream market and can change at any time, including between your /servers call and your /buy. /buy takes no maximum-price parameter, so call /servers right before /buy and read order.price afterwards.
  • price_from in /services is the lowest of your prices across the sources that list the service — an indication, not necessarily a price you can buy at (see 4.2).

2.2 Timeouts, retries and the rate limit

  • /buy can take up to about 90 seconds (some tiers need several upstream calls before the number is known). Use a client timeout of at least 120 s.
  • Never repeat a /buy automatically after a timeout or a dropped connection. The order may already exist and be charged, no endpoint lists orders, and a repeat rents a second number. Instead, check /balance and look at your recent orders on the website's OTP page (https://acckit.com/buy-otp, logged in) — API orders are listed there too. A number you never use is normally refunded at the end of its hold time. An HTTP 429 is different: it is rejected before anything happens, so the same /buy can be sent again after Retry-After seconds.
  • /check and /cancel refresh the order from upstream and can take 30 s or more. Use a 60 s timeout. Both are safe to repeat.
  • /countries, /services and /servers are usually quick but can take several seconds while their data is refreshed. Use a 60 s timeout.
  • The 240 requests per minute are counted per api_key across every OTP call, /buy and /servers included. Polling one order every 3 s uses 20 requests per minute, so one key can follow at most 12 orders at that pace (20 at a 5 s pace), minus what your other calls use. Successful responses carry X-RateLimit-Limit and X-RateLimit-Remaining; on HTTP 429 wait Retry-After seconds.

3. Endpoint index

Method Path Key Purpose
GET /countries no Countries on sale
GET /services?country= optional (your prices) Overview of the services in one country: lowest price and total stock
GET /servers?country=&service= optional (your prices) The rows you can buy now for that pair, best first
POST /buy yes Charge the wallet and rent a number
GET or POST /check yes Poll one order
POST /cancel yes Give up an order before its code arrives → refund
GET or POST /balance yes Wallet balance in VND

A wrong HTTP method (for example GET /buy) answers HTTP 405.

4. Endpoints

4.1 GET /countries

curl -H "Accept: application/json" "https://acckit.com/api/v1/otp/countries"
{"status": true, "data": [
  {"country": "afghanistan", "name": "Afghanistan", "prefix": "+93", "iso": "af"},
  {"country": "philippines", "name": "Philippines", "prefix": "+63", "iso": "ph"}
]}

country is the slug to send to the other endpoints, name the English name, prefix the dialling code and iso the 2-letter code (prefix and iso can be empty for a few countries). Sorted by name. Countries the shop does not sell are left out.

4.2 GET /services

Param Required Description
country yes Country slug from /countries
api_key no Send it to get price_from at your price
curl -H "Accept: application/json" "https://acckit.com/api/v1/otp/services?country=philippines&api_key=YOUR_API_KEY"
{"status": true, "data": [
  {"service": "facebook", "price_from": 2785, "qty": 653226, "vip": true},
  {"service": "google", "price_from": 3100, "qty": 120544, "vip": true}
]}
Field Meaning
service Slug to send to /servers and /buy (e.g. facebook, google, telegram, whatsapp, tiktok, shopee)
price_from Lowest price across the sources that list this service, VND. Your price when you sent a valid api_key, otherwise the list price
qty Sum of the stock reported for the normal, vip and vip2 tiers. A service that only vip3 sells shows 1
vip true when the vip tier lists this service. The other VIP tiers (vip2–vip4) are not reflected here — check /servers

Sorted alphabetically by service. Services the shop does not sell, and services whose lowest list price is above the shop's price ceiling, are left out.

This list is an overview built from the source catalogues, not from the rows you can buy:

  • It ignores servers that are temporarily hidden (see 4.3), so price_from can be lower than any row /servers returns, and /servers can return [] for a listed service.
  • It does not include the vip4 tier (Vietnam), and of the services that only vip3 (USA) sells it lists just a few popular ones. /servers can therefore return rows for a service that is not listed here — if you know the slug, call /servers directly.
  • /servers is authoritative.

4.3 GET /servers

Param Required Description
country yes Country slug
service yes Service slug
api_key no Send it to get price at your price, i.e. what /buy charges
curl -H "Accept: application/json" "https://acckit.com/api/v1/otp/servers?country=philippines&service=facebook&api_key=YOUR_API_KEY"
{"status": true, "data": [
  {"server": "vip1", "server_name": "SV VIP 1", "operator": "vip1", "price": 4199, "qty": 148149, "rate": null, "tier": "vip1", "hold": 15, "ve_rate": 54, "ve_mau": 67, "ghim": false},
  {"server": "vip2", "server_name": "SV VIP 2", "operator": "vip2", "price": 3100, "qty": 79124, "rate": null, "tier": "vip2", "hold": 15, "ve_rate": null, "ve_mau": 117, "ghim": false},
  {"server": "virtual34", "server_name": "SV thường (virtual34)", "operator": "virtual34", "price": 2785, "qty": 400458, "rate": 72.5, "tier": "normal", "hold": 15, "ve_rate": 31, "ve_mau": 29, "ghim": false},
  {"server": "virtual38", "server_name": "SV thường (virtual38)", "operator": "virtual38", "price": 2890, "qty": 23110, "rate": 0, "tier": "normal", "hold": 15, "ve_rate": 12, "ve_mau": 17, "ghim": false}
]}
Field Meaning
server Server code: vip1, vip2, vip3, vip4 (VIP servers) or operator code for standard server (e.g. virtual34). Send it in /buy as operator or server
server_name Display label (e.g. SV VIP 1, SV VIP 2, SV VIP 3 (US), SV VIP 4, SV thường (virtual34))
operator Server identifier (matches server). Send it in /buy as operator
tier Service level of the row: vip1, vip2, vip3, vip4, or normal
price VND per number: your price with a valid api_key, otherwise the list price. It is a quote: /buy recomputes it from live data (section 2.1)
qty Stock reported, an estimate (a server can still run out). Always 1 on vip3 and vip4, whose providers publish no stock — do not filter those rows on qty
rate Success rate published for standard servers (0 when not published). Always null on VIP tiers. Prefer ve_rate
hold Usual hold time of the server, in minutes. The order's own hold and expires_ts are what count
ve_rate Our measured success rate, 0–100, or null — see below
ve_mau Number of finished orders behind ve_rate — see below
ghim true = a newly added server pinned to the top of the list until it has statistics. Unproven

Tiers:

tier server Countries Usual hold qty
normal several per country, e.g. virtual34 many 15 stock estimate
vip1 vip1 many 15 stock estimate
vip2 vip2 many 15 stock estimate
vip3 vip3 USA only 15 always 1
vip4 vip4 Vietnam only 10 (set by the shop) always 1

Each VIP tier has at most one row per country + service. A VIP tier is not automatically more expensive: prices depend on the tier and the country.

ve_rate and ve_mau. ve_rate is the share of our own orders for this country + service over the last 7 days that received a code, counting only finished orders (RECEIVED or REFUNDED):

  • it is measured on this exact server once the server has at least 6 finished orders; before that, on all servers of the same tier for this country + service;
  • it is set to 0 early when the first 3 or more finished orders all failed;
  • it is null when there is not enough data yet.

ve_mau is the number of finished orders behind ve_rate. When ve_rate is null, ve_mau is the total number of finished orders for this country + service across all servers — context only, not this row's own count. Statistics are refreshed every 10 minutes.

Order of the rows. Rows are grouped as below; inside a group they are sorted by list price, cheapest first:

  1. ghim: true rows (new servers still collecting statistics);
  2. ve_rate 45 or more;
  3. ve_rate null, with a list price at most 1.3 × the cheapest row's;
  4. ve_rate 20 to 44;
  5. ve_rate null, with a list price above 1.3 × the cheapest row's;
  6. ve_rate below 20.

So the first row is not always a proven one. If you want a server with a track record, take the first row with "ghim": false and a ve_rate.

Rows that are not listed. /servers shows only what can be bought now. A row is left out when:

  • the shop does not sell that country or service (then data is [] and nothing can be bought);
  • its upstream is temporarily unavailable (hidden for at least 10 minutes at a time);
  • the server has just run out of numbers: hidden for 5 minutes, then 30 minutes, then 2 hours if it keeps running out. This applies to normal, vip1 and vip2; vip3 and vip4 rows usually stay listed;
  • its list price is above the shop's price ceiling;
  • it delivered no code at all over at least 10 recent finished orders (ve_rate 0 with ve_mau 10 or more).

An unknown or misspelled country or service also gives {"status": true, "data": []}.

4.4 POST /buy

Param Type Required Description
api_key string yes Your API key
country string yes Country slug
service string yes Service slug
operator string yes* server or operator of the chosen /servers row (vip1, vip2, vip3, vip4, or virtual34, etc.)
server string yes* Alias for operator. You can send either operator or server

How the row is chosen:

  • Send operator (or server) equal to the server code (vip1, vip2, vip3, vip4, or standard code).

What happens: /buy reloads the live server list (the same list /servers returns), finds the row, computes your price, debits your wallet and asks for a number. If no number is issued, the money is refunded before the response. This can take up to about 90 s — read section 2.2 before you set timeouts or retries.

curl -X POST --max-time 120 -H "Accept: application/json" "https://acckit.com/api/v1/otp/buy" \
  -d "api_key=YOUR_API_KEY" -d "country=philippines" -d "service=facebook" \
  -d "operator=vip1"

Success:

{
  "status": true,
  "order": {
    "id": 214,
    "phone": "+639171234567",
    "status": "WAITING",
    "code": null,
    "text": null,
    "product": "facebook",
    "country": "philippines",
    "operator": "virtual34",
    "price": 2785,
    "expires_at": "18:45:00 15/08/2026",
    "expires_ts": 1786794300,
    "vip": false,
    "hold": 15,
    "expires_at_iso": "2026-08-15T18:45:00+07:00"
  }
}
Field Meaning
id Order id — keep it for /check and /cancel; no endpoint lists your orders
phone The rented number in international format with +. Very rarely null when the number could not be read back — cancel such an order (full refund)
status WAITING after a successful /buy. See section 5
code The OTP code as a string (usually digits) once status is RECEIVED, otherwise null
text The SMS as the source returns it: the full message when the source provides it (normal, vip3, vip4); on vip and vip2 usually just the code. null until RECEIVED
product, country, operator The service, country and operator you bought
price VND charged — exactly what a refund returns
expires_at Deadline of the hold, Vietnam time (UTC+7), format HH:MM:SS DD/MM/YYYY
expires_ts Same moment as a unix timestamp — use this one
expires_at_iso Same moment in ISO-8601 with offset
vip true for every VIP tier (vip to vip4), false for normal
hold Length of this order's hold, in minutes (rounded). It can differ from the row's hold

Failures come with HTTP 200 and always carry sold_out, need_deposit and thieu (except the invalid-key answer):

{"status": false, "msg": "Insufficient balance!", "sold_out": false, "need_deposit": true, "thieu": 1215}
{"status": false, "msg": "This server is out of stock, please pick another.", "sold_out": true, "need_deposit": false, "thieu": 0}
Field Meaning
sold_out true → this row cannot deliver right now. Nothing was charged, or it was already refunded. Reload /servers and choose a different server
need_deposit true → your balance is below the price. With thieu equal to 0 it means a temporary error while charging: retry
thieu VND missing: the order price minus your current balance

All failure messages of /buy:

msg Flags Charged? What to do
Insufficient balance! need_deposit: true no Top up at least thieu VND. If thieu is 0, retry
This server is out of stock, please pick another. sold_out: true no The row is not in the current /servers list: it ran out, is hidden, its price moved above the ceiling, or your country / service / operator match no row (check the spelling). Reload /servers
This country/service just ran out of numbers — your money has been refunded. Please pick another country or service. sold_out: true refunded A VIP tier has no number for this country + service right now. Pick another row, or another country
Could not get a number right now — your money has been refunded. Please try another server. sold_out: true refunded A normal server ran out (it is now hidden for a while), or the source is temporarily unavailable (all its rows are hidden for at least 10 minutes). Pick another row
same text as above sold_out: false refunded Any other upstream error; nothing is hidden. Try another row, or the same one a little later
This order was cancelled while the number was being issued — your money has been refunded. sold_out: false refunded (by the cancel) The order was cancelled — for example on the website — while /buy was still waiting for the number. The number was released. Buy again if you still need one
Invalid api_key none no Key missing or wrong

After a sold_out failure on a vip3 or vip4 row, that row can stay listed — even first, pinned with ghim: true. Remember the rows that just failed for about 10 minutes and skip them, or you may pay and be refunded on the same row again and again.

4.5 GET or POST /check

Param Required Description
api_key yes Your API key
id yes Order id
curl --max-time 60 -H "Accept: application/json" "https://acckit.com/api/v1/otp/check?api_key=YOUR_API_KEY&id=214"
{"status": true, "order": {"id": 214, "phone": "+639171234567", "status": "RECEIVED", "code": "384729", "text": "384729 is your Facebook confirmation code", "product": "facebook", "country": "philippines", "operator": "virtual34", "price": 2785, "expires_at": "18:45:00 15/08/2026", "expires_ts": 1786794300, "vip": false, "hold": 15, "expires_at_iso": "2026-08-15T18:45:00+07:00"}}

While the order is open, each call first asks the source for news, which can take 30 s or more, and processes the refund once the order is over (section 5). A final order (RECEIVED or REFUNDED) is answered from our records at once, as often as you like. Unknown id or another customer's order → {"status": false, "msg": "Order not found"}. The response does not include your balance: call /balance.

4.6 POST /cancel

Param Required Description
api_key yes Your API key
id yes Order id
curl -X POST --max-time 60 -H "Accept: application/json" "https://acckit.com/api/v1/otp/cancel" \
  -d "api_key=YOUR_API_KEY" -d "id=214"
  • Order still open (no code yet), any tier, any time → full refund at once: {"status": true, "order": {... "status": "REFUNDED" ...}}. Cancelling means giving up: an SMS that arrives afterwards, or that reached the source but was not fetched yet, is discarded. Do not cancel an order whose code you still expect.
  • A code landed at the same moment → {"status": true, "order": {... "status": "RECEIVED", "code": "..." ...}} and no refund. Always read order.status in the reply.
  • Already refunded → {"status": true, "order": {...}} again, so a repeated cancel is harmless.
  • Code already received → {"status": false, "msg": "This order already received a code."} — a used number cannot be refunded.

4.7 GET or POST /balance

curl -X POST -H "Accept: application/json" "https://acckit.com/api/v1/otp/balance" -d "api_key=YOUR_API_KEY"
{"status": true, "balance": 152000, "currency": "VND"}

Works even when the OTP service is switched off. The wallet is shared with the Reseller and SMM APIs and with the website. /buy and /check do not return the balance.

5. Order statuses and refunds

status Meaning What to do
PENDING Internal: exists only while /buy is still running, and /buy never returns it. If such an order is cancelled (the website shows it), it is refunded and /buy answers with the "cancelled while the number was being issued" failure Treat as WAITING
WAITING Number issued, waiting for the SMS Submit phone to the target app, keep polling
RECEIVED Code arrived. Final; the order stays charged Read code / text
REFUNDED Final. order.price is back in your wallet: no code before the deadline, the rental ended early upstream, or you cancelled Buy again, preferably on another row

How refunds work:

  • Deadline. expires_ts is the order's own deadline: the source's hold window, but never more than 15 minutes after the number is issued. The hold column of /servers is only the usual value for the tier.
  • Automatic refund. An open order is refunded when the source reports that the rental ended without a code (this can happen before expires_ts), or once expires_ts has passed by 30 seconds. This check runs on your /check calls and on a background job every minute, so you do not have to poll for the refund: it normally lands within about 2 minutes after expires_ts.
  • Late codes still count. A code that arrives before the refund is processed makes the order RECEIVED, even a few seconds after expires_ts.
  • If you stop polling, the background job keeps collecting codes and refunds. Nothing is lost: a later /check shows the result.
  • Ignored SMS. A code that the same phone number already delivered on an earlier order, or an SMS sent before the rental started, is ignored (it belongs to a previous user of that number). The order keeps waiting and is refunded at the deadline if no new code arrives.
  • One code per order. Once RECEIVED, the order is final and later SMS are not delivered.
  • Failed /buy. If no number could be issued after the debit, the refund is made before /buy answers.

6. Error reference

HTTP Body When
200 {"status": false, "msg": "Invalid api_key"} Key missing or wrong on /buy, /check, /cancel, /balance
200 {"status": false, "msg": "Order not found"} Wrong id, or another customer's order
200 {"status": false, "msg": "...", "sold_out": true, ...} /buy: that row cannot deliver now. Reload /servers and choose another row (4.4)
200 {"status": false, "msg": "Insufficient balance!", "need_deposit": true, "thieu": N, ...} /buy: top up at least N VND (N = 0: temporary error, retry)
200 {"status": false, "msg": "Could not get a number right now — ...", "sold_out": false, ...} /buy: upstream error, already refunded
200 {"status": false, "msg": "This order already received a code."} /cancel after the code arrived
422 {"message": "The given data was invalid.", "errors": {...}} Missing or invalid country, service, operator or id (only with Accept: application/json; otherwise HTTP 302)
404 not meaningful OTP service switched off (every endpoint except /balance), or a wrong path
405 not meaningful Wrong HTTP method, e.g. GET /buy
429 {"message": "Too Many Attempts."} with Accept: application/json (an HTML page without it), plus a Retry-After header More than 240 requests per minute for this key

7. Code examples

cURL

BASE="https://acckit.com/api/v1/otp"; KEY="YOUR_API_KEY"; H="Accept: application/json"
curl -s -H "$H" "$BASE/servers?country=philippines&service=facebook&api_key=$KEY"
curl -s -X POST --max-time 120 -H "$H" "$BASE/buy" -d "api_key=$KEY" -d "country=philippines" -d "service=facebook" -d "operator=virtual34" -d "provider=fivesim"
curl -s --max-time 60 -H "$H" "$BASE/check?api_key=$KEY&id=214"
curl -s -X POST --max-time 60 -H "$H" "$BASE/cancel" -d "api_key=$KEY" -d "id=214"
curl -s -H "$H" "$BASE/balance?api_key=$KEY"

Python — buy a number and wait for the code

import time
import requests

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


def call(method, path, timeout, **params):
    """One API call. Repeats only after HTTP 429, which is rejected before anything happens."""
    params["api_key"] = KEY
    while True:
        if method == "GET":
            r = requests.get(BASE + path, params=params, headers=H, timeout=timeout)
        else:
            r = requests.post(BASE + path, data=params, headers=H, timeout=timeout)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "10")))
            continue
        r.raise_for_status()
        return r.json()


def get_code(country, service, on_number, max_tries=3):
    failed = set()                                       # (provider, operator) rows that just failed
    for _ in range(max_tries):
        rows = call("GET", "/servers", 60, country=country, service=service)["data"]   # your prices
        rows = [r for r in rows if (r["provider"], r["operator"]) not in failed]
        if not rows:
            raise RuntimeError("nothing to buy for this country/service right now")
        row = next((r for r in rows if not r["ghim"]), rows[0])   # prefer rows with statistics
        failed.add((row["provider"], row["operator"]))

        # 120 s timeout. If it times out, do NOT call /buy again: the number may already be rented.
        res = call("POST", "/buy", 120, country=country, service=service,
                   operator=row["operator"], provider=row["provider"])
        if not res["status"]:
            if "sold_out" not in res:                    # e.g. "Invalid api_key"
                raise RuntimeError(res["msg"])
            if res["need_deposit"] and res["thieu"] > 0:
                raise RuntimeError(f"top up at least {res['thieu']} VND")
            if res["need_deposit"]:                      # thieu == 0: temporary error, same row is fine
                failed.discard((row["provider"], row["operator"]))
                time.sleep(5)
            continue                                     # not charged, or already refunded: next row

        order = res["order"]
        if not order["phone"]:                           # very rare: no usable number, take the refund
            call("POST", "/cancel", 60, id=order["id"])
            continue
        on_number(order["phone"])                        # submit the number to the target app here

        while time.time() < order["expires_ts"] + 180:   # the refund lands shortly after expires_ts
            time.sleep(4)                                # 3-5 s per order
            try:
                c = call("GET", "/check", 60, id=order["id"])
            except requests.RequestException:
                continue                                 # /check is safe to repeat
            if not c["status"]:
                raise RuntimeError(c["msg"])
            o = c["order"]
            if o["status"] == "RECEIVED":
                return o["phone"], o["code"]
            if o["status"] == "REFUNDED":
                break                                    # no code in time; money is back, try another row
        else:                                            # still open after the deadline: give up
            c = call("POST", "/cancel", 60, id=order["id"])
            if not c["status"]:                          # "This order already received a code."
                c = call("GET", "/check", 60, id=order["id"])
            if c["order"]["status"] == "RECEIVED":       # the code won the race
                return c["order"]["phone"], c["order"]["code"]
    raise RuntimeError(f"no code after {max_tries} numbers")


print(get_code("philippines", "facebook", on_number=lambda phone: print("use this number:", phone)))

8. Notes

  • Choosing a row. Prefer a high ve_rate backed by a large ve_mau. null means "not measured yet", and ghim: true rows are new and unproven. A VIP tier is not automatically dearer or more reliable: compare price and ve_rate row by row.
  • Trying again. After a failed /buy or a REFUNDED order, call /servers again and usually pick a different row. Skip rows that failed in the last 10 minutes or so.
  • Branch on fields, never on msg. Use status, sold_out, need_deposit, thieu and order.status.
  • Keep every order.id. No endpoint lists your orders; the website's OTP page shows the latest ones.
  • All amounts are VND integers. The wallet is shared with the Reseller and SMM APIs and with the website.

3. SMM API — likes, followers, views (Buff)

Order social-media interactions through REST endpoints or the SMM Panel v2 standard.

Tải .md / Download Xem .md / View
File: acckit-smm-api.md · raw URL for your AI agent: https://acckit.com/api-docs/smm.md

AccKit SMM API v1 — likes, followers, views (Buff)

Tiếng Việt (tóm tắt nhanh): API buff like / follow / view cho Facebook, TikTok, Instagram, YouTube, Telegram, X, Shopee… Địa chỉ gốc https://acckit.com/api/v1/smm, dùng chung api_key với API bán hàng và API OTP, tiền VND. rate là giá cho 1.000 đơn vị; gọi /services kèm key thì rate là giá của chính tài khoản bạn (đã trừ chiết khấu), tiền bị trừ khi đặt = ceil(số lượng × rate / 1000). Hai kiểu gọi: REST (/services, /buy, /status, /cancel, /balance) và chuẩn SMM Panel v2 (POST /api/v1/smm với key + action). Khác chuẩn v2: tiền VND, lỗi trả kèm mã HTTP 4xx/5xx, cancel chỉ nhận một mã đơn qua order, không có refill / drip-feed / bình luận tự nhập qua API (chỉ đặt gói type = Default). Hủy đơn: KHÔNG hoàn ngay — đơn ở Canceling ít nhất 12 giờ (refund_after) để chốt đúng số lượng đã chạy, sau đó phần chưa chạy được hoàn tự động. Đơn Partial hoặc bị nhà cung cấp hủy được hoàn tự động trong khoảng 10 phút; mỗi đơn chỉ hoàn một lần. Luôn gửi header Accept: application/json và URL-encode link. Chi tiết tiếng Anh bên dưới; có thể đưa nguyên file này cho AI / agent của bạn.

Document version 2026-10-02 — checked against the source code on 2026-10-02. This copy: https://acckit.com, 2026-10-31.

1. Quick facts

Item Value
Base URL https://acckit.com/api/v1/smm
Authentication Your key as api_key or key (form field, query parameter or JSON field), or header Authorization: Bearer YOUR_API_KEY, or header X-API-Key: YOUR_API_KEY. Checked in that order; the first non-empty one is used
Getting the key Log in → Account page (https://acckit.com/account) → section API. The same key works for the Reseller API and the OTP API. Keys are per site: call the API on the domain where your account was created
Request encoding application/x-www-form-urlencoded (recommended), query string, or a JSON body (Content-Type: application/json) with the same field names. URL-encode link
Always send Accept: application/json. The endpoints' own answers are JSON anyway, but without this header framework-level errors (429, 405, unknown path, maintenance, crashes) come back as HTML pages
Two calling styles REST endpoints, and the SMM Panel v2 convention: POST https://acckit.com/api/v1/smm with key + action (differences in section 10)
Money VND. rate = price per 1,000 units. With your key, rate is your own price (account discount applied). Charge = ceil(quantity × rate / 1000) VND, an integer
What you can order Services with type = Default (a link and a quantity). Only service, link and quantity are read: no drip-feed, no custom comments, no refill through the API
Rate limit 60 requests per minute per client IP, shared with the Reseller API (the OTP API has its own quota). HTTP 429 with Retry-After when exceeded
Failure shape {"status": false, "error": "<stable English text>", "message": "<text for humans>"} with HTTP 400, 401, 404, 500 or 503
Order ids The shop's own ids, shown as #BUFF-<id> on the website
Statuses Verifying (charged and held until the order is confirmed — rare), Pending, In progress, Processing, Canceling (you cancelled; held at least 12 hours before the refund), and the final Completed, Partial, Canceled, Refunded
Refunds Your cancel: never at once — the order stays Canceling for at least 12 hours, then the undelivered part (the provider's final count) is refunded automatically. Partial or provider-cancelled orders: automatic, usually within 10 minutes. An order is never refunded twice

2. Endpoints and typical flow

REST (https://acckit.com/api/v1/smm/…) v2 (POST https://acckit.com/api/v1/smm) Key Purpose
GET or POST /services action=services optional Catalogue: price per 1,000 (yours when a key is sent), min / max, refill and cancel flags
POST /buy or POST /order action=add required Create an order and pay for it
GET or POST /status action=status required One order (checked live) or several (stored data)
POST /cancel action=cancel required Cancel one running order; the undelivered part is refunded automatically, at least 12 hours later
GET or POST /balance action=balance required Wallet balance
  • The v2 URL accepts POST only. Any other action (for example refill or refill_status) answers HTTP 400 Incorrect request action.
  • /buy, /order and /cancel accept POST only; a GET answers HTTP 405.
  • For AI agents that also use the Reseller API: POST https://acckit.com/api/v1/smm/order creates and charges a Buff order, while the Reseller API's POST https://acckit.com/api/v1/order only reads an order. Do not mix them up.

Typical flow:

  1. POST /services with your key → pick a service with type = Default by category and name; note its rate, min, max, cancel and refill.
  2. POST /buy with service_id, link (URL-encoded) and quantity → keep order_id and charge.
  3. Poll /status until the status is final (section 5): one id every few minutes, or many ids in one call every 10 minutes.
  4. Optional: POST /cancel while the order is still running, if the service has cancel: true.
  5. Partial and provider-cancelled orders are refunded automatically; there is nothing to call.

3. Services and prices — GET or POST /services (or action=services)

# list prices, no key needed
curl -H "Accept: application/json" "https://acckit.com/api/v1/smm/services"

# your own prices, filtered by category
curl -X POST "https://acckit.com/api/v1/smm/services" -H "Accept: application/json" \
  -d "api_key=YOUR_API_KEY" --data-urlencode "category=Facebook Post Likes"
Param Required Description
api_key or key (or a header) no With a valid key, rate is your price. No key, or a wrong key, gives list prices (no error)
category no Case-insensitive substring filter on category

The answer is a bare JSON array of the enabled services, cheapest list price first. Illustrative example, called with the key of an account that has a 10 % discount:

[
  {
    "service": 2936,
    "name": "Facebook Post Likes [EMOTICONS] [LOVE] [Refill: No] [Max: 10K] [Start Time: 0 - 1 Hr] [Speed: 5K/DAY]",
    "type": "Default",
    "category": "Facebook Post Likes [EMOJI]",
    "rate": 4843.8,
    "min": 10,
    "max": 10000,
    "refill": false,
    "cancel": true
  }
]
Field Meaning
service Service id: send it as service_id (REST) or service (v2)
name Description of the service (quality, speed, start time, refill period…), not translated
type Order type. Only Default can be ordered through this API; skip every other type
category Platform + action group. It also decides which links are accepted (section 4)
rate VND per 1,000 units: yours when you sent a key, otherwise the list price. A JSON number with up to 2 decimals
min / max Allowed quantity per order
refill The service has a refill (top-up) guarantee. Refills are requested on the website, not through the API (section 7)
cancel true: a running order can be cancelled with /cancel

Pricing and your discount

  • List price = rate from a call without a key. It is the same for every customer.
  • Your discount is a whole-number percentage the shop sets on your account (ask support; it cannot be changed through the API). It applies to Buff orders through the API and on the website alike; when you are logged in, the website's Buff page shows the same prices.
  • Your rate = list × (100 − discount) / 100, rounded up to 0.01 VND, but never below the shop's own cost for that service. On low-margin services the discount stops there, so your rate can be higher than list × (1 − discount).
  • Charge = ceil(quantity × your rate / 1000) VND, computed exactly (no floating-point drift), with the same formula as the website. It is deducted when the order is created, returned as charge, and fixed on the order.
  • Prices follow the shop's catalogue and can change; the price at the moment of /buy applies. Read /services with your key before ordering.

Worked example, account with a 10 % discount:

Example List price Your price
Service A, rate 45,000 40,500
Service A, 1,000 units 45,000 VND 40,500 VND
Service B, rate 5,382 4,843.8
Service B, 500 units 2,691 VND 2,422 VND (2,421.9 rounded up)

4. Create an order — POST /buy (or /order, or action=add)

Param Type Required Description
api_key or key string yes Your API key (or a header, see section 1)
service_id or service integer yes Service id from /services (type = Default)
link string yes Target URL (post, video, profile, channel…) or @username where allowed. URL-encode it. It must pass the link rules below
quantity integer yes Between the service's min and max

Any other field is ignored, including runs / interval (drip-feed), comments, usernames and answer_number: a request with runs and interval becomes one normal order of quantity units (charged for quantity), and services that need such fields (any type other than Default) are normally refused by the provider (Provider error, nothing charged).

curl -X POST "https://acckit.com/api/v1/smm/buy" -H "Accept: application/json" \
  -d "api_key=YOUR_API_KEY" -d "service_id=2936" -d "quantity=500" \
  --data-urlencode "link=https://www.facebook.com/permalink.php?story_fbid=123&id=456"

Use --data-urlencode (or your HTTP library's form encoder) for link. With a plain -d "link=…&id=456", curl sends &id=456 as a separate field: the order is placed, and charged, on the cut-down link …?story_fbid=123.

REST success (HTTP 200), same account (10 % discount):

{
  "status": true,
  "message": "Tạo đơn Buff thành công! Mã đơn: #BUFF-1234",
  "order_id": 1234,
  "order_status": "Pending",
  "service_id": 2936,
  "quantity": 500,
  "charge": 2422,
  "balance": 97578,
  "currency": "VND"
}

charge is the amount deducted in VND (integer). balance is your balance right after the order, read from the database (integer).

order_status is normally Pending. It is Verifying when the order was sent on but no confirmation came back in time: the charge is held and the order is checked on our side, then it either continues as a normal order (Pending / In progress …) or ends Canceled with the full amount refunded. A Verifying order cannot be cancelled. The v2 answer ({"order": 1234}) is the same in both cases; read the status with /status.

v2 success (action=add): {"order": 1234}. The charge is not in this answer; use action=status or the formula in section 3.

Checks, in this order:

HTTP error Cause
401 Incorrect API key Key missing or wrong
400 Service ID is required service_id / service missing or not a number
400 Link is required link missing or empty
400 Quantity must be greater than 0 quantity missing, zero, negative or not a number
404 Service not found or disabled Unknown or paused service id
400 Quantity must be between MIN and MAX Outside the service's range (the real numbers are filled in)
400 Invalid link format The link does not fit the service (link rules below); message says why
400 Not enough balance Balance below the charge (message states the amount needed), or another order or payment took the money at the same moment
500 Provider error: <reason> The order was refused, or could not be sent. Nothing is charged. <reason> is a stable code: link_busy (this link already has an order in progress — wait until it finishes), invalid_link, invalid_quantity, or unavailable (the service is not taking orders right now — try later or pick another service); message explains in Vietnamese
500 System error Unexpected error. The order is rolled back; nothing is charged. The details are logged on our side, not returned

They are checked before any money moves. The platform comes from the service's category: the first row that matches, from top to bottom.

Platform Keyword in category Accepted link
Facebook facebook, or the word fb URL on facebook.com, fb.com, fb.watch, fb.me, fb.gg
TikTok tiktok URL on tiktok.com, or @username
Instagram / Threads instagram or threads URL on instagram.com, instagr.am, threads.net, threads.com, or @username
YouTube youtube, or the word yt URL on youtube.com, youtu.be. A bare @handle is rejected: send https://www.youtube.com/@handle
Telegram telegram URL on t.me, telegram.me, telegram.dog, or @username
Shopee shopee URL on shopee.vn, shopee.co.id, shopee.ph, shopee.sg, shopee.com.my, shopee.co.th, shopee.tw, shopee.com.br, shopee.cl, shopee.com.co, shopee.com.mx, shp.ee
X / Twitter twitter, or a category starting with X - URL on x.com, twitter.com, or @username
Google google Any http(s) URL; a bare domain such as https://example.com is fine
Spotify spotify URL on spotify.com, spotify.link, or a URI spotify:<type>:<id>
Discord discord URL on discord.gg, discord.com, discordapp.com
Anything else none of the above Any http(s) URL (a bare domain is fine), or @username

For every link:

  • Subdomains are fine (m.facebook.com, vm.tiktok.com, www.…). https:// may be left out (it is assumed); only http and https are accepted.
  • No spaces, and the host must be a domain name (IP addresses are rejected).
  • On a listed platform domain the URL must point at something: a bare https://www.tiktok.com/ (no path, no query) is rejected.
  • @username means @ followed by 2 to 60 letters, digits, . or _. A username without @, a numeric id or any other text is rejected.
  • Links shorter than 3 characters are rejected.
  • The link is passed to the provider exactly as you sent it (spaces at both ends are trimmed).

Also rejected, with a specific message (words are matched case-insensitively in the link and in the service's category + name):

Service Rejected when the link contains Send instead
Facebook tiktok.com, youtube.com, youtu.be, instagram.com, t.me/ a Facebook link
Facebook, category or name containing page like, page follow, profile, like trang, follow trang, trang cá nhân, sub cá nhân or người theo dõi /posts/, /photos/, /photo.php, /videos/, /video.php, /watch, /reel/, /reels/, permalink.php, story_fbid=, fbid=, story.php the page or profile URL, e.g. https://www.facebook.com/username or https://www.facebook.com/profile.php?id=…
TikTok facebook.com, fb.com, youtube.com, instagram.com a TikTok link
TikTok, category or name containing follower or theo dõi /video/ the profile: https://www.tiktok.com/@username or @username
YouTube facebook.com, tiktok.com, instagram.com a YouTube link
YouTube, category or name containing sub (subscribers) or đăng ký watch?v=, youtu.be/, /shorts/ the channel: https://www.youtube.com/@channel or https://www.youtube.com/channel/…
Instagram / Threads facebook.com, tiktok.com, youtube.com an Instagram or Threads link
Instagram / Threads, category or name containing follower or theo dõi /p/, /reel/, /tv/ the profile: https://www.instagram.com/username or @username
Telegram, Shopee, X / Twitter facebook.com, tiktok.com, youtube.com a link of the service's platform

Every link failure answers HTTP 400 with error = Invalid link format. Its message is Vietnamese for the too-short, other-platform and wrong-object cases, and English when the link is simply not a link of the platform, for example Please enter a valid YouTube link (https://...). or Please enter a valid TikTok link (https://...) or @username.

Payment, timeouts and duplicates

  • The charge is taken in one database transaction together with the order. The balance is reduced only if it still covers the charge, so two parallel orders can never push it below zero (the second one gets Not enough balance). The order is then sent to the provider and saved. If the provider refuses, or anything else fails, the whole transaction is rolled back and nothing is charged.
  • A /buy call waits for the provider: usually a few seconds, but up to about a minute in the worst case. Use a client timeout of 90 seconds.
  • There is no idempotency key and no order-list endpoint. If a /buy call times out, do not resend blindly: compare /balance with your balance before the call. If it dropped by the charge, the order exists; find its id on the Buff order history page of the website (https://acckit.com/buff-like/history), which also lists the orders placed through the API.

5. Order status — GET or POST /status (or action=status)

Param Required Description
api_key or key (or a header) yes Your API key
orders, order_id or order yes One order id, or several separated by commas (1234,1235). If you send more than one of these fields, orders wins, then order_id, then order

The shape of the answer depends on how many ids you send, not on the field name:

  • One id (orders=1234 included): the order is first refreshed live from the provider (unless it is already Completed, Canceled or Refunded), then returned. This can take up to about 30 seconds; if the provider does not answer, the stored values are returned.
  • Two or more ids: a JSON object keyed by order id, built from stored data without asking the provider, the same for REST and v2. Stored data are refreshed by a background job about every 10 minutes, and whenever a one-id call (or the website) refreshes that order. Right after /buy an order shows Pending, start_count "0" and remains equal to the quantity until its first refresh. Unknown ids and other customers' ids get {"error": "Incorrect order ID"} (still HTTP 200).

REST, one id:

curl -X POST "https://acckit.com/api/v1/smm/status" -H "Accept: application/json" \
  -d "api_key=YOUR_API_KEY" -d "order_id=1234"
{
  "status": true,
  "order_id": 1234,
  "service_id": 2936,
  "link": "https://www.facebook.com/permalink.php?story_fbid=123&id=456",
  "quantity": 500,
  "charge": 2422,
  "order_status": "In progress",
  "start_count": "150",
  "remains": "200",
  "currency": "VND",
  "created_at": "2026-09-30T09:11:00+07:00"
}

v2, one id (action=status with order=1234):

{"charge": "2422", "start_count": "150", "status": "In progress", "remains": "200", "currency": "VND"}

Two or more ids, REST or v2 (orders=1234,1235,9999):

{
  "1234": {"charge": "2422", "start_count": "150", "status": "In progress", "remains": "200", "currency": "VND"},
  "1235": {"charge": "40500", "start_count": "0", "status": "Pending", "remains": "1000", "currency": "VND"},
  "9999": {"error": "Incorrect order ID"}
}

Types: charge is the whole amount in VND you paid for the order (REST one-id answer: a number; v2 and multi-id answers: a string such as "2422"). start_count and remains are strings. created_at is ISO-8601 in Vietnam time (UTC+7).

Errors: 401 Incorrect API key; 400 Order ID is required (no id sent); 404 Incorrect order ID (one id that is unknown or not yours).

Polling: a one-id call is a live provider call and counts against the shared limit of 60 requests per minute. Poll one order every 2 to 5 minutes, or many orders with one multi-id call every 10 minutes (calling it more often mostly returns the same stored values).

Status values

Status Meaning Final
Verifying Charged and held, but the order's confirmation has not come back yet; checked on our side, then it continues as a normal order or ends Canceled with a full refund no
Pending Accepted, not started yet no
In progress / Processing Running; remains goes down no
Canceling You cancelled and the provider accepted. Held for at least 12 hours after the cancel (refund_after), because the provider may keep delivering for a while and sometimes corrects its final count later; then the order takes the provider's final status and the undelivered part is refunded automatically no
Completed Fully delivered yes
Partial The provider stopped early; the undelivered remains is refunded automatically, usually within 10 minutes yes
Canceled (also spelled Cancelled) Cancelled by you or by the provider; the undelivered part is refunded (your cancels: at least 12 hours after the cancel; provider cancels: automatically within about 10 minutes). A cancelled order that was partly delivered ends as Partial instead yes
Refunded Rare; handled like Canceled yes

Most status texts come from the provider: compare them case-insensitively, and treat any value not in this table as not final.

6. Cancel — POST /cancel (or action=cancel)

Param Required Description
api_key or key (or a header) yes Your API key
order_id or order yes One order id
  • One order per call. The v2 orders list is not read: a request with only orders=… answers 404 Incorrect order ID, and in order=1234,1235 only 1234 is used.
  • Possible only when the service has cancel: true and the order is not Completed, Partial, Canceled or Refunded, and not already Canceling.
  • The order is marked Canceling, the provider is asked to cancel it, and nothing is refunded in this answer (cancel_pending: true, refund_amount: 0). Accepting a cancel is not the end of it: the provider stops the order a little later, may deliver more in the meantime, and sometimes corrects its final count hours later. So the order is held for at least 12 hours (refund_after). After that, once the provider reports a final status (Canceled, Partial or Completed), the order takes it and the background job credits floor(remains × charge / quantity) VND using the provider's final remains, in exact integer arithmetic, never more than charge. Poll /status to see the final status.
  • An order is refunded at most once: the background job, the website and this endpoint use the same refund reference, so a cancelled order cannot be paid twice.
  • The call waits for the provider (up to two requests) and can take up to about 60 seconds. Use a client timeout of 90 seconds.
curl -X POST "https://acckit.com/api/v1/smm/cancel" -H "Accept: application/json" \
  -d "api_key=YOUR_API_KEY" -d "order_id=1234"

Success (HTTP 200) — nothing is refunded in this answer; the refund follows automatically after refund_after. Say 200 of the 500 units end up undelivered and the charge was 2,422 VND: the refund is then floor(200 × 2,422 / 500) = 968 VND.

{
  "status": true,
  "message": "Đã gửi yêu cầu hủy. Tiền phần chưa chạy tự hoàn sau 12 giờ (từ 20:15 02/10), theo số lượng thực chạy.",
  "order_id": 1234,
  "order_status": "Canceling",
  "cancel_pending": true,
  "refund_after": "2026-10-02T20:15:04+07:00",
  "refund_amount": 0,
  "new_balance": 97578,
  "currency": "VND"
}

refund_amount and new_balance are integers in VND. refund_after is ISO-8601 in Vietnam time (UTC+7): the earliest moment the refund can be paid (it is paid at the first background run after that moment at which the provider's status is final). action=cancel answers with this same body, not with the v2 list [{"order": 1234, "cancel": 1}].

HTTP error Meaning and what to do
401 Incorrect API key Key missing or wrong
404 Incorrect order ID No id, unknown id, another customer's order, or only orders= sent
400 Order already finalized The order is Completed, Partial, Canceled or Refunded. Nothing to cancel; Partial is refunded automatically
400 Service does not support cancel The service has cancel: false
400 Cancel already in progress A cancel of this order is already being processed (status Canceling, from the website or another call); check /status in a minute
400 Order is being verified The order is Verifying (see section 4); it cannot be cancelled until it is confirmed
503 Provider unavailable, try again later The provider did not answer. The order is back to its previous status and keeps running; retry in a few minutes
400 Cancel rejected The provider refused (usually the order is too far along). The order keeps running
500 Failed to process cancel Unexpected error. Check /status later: an order that ended up Canceled is refunded by the background job

7. Automatic refunds and refill

Situation Refund When
You cancel (API or website) floor(remains × charge / quantity), with the provider's final remains Automatically, at least 12 hours after the cancel (refund_after), once the provider's status is final
The provider ends the order as Partial Same formula, with the provider's final remains Automatically, usually within 10 minutes
The provider cancels the order (Canceled, Refunded) Same formula Automatically, usually within 10 minutes
Completed Nothing —
  • Automatic refunds come from a background job that re-reads running orders from the provider every 10 minutes. A /status call does not pay a refund by itself; pressing Sync on the order in the Buff order history page of the website does, at once.
  • Every order is refunded at most once, whoever triggers it. The wallet entry reads Hoàn tiền đơn Buff #BUFF-<id> ….
  • Orders are settled against what was actually delivered: any difference is charged or credited once per order, in a single wallet entry reading Đơn buff #BUFF-<id>: đã chạy <delivered>/<quantity> — thu …đ (or — hoàn …đ), or Đơn buff 1 phần … when only part of the order was delivered.

Refill (top-up after a drop) is not available through the API: there is no refill endpoint, and action=refill / action=refill_status answer 400 Incorrect request action. For services with refill: true, open the Buff order history page (https://acckit.com/buff-like/history), where orders placed through the API are listed too, and press Refill (shown while the order is Completed, Partial, In progress or Processing), or contact support.

8. Balance — GET or POST /balance (or action=balance)

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

REST: {"status": true, "balance": 98546, "formatted": "98,546đ", "currency": "VND"}

v2 (action=balance): {"balance": "98546", "currency": "VND"}

It is the same wallet you use for everything else on the site.

9. Errors, rate limit and languages

The endpoints' own answers are always JSON:

HTTP Body When
401 {"status": false, "error": "Incorrect API key", "message": "API Key không hợp lệ."} Key missing or wrong (every endpoint except /services)
400 {"status": false, "error": "…", "message": "…"} Missing or invalid field, bad link, not enough balance, cancel not possible, unknown v2 action (Incorrect request action)
404 same shape Service not found or disabled, Incorrect order ID
500 same shape Provider error: …, System error (nothing charged), Failed to process cancel
503 same shape Provider unavailable, try again later (cancel only)

Errors raised by the framework before an endpoint runs have a different body, {"message": "…"} without status or error, and are HTML pages unless you send Accept: application/json:

HTTP When
429 Rate limit exceeded; wait the number of seconds given in the Retry-After header
405 Wrong method, e.g. GET on /buy, /order, /cancel or on the v2 URL
404 Unknown path, e.g. /api/v1/smm/refill
503 Site maintenance
500 Unexpected server error

Always check the HTTP status code before parsing the body.

Rate limit: 60 requests per minute per client IP, counted together with the Reseller API and every other https://acckit.com/api/v1/… call except the OTP API, which has its own quota. Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining.

Languages: error is a stable English text: branch on it (for Provider error: … only the prefix is stable). message is for people: mostly Vietnamese, while the "Please enter a valid … link" messages are English. name and category are not translated. The API-language setting on the API settings page does not apply to this API.

JSON encoding: non-ASCII characters and / are escaped (\u00e0 for à, \/ for /); every JSON parser decodes them. The examples in this document are shown decoded.

10. SMM Panel v2 compatibility

Panel scripts that follow the SMM Panel v2 convention can point at POST https://acckit.com/api/v1/smm with key + action. What each action does here:

action Behaviour on this API
services Array of service, name, type, category, rate, min, max, refill, cancel. rate is VND per 1,000 and is your own price when key is sent; rate, min and max are JSON numbers; there is no dripfeed field
add Reads only service, link and quantity; answers {"order": 1234}. Drip-feed, comment, username and poll fields are ignored: order type = Default services only
status order=1234 (or orders=1234) gives a flat object with charge, start_count, status, remains, currency; orders=1234,1235 gives an object keyed by id
cancel One id in order (or order_id); orders is not read. Answers the REST body of section 6, not [{"order": 1234, "cancel": 1}]
balance {"balance": "98546", "currency": "VND"}
refill, refill_status, anything else HTTP 400 Incorrect request action

Errors come with HTTP 4xx / 5xx and the body {"status": false, "error": "…", "message": "…"} instead of HTTP 200. Scripts that look for an error field still find it, as long as they read the body of non-200 answers instead of treating them as network failures.

11. Code examples

cURL — v2 (works like any SMM-panel client)

BASE="https://acckit.com/api/v1/smm"; KEY="YOUR_API_KEY"; H="Accept: application/json"
curl -s -X POST "$BASE" -H "$H" -d "key=$KEY" -d "action=services" | head -c 600; echo
curl -s -X POST "$BASE" -H "$H" -d "key=$KEY" -d "action=add" -d "service=2936" -d "quantity=500" \
  --data-urlencode "link=https://www.facebook.com/permalink.php?story_fbid=123&id=456"
curl -s -X POST "$BASE" -H "$H" -d "key=$KEY" -d "action=status" -d "order=1234"
curl -s -X POST "$BASE" -H "$H" -d "key=$KEY" -d "action=status" -d "orders=1234,1235"
curl -s -X POST "$BASE" -H "$H" -d "key=$KEY" -d "action=cancel" -d "order=1234"
curl -s -X POST "$BASE" -H "$H" -d "key=$KEY" -d "action=balance"

Python — REST

import time
from decimal import Decimal, ROUND_CEILING

import requests

BASE = "https://acckit.com/api/v1/smm"
KEY = "YOUR_API_KEY"
HEADERS = {"Accept": "application/json"}          # JSON errors instead of HTML pages
FINAL = {"completed", "partial", "canceled", "cancelled", "refunded"}


def call(path, **fields):
    """POST one request, waiting on HTTP 429. Returns (http_code, parsed JSON or None)."""
    for _ in range(5):
        r = requests.post(BASE + path, data={"api_key": KEY, **fields}, headers=HEADERS, timeout=90)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "60")))
            continue
        try:
            return r.status_code, r.json()
        except ValueError:
            return r.status_code, None
    raise RuntimeError("still rate-limited")


def expected_charge(quantity, rate):
    """Same rounding as the server: ceil(quantity * rate / 1000), exact."""
    return int((Decimal(quantity) * Decimal(str(rate)) / 1000).to_integral_value(rounding=ROUND_CEILING))


# 1. Your own prices; keep Default-type services only (cheapest list price first)
code, services = call("/services", category="Facebook Post Likes")
svc = next(s for s in services if s["type"] == "Default")
qty = min(max(svc["min"], 100), svc["max"])
print(svc["service"], svc["name"], "rate", svc["rate"], "expected charge", expected_charge(qty, svc["rate"]))

# 2. Order: requests URL-encodes form fields, so "&" inside the link is safe
code, res = call("/buy", service_id=svc["service"], quantity=qty,
                 link="https://www.facebook.com/permalink.php?story_fbid=123&id=456")
if code != 200 or not res or res.get("status") is not True:
    raise SystemExit(f"HTTP {code}: {res}")
order_id = res["order_id"]
print("order", order_id, "charged", res["charge"], "VND, balance", res["balance"])

# 3. Poll this one order (live check) every 3 minutes until it is final
while True:
    code, st = call("/status", order_id=order_id)
    if code == 200 and st and st.get("status") is True:
        print(st["order_status"], "remains", st["remains"])
        if st["order_status"].lower() in FINAL:
            break
    time.sleep(180)

PHP — v2 client class (drop-in for panels, PHP 7.4+)

<?php
// Every method returns [httpCode, decodedBody].
class AccKitSmm
{
    private string $url = 'https://acckit.com/api/v1/smm';
    private string $key;

    public function __construct(string $key) { $this->key = $key; }

    public function services(): array { return $this->call(['action' => 'services']); }

    public function add(int $service, string $link, int $quantity): array
    {
        return $this->call(['action' => 'add', 'service' => $service, 'link' => $link, 'quantity' => $quantity]);
    }

    /** One order: flat body with charge, start_count, status, remains, currency. */
    public function status(int $order): array { return $this->call(['action' => 'status', 'order' => $order]); }

    /** Two or more orders: body keyed by order id. */
    public function multiStatus(array $orders): array
    {
        return $this->call(['action' => 'status', 'orders' => implode(',', array_map('intval', $orders))]);
    }

    /** One order per call. */
    public function cancel(int $order): array { return $this->call(['action' => 'cancel', 'order' => $order]); }

    public function balance(): array { return $this->call(['action' => 'balance']); }

    private function call(array $fields): array
    {
        $ch = curl_init($this->url);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => http_build_query($fields + ['key' => $this->key]), // URL-encodes the link
            CURLOPT_HTTPHEADER     => ['Accept: application/json'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 90,
        ]);
        $body = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        return [$code, is_string($body) ? json_decode($body, true) : null];
    }
}

$api = new AccKitSmm('YOUR_API_KEY');
[$code, $res] = $api->add(2936, 'https://www.facebook.com/permalink.php?story_fbid=123&id=456', 500);
if ($code === 200 && isset($res['order'])) {
    echo "order #{$res['order']}\n";
} else {
    echo "HTTP {$code}: " . ($res['error'] ?? 'no JSON body') . "\n";
}

12. Checklist for developers and AI agents

  • Send Accept: application/json, check the HTTP code before parsing, and on 429 wait Retry-After seconds.
  • Call /services with your key at run time, keep only type = Default, and choose by category + name. Ids and prices change; do not hard-code them.
  • Respect min / max, URL-encode link, and make the link fit the platform and the object type (section 4).
  • The charge is ceil(quantity × rate / 1000) with your own rate; the /buy answer states the exact charge.
  • /buy has no idempotency key: after a timeout, check /balance and the Buff order history page before sending again.
  • Poll with one multi-id call every 10 minutes, or one id every few minutes; the 60-per-minute limit is shared with the Reseller API.
  • Final statuses: Completed, Partial, Canceled / Cancelled, Refunded (compare case-insensitively). Anything else is still running.
  • /cancel takes one id and refunds nothing at once: the undelivered part follows automatically at least 12 hours later (refund_after); Partial and provider-cancelled orders are refunded automatically; no order is ever refunded twice.
  • Refill is website-only.
  • POST /api/v1/smm/order places a paid order; it is not the Reseller API's read-only POST /api/v1/order.
  • Keep the key on your server, never in browser code, and prefer a POST body or a header to a URL that may be logged.
  • Branch on error; show message to people.

4. Storefront integration — sell our products on your own website

Step-by-step build guide: mirror the catalogue, price it, show it, take payment, deliver, reconcile. Includes a paste-ready brief for an AI coding agent.

Tải .md / Download Xem .md / View
File: acckit-storefront-integration.md · raw URL for your AI agent: https://acckit.com/api-docs/storefront.md

Storefront integration — sell our products on your own website

Tiếng Việt (tóm tắt nhanh): Hướng dẫn dựng web riêng bán lại sản phẩm của chúng tôi qua Reseller API (https://acckit.com/api/v1, một api_key, tiền VND, luôn gửi Accept: application/json). Đồng bộ danh mục từ GET /categories 5–10 phút/lần: hàng hết là biến khỏi danh sách (không hiện số 0) — đánh dấu ẩn, đừng xoá. Giá trong danh sách là giá niêm yết; giá thật bị trừ = giá bậc rồi trừ chiết khấu tài khoản (không bao giờ cao hơn giá niêm yết lúc mua), xem ở price/total_price của /order. POST /buy chờ tới 120 giây; hết giờ, lỗi mạng hay HTTP 5xx thì không gọi lại mà đối soát /orders theo id_product, đủ 3 phút mới kết luận "chưa mua". pending: true = đã trừ tiền, tài khoản về sau. Lưu dữ liệu giao ngay (bên này xoá sau 15 ngày, 03:30 giờ Việt Nam); bảo hành chỉ làm trên web, trong 48 giờ. Mục 11 là bản dặn việc cho AI/agent.

Build guide, version 2026-09-30 — generated on 2026-10-31. Site: https://acckit.com

This is the build guide: how to mirror our catalogue into your own shop, price it, sell it to your customers, deliver what they bought, and keep your books straight when something goes wrong. Every endpoint, field and error string is described in the Reseller API reference on https://acckit.com/api-docs; this guide assumes it and does not repeat it.

Written so that a developer or an AI coding agent can build straight from it. Section 11 is a paste-ready brief for an agent. Section 14 lists what changed on 30 September 2026 — read it if you integrated before that date.


1. What you are building

Your customers never talk to us. They see your website, your prices, your brand. Behind it:

your visitor → your shop → your server → https://acckit.com/api/v1 → our catalogue and stock
                               ↑
                     your api_key lives here, and only here

Two ways to run it. Pick one before you write code.

Dropship (recommended) Buy-ahead stock
When you call /buy After your customer pays you Before, in bulk
Your money at risk Only the order in progress Everything you pre-bought
Stock You buy what exists at that moment; a purchase can still fail or come back short Yours once bought
Delivery speed Usually seconds, up to about 90 s; some orders are delivered minutes later (§6.5) Instant, from your database
Our warranty (§7.4) 48 hours from the moment your customer buys 48 hours from the moment you stocked up
Main work Unknown outcomes (§6.6) and pending orders (§6.5) Inventory, ageing stock, accounts that stop working

Start with dropship. Move to buy-ahead only for products you sell constantly and that sell out quickly.


2. Before the first line of code

  1. Use a dedicated account for the shop. /orders lists every purchase of the account — including anything bought by hand on our website — and the reconciliation in §6.6 assumes that every order on the account came from your integration. Discounts are set per account by us, so ask for yours on the new account too.
  2. Get the key. Log in and open https://acckit.com/setting_api (the key is also on your Account page); copy api_key, or generate one if there is none. The same key works for the Reseller API, the OTP API and the SMM API. Changing it kills the old key immediately. It only works on https://acckit.com, the site where your account lives.
  3. Pick the catalogue language. On the same page, https://acckit.com/setting_api, choose "API language": Vietnamese, English, Indonesian, Portuguese (Brazil), Simplified Chinese or Thai. It changes name and desc in the catalogue and the product name shown in /orders and /order; your code does not change. A text that has no translation in your language comes in English, or else in the Vietnamese original. Until you choose, you get the site default. Run a full catalogue sync right after changing it.
  4. Top up. /buy charges your wallet. It checks your balance against the whole order (quantity × unit price) first; if it is not enough you get Insufficient balance! and nothing happens.
  5. Check that it is alive before building anything:
curl -s "https://acckit.com/api/v1/categories" -H "Accept: application/json" | head -c 400
curl -s -X POST "https://acckit.com/api/v1/balance" -H "Accept: application/json" \
     -d "api_key=YOUR_API_KEY"
# {"status":true,"balance":150000}

Rules that bite if you skip them:

  • Accept: application/json on every request. Without it a validation error comes back as an HTTP 302 redirect to an HTML page instead of an HTTP 422 JSON error, and your JSON parser reports something unrelated and confusing.
  • The key never leaves your server. No browser fetch, no mobile app, no NEXT_PUBLIC_* variable, no repository. Anyone with the key spends your wallet.
  • Refuse to start with an empty key. A missing or blank key never matches an account (you get Invalid api_key, or HTTP 422 on /buy, /orders and /order), but an unset environment variable quietly becomes an empty string and leaves your shop looking "up" while every purchase fails.

3. What the API gives you, and what it does not

Know the limits before you design the UI, not after.

You get

Data From Notes
Categories with an icon GET /categories icon is an absolute image URL, or null
Products nested in their category GET /categories The only place a product's category appears
The products of one category GET /category/{id} Same product objects, no category id
Name, description, list price, stock both catalogue endpoints Keys: id_product, name, desc, price, quantity
Wallet balance POST /balance VND
A purchase and its accounts POST /buy Accounts in the reply, unless the order is pending (§6.5)
One order again POST /order Unit price and total_price charged, id_product, the accounts
Order history POST /orders 10 per page, newest first, with id_product

You do not get — design around these

Missing What to do instead
A category id on product objects Build the catalogue from GET /categories and store the category id yourself (§4)
Product images Use the category icon, or your own artwork
Your discount or volume tiers in the listings Listings show the list price only; what you paid is in /order (§5.1)
The amount charged, in the /buy reply Call /order once after each purchase (§6.9)
Minimum order quantities Learn each one from the Minimum quantity is N error (§5.2)
A maximum-price parameter on /buy Re-read the price right before buying (§6.1)
Webhooks Poll: the catalogue on a schedule, orders after buying
An idempotency key on /buy Never resend after an unknown outcome; reconcile (§6.6)
Stock reservation Treat stock errors at checkout as normal (§6.7)
A "pending / delivered" status in /orders Read /order: an empty data on a recent order means pending (§6.5)
Warranty through the API Claims are made on our website (§7.4)
A sandbox or test mode Every call is real money; test with quantity=1 on the cheapest product
Any currency other than VND Convert for display only (§5.4)

4. Step 1 — mirror the catalogue

4.1 Read it from one endpoint

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

The key is optional on this endpoint; it only selects your API language.

{"data": [
  {"id": 211, "name": "Aged accounts", "icon": "https://acckit.com/assets/media/country/185-indonesia.png",
   "list_product": [
     {"id_product": 124, "name": "Aged account, 2FA", "desc": "uid|pass|2fa|mail|mailpass",
      "price": 42000, "quantity": 89},
     {"id_product": 131, "name": "Aged account, no 2FA", "desc": "Checked live before delivery",
      "price": 25000, "quantity": 7}
   ]},
  {"id": 310, "name": "Mail", "icon": null,
   "list_product": [
     {"id_product": 402, "name": "Mail account", "desc": "", "price": 15500, "quantity": 240}
   ]}
]}

Values are illustrative. Categories and products come in our display order.

GET /category/{id} returns {"data": [ ...product objects... ]} for one category. It is handy to refresh a price right before a purchase (§6.1), but its objects carry no category id, and it returns an empty list for a category that is hidden or has nothing on sale.

4.2 Store it on your side

Never call our API while rendering a page. Mirror into your database, render from your database. Minimum workable schema (MySQL flavour):

CREATE TABLE src_category (
  id            INT PRIMARY KEY,                   -- our category id
  name          VARCHAR(255) NOT NULL,
  icon_url      VARCHAR(512) NULL,
  position      INT NOT NULL DEFAULT 0,            -- order in which we returned it
  listed        TINYINT(1) NOT NULL DEFAULT 1,     -- 0 = absent from the last good sync
  seen_at       DATETIME NOT NULL
);

CREATE TABLE src_product (
  id            INT PRIMARY KEY,                   -- our id_product: the key you send to /buy
  category_id   INT NOT NULL,                      -- from the nesting in /categories; it can change
  name          VARCHAR(512) NOT NULL,
  descr         TEXT NULL,
  list_vnd      DECIMAL(14,2) NOT NULL,            -- our list price at the last sync
  stock         INT NOT NULL DEFAULT 0,            -- approximate (§5.2)
  listed        TINYINT(1) NOT NULL DEFAULT 1,     -- 0 = sold out, paused or withdrawn
  min_qty       INT NOT NULL DEFAULT 1,            -- learned from "Minimum quantity is N"
  sell_vnd      DECIMAL(14,2) NULL,                -- YOUR price to your customer
  price_locked  TINYINT(1) NOT NULL DEFAULT 0,     -- 1 = set by an operator, the sync leaves it alone
  seen_at       DATETIME NOT NULL,
  INDEX (category_id), INDEX (listed)
);

CREATE TABLE src_purchase (
  id            BIGINT AUTO_INCREMENT PRIMARY KEY,
  shop_order_id BIGINT NOT NULL,                   -- your customer's order
  id_product    INT NOT NULL,
  quantity      INT NOT NULL,
  expected_unit DECIMAL(14,2) NOT NULL,            -- list price you confirmed just before /buy
  sent_at       BIGINT NOT NULL,                   -- unix time, written BEFORE /buy is sent
  state         VARCHAR(16) NOT NULL,              -- sending, delivered, short, awaiting, unknown,
                                                   -- not_bought, failed, refunded
  order_id      BIGINT NULL UNIQUE,                -- our order id once known: one order, one purchase
  unit_vnd      DECIMAL(14,2) NULL,                -- price from /order
  total_vnd     DECIMAL(14,2) NULL,                -- total_price from /order
  charged_qty   INT NULL,                          -- quantity from /order
  last_msg      VARCHAR(255) NULL,
  INDEX (state), INDEX (id_product, state)
);

CREATE TABLE src_line (
  id            BIGINT AUTO_INCREMENT PRIMARY KEY,
  order_id      BIGINT NOT NULL,                   -- our order id
  uid           VARCHAR(255) NOT NULL,
  full_info     TEXT NOT NULL,                     -- encrypted at rest
  INDEX (order_id)
);

id_product is stable — use it as the primary key so that a re-sync is an upsert, never a duplicate.

4.3 The sync job

Run it every 5–10 minutes. More often buys you nothing — our own copy of partner prices and stock refreshes about every 5 minutes — and you share a 60 requests/minute budget with checkout traffic (§7.5).

1. r = GET /categories?api_key=KEY
2. If the call failed, the reply is not JSON, or r.data is missing or empty:
       log it, alert if it keeps happening, and STOP.
       Never un-list your catalogue because one request failed.
3. now = current time
4. For each category c, at position i:
       upsert src_category (id = c.id, name = c.name, icon_url = c.icon, position = i,
                            listed = 1, seen_at = now)
       For each product p in c.list_product:
           upsert src_product (id = p.id_product, category_id = c.id,     ← every run: products move
                               name = p.name, descr = p.desc, list_vnd = p.price,
                               stock = p.quantity, listed = (p.quantity > 0), seen_at = now)
           new product                    → sell_vnd = your pricing rule (§5.1)
           existing, priced by your rule  → recompute sell_vnd from the new list_vnd
           existing, price_locked         → keep sell_vnd; alert if list_vnd has come close to it
5. Every product and every category with seen_at < now → listed = 0.   NEVER DELETE.

Step 5 is normal business, not an error. Products leave the listing when they sell out or are paused, and come back later. If you delete the row you lose your price, your slug, your SEO and every order that pointed at it. Un-list it; delete never.

4.4 What changes under you

  • Sold out means gone. A product that sells out disappears from both catalogue endpoints — immediately when the last unit sells through us, otherwise within minutes — instead of showing quantity: 0. A category with nothing left on sale disappears too. Both come back by themselves when restocked. If you ever do see quantity: 0, treat it as sold out.
  • Paused means gone. The same happens when we take a product off sale, for example while a delivery problem is being fixed. /buy refuses anything the listings do not show (Product is not for sale right now).
  • Products move between categories. Update category_id on every full sync, and never un-list a product just because it is missing from one /category/{id} response.
  • Names and descriptions change — when translations are added, or when a product is renamed. Key everything on id_product, never on names.
  • List prices change. Prices of products we source from partners are refreshed about every 5 minutes (§5.1).
  • Stock is approximate for products we source from partners (§5.2).

5. Step 2 — put it on your site

5.1 Your price, and what we charge you

price in the listings is our list price. What we actually charge per unit is worked out at the moment of /buy:

  1. Start from the list price.
  2. If your quantity reaches a volume tier of that product, use the tier price — the lowest price among the tiers your quantity reaches (normally the tier with the highest threshold not above your quantity), so buying more is never dearer per unit. A tier is never dearer than the list price.
  3. Subtract your account discount: a percentage we set per account, which you cannot change. On some products the discount is capped.

It is the same calculation on our website and through the API, and the unit price charged is never above the list price at the moment you buy. The listings never show tiers or your discount; to see them, open the product's Buy dialog on our website while logged in with the same account. After buying, the unit price actually charged is price in /order and /orders, and the amount charged for the order is total_price.

Pricing safely:

  • Put your margin on the list price. Because you are never charged more than the list price of the moment, a sell price above it keeps its margin even with no discount at all.
  • Re-read the list price right before buying (§6.1). Partner-sourced prices can change every few minutes, /buy charges the price of that moment, and there is no maximum-price parameter.
  • Record what you paid from /order after every purchase (§6.9) and compare it with what you expected.

Common rules — pick one and keep it in one place in your code:

percentage   sell = ceil(list * 1.20 / 1000) * 1000    -- +20 %, rounded up to 1,000 VND
fixed        sell = list + 15000
tiered       cheap items +30 %, expensive items +12 %
manual       an operator sets sell per product; flag it (price_locked) so the sync never touches it

Orders placed before 30 September 2026 were priced differently: a volume tier replaced your discount instead of combining with it.

5.2 Stock

quantity changes while your page is open. Treat it as advice, not a promise.

  • For our own stock it is an exact live count. For products we source from partners it is our copy, refreshed about every 5 minutes and lowered as we sell, so /buy can still fail (Product temporarily unavailable, …) while quantity is positive.
  • /buy refuses more than the listed stock (Quantity exceeds available stock).
  • Some products have a minimum order quantity, which the listings do not show. /buy then answers Minimum quantity is N: store N for that product (min_qty) and enforce it from then on.

A display that works:

  • quantity > 20 → "In stock"
  • 1 … 20 → show the number; it creates urgency and it is honest
  • not listed → "Out of stock": keep the page up for SEO, hide the Buy button, and refuse the purchase on your server, not only in the UI

5.3 The description field

desc is a short free-text note written for a human buyer. Sometimes it lists the field order of the delivered line (uid|pass|2fa|mail|mailpass), often it says something else entirely (Checked live before delivery), it can be empty, and it can change at any sync. It is not a format contract. Show it on your product page as plain text; for handling the delivered lines see §6.8.

5.4 Currency and numbers

Every amount is VND. Parse amounts as decimal numbers: they are normally whole numbers but can be sent as, for example, 44100.0. If your shop charges in another currency, convert for display and for charging your customer only; keep VND as the authoritative number, store the rate you used with each order, and never round in a way that makes you sell below what you paid.

price in /order is the unit price. Since 30 September 2026 it is a whole number of VND (rounded up) and total_price = price × quantity; older orders can differ by a few VND. Treat total_price as the amount charged.

5.5 Language

Set once on https://acckit.com/setting_api (§2). One response carries one language. If you run a multilingual shop, sync in your primary language and translate the rest on your side.


6. Step 3 — the purchase flow

6.1 Order of operations

Money out before goods in is how resellers lose money. This order matters:

1. Customer clicks Buy on your site.
2. YOUR server checks: product listed; quantity ≥ its known minimum and ≤ stock;
   no other purchase of this product in flight or unresolved (if there is: queue this one,
   or ask the customer to try again in a few minutes).
3. Refresh: GET /category/{category_id} and find the product in it.
   Not there → GET /categories.  Not there either → "no longer available", stop.
   Fresh price above the list price you priced from → re-price and ask the customer to
   confirm, or absorb it if your margin allows.
4. Take or authorise your customer's payment.
5. Write the purchase row: id_product, quantity, expected unit price,
   sent_at = now, state = "sending".                  ← BEFORE calling /buy
6. POST /buy with a 120-second timeout (§6.4).
7. Act on the outcome (§6.3).
8. Whenever an order exists: POST /order once and store price and total_price (§6.9).

One /buy per product at a time. Keep at most one purchase of a given product in flight, and none while an earlier one is still unresolved (§6.6): two unknown outcomes for the same product cannot be told apart. Different products can be bought in parallel.

A purchase still in state "sending" a few minutes later — your process died mid-call — is an unknown outcome too.

6.2 The call

curl -X POST "https://acckit.com/api/v1/buy" -H "Accept: application/json" \
  -d "api_key=YOUR_API_KEY" -d "id_product=124" -d "quantity=2"
{"status": true, "order_id": 12345,
 "data": [
   {"buy_id": 12345, "uid": "1000123", "full_info": "1000123|Pass1|2FASECRET1|[email protected]|MailPass1", "type": 124},
   {"buy_id": 12345, "uid": "1000456", "full_info": "1000456|Pass2|2FASECRET2|[email protected]|MailPass2", "type": 124}
 ]}
  • quantity must be a whole number, 1 or more. A decimal such as 2.5 gets HTTP 422.
  • order_id is your receipt; buy_id repeats it on every line.
  • In /buy lines, type is the product id. In /orders and /order the key type holds the product name, so use id_product there.
  • The reply does not say how much you were charged — see §6.9.

6.3 The possible outcomes

What comes back Meaning Charged What you do
HTTP 200, status: true, as many lines as you ordered Delivered Yes Store the lines, then fulfil
HTTP 200, status: true, fewer lines than you ordered Short delivery Normally only for the lines received; check (§6.9) Fulfil what came, refund your customer the rest
HTTP 200, status: true, pending: true, empty data Order placed, accounts come later Yes, the full quantity §6.5
HTTP 200, status: false Refused; no order exists No Map msg (§6.7)
HTTP 429 Rate limited; not processed No Wait Retry-After seconds; you may send it again
Any other HTTP 4xx, such as 422 Invalid request; not processed No It is your bug: fix it
Timeout, network error, HTTP 5xx, or a reply that is not JSON Unknown Maybe Never resend; reconcile (§6.6)

Resending /buy is safe only when you know the previous attempt bought nothing: status: false, an HTTP 4xx, or a reconciliation that concluded "not bought".

6.4 Timeouts

Give /buy 120 seconds (and about 10 seconds to connect). Our own stock answers in about a second and most partner-sourced products within seconds, but a purchase can legitimately take up to about 90 seconds: we wait up to 60 seconds for a partner, and for some products up to about half a minute more for the accounts to be ready. A short client timeout turns purchases that succeeded — or that would have failed cleanly after 60 seconds with System error! — into unknown outcomes.

The purchase runs to completion on our side even if your client gives up. A proxy in front of our server may also give up before we answer and send you an HTTP 5xx error page (HTML, not JSON); that is an unknown outcome too.

For every other call, 30 seconds is plenty.

6.5 Pending orders — charged, accounts come later

For some products the accounts are prepared after the order is placed. If they are not ready while /buy waits, you get:

{"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."}
  • You have been charged for the full quantity, and the order exists.
  • Store order_id at once, mark the purchase "awaiting", and tell your customer the order is being prepared.
  • Poll POST /order every 1–2 minutes for the first hour, then every 10 minutes. When data fills, deliver as usual (§6.8) and run the short-delivery check (§6.9).
  • We retry automatically every minute for 24 hours. Still empty after that: contact support with the order_id.
  • If /order starts answering Order not found, the order was refunded and the money is back in your wallet (§7.3). Refund your customer.

An empty data in /order means "pending" only for a recent order. For an order older than the retention period (§7.2) it means the lines have been purged.

6.6 Reconciliation — when the outcome is unknown

There is no idempotency key: resending /buy after a timeout can buy twice and charge twice. When the outcome is unknown, run this from a background job, not while your customer waits:

Every minute, for each purchase in state "unknown" (or "sending" for more than 5 minutes):
  1. Read POST /orders?page=1, 2, … (newest first, 10 per page) until the oldest order on the page
     was created more than 2 minutes before sent_at, or you reach meta.last_page.
  2. Candidates are orders with
        id_product == the product you tried to buy,
        created_at ≥ sent_at − 2 minutes        (allows for clock differences),
        quantity ≤ the quantity you asked for   (a short delivery records fewer),
        an id that is not already in your database.
  3. Exactly one candidate → attach it (the unique key on order_id stops it being attached twice),
     then treat it like a normal result: POST /order, store the lines — an empty data means
     pending (§6.5).
  4. More than one candidate → stop and alert a human. Do not guess.
  5. None, and at least 3 minutes have passed since sent_at → the purchase did not happen:
     state "not_bought", refund or release your customer's payment. A new /buy is now safe.
  6. None yet → check again at the next run.

Why 3 minutes: an order only shows up in /orders once the purchase has finished, which can be about 90 seconds after we receive the request. Checking after a few seconds and concluding "not bought" is exactly how resellers end up paying twice.

created_at is an ISO-8601 timestamp in UTC, for example 2026-09-30T08:15:30.000000Z.

One more sweep catches the rest. Every 10 minutes, read the first page or two of /orders and check that every order older than 5 minutes is in your database. On a dedicated account, an unknown id is a purchase you were charged for but never recorded — your server crashed between our reply and your database write, or an order finished after you had concluded "not bought". Attach it to the matching purchase (same id_product, within the last 24 hours) or alert a human. The accounts are yours either way.

6.7 Mapping our errors to your customer

Every business failure is HTTP 200 with status: false. Nothing was charged and no order exists. Never show our raw string to a buyer.

msg from us What happened Show your customer Your side
Invalid api_key Key wrong, changed, or from another site "Temporarily unavailable" Alert yourself: the shop is down until you fix it
Product ID does not exist No such product (deleted, or you sent a category id) "No longer available" Un-list it; run a sync
Product is not for sale right now The product exists but is not listed now: sold out or paused "Out of stock" Un-list it until a sync lists it again
Minimum quantity must be at least 1 quantity was 0 or negative Nothing: it is your bug Validate quantity ≥ 1
Minimum quantity is N Below this product's minimum "Minimum order is N" Store N, enforce it
Quantity exceeds available stock More than the listed stock "Only X left — order fewer" Run a sync, offer fewer
Insufficient balance! Your wallet cannot cover quantity × unit price "Temporarily unavailable" Top up; alert loudly (§7.1)
Product temporarily unavailable, please try again later or contact support. It could not be delivered right now "Out of stock, try again later" Un-list it until a sync lists it again
System error! Unexpected failure "Try again in a few minutes" Log it; retrying later is safe

HTTP 422 means your request was malformed — your bug, not a customer problem. HTTP 429 means you went over the rate limit (§7.5).

6.8 Delivering to your customer

Each line of data is one account: uid (its identifier, usually the first field of full_info) and full_info (the whole account on one line, usually with fields separated by |).

  • Always give your customer the raw full_info line, and a download as .txt button. They will want to keep it.
  • Split a line into named fields only for products whose layout you have checked on a real delivery, and keep that mapping per product on your side. Do not derive it from desc (§5.3), do not assume a field count, and fall back to the raw line whenever a line does not fit.
  • /order lines also carry link_backup. Never show it to your customers: it points to our website, only works in a browser logged in to your account, and answers 404 for most products.
  • Treat the lines as credentials: encrypt them at rest, and keep them out of logs, URLs and plain-text e-mail.
// Only for a product whose layout you verified on a real delivery:
$layout = ['uid', 'password', '2fa', 'mail', 'mail_password'];   // your mapping for this product
$fields = explode('|', $line['full_info']);
$parsed = count($fields) === count($layout) ? array_combine($layout, $fields) : null;   // null → show the raw line

6.9 Record what you paid

The /buy reply has no price. After every purchase that created an order — delivered, short, pending, or found by reconciliation — call POST /order once and store:

  • price (the unit price charged) and total_price (the amount charged for the order, §5.4);
  • quantity — the number of accounts you were charged for.

Then run the short-delivery check: if the order is not pending and data has fewer lines than quantity, you were charged for accounts you did not receive — contact support with the order_id. Normally a short delivery records only the lines delivered, so quantity equals the number of lines and you paid only for those.

/order returns the same uid and full_info as /buy; its lines have no buy_id or type, but they add link_backup (§6.8).


7. Step 4 — running it

7.1 Watch your balance

A wallet that hits zero looks to your customers like a broken website.

Every 10 minutes:  POST /balance
If balance < (your busiest hour of sales) × 3   → alert a human

Alert through something a human actually reads — Telegram, SMS, PagerDuty. An e-mail nobody opens is not an alert.

7.2 Retention — back the data up yourself

We delete the delivered account lines of every order older than the retention period — currently 15 days — in a job that runs every day at 03:30 Vietnam time (UTC+7), which is 20:30 UTC the day before. The lines therefore stay available for 15 to 16 days after purchase. We may change the period (never to less than 3 days). The order itself remains: /order then returns it with an empty data array.

Store every delivered line in your own database the moment you receive it. /order is not your archive.

7.3 Refunds

If we refund an order — for example a pending order that was never delivered — its total_price goes back to your wallet and the order disappears from the API: /order answers Order not found and /orders stops listing it. Order not found for an order_id you stored means exactly that: check your balance and refund your customer.

7.4 Warranty — on our website only

  • Warranty claims are made on our website, never through the API. Log in with the account that bought, open the order from your order history, select the accounts concerned, give a reason and attach 1 to 5 screenshots.
  • You can claim within 48 hours of the purchase, once per account.
  • We answer with one of: a refund of the unit price you paid, to your wallet (the order stays as it is in /orders); a replacement account, shown only in the claim details on the website ("My warranty claims") and never added to /order — pass it on to your customer yourself; or a rejection, with the reason and evidence.
  • Offer your own customers a shorter warranty than 48 hours (24 hours, say) so that you still have time to claim with us. For buy-ahead stock, our 48 hours run from when you bought, not from when your customer does.

7.5 Rate limits and backoff

  • 60 requests per minute per IP address, one counter shared by every API call from that address except the OTP API — so Reseller API and SMM API calls count together. The OTP API has its own budget of 240 per minute per key.
  • Over the limit you get HTTP 429 with {"message": "Too Many Attempts."} and a Retry-After header in seconds. Wait that long (never more than 60 seconds) before the next call. Normal replies carry X-RateLimit-Limit and X-RateLimit-Remaining: slow down before the second one reaches zero.
  • A 429 means the request was not processed, so sending it again later is safe — even for /buy.
  • On HTTP 5xx from a read-only call (/categories, /category, /orders, /order, /balance), back off 2, 4, 8, 16 seconds, then alert. Never resend /buy after a 5xx — reconcile (§6.6).
  • Several servers behind one public IP share one budget. Send all API calls through a single worker.

A rough budget:

Job Requests
Catalogue sync, every 5–10 minutes 1 per run
Balance alarm, every 10 minutes 1 per run
Each checkout About 3: price refresh, /buy, /order
Each pending order 1 per poll
Each unknown outcome 1–2 per check (pages of /orders)

7.6 Logging

Log every request and response with a correlation id and your purchase id, with api_key and full_info redacted. When an order goes wrong, being able to show what we sent you and when is what saves you.


8. Worked example — PHP 8.0+

A client that classifies every reply the way §6.3 needs. It needs ext-curl.

<?php
// PHP 8.0+ with ext-curl.

final class ApiUnknown extends RuntimeException {}    // timeout, network error, HTTP 5xx, non-JSON: outcome unknown
final class ApiRejected extends RuntimeException {}   // HTTP 4xx other than 429: not processed
final class ApiRateLimited extends RuntimeException   // HTTP 429: not processed
{
    public function __construct(public int $retryAfter)
    {
        parent::__construct('rate limited');
    }
}

final class ResellerApi
{
    private const BASE = 'https://acckit.com/api/v1';
    private string $key;

    public function __construct(string $key)
    {
        $this->key = trim($key);
        if ($this->key === '') {
            throw new InvalidArgumentException('API key is empty - refusing to start');
        }
    }

    /** GET when $body is null, POST otherwise. Returns the decoded JSON of a non-error reply. */
    private function call(string $path, ?array $body = null, int $timeout = 30): array
    {
        $headers = [];
        $ch = curl_init(self::BASE . $path);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT        => $timeout,
            CURLOPT_HTTPHEADER     => ['Accept: application/json'],
            CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$headers): int {
                $kv = explode(':', $line, 2);
                if (count($kv) === 2) {
                    $headers[strtolower(trim($kv[0]))] = trim($kv[1]);
                }
                return strlen($line);
            },
        ]);
        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($body + ['api_key' => $this->key]));
        }
        $raw  = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);

        if ($raw === false) {
            throw new ApiUnknown('network: ' . curl_error($ch));
        }
        if ($code === 429) {
            throw new ApiRateLimited(max(1, (int) ($headers['retry-after'] ?? 60)));
        }
        if ($code >= 500) {
            throw new ApiUnknown("HTTP $code");
        }
        if ($code >= 400) {
            throw new ApiRejected("HTTP $code: " . substr($raw, 0, 300));
        }
        $json = json_decode($raw, true);
        if (!is_array($json)) {
            throw new ApiUnknown("non-JSON reply (HTTP $code)");
        }
        return $json;
    }

    public function categories(): array
    {
        return $this->call('/categories?api_key=' . urlencode($this->key))['data'] ?? [];
    }

    public function category(int $categoryId): array
    {
        return $this->call("/category/$categoryId?api_key=" . urlencode($this->key))['data'] ?? [];
    }

    public function balance(): float
    {
        $r = $this->call('/balance', []);
        if (empty($r['status'])) {
            throw new RuntimeException('balance: ' . ($r['msg'] ?? 'unexpected reply'));
        }
        return (float) $r['balance'];
    }

    /** The order, or null when it does not exist (it never did, or it was refunded). */
    public function order(int $orderId): ?array
    {
        $r = $this->call('/order', ['order_id' => $orderId]);
        if (isset($r['data']['id'])) {
            return $r['data'];
        }
        if (($r['msg'] ?? '') === 'Order not found') {
            return null;
        }
        throw new RuntimeException('order: ' . ($r['msg'] ?? 'unexpected reply'));
    }

    /** One page of /orders: ['data' => up to 10 orders, newest first, 'meta' => [...]]. */
    public function orders(int $page = 1): array
    {
        $r = $this->call("/orders?page=$page", []);
        if (!isset($r['data']) || !is_array($r['data'])) {
            throw new RuntimeException('orders: ' . ($r['msg'] ?? 'unexpected reply'));
        }
        return $r;
    }

    /**
     * One purchase attempt. state: delivered | short | awaiting | failed | not_sent | unknown.
     * Only 'failed' and 'not_sent' mean that nothing was bought; 'unknown' must be reconciled.
     */
    public function buy(int $idProduct, int $qty): array
    {
        try {
            $r = $this->call('/buy', ['id_product' => $idProduct, 'quantity' => $qty], 120);
        } catch (ApiRateLimited $e) {
            return ['state' => 'not_sent', 'retry_after' => $e->retryAfter];
        } catch (ApiRejected $e) {
            return ['state' => 'failed', 'msg' => $e->getMessage()];
        } catch (ApiUnknown $e) {
            return ['state' => 'unknown', 'msg' => $e->getMessage()];   // never resend: reconcile (§6.6)
        }
        if (empty($r['status'])) {
            return ['state' => 'failed', 'msg' => (string) ($r['msg'] ?? 'unknown error')];
        }
        $orderId = (int) $r['order_id'];
        if (!empty($r['pending'])) {
            return ['state' => 'awaiting', 'order_id' => $orderId, 'lines' => []];
        }
        $lines = is_array($r['data'] ?? null) ? $r['data'] : [];
        return ['state' => count($lines) < $qty ? 'short' : 'delivered', 'order_id' => $orderId, 'lines' => $lines];
    }

    /**
     * Unknown outcome: look for the order that /buy may have created (§6.6).
     * $sentAt  unix time stored just BEFORE sending /buy
     * $isKnown function (int $orderId): bool — is this order id already in your database?
     * Returns the order summary, or null if there is none (yet).
     */
    public function findOrder(int $idProduct, int $qty, int $sentAt, callable $isKnown): ?array
    {
        $since = $sentAt - 120;                                        // allow for clock differences
        $hits  = [];
        for ($page = 1; $page <= 10; $page++) {
            $r = $this->orders($page);
            foreach ($r['data'] as $o) {
                $t = strtotime((string) $o['created_at']);
                if ($t === false || $t < $since) continue;
                if ((int) ($o['id_product'] ?? 0) !== $idProduct) continue;
                if ((int) $o['quantity'] > $qty) continue;             // a short delivery records fewer
                if ($isKnown((int) $o['id'])) continue;
                $hits[] = $o;
            }
            $last = end($r['data']);
            if ($last === false || strtotime((string) $last['created_at']) < $since
                || $page >= (int) ($r['meta']['last_page'] ?? 1)) {
                break;
            }
        }
        if (count($hits) > 1) {
            throw new RuntimeException('Several candidate orders - resolve by hand');
        }
        return $hits[0] ?? null;
    }
}

Usage. Every function that is not a method of ResellerApi is your own code.

$api = new ResellerApi((string) getenv('RESELLER_API_KEY'));    // throws if the key is missing or blank

// 1) Catalogue sync — cron every 5-10 minutes (§4.3)
$cats = $api->categories();
if (!$cats) {
    exit("empty catalogue reply - keeping the previous catalogue\n");
}
$now = date('Y-m-d H:i:s');
foreach ($cats as $pos => $cat) {
    upsertCategory($cat['id'], $cat['name'], $cat['icon'], $pos, $now);
    foreach ($cat['list_product'] as $p) {
        upsertProduct($p['id_product'], $cat['id'], $p['name'], $p['desc'], $p['price'], $p['quantity'], $now);
    }
}
unlistNotSeenSince($now);                                        // listed = 0, never delete

// 2) Checkout — after the checks, the price refresh and the payment of §6.1
$purchaseId = insertPurchase($shopOrderId, 124, 2, time());     // state 'sending', sent_at BEFORE /buy
$res = $api->buy(124, 2);
match ($res['state']) {
    'delivered', 'short' => saveLinesThenFulfil($purchaseId, $res['order_id'], $res['lines']),
    'awaiting'           => markAwaiting($purchaseId, $res['order_id']),
    'unknown'            => markUnknown($purchaseId),            // job 3 settles it
    default              => markFailedAndRefund($purchaseId, $res['msg'] ?? 'rate limited, nothing bought'),
};

// 3) Unknown outcomes — cron every minute (§6.6)
foreach (purchasesToReconcile() as $p) {                         // 'unknown', or 'sending' for > 5 minutes
    $o = $api->findOrder($p['id_product'], $p['quantity'], $p['sent_at'], 'isKnownOrderId');
    if ($o !== null) {
        attachOrder($p['id'], (int) $o['id']);                   // unique key on order_id
    } elseif (time() >= $p['sent_at'] + 180) {
        markNotBoughtAndRefund($p['id']);                        // the 10-minute sweep keeps watching
    }
}

// 4) Orders not priced yet, or still pending — cron every 2 minutes (§6.5, §6.9)
foreach (purchasesWithOpenOrder() as $p) {
    $o = $api->order($p['order_id']);
    if ($o === null) {
        markRefunded($p['id']);                                  // refunded: the money is back in your wallet
        continue;
    }
    savePrice($p['id'], $o['price'], $o['total_price'], $o['quantity']);
    if (count($o['data']) === 0) {
        continue;                                                // still pending; alert a human after 24 h
    }
    saveLines($p['id'], $o['data']);                             // skips lines you already have
    if (count($o['data']) < (int) $o['quantity']) {
        alertHuman("Order {$p['order_id']}: charged for {$o['quantity']}, received " . count($o['data']));
    }
}

9. Worked example — Node.js 18+

An ES module (save it as .mjs, or set "type": "module"). The jobs are the same as in §8.

// Node.js 18+ (global fetch), ES module.
const BASE = 'https://acckit.com/api/v1';
const KEY = (process.env.RESELLER_API_KEY ?? '').trim();
if (!KEY) throw new Error('RESELLER_API_KEY is empty - refusing to start');
const Q = `api_key=${encodeURIComponent(KEY)}`;

export class Unknown extends Error {}    // timeout, network error, HTTP 5xx, non-JSON: outcome unknown
export class Rejected extends Error {}   // HTTP 4xx other than 429: not processed
export class RateLimited extends Error { // HTTP 429: not processed
  constructor(retryAfter) { super('rate limited'); this.retryAfter = retryAfter; }
}

async function call(path, body = null, ms = 30000) {
  let res;
  try {
    res = await fetch(BASE + path, {
      method: body ? 'POST' : 'GET',
      headers: { Accept: 'application/json' },
      body: body ? new URLSearchParams({ ...body, api_key: KEY }) : undefined,
      signal: AbortSignal.timeout(ms),
    });
  } catch (e) { throw new Unknown(String(e)); }
  if (res.status === 429) throw new RateLimited(Number(res.headers.get('retry-after')) || 60);
  if (res.status >= 500) throw new Unknown(`HTTP ${res.status}`);
  if (res.status >= 400) throw new Rejected(`HTTP ${res.status}: ${(await res.text().catch(() => '')).slice(0, 300)}`);
  try { return await res.json(); } catch { throw new Unknown(`non-JSON reply (HTTP ${res.status})`); }
}

export const categories = async () => (await call(`/categories?${Q}`)).data ?? [];
export const category = async (id) => (await call(`/category/${Number(id)}?${Q}`)).data ?? [];

export async function balance() {
  const r = await call('/balance', {});
  if (!r.status) throw new Error(`balance: ${r.msg}`);
  return Number(r.balance);
}

export async function getOrder(orderId) {   // null = never existed, or refunded
  const r = await call('/order', { order_id: orderId });
  if (r.data && r.data.id !== undefined) return r.data;
  if (r.msg === 'Order not found') return null;
  throw new Error(`order: ${r.msg}`);
}

export async function listOrders(page = 1) {
  const r = await call(`/orders?page=${Number(page)}`, {});
  if (!Array.isArray(r.data)) throw new Error(`orders: ${r.msg}`);
  return r;
}

export async function buy(idProduct, quantity) {
  let r;
  try {
    r = await call('/buy', { id_product: idProduct, quantity }, 120000);
  } catch (e) {
    if (e instanceof RateLimited) return { state: 'not_sent', retryAfter: e.retryAfter };
    if (e instanceof Rejected) return { state: 'failed', msg: e.message };
    if (e instanceof Unknown) return { state: 'unknown', msg: e.message };   // never resend: reconcile
    throw e;
  }
  if (!r.status) return { state: 'failed', msg: r.msg ?? 'unknown error' };
  const orderId = Number(r.order_id);
  if (r.pending) return { state: 'awaiting', orderId, lines: [] };
  const lines = Array.isArray(r.data) ? r.data : [];
  return { state: lines.length < quantity ? 'short' : 'delivered', orderId, lines };
}

// sentAtMs: Date.now() stored just BEFORE /buy.  isKnown(orderId): already in your database?
export async function findOrder(idProduct, quantity, sentAtMs, isKnown) {
  const since = sentAtMs - 120000;
  const hits = [];
  for (let page = 1; page <= 10; page++) {
    const r = await listOrders(page);
    for (const o of r.data) {
      if (!(Date.parse(o.created_at) >= since)) continue;
      if (Number(o.id_product) !== idProduct || Number(o.quantity) > quantity) continue;
      if (await isKnown(Number(o.id))) continue;
      hits.push(o);
    }
    const last = r.data.at(-1);
    if (!last || !(Date.parse(last.created_at) >= since) || page >= Number(r.meta?.last_page ?? 1)) break;
  }
  if (hits.length > 1) throw new Error('several candidate orders - resolve by hand');
  return hits[0] ?? null;
}

10. Worked example — Python 3.8+

Uses requests. The jobs are the same as in §8.

# Python 3.8+ — pip install requests
import os
from datetime import datetime

import requests

BASE = "https://acckit.com/api/v1"
KEY = os.environ.get("RESELLER_API_KEY", "").strip()
if not KEY:
    raise SystemExit("RESELLER_API_KEY is empty - refusing to start")
HEADERS = {"Accept": "application/json"}


class Unknown(Exception):
    """Timeout, network error, HTTP 5xx or non-JSON reply: the outcome is unknown."""


class Rejected(Exception):
    """HTTP 4xx other than 429: the request was not processed."""


class RateLimited(Exception):
    """HTTP 429: not processed; wait retry_after seconds."""

    def __init__(self, retry_after):
        super().__init__("rate limited")
        self.retry_after = retry_after


def _call(path, data=None, timeout=30):
    try:
        if data is None:
            r = requests.get(BASE + path, params={"api_key": KEY}, headers=HEADERS, timeout=(10, timeout))
        else:
            r = requests.post(BASE + path, data={**data, "api_key": KEY}, headers=HEADERS, timeout=(10, timeout))
    except requests.RequestException as e:
        raise Unknown(str(e))
    if r.status_code == 429:
        retry = r.headers.get("Retry-After", "60")
        raise RateLimited(int(retry) if retry.isdigit() else 60)
    if r.status_code >= 500:
        raise Unknown(f"HTTP {r.status_code}")
    if r.status_code >= 400:
        raise Rejected(f"HTTP {r.status_code}: {r.text[:300]}")
    try:
        return r.json()
    except ValueError:
        raise Unknown(f"non-JSON reply (HTTP {r.status_code})")


def categories():
    return _call("/categories").get("data", [])


def category(category_id):
    return _call(f"/category/{int(category_id)}").get("data", [])


def balance():
    r = _call("/balance", {})
    if not r.get("status"):
        raise RuntimeError(f"balance: {r.get('msg')}")
    return float(r["balance"])


def get_order(order_id):
    """The order, or None when it does not exist (it never did, or it was refunded)."""
    r = _call("/order", {"order_id": order_id})
    if isinstance(r.get("data"), dict) and "id" in r["data"]:
        return r["data"]
    if r.get("msg") == "Order not found":
        return None
    raise RuntimeError(f"order: {r.get('msg')}")


def list_orders(page=1):
    r = _call(f"/orders?page={int(page)}", {})
    if not isinstance(r.get("data"), list):
        raise RuntimeError(f"orders: {r.get('msg')}")
    return r


def buy(id_product, quantity):
    try:
        r = _call("/buy", {"id_product": id_product, "quantity": quantity}, timeout=120)
    except RateLimited as e:
        return {"state": "not_sent", "retry_after": e.retry_after}
    except Rejected as e:
        return {"state": "failed", "msg": str(e)}
    except Unknown as e:
        return {"state": "unknown", "msg": str(e)}  # never resend: reconcile (6.6)
    if not r.get("status"):
        return {"state": "failed", "msg": r.get("msg", "unknown error")}
    order_id = int(r["order_id"])
    if r.get("pending"):
        return {"state": "awaiting", "order_id": order_id, "lines": []}
    lines = r.get("data") or []
    return {"state": "short" if len(lines) < quantity else "delivered", "order_id": order_id, "lines": lines}


def _ts(value):
    return datetime.fromisoformat(str(value).replace("Z", "+00:00")).timestamp()


def find_order(id_product, quantity, sent_at, is_known):
    """sent_at: unix time stored just BEFORE /buy; is_known(order_id) -> True if already in your database."""
    since = sent_at - 120
    hits = []
    for page in range(1, 11):
        r = list_orders(page)
        items = r["data"]
        for o in items:
            if _ts(o["created_at"]) < since or int(o.get("id_product") or 0) != id_product:
                continue
            if int(o["quantity"]) > quantity or is_known(int(o["id"])):
                continue
            hits.append(o)
        if not items or _ts(items[-1]["created_at"]) < since or page >= int(r.get("meta", {}).get("last_page", 1)):
            break
    if len(hits) > 1:
        raise RuntimeError("several candidate orders - resolve by hand")
    return hits[0] if hits else None

11. Brief for an AI coding agent

Paste this verbatim into the agent, together with this file and the Reseller API reference.

You are integrating the Reseller API of https://acckit.com into an existing e-commerce site so that the site can resell these products under its own brand.

Base URL https://acckit.com/api/v1 · Auth an api_key form or query field on every call · Header Accept: application/json on every call · Money VND, parsed as decimals · Rate limit 60 requests/minute per IP, shared with the SMM API.

Build, in this order:

  1. A server-side API client. api_key comes from an environment variable; refuse to start if it is empty. Timeouts: 120 s for POST /buy, 30 s for everything else. Classify every reply: HTTP 2xx with JSON → use it; HTTP 429 → not processed, wait Retry-After seconds; any other HTTP 4xx → not processed, a bug to fix; timeout, network error, HTTP 5xx or a body that is not JSON → outcome unknown. Never log api_key or full_info.
  2. Tables: categories; products keyed by our id_product, with a category_id you fill from the nesting in GET /categories (and update on every sync), the list price, stock, a listed flag, min_qty, and your own price; purchases (id_product, quantity, expected unit price, sent_at, state, our order_id under a unique key, price, total_price); delivered lines, encrypted at rest.
  3. A catalogue sync every 5–10 minutes, from GET /categories only. A failed, non-JSON or empty reply changes nothing. Upsert everything seen with listed = 1; everything not seen gets listed = 0. Never delete. Sold-out products disappear from the listing instead of showing quantity 0, and come back later; products can change category; names and desc can change.
  4. Catalogue and product pages rendered from your own tables, never from a live API call. Your price is a margin on our list price (the unit price we charge is never above the list price at the moment of purchase). Never show our prices.
  5. A checkout that: checks the product is listed, quantity ≥ min_qty and ≤ stock, and no other purchase of this product is in flight or unresolved → refreshes the product with GET /category/{category_id} (fallback GET /categories; missing → unavailable; price above the one you priced from → re-price or confirm) → takes the customer's money → stores the purchase with sent_at and state sending → calls POST /buy (id_product, a whole-number quantity).
  6. Reply handling: status:true with lines → store every line, then deliver; fewer lines than ordered → deliver those, refund the rest; pending:true → charged in full, no lines yet, state awaiting; status:false → nothing charged, refund, show a mapped message (on Minimum quantity is N store N); HTTP 4xx → nothing charged; unknown → state unknown, and never resend POST /buy.
  7. A job every minute for purchases in state unknown (or sending for more than 5 minutes): read POST /orders?page=1,2,… (10 per page, newest first) back to sent_at − 2 min. A candidate has the same id_product, created_at (ISO-8601 UTC) ≥ sent_at − 2 min, quantity ≤ the requested quantity, and an id not yet in your database. One candidate → attach it; several → alert a human; none and at least 3 minutes since sent_at → not bought, refund. In /orders and /order, type is the product NAME; match on id_product.
  8. A job every 2 minutes for purchases that have an order_id but no recorded price, or are awaiting: call POST /order; store price, total_price, quantity and the lines. Order not found → refunded (money back in the wallet); empty data → still pending (alert a human after 24 hours); fewer lines than quantity → alert a human (contact support).
  9. A sweep every 10 minutes: every order older than 5 minutes on the first pages of /orders must be in your database; otherwise attach it to the matching purchase or alert a human.
  10. A balance alarm: POST /balance every 10 minutes; alert a human when the balance falls below three times your busiest hour of sales.

Constraints you must respect:

  • Use a dedicated account for the shop: /orders also lists purchases made on the website.
  • At most one POST /buy per product in flight.
  • Stay under 60 requests per minute in total; on HTTP 429 wait Retry-After.
  • desc is free text, not a field layout. Deliver full_info raw; split it only with per-product layouts verified on a real delivery; never show link_backup to customers.
  • Copy every delivered line into your own storage immediately: ours are deleted after about 15 days (daily at 03:30 Vietnam time, 20:30 UTC).
  • Warranty claims (within 48 hours of purchase) are made on the website only, and replacements never appear in /order.
  • There is no sandbox: every call spends real money. Test with quantity=1 on the cheapest product.

Do not invent endpoints, fields or parameters. If something you need is not in this document or in the Reseller API reference, say so instead of guessing.


12. Go-live checklist

Correctness

  • The catalogue renders from your own database, not from a live API call
  • category_id comes from the nesting in GET /categories and is updated on every sync
  • Products that vanish are un-listed, never deleted, and are listed again when they come back
  • A failed or empty sync leaves the previous catalogue intact
  • Your prices, not ours, are shown everywhere
  • A product that is not listed cannot be bought — checked on your server, not only in the UI
  • The minimum learned from Minimum quantity is N is enforced

Money

  • /buy has a 120-second timeout and is never resent after an unknown outcome
  • The purchase row, with sent_at, is written before /buy is sent
  • At most one /buy per product is in flight
  • Reconciliation matches on id_product and waits 3 minutes before concluding "not bought"
  • Pending orders are polled until they are delivered, refunded, or 24 hours old
  • price and total_price are recorded from /order for every order, and short deliveries raise an alert
  • Every failure path refunds or releases the customer's payment
  • The balance alarm reaches a human

Data

  • Every delivered line is stored on your side the moment it arrives
  • Account lines are encrypted at rest and absent from logs
  • The customer can download their purchase as a file
  • link_backup is never shown to customers

Operations

  • Accept: application/json on every request
  • The app refuses to start with an empty api_key; the key comes from the environment, is not in your repository, and has never been sent to a browser
  • A dedicated account is used for the shop
  • Under 60 requests/minute (Reseller and SMM APIs together), from a single worker; HTTP 429 waits for Retry-After
  • Your warranty to customers is shorter than our 48 hours, and someone knows how to file a claim on our website

Tested once for real (this costs real money)

  • Buy 1 unit of the cheapest product end to end; the recorded price matches /order
  • Make your client throw a timeout right after sending a real 1-unit /buy (ignore our reply): reconciliation must attach the order within 3 minutes
  • Make your client throw just before sending: reconciliation must conclude "not bought" after 3 minutes, and the customer is refunded
  • Ask for more units than your balance covers: the customer sees a friendly message and is refunded
  • Start the app with RESELLER_API_KEY unset: it refuses to start

13. Frequently hit problems

"The API does not return the category of a product." Correct — a product object has only id_product, name, desc, price and quantity. The relationship exists only as the nesting in GET /categories. Read that endpoint, store category_id yourself, and update it on every sync: products can move.

"I get an HTML page instead of JSON." You are missing Accept: application/json, so a validation error redirected you to a web page — or a proxy answered with an error page during a very long purchase. For /buy the second case is an unknown outcome: reconcile (§6.6).

"type in /orders is a name, not the product id." Yes, it is the product name in your API language. Use id_product.

"A product I sell disappeared from /categories." It sold out or was paused. Un-list it; it comes back by itself. /buy on it answers Product is not for sale right now.

"I was charged a different unit price than the listing showed." The charge is the volume tier (if your quantity reaches one) minus your account discount, and never more than the list price at the moment of purchase. If it is higher than what you synced, the list price changed after your sync: refresh right before buying (§6.1).

"/order returns an empty data array." On a recent order it is still pending (§6.5). On an order older than the retention period the lines were purged (§7.2) — which is why you store them yourself at delivery.

"/order says Order not found for an order id I stored." The order was refunded and the money is back in your wallet (§7.3) — or you are calling with another account's key, or on another site's domain.

"I was charged but got no accounts." Either the order is pending (pending: true — §6.5), or your call ended without a reply and the order exists: reconcile (§6.6) before doing anything else. If /order shows fewer lines than its quantity on an order that is not pending, contact support with the order_id.

"link_backup gives a 404 or a login page." It is not meant for your customers: it works only in a browser logged in to your account, and most products have no backup file (§6.8).

"Can I see my discount and tier prices through the API?" No. Open the product's Buy dialog on our website while logged in; after buying, read price in /order.

"I get 429 although my shop is quiet." The limit is per IP address and shared with SMM API calls and with every other server behind the same address (§7.5).

"Can I get notified when something changes?" No webhooks today. Poll on a schedule.


14. What changed on 30 September 2026

  • /orders items and /order have a new last key, id_product (the product id). Reconcile with it: the key type there has always been the product name, so matching type against a product id never worked.
  • Unit price: the volume tier price first, then your account discount on top of it (before, a tier price replaced your discount), and never above the list price at the moment of purchase.
  • /buy only sells what the listings show. A product that exists but is not listed answers Product is not for sale right now.
  • quantity must be a whole number; anything else gets HTTP 422.
  • Minimum order quantities are now enforced through the API for every product (Minimum quantity is N).
  • For products delivered in the reply, a short delivery is charged and recorded for the lines delivered only, and a delivery with no lines at all is a failure: nothing charged, no order.
  • /balance with a missing or blank key answers Invalid api_key.

Questions this guide does not answer belong in the Reseller API reference (every endpoint, field and error string), the OTP API reference (renting phone numbers) or the SMM API reference (likes, followers, views). All three are on https://acckit.com/api-docs, each downloadable as Markdown.

Questions? Contact support on the site. © 2026 acckit.com