# AccKit OTP API v1 — rent a phone number, receive the SMS code > **Tiếng Việt (tóm tắt nhanh):** API thuê số điện thoại nhận mã OTP cho tool. Địa chỉ gốc `https://acckit.com/api/v1/otp`, dùng chung `api_key` với API bán hàng, tiền tệ **VND**. Gửi kèm `api_key` ở `/services` và `/servers` để thấy **giá của chính tài khoản bạn** (đã trừ chiết khấu) — `/buy` trừ đúng giá đó. Luồng: `GET /servers` → chọn một dòng → `POST /buy` với đúng `operator` (hoặc `server`: `vip1`, `vip2`, `vip3`, `vip4`, hoặc mã SV thường) của dòng đó → hỏi `GET /check` mỗi 3–5 giây tới khi `status` là `RECEIVED` (có mã) hoặc `REFUNDED` (đã hoàn tiền). Mỗi số giữ tối đa 15 phút (hạn thật là `expires_ts` của đơn); không có mã thì tự hoàn 100%. `/buy` có thể mất tới ~90 giây: đặt timeout ≥ 120 giây và **không tự gửi lại `/buy` khi bị timeout**. Chi tiết bằng tiếng Anh bên dưới; có thể đưa nguyên file này cho AI / agent của bạn. *Document version 2026-09-30 — checked against the source code on 2026-09-30. This copy: https://acckit.com, 2026-10-31.* ## 1. Quick facts | Item | Value | |---|---| | Base URL | `https://acckit.com/api/v1/otp` | | Authentication | The same `api_key` as the Reseller API (Account page → section API), sent as a query parameter, a form field or a JSON body field. Required on `/buy`, `/check`, `/cancel`, `/balance`. Optional on `/services` and `/servers`, where it switches the prices to yours | | Currency | **VND**, integers | | Your price | List price minus your account discount, never below the shop's cost, rounded up to a whole VND. Send `api_key` to `/services` and `/servers` to see it (section 2.1) | | Charging | `/buy` debits your wallet, then requests the number. If no number is issued, the money is back before `/buy` answers | | Hold time | At most 15 minutes after the number is issued. The real deadline of each order is its `expires_ts` | | Refund | Automatic and full (`order.price`) when no code arrives before the deadline or the rental ends early upstream. Immediate when you cancel before a code arrives | | One order, one code | Each order delivers one SMS code. Need another code → buy again | | Rate limit | 240 requests per minute per `api_key`, shared by all OTP endpoints (per IP for calls without a key). HTTP 429 with a `Retry-After` header when exceeded | | Slow calls | `/buy` can take up to about 90 s; `/check` and `/cancel` 30 s or more. Use client timeouts of 120 s and 60 s. Never repeat a `/buy` automatically after a timeout (section 2.2) | | Response shape | `{"status": true, ...}` or `{"status": false, "msg": "..."}` with HTTP 200 | | Validation errors | HTTP 422 JSON when you send `Accept: application/json`; without it the server answers HTTP 302. Always send the header | | Messages | Fixed English texts. The language chosen for the Reseller API does not apply here. Branch on the fields (`status`, `sold_out`, `need_deposit`, `thieu`, `order.status`), not on `msg` | | Module switched off | If the shop switches the OTP service off, every endpoint except `/balance` answers HTTP 404 | | Time zone | `expires_at` is Vietnam time (UTC+7). Prefer `expires_ts` (unix seconds) or `expires_at_iso` (ISO-8601 with offset) | ## 2. Flow 1. `GET /countries` → pick a `country` slug (e.g. `philippines`). 2. `GET /services?country=…&api_key=…` → pick a `service` slug (e.g. `facebook`). This list is only an overview. 3. `GET /servers?country=…&service=…&api_key=…` → the rows you can buy right now, each with **your** `price`. Take one row's `server` or `operator` (`vip1`, `vip2`, `vip3`, `vip4`, or standard server code). 4. `POST /buy` with `api_key`, `country`, `service`, `operator` (or `server`) → you get `order.id`, `order.phone` and `order.expires_ts`. Keep the `id`: no endpoint lists your orders. 5. Submit `order.phone` to the app you are verifying. 6. Poll `GET /check?id=…` every 3–5 s until `order.status` is `RECEIVED` (read `order.code`) or `REFUNDED` (no code, the money is back). 7. Optional: `POST /cancel` while the order is still `WAITING` to give up and get the refund at once. ### 2.1 Pricing and your discount - Every row has a **list price**. Your account can have a **discount**: a percentage the shop sets on your account (the same reseller discount as in the Reseller API). You cannot change it through the API. - **Your price** = list price minus your discount, rounded **up** to a whole VND. The discount never takes a price below the shop's cost for that number, so on some rows your price is higher than the plain percentage would give (it is never above the list price). - Send your `api_key` on `/services` and `/servers` to see your prices. Without a key, or with a wrong key, both endpoints answer with list prices; a wrong key is **not** reported as an error there. When you are logged in, the website shows you the same prices. - `/buy` charges exactly the `price` that a keyed `/servers` call shows for that row at that moment: both use the same rule. `order.price` is the amount charged, and a refund returns exactly `order.price`. - Prices follow the upstream market and can change at any time, including between your `/servers` call and your `/buy`. `/buy` takes no maximum-price parameter, so call `/servers` right before `/buy` and read `order.price` afterwards. - `price_from` in `/services` is the lowest of your prices across the sources that list the service — an indication, not necessarily a price you can buy at (see 4.2). ### 2.2 Timeouts, retries and the rate limit - **`/buy` can take up to about 90 seconds** (some tiers need several upstream calls before the number is known). Use a client timeout of at least 120 s. - **Never repeat a `/buy` automatically after a timeout or a dropped connection.** The order may already exist and be charged, no endpoint lists orders, and a repeat rents a second number. Instead, check `/balance` and look at your recent orders on the website's OTP page (`https://acckit.com/buy-otp`, logged in) — API orders are listed there too. A number you never use is normally refunded at the end of its hold time. An HTTP 429 is different: it is rejected before anything happens, so the same `/buy` can be sent again after `Retry-After` seconds. - `/check` and `/cancel` refresh the order from upstream and can take 30 s or more. Use a 60 s timeout. Both are safe to repeat. - `/countries`, `/services` and `/servers` are usually quick but can take several seconds while their data is refreshed. Use a 60 s timeout. - The 240 requests per minute are counted per `api_key` across every OTP call, `/buy` and `/servers` included. Polling one order every 3 s uses 20 requests per minute, so one key can follow at most 12 orders at that pace (20 at a 5 s pace), minus what your other calls use. Successful responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`; on HTTP 429 wait `Retry-After` seconds. ## 3. Endpoint index | Method | Path | Key | Purpose | |---|---|---|---| | GET | `/countries` | no | Countries on sale | | GET | `/services?country=` | optional (your prices) | Overview of the services in one country: lowest price and total stock | | GET | `/servers?country=&service=` | optional (your prices) | The rows you can buy now for that pair, best first | | POST | `/buy` | yes | Charge the wallet and rent a number | | GET or POST | `/check` | yes | Poll one order | | POST | `/cancel` | yes | Give up an order before its code arrives → refund | | GET or POST | `/balance` | yes | Wallet balance in VND | A wrong HTTP method (for example `GET /buy`) answers HTTP 405. ## 4. Endpoints ### 4.1 GET /countries ```bash curl -H "Accept: application/json" "https://acckit.com/api/v1/otp/countries" ``` ```json {"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 | ```bash curl -H "Accept: application/json" "https://acckit.com/api/v1/otp/services?country=philippines&api_key=YOUR_API_KEY" ``` ```json {"status": true, "data": [ {"service": "facebook", "price_from": 2785, "qty": 653226, "vip": true}, {"service": "google", "price_from": 3100, "qty": 120544, "vip": true} ]} ``` | Field | Meaning | |---|---| | `service` | Slug to send to `/servers` and `/buy` (e.g. `facebook`, `google`, `telegram`, `whatsapp`, `tiktok`, `shopee`) | | `price_from` | Lowest price across the sources that list this service, VND. Your price when you sent a valid `api_key`, otherwise the list price | | `qty` | Sum of the stock reported for the `normal`, `vip` and `vip2` tiers. A service that only `vip3` sells shows `1` | | `vip` | `true` when the `vip` tier lists this service. The other VIP tiers (`vip2`–`vip4`) are not reflected here — check `/servers` | Sorted alphabetically by `service`. Services the shop does not sell, and services whose lowest list price is above the shop's price ceiling, are left out. This list is an **overview built from the source catalogues**, not from the rows you can buy: - It ignores servers that are temporarily hidden (see 4.3), so `price_from` can be lower than any row `/servers` returns, and `/servers` can return `[]` for a listed service. - It does not include the `vip4` tier (Vietnam), and of the services that only `vip3` (USA) sells it lists just a few popular ones. `/servers` can therefore return rows for a service that is not listed here — if you know the slug, call `/servers` directly. - `/servers` is authoritative. ### 4.3 GET /servers | Param | Required | Description | |---|---|---| | `country` | yes | Country slug | | `service` | yes | Service slug | | `api_key` | no | Send it to get `price` at your price, i.e. what `/buy` charges | ```bash curl -H "Accept: application/json" "https://acckit.com/api/v1/otp/servers?country=philippines&service=facebook&api_key=YOUR_API_KEY" ``` ```json {"status": true, "data": [ {"server": "vip1", "server_name": "SV VIP 1", "operator": "vip1", "price": 4199, "qty": 148149, "rate": null, "tier": "vip1", "hold": 15, "ve_rate": 54, "ve_mau": 67, "ghim": false}, {"server": "vip2", "server_name": "SV VIP 2", "operator": "vip2", "price": 3100, "qty": 79124, "rate": null, "tier": "vip2", "hold": 15, "ve_rate": null, "ve_mau": 117, "ghim": false}, {"server": "virtual34", "server_name": "SV thường (virtual34)", "operator": "virtual34", "price": 2785, "qty": 400458, "rate": 72.5, "tier": "normal", "hold": 15, "ve_rate": 31, "ve_mau": 29, "ghim": false}, {"server": "virtual38", "server_name": "SV thường (virtual38)", "operator": "virtual38", "price": 2890, "qty": 23110, "rate": 0, "tier": "normal", "hold": 15, "ve_rate": 12, "ve_mau": 17, "ghim": false} ]} ``` | Field | Meaning | |---|---| | `server` | Server code: `vip1`, `vip2`, `vip3`, `vip4` (VIP servers) or operator code for standard server (e.g. `virtual34`). Send it in `/buy` as `operator` or `server` | | `server_name` | Display label (e.g. `SV VIP 1`, `SV VIP 2`, `SV VIP 3 (US)`, `SV VIP 4`, `SV thường (virtual34)`) | | `operator` | Server identifier (matches `server`). Send it in `/buy` as `operator` | | `tier` | Service level of the row: `vip1`, `vip2`, `vip3`, `vip4`, or `normal` | | `price` | VND per number: your price with a valid `api_key`, otherwise the list price. It is a quote: `/buy` recomputes it from live data (section 2.1) | | `qty` | Stock reported, an estimate (a server can still run out). Always `1` on `vip3` and `vip4`, whose providers publish no stock — do not filter those rows on `qty` | | `rate` | Success rate published for standard servers (`0` when not published). Always `null` on VIP tiers. Prefer `ve_rate` | | `hold` | Usual hold time of the server, in minutes. The order's own `hold` and `expires_ts` are what count | | `ve_rate` | Our measured success rate, 0–100, or `null` — see below | | `ve_mau` | Number of finished orders behind `ve_rate` — see below | | `ghim` | `true` = a newly added server pinned to the top of the list until it has statistics. Unproven | Tiers: | `tier` | `server` | Countries | Usual `hold` | `qty` | |---|---|---|---|---| | `normal` | several per country, e.g. `virtual34` | many | 15 | stock estimate | | `vip1` | `vip1` | many | 15 | stock estimate | | `vip2` | `vip2` | many | 15 | stock estimate | | `vip3` | `vip3` | USA only | 15 | always `1` | | `vip4` | `vip4` | Vietnam only | 10 (set by the shop) | always `1` | Each VIP tier has at most one row per country + service. A VIP tier is not automatically more expensive: prices depend on the tier and the country. **`ve_rate` and `ve_mau`.** `ve_rate` is the share of our own orders for this country + service over the last 7 days that received a code, counting only finished orders (`RECEIVED` or `REFUNDED`): - it is measured on this exact server once the server has at least 6 finished orders; before that, on all servers of the same tier for this country + service; - it is set to `0` early when the first 3 or more finished orders all failed; - it is `null` when there is not enough data yet. `ve_mau` is the number of finished orders behind `ve_rate`. When `ve_rate` is `null`, `ve_mau` is the total number of finished orders for this country + service across all servers — context only, not this row's own count. Statistics are refreshed every 10 minutes. **Order of the rows.** Rows are grouped as below; inside a group they are sorted by list price, cheapest first: 1. `ghim: true` rows (new servers still collecting statistics); 2. `ve_rate` 45 or more; 3. `ve_rate` `null`, with a list price at most 1.3 × the cheapest row's; 4. `ve_rate` 20 to 44; 5. `ve_rate` `null`, with a list price above 1.3 × the cheapest row's; 6. `ve_rate` below 20. So the first row is not always a proven one. If you want a server with a track record, take the first row with `"ghim": false` and a `ve_rate`. **Rows that are not listed.** `/servers` shows only what can be bought now. A row is left out when: - the shop does not sell that country or service (then `data` is `[]` and nothing can be bought); - its upstream is temporarily unavailable (hidden for at least 10 minutes at a time); - the server has just run out of numbers: hidden for 5 minutes, then 30 minutes, then 2 hours if it keeps running out. This applies to `normal`, `vip1` and `vip2`; `vip3` and `vip4` rows usually stay listed; - its list price is above the shop's price ceiling; - it delivered no code at all over at least 10 recent finished orders (`ve_rate` 0 with `ve_mau` 10 or more). An unknown or misspelled `country` or `service` also gives `{"status": true, "data": []}`. ### 4.4 POST /buy | Param | Type | Required | Description | |---|---|---|---| | `api_key` | string | yes | Your API key | | `country` | string | yes | Country slug | | `service` | string | yes | Service slug | | `operator` | string | yes* | `server` or `operator` of the chosen `/servers` row (`vip1`, `vip2`, `vip3`, `vip4`, or `virtual34`, etc.) | | `server` | string | yes* | Alias for `operator`. You can send either `operator` or `server` | How the row is chosen: - Send `operator` (or `server`) equal to the server code (`vip1`, `vip2`, `vip3`, `vip4`, or standard code). What happens: `/buy` reloads the live server list (the same list `/servers` returns), finds the row, computes your price, debits your wallet and asks for a number. If no number is issued, the money is refunded before the response. **This can take up to about 90 s** — read section 2.2 before you set timeouts or retries. ```bash 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: ```json { "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): ```json {"status": false, "msg": "Insufficient balance!", "sold_out": false, "need_deposit": true, "thieu": 1215} ``` ```json {"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 | ```bash curl --max-time 60 -H "Accept: application/json" "https://acckit.com/api/v1/otp/check?api_key=YOUR_API_KEY&id=214" ``` ```json {"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 | ```bash curl -X POST --max-time 60 -H "Accept: application/json" "https://acckit.com/api/v1/otp/cancel" \ -d "api_key=YOUR_API_KEY" -d "id=214" ``` - Order still open (no code yet), any tier, any time → full refund at once: `{"status": true, "order": {... "status": "REFUNDED" ...}}`. Cancelling means giving up: an SMS that arrives afterwards, or that reached the source but was not fetched yet, is discarded. Do not cancel an order whose code you still expect. - A code landed at the same moment → `{"status": true, "order": {... "status": "RECEIVED", "code": "..." ...}}` and no refund. **Always read `order.status` in the reply.** - Already refunded → `{"status": true, "order": {...}}` again, so a repeated cancel is harmless. - Code already received → `{"status": false, "msg": "This order already received a code."}` — a used number cannot be refunded. ### 4.7 GET or POST /balance ```bash curl -X POST -H "Accept: application/json" "https://acckit.com/api/v1/otp/balance" -d "api_key=YOUR_API_KEY" ``` ```json {"status": true, "balance": 152000, "currency": "VND"} ``` Works even when the OTP service is switched off. The wallet is shared with the Reseller and SMM APIs and with the website. `/buy` and `/check` do not return the balance. ## 5. Order statuses and refunds | `status` | Meaning | What to do | |---|---|---| | `PENDING` | Internal: exists only while `/buy` is still running, and `/buy` never returns it. If such an order is cancelled (the website shows it), it is refunded and `/buy` answers with the "cancelled while the number was being issued" failure | Treat as `WAITING` | | `WAITING` | Number issued, waiting for the SMS | Submit `phone` to the target app, keep polling | | `RECEIVED` | Code arrived. Final; the order stays charged | Read `code` / `text` | | `REFUNDED` | Final. `order.price` is back in your wallet: no code before the deadline, the rental ended early upstream, or you cancelled | Buy again, preferably on another row | How refunds work: - **Deadline.** `expires_ts` is the order's own deadline: the source's hold window, but never more than 15 minutes after the number is issued. The `hold` column of `/servers` is only the usual value for the tier. - **Automatic refund.** An open order is refunded when the source reports that the rental ended without a code (this can happen before `expires_ts`), or once `expires_ts` has passed by 30 seconds. This check runs on your `/check` calls and on a background job every minute, so you do not have to poll for the refund: it normally lands within about 2 minutes after `expires_ts`. - **Late codes still count.** A code that arrives before the refund is processed makes the order `RECEIVED`, even a few seconds after `expires_ts`. - **If you stop polling,** the background job keeps collecting codes and refunds. Nothing is lost: a later `/check` shows the result. - **Ignored SMS.** A code that the same phone number already delivered on an earlier order, or an SMS sent before the rental started, is ignored (it belongs to a previous user of that number). The order keeps waiting and is refunded at the deadline if no new code arrives. - **One code per order.** Once `RECEIVED`, the order is final and later SMS are not delivered. - **Failed `/buy`.** If no number could be issued after the debit, the refund is made before `/buy` answers. ## 6. Error reference | HTTP | Body | When | |---|---|---| | 200 | `{"status": false, "msg": "Invalid api_key"}` | Key missing or wrong on `/buy`, `/check`, `/cancel`, `/balance` | | 200 | `{"status": false, "msg": "Order not found"}` | Wrong `id`, or another customer's order | | 200 | `{"status": false, "msg": "...", "sold_out": true, ...}` | `/buy`: that row cannot deliver now. Reload `/servers` and choose another row (4.4) | | 200 | `{"status": false, "msg": "Insufficient balance!", "need_deposit": true, "thieu": N, ...}` | `/buy`: top up at least N VND (N = 0: temporary error, retry) | | 200 | `{"status": false, "msg": "Could not get a number right now — ...", "sold_out": false, ...}` | `/buy`: upstream error, already refunded | | 200 | `{"status": false, "msg": "This order already received a code."}` | `/cancel` after the code arrived | | 422 | `{"message": "The given data was invalid.", "errors": {...}}` | Missing or invalid `country`, `service`, `operator` or `id` (only with `Accept: application/json`; otherwise HTTP 302) | | 404 | not meaningful | OTP service switched off (every endpoint except `/balance`), or a wrong path | | 405 | not meaningful | Wrong HTTP method, e.g. `GET /buy` | | 429 | `{"message": "Too Many Attempts."}` with `Accept: application/json` (an HTML page without it), plus a `Retry-After` header | More than 240 requests per minute for this key | ## 7. Code examples ### cURL ```bash 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 ```python import time import requests BASE = "https://acckit.com/api/v1/otp" KEY = "YOUR_API_KEY" H = {"Accept": "application/json"} def call(method, path, timeout, **params): """One API call. Repeats only after HTTP 429, which is rejected before anything happens.""" params["api_key"] = KEY while True: if method == "GET": r = requests.get(BASE + path, params=params, headers=H, timeout=timeout) else: r = requests.post(BASE + path, data=params, headers=H, timeout=timeout) if r.status_code == 429: time.sleep(int(r.headers.get("Retry-After", "10"))) continue r.raise_for_status() return r.json() def get_code(country, service, on_number, max_tries=3): failed = set() # (provider, operator) rows that just failed for _ in range(max_tries): rows = call("GET", "/servers", 60, country=country, service=service)["data"] # your prices rows = [r for r in rows if (r["provider"], r["operator"]) not in failed] if not rows: raise RuntimeError("nothing to buy for this country/service right now") row = next((r for r in rows if not r["ghim"]), rows[0]) # prefer rows with statistics failed.add((row["provider"], row["operator"])) # 120 s timeout. If it times out, do NOT call /buy again: the number may already be rented. res = call("POST", "/buy", 120, country=country, service=service, operator=row["operator"], provider=row["provider"]) if not res["status"]: if "sold_out" not in res: # e.g. "Invalid api_key" raise RuntimeError(res["msg"]) if res["need_deposit"] and res["thieu"] > 0: raise RuntimeError(f"top up at least {res['thieu']} VND") if res["need_deposit"]: # thieu == 0: temporary error, same row is fine failed.discard((row["provider"], row["operator"])) time.sleep(5) continue # not charged, or already refunded: next row order = res["order"] if not order["phone"]: # very rare: no usable number, take the refund call("POST", "/cancel", 60, id=order["id"]) continue on_number(order["phone"]) # submit the number to the target app here while time.time() < order["expires_ts"] + 180: # the refund lands shortly after expires_ts time.sleep(4) # 3-5 s per order try: c = call("GET", "/check", 60, id=order["id"]) except requests.RequestException: continue # /check is safe to repeat if not c["status"]: raise RuntimeError(c["msg"]) o = c["order"] if o["status"] == "RECEIVED": return o["phone"], o["code"] if o["status"] == "REFUNDED": break # no code in time; money is back, try another row else: # still open after the deadline: give up c = call("POST", "/cancel", 60, id=order["id"]) if not c["status"]: # "This order already received a code." c = call("GET", "/check", 60, id=order["id"]) if c["order"]["status"] == "RECEIVED": # the code won the race return c["order"]["phone"], c["order"]["code"] raise RuntimeError(f"no code after {max_tries} numbers") print(get_code("philippines", "facebook", on_number=lambda phone: print("use this number:", phone))) ``` ## 8. Notes - **Choosing a row.** Prefer a high `ve_rate` backed by a large `ve_mau`. `null` means "not measured yet", and `ghim: true` rows are new and unproven. A VIP tier is not automatically dearer or more reliable: compare `price` and `ve_rate` row by row. - **Trying again.** After a failed `/buy` or a `REFUNDED` order, call `/servers` again and usually pick a different row. Skip rows that failed in the last 10 minutes or so. - **Branch on fields, never on `msg`.** Use `status`, `sold_out`, `need_deposit`, `thieu` and `order.status`. - **Keep every `order.id`.** No endpoint lists your orders; the website's OTP page shows the latest ones. - All amounts are VND integers. The wallet is shared with the Reseller and SMM APIs and with the website.