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.
acckit-reseller-api.md · raw URL for your AI agent: https://acckit.com/api-docs/general.mdAccKit 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ằngapi_keylấ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ấyid_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_pricecủa/ordervà/orders./buycó 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/buythì KHÔNG gọi lại — đợi khoảng 2 phút rồi tìm đơn trong/orderstheoid_product. Luôn gửi headerAccept: 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:
-
Volume tier. If the product has volume tiers, the lowest tier price among the tiers your
quantityreaches 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. - 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.
- 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,
/orderand/ordersreturnprice(unit price charged) andtotal_price(amount charged). The/buyresponse 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_priceis exactlyprice×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/categoriesand/category/{id}, and product names (type) in/orderand/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.
-
msgtexts 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:
- The fields are validated: HTTP 422 if
api_key,id_productorquantityis missing or malformed (quantitymust be an integer). - The key is checked:
Invalid api_key. - The product is checked:
Product is not for sale right nowif it is hidden (anything/categoriesdoes not list, including products withdrawn automatically while their supplier is failing),Product ID does not existif the id is unknown. - The quantity is checked:
Minimum quantity must be at least 1,Minimum quantity is N,Quantity exceeds available stock. - The unit price is computed (section 3):
Insufficient balance!if your balance is below unit price ×quantity. - 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 missingapi_keyororder_idgives 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": "« 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 »", "active": false}
],
"path": "https://acckit.com/api/v1/orders",
"per_page": 10,
"to": 10,
"total": 14
}
}
- Each item has exactly the fields of
/orderexcept 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
linksandmeta.linksare plain page URLs. Call them with POST and yourapi_key; a GET answers HTTP 405.meta.linksis 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/orderand/orders. - The shop asks the supplier again every minute and adds the accounts to the order as soon as they arrive.
/orderhas no pending flag: an emptydataon a recent order means it is still pending. - Poll
/orderwith theorder_idevery 1 to 2 minutes. Oncedatais 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
quantityequals the number of accounts you received, andtotal_priceis 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
quantityandtotal_pricestay 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 indatathan itsquantity, contact support with theorder_id. - If nothing at all can be delivered,
/buyfails (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_pricegoes back to your balance and the order is removed: it disappears from/orders, and/orderanswersOrder not found. Keep your own record oforder_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:
-
/orderstill returns the order, butdatais[]; - the order stays in
/orders; -
link_backuplinks 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:
- Use a client timeout of at least 120 seconds for
/buy. -
Never repeat a
/buycall 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. - Safe to repeat: HTTP 429 (the request was rejected before processing; wait
Retry-Afterseconds), HTTP 422 (fix the fields first) and any HTTP 200{"status": false, ...}answer (nothing was charged; fix the cause first). - 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):
- Wait about 2 minutes.
- POST
/orders(page 1, and page 2 if you place many orders). A match is an order with the sameid_product, acreated_at(UTC) no earlier than about 2 minutes before you sent the request (the margin absorbs clock differences), and anidyou have not already recorded for another purchase. Purchases made on the website with the same account appear in this list too. - Found: POST
/orderwith thatid, store the accounts and mark the purchase done. Itsquantityandtotal_pricetell you what was delivered and charged. - 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
/balancewith 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/v1endpoints called from the same IP. The OTP API (/api/v1/otp/...) has its own budget of 240 requests per minute perapi_key. - Every response carries
X-RateLimit-LimitandX-RateLimit-Remaining. A 429 also carriesRetry-After(seconds) andX-RateLimit-Reset(Unix timestamp). - Spend the budget carefully: cache
/categoriesand 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/balancein 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/jsonon 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_productfromlist_product[]; never send a categoryidto/buy. - Treat catalogue prices as list prices. Your real charge follows section 3 and is returned by
/orderand/orders(price,total_price). - Detect failures with
status === false. A successful/orderhas nostatuskey and is not wrapped indata; a successful/ordersor/categorieshas nostatuskey either. - In
/orderand/orders,typeis the product name; match onid_product. - Save delivered accounts immediately: they are deleted after 15 days.
- Handle
pending: trueby polling/order(section 6.1), and compare the accounts received withquantity(section 6.2). - Never repeat
/buyafter a timeout, a 5xx or an unreadable body: reconcile through/orders(section 7). Use a/buytimeout of at least 120 seconds. - Expect refunded orders to disappear from
/orders(section 6.3). - Stock (
quantity) changes constantly. TreatQuantity exceeds available stockas "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
/categoriesinstead 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.
acckit-otp-api.md · raw URL for your AI agent: https://acckit.com/api-docs/otp.mdAccKit 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 chungapi_keyvới API bán hàng, tiền tệ VND. Gửi kèmapi_keyở/servicesvà/serversđể thấy giá của chính tài khoản bạn (đã trừ chiết khấu) —/buytrừ đúng giá đó. Luồng:GET /servers→ chọn một dòng →POST /buyvới đúngoperator(hoặcserver:vip1,vip2,vip3,vip4, hoặc mã SV thường) của dòng đó → hỏiGET /checkmỗi 3–5 giây tới khistatuslàRECEIVED(có mã) hoặcREFUNDED(đã hoàn tiền). Mỗi số giữ tối đa 15 phút (hạn thật làexpires_tscủa đơn); không có mã thì tự hoàn 100%./buycó thể mất tới ~90 giây: đặt timeout ≥ 120 giây và không tự gửi lại/buykhi 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
-
GET /countries→ pick acountryslug (e.g.philippines). -
GET /services?country=…&api_key=…→ pick aserviceslug (e.g.facebook). This list is only an overview. -
GET /servers?country=…&service=…&api_key=…→ the rows you can buy right now, each with yourprice. Take one row'sserveroroperator(vip1,vip2,vip3,vip4, or standard server code). -
POST /buywithapi_key,country,service,operator(orserver) → you getorder.id,order.phoneandorder.expires_ts. Keep theid: no endpoint lists your orders. - Submit
order.phoneto the app you are verifying. - Poll
GET /check?id=…every 3–5 s untilorder.statusisRECEIVED(readorder.code) orREFUNDED(no code, the money is back). - Optional:
POST /cancelwhile the order is stillWAITINGto 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_keyon/servicesand/serversto 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. -
/buycharges exactly thepricethat a keyed/serverscall shows for that row at that moment: both use the same rule.order.priceis the amount charged, and a refund returns exactlyorder.price. - Prices follow the upstream market and can change at any time, including between your
/serverscall and your/buy./buytakes no maximum-price parameter, so call/serversright before/buyand readorder.priceafterwards. -
price_fromin/servicesis 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
-
/buycan 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
/buyautomatically 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/balanceand 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/buycan be sent again afterRetry-Afterseconds. -
/checkand/cancelrefresh the order from upstream and can take 30 s or more. Use a 60 s timeout. Both are safe to repeat. -
/countries,/servicesand/serversare 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_keyacross every OTP call,/buyand/serversincluded. 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 carryX-RateLimit-LimitandX-RateLimit-Remaining; on HTTP 429 waitRetry-Afterseconds.
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_fromcan be lower than any row/serversreturns, and/serverscan return[]for a listed service. - It does not include the
vip4tier (Vietnam), and of the services that onlyvip3(USA) sells it lists just a few popular ones./serverscan therefore return rows for a service that is not listed here — if you know the slug, call/serversdirectly. -
/serversis 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
0early when the first 3 or more finished orders all failed; - it is
nullwhen 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:
-
ghim: truerows (new servers still collecting statistics); -
ve_rate45 or more; -
ve_ratenull, with a list price at most 1.3 × the cheapest row's; -
ve_rate20 to 44; -
ve_ratenull, with a list price above 1.3 × the cheapest row's; -
ve_ratebelow 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
datais[]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,vip1andvip2;vip3andvip4rows 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_rate0 withve_mau10 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(orserver) 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 readorder.statusin 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_tsis the order's own deadline: the source's hold window, but never more than 15 minutes after the number is issued. Theholdcolumn of/serversis 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 onceexpires_tshas passed by 30 seconds. This check runs on your/checkcalls and on a background job every minute, so you do not have to poll for the refund: it normally lands within about 2 minutes afterexpires_ts. -
Late codes still count. A code that arrives before the refund is processed makes the order
RECEIVED, even a few seconds afterexpires_ts. -
If you stop polling, the background job keeps collecting codes and refunds. Nothing is lost: a later
/checkshows 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/buyanswers.
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_ratebacked by a largeve_mau.nullmeans "not measured yet", andghim: truerows are new and unproven. A VIP tier is not automatically dearer or more reliable: comparepriceandve_raterow by row. -
Trying again. After a failed
/buyor aREFUNDEDorder, call/serversagain and usually pick a different row. Skip rows that failed in the last 10 minutes or so. -
Branch on fields, never on
msg. Usestatus,sold_out,need_deposit,thieuandorder.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.
acckit-smm-api.md · raw URL for your AI agent: https://acckit.com/api-docs/smm.mdAccKit 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 chungapi_keyvới API bán hàng và API OTP, tiền VND.ratelà giá cho 1.000 đơn vị; gọi/serviceskèm key thìratelà 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/smmvớikey+action). Khác chuẩn v2: tiền VND, lỗi trả kèm mã HTTP 4xx/5xx,cancelchỉ nhận một mã đơn quaorder, không có refill / drip-feed / bình luận tự nhập qua API (chỉ đặt góitype=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. ĐơnPartialhoặ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 headerAccept: application/jsonvà URL-encodelink. 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 examplerefillorrefill_status) answers HTTP 400Incorrect request action. -
/buy,/orderand/cancelaccept POST only; a GET answers HTTP 405. - For AI agents that also use the Reseller API:
POST https://acckit.com/api/v1/smm/ordercreates and charges a Buff order, while the Reseller API'sPOST https://acckit.com/api/v1/orderonly reads an order. Do not mix them up.
Typical flow:
-
POST /serviceswith your key → pick a service withtype=Defaultbycategoryandname; note itsrate,min,max,cancelandrefill. -
POST /buywithservice_id,link(URL-encoded) andquantity→ keeporder_idandcharge. - Poll
/statusuntil the status is final (section 5): one id every few minutes, or many ids in one call every 10 minutes. - Optional:
POST /cancelwhile the order is still running, if the service hascancel: true. -
Partialand 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 =
ratefrom 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 ascharge, and fixed on the order. - Prices follow the shop's catalogue and can change; the price at the moment of
/buyapplies. Read/serviceswith 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 |
Link rules
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, 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 |
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); onlyhttpandhttpsare 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. -
@usernamemeans@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 |
|---|---|---|
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
/buycall 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
/buycall times out, do not resend blindly: compare/balancewith 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=1234included): the order is first refreshed live from the provider (unless it is alreadyCompleted,CanceledorRefunded), 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
/buyan order showsPending,start_count"0"andremainsequal 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
orderslist is not read: a request with onlyorders=…answers 404Incorrect order ID, and inorder=1234,1235only1234is used. - Possible only when the service has
cancel: trueand the order is notCompleted,Partial,CanceledorRefunded, and not alreadyCanceling. - 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,PartialorCompleted), the order takes it and the background job creditsfloor(remains × charge / quantity)VND using the provider's finalremains, in exact integer arithmetic, never more thancharge. Poll/statusto 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
/statuscall 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 waitRetry-Afterseconds. - Call
/serviceswith your key at run time, keep onlytype=Default, and choose bycategory+name. Ids and prices change; do not hard-code them. - Respect
min/max, URL-encodelink, and make the link fit the platform and the object type (section 4). - The charge is
ceil(quantity × rate / 1000)with your ownrate; the/buyanswer states the exactcharge. -
/buyhas no idempotency key: after a timeout, check/balanceand 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. -
/canceltakes one id and refunds nothing at once: the undelivered part follows automatically at least 12 hours later (refund_after);Partialand provider-cancelled orders are refunded automatically; no order is ever refunded twice. - Refill is website-only.
-
POST /api/v1/smm/orderplaces a paid order; it is not the Reseller API's read-onlyPOST /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; showmessageto 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.
acckit-storefront-integration.md · raw URL for your AI agent: https://acckit.com/api-docs/storefront.mdStorefront 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ộtapi_key, tiền VND, luôn gửiAccept: application/json). Đồng bộ danh mục từGET /categories5–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_pricecủa/order.POST /buychờ 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/orderstheoid_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
-
Use a dedicated account for the shop.
/orderslists 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. -
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. -
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
nameanddescin the catalogue and the product name shown in/ordersand/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. -
Top up.
/buycharges your wallet. It checks your balance against the whole order (quantity × unit price) first; if it is not enough you getInsufficient balance!and nothing happens. - 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/jsonon 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,/ordersand/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 seequantity: 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.
/buyrefuses anything the listings do not show (Product is not for sale right now). -
Products move between categories. Update
category_idon 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:
- Start from the list price.
- 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.
- 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,
/buycharges the price of that moment, and there is no maximum-price parameter. -
Record what you paid from
/orderafter 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
/buycan still fail (Product temporarily unavailable, …) whilequantityis positive. -
/buyrefuses more than the listed stock (Quantity exceeds available stock). - Some products have a minimum order quantity, which the listings do not show.
/buythen answersMinimum 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}
]}
-
quantitymust be a whole number, 1 or more. A decimal such as2.5gets HTTP 422. -
order_idis your receipt;buy_idrepeats it on every line. - In
/buylines,typeis the product id. In/ordersand/orderthe keytypeholds the product name, so useid_productthere. - 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_idat once, mark the purchase "awaiting", and tell your customer the order is being prepared. - Poll
POST /orderevery 1–2 minutes for the first hour, then every 10 minutes. Whendatafills, 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
/orderstarts answeringOrder 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_infoline, 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. -
/orderlines also carrylink_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) andtotal_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 aRetry-Afterheader in seconds. Wait that long (never more than 60 seconds) before the next call. Normal replies carryX-RateLimit-LimitandX-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/buyafter 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 anapi_keyform or query field on every call · HeaderAccept: application/jsonon every call · Money VND, parsed as decimals · Rate limit 60 requests/minute per IP, shared with the SMM API.Build, in this order:
- A server-side API client.
api_keycomes from an environment variable; refuse to start if it is empty. Timeouts: 120 s forPOST /buy, 30 s for everything else. Classify every reply: HTTP 2xx with JSON → use it; HTTP 429 → not processed, waitRetry-Afterseconds; 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 logapi_keyorfull_info.- Tables: categories; products keyed by our
id_product, with acategory_idyou fill from the nesting inGET /categories(and update on every sync), the list price, stock, alistedflag,min_qty, and your own price; purchases (id_product, quantity, expected unit price,sent_at, state, ourorder_idunder a unique key,price,total_price); delivered lines, encrypted at rest.- A catalogue sync every 5–10 minutes, from
GET /categoriesonly. A failed, non-JSON or empty reply changes nothing. Upsert everything seen withlisted = 1; everything not seen getslisted = 0. Never delete. Sold-out products disappear from the listing instead of showing quantity 0, and come back later; products can change category; names anddesccan change.- 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.- A checkout that: checks the product is listed, quantity ≥
min_qtyand ≤ stock, and no other purchase of this product is in flight or unresolved → refreshes the product withGET /category/{category_id}(fallbackGET /categories; missing → unavailable; price above the one you priced from → re-price or confirm) → takes the customer's money → stores the purchase withsent_atand statesending→ callsPOST /buy(id_product, a whole-numberquantity).- Reply handling:
status:truewith lines → store every line, then deliver; fewer lines than ordered → deliver those, refund the rest;pending:true→ charged in full, no lines yet, stateawaiting;status:false→ nothing charged, refund, show a mapped message (onMinimum quantity is Nstore N); HTTP 4xx → nothing charged; unknown → stateunknown, and never resendPOST /buy.- A job every minute for purchases in state
unknown(orsendingfor more than 5 minutes): readPOST /orders?page=1,2,…(10 per page, newest first) back tosent_at − 2 min. A candidate has the sameid_product,created_at(ISO-8601 UTC) ≥sent_at − 2 min,quantity≤ the requested quantity, and anidnot yet in your database. One candidate → attach it; several → alert a human; none and at least 3 minutes sincesent_at→ not bought, refund. In/ordersand/order,typeis the product NAME; match onid_product.- A job every 2 minutes for purchases that have an
order_idbut no recorded price, or areawaiting: callPOST /order; storeprice,total_price,quantityand the lines.Order not found→ refunded (money back in the wallet); emptydata→ still pending (alert a human after 24 hours); fewer lines thanquantity→ alert a human (contact support).- A sweep every 10 minutes: every order older than 5 minutes on the first pages of
/ordersmust be in your database; otherwise attach it to the matching purchase or alert a human.- A balance alarm:
POST /balanceevery 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:
/ordersalso lists purchases made on the website.- At most one
POST /buyper product in flight.- Stay under 60 requests per minute in total; on HTTP 429 wait
Retry-After.descis free text, not a field layout. Deliverfull_inforaw; split it only with per-product layouts verified on a real delivery; never showlink_backupto 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=1on 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_idcomes from the nesting inGET /categoriesand 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 Nis enforced
Money
-
/buyhas a 120-second timeout and is never resent after an unknown outcome - The purchase row, with
sent_at, is written before/buyis sent - At most one
/buyper product is in flight - Reconciliation matches on
id_productand waits 3 minutes before concluding "not bought" - Pending orders are polled until they are delivered, refunded, or 24 hours old
-
priceandtotal_priceare recorded from/orderfor 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_backupis never shown to customers
Operations
-
Accept: application/jsonon 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
pricematches/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_KEYunset: 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
-
/ordersitems and/orderhave a new last key,id_product(the product id). Reconcile with it: the keytypethere has always been the product name, so matchingtypeagainst 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.
-
/buyonly sells what the listings show. A product that exists but is not listed answersProduct is not for sale right now. -
quantitymust 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.
-
/balancewith a missing or blank key answersInvalid 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