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

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

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

## 1. Quick facts

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

## 2. Endpoints and typical flow

| REST (`https://acckit.com/api/v1/smm/…`) | v2 (`POST https://acckit.com/api/v1/smm`) | Key | Purpose |
|---|---|---|---|
| GET or POST `/services` | `action=services` | optional | Catalogue: price per 1,000 (yours when a key is sent), min / max, refill and cancel flags |
| POST `/buy` or POST `/order` | `action=add` | required | Create an order and pay for it |
| GET or POST `/status` | `action=status` | required | One order (checked live) or several (stored data) |
| POST `/cancel` | `action=cancel` | required | Cancel one running order; the undelivered part is refunded automatically, at least 12 hours later |
| GET or POST `/balance` | `action=balance` | required | Wallet balance |

- The v2 URL accepts POST only. Any other `action` (for example `refill` or `refill_status`) answers HTTP 400 `Incorrect request action`.
- `/buy`, `/order` and `/cancel` accept POST only; a GET answers HTTP 405.
- For AI agents that also use the Reseller API: `POST https://acckit.com/api/v1/smm/order` **creates and charges** a Buff order, while the Reseller API's `POST https://acckit.com/api/v1/order` only reads an order. Do not mix them up.

Typical flow:

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

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

```bash
# 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:

```json
[
  {
    "service": 2936,
    "name": "Facebook Post Likes [EMOTICONS] [LOVE] [Refill: No] [Max: 10K] [Start Time: 0 - 1 Hr] [Speed: 5K/DAY]",
    "type": "Default",
    "category": "Facebook Post Likes [EMOJI]",
    "rate": 4843.8,
    "min": 10,
    "max": 10000,
    "refill": false,
    "cancel": true
  }
]
```

| Field | Meaning |
|---|---|
| `service` | Service id: send it as `service_id` (REST) or `service` (v2) |
| `name` | Description of the service (quality, speed, start time, refill period…), not translated |
| `type` | Order type. Only `Default` can be ordered through this API; skip every other type |
| `category` | Platform + action group. It also decides which links are accepted (section 4) |
| `rate` | VND per 1,000 units: yours when you sent a key, otherwise the list price. A JSON number with up to 2 decimals |
| `min` / `max` | Allowed `quantity` per order |
| `refill` | The service has a refill (top-up) guarantee. Refills are requested on the website, not through the API (section 7) |
| `cancel` | `true`: a running order can be cancelled with `/cancel` |

### Pricing and your discount

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

Worked example, account with a 10 % discount:

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

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

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

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

```bash
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):

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

For every link:

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

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

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

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

### Payment, timeouts and duplicates

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

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

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

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

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

REST, one id:

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

```json
{
  "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`):

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

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

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

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

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

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

### Status values

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

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

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

| Param | Required | Description |
|---|---|---|
| `api_key` or `key` (or a header) | yes | Your API key |
| `order_id` or `order` | yes | **One** order id |

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

```bash
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.

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

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

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

## 7. Automatic refunds and refill

| Situation | Refund | When |
|---|---|---|
| You cancel (API or website) | `floor(remains × charge / quantity)`, with the provider's final `remains` | Automatically, at least 12 hours after the cancel (`refund_after`), once the provider's status is final |
| The provider ends the order as `Partial` | Same formula, with the provider's final `remains` | Automatically, usually within 10 minutes |
| The provider cancels the order (`Canceled`, `Refunded`) | Same formula | Automatically, usually within 10 minutes |
| `Completed` | Nothing | — |

- Automatic refunds come from a background job that re-reads running orders from the provider every 10 minutes. A `/status` call does not pay a refund by itself; pressing **Sync** on the order in the Buff order history page of the website does, at once.
- Every order is refunded at most once, whoever triggers it. The wallet entry reads `Hoàn tiền đơn Buff #BUFF-<id> …`.
- Orders are settled against what was actually delivered: any difference is charged or credited once per order, in a single wallet entry reading `Đơn buff #BUFF-<id>: đã chạy <delivered>/<quantity> — thu …đ` (or `— hoàn …đ`), or `Đơn buff 1 phần …` when only part of the order was delivered.

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

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

```bash
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)

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

```python
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
<?php
// Every method returns [httpCode, decodedBody].
class AccKitSmm
{
    private string $url = 'https://acckit.com/api/v1/smm';
    private string $key;

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

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

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

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

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

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

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

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

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

## 12. Checklist for developers and AI agents

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