SMM panel API integration is a smaller job than most people expect: one endpoint, one key, seven actions and no OAuth dance. This guide takes you from an empty file to a working integration — service sync, order placement, batched status polling, refills and error handling — with runnable examples in PHP, Python and Node.js against the 9SMM API v2.
What the API gives you
Everything the dashboard does, minus the clicking:
| Action | What it does |
|---|---|
services | Returns the live catalogue with IDs, rates, min/max and capability flags |
add | Places an order and deducts the charge immediately |
status | Returns charge, start count, status and remains — one order or up to 100 |
refill | Requests a refill on eligible orders |
refill_status | Checks a refill request |
cancel | Requests cancellation on up to 100 orders |
balance | Returns your USD balance |
That is the whole surface. If you have integrated any panel before, the shape will be familiar — this interface is close to an industry convention, which is also why switching suppliers is mostly a matter of changing a base URL and a key.
Before you write code
- Get a key. It lives in your panel account and starts with
smm_. Each account has one active key; regenerating it invalidates the old one instantly. - Fund the balance.
addfails withNot enough funds on balancerather than queueing. See payment methods. - Know the limit. 1,200 requests per rolling minute per key. Over that you get HTTP 429 with
{"error": "Rate limit exceeded"}— the standard 429 semantics apply, so back off rather than retry in a tight loop. - Keep the key server-side. Never in browser JavaScript, never in a mobile binary. A leaked key spends your balance.
Endpoint and authentication
POST https://9smm.com/api/v2
Content-Type: application/x-www-form-urlencoded
Authentication is a key parameter in the body — no header, no token exchange. Every request carries key and action. Rates, charges and balances are USD throughout.
One thing to internalise before anything else: most errors return HTTP 200 with an error field in the body. A 200 is not success. Check for error on every single response.
Step 1: Pull the service list
Start here, because every later call needs a service ID.
import requests
API = "https://9smm.com/api/v2"
KEY = "smm_your_key_here"
def call(payload):
r = requests.post(API, data={"key": KEY, **payload}, timeout=30)
r.raise_for_status()
data = r.json()
if isinstance(data, dict) and "error" in data:
raise RuntimeError(data["error"])
return data
services = call({"action": "services"})
for s in services[:3]:
print(s["service"], s["name"], s["rate"], s["min"], s["max"], s["refill"])
Each entry looks like this:
{
"service": 1,
"name": "Instagram Followers",
"type": "Default",
"category": "Instagram — Followers",
"rate": "0.90",
"min": 10,
"max": 100000,
"refill": true,
"cancel": true,
"dripfeed": true
}
Cache it and refresh on a schedule — hourly is plenty. Do not call services before every order; that is how people hit the rate limit on day one. Note that rate is a string: parse it as a decimal rather than a float if you bill customers from it.
The charge for any order is rate × quantity ÷ 1,000, so you can compute and display the cost before submitting.
Step 2: Place an order
PHP, with cURL:
<?php
function smm(array $params) {
$params['key'] = 'smm_your_key_here';
$ch = curl_init('https://9smm.com/api/v2');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
curl_close($ch);
$data = json_decode($raw, true);
if (isset($data['error'])) {
throw new RuntimeException($data['error']);
}
return $data;
}
$order = smm([
'action' => 'add',
'service' => 1,
'link' => 'https://www.instagram.com/your_profile/',
'quantity' => 1000,
]);
echo $order['order']; // 23501
Node.js, the same call:
const API = "https://9smm.com/api/v2";
const KEY = "smm_your_key_here";
async function smm(params) {
const res = await fetch(API, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ key: KEY, ...params }),
});
const data = await res.json();
if (data && data.error) throw new Error(data.error);
return data;
}
const { order } = await smm({
action: "add",
service: "1",
link: "https://www.instagram.com/your_profile/",
quantity: "1000",
});
The response is {"order": 23501}. Store that ID against your own customer reference immediately — the panel knows orders, not your customers.
Parameters for add:
| Parameter | Required | Notes |
|---|---|---|
service | yes | Service ID |
link | yes | Public profile, post or video URL |
quantity | yes | Within min/max. Omit for Custom Comments |
comments | no | Custom Comments only, one per line, under 500 characters each |
runs | no | Drip-feed, 2 to 100 |
interval | no | Drip-feed, 0 to 1,440 minutes |
Step 3: Drip-feed
Only on services where dripfeed is true. quantity is the total across all runs, and each individual run (quantity ÷ runs) must still fall within the service's min and max — that constraint causes most Incorrect quantity errors.
action=add&service=1&link=https://www.instagram.com/your_profile/
&quantity=5000&runs=5&interval=60
Five runs of 1,000, an hour apart. Why bother is covered in drip-feed and refill explained.
Step 4: Poll status in batches
The mistake here is one request per order. Use orders with up to 100 comma-separated IDs:
ids = ",".join(str(i) for i in order_ids[:100])
result = requests.post(API, data={
"key": KEY, "action": "status", "orders": ids
}, timeout=30).json()
for oid, info in result.items():
if "error" in info:
log.warning("order %s: %s", oid, info["error"])
continue
print(oid, info["status"], info["remains"], info["charge"])
A multi-status response is keyed by order ID, and a bad ID returns its own error entry without breaking the rest:
{
"23501": {"charge": "0.90", "start_count": 3572, "status": "Completed", "remains": 0, "currency": "USD"},
"99999": {"error": "Incorrect order ID"}
}
Statuses are Pending, In progress, Processing, Completed, Partial and Canceled. The first three are non-terminal; stop polling an order once it reaches one of the last three. Before delivery starts, remains equals the ordered quantity and start_count is 0 — that is normal, not a stalled order. SMM order status explains each state in plain language.
Poll on a sane schedule: every few minutes for active orders, never in a loop with no sleep.
Step 5: Refill and cancel
action=refill&orders=23501,23510
Returns an array, one entry per order:
[{"order": 23501, "refill": 23501},
{"order": 23510, "error": "Refill not available"}]
The refill ID equals the order ID. Check progress with action=refill_status&refill=23501, which returns Pending, In progress, Completed or Rejected.
Refill requires refill: true on the service, a Completed or Partial order, no previous refill request, and the order to be inside the refill window counted from the order date. Track that deadline in your own system — the API will not remind you.
cancel takes orders (up to 100), works on services with cancel: true while the order is Pending, In progress or Processing, and returns {"order": 23501, "cancel": 1} on acceptance. cancel: 1 means recorded, not finished — poll status for the outcome.
Step 6: Handle errors like you mean it
Match on the exact strings. All return HTTP 200 except the rate limit:
| Error | Usual cause |
|---|---|
Invalid API key | Key wrong, regenerated, or account inactive |
Rate limit exceeded | Over 1,200 per minute (HTTP 429) |
Incorrect service ID | Unavailable service, or drip-feed on a service without it |
Incorrect link | Empty link, or a duplicate active order on that service and link |
Incorrect quantity | Outside min/max, bad run size, or interval outside 0 to 1,440 |
Not enough funds on balance | Top up |
Incorrect order ID | Not yours, or not numeric |
Too many orders (max 100) | Chunk your batch |
Refill not available / Cancel not available | Conditions not met |
Internal error | Retry with backoff, then contact support |
The one that bites hardest in production: if add times out, do not resend blindly. Check status or the Orders page first. A resend can create a duplicate paid order.
Production checklist
Before you call an SMM panel API integration finished:
- Key in a secret store, never in the repository.
- Service catalogue cached and refreshed on a schedule.
- Every response checked for
errorbefore use. - Order IDs persisted against your own references the moment
addreturns. - Status polling batched, scheduled, and stopped at terminal states.
- Refill deadlines tracked locally.
- Balance monitored with an alert threshold, because
addfails rather than queues. - Exponential backoff on 429 and
Internal error.
FAQ
Is the API the same across SMM panels? Broadly yes — most follow the same action names and parameters, which is why swapping providers is usually a config change. Verify the error strings and limits, which do vary. See SMM API provider.
Do API orders cost more than dashboard orders? No. Same rates, same balance.
Can I call the API from the browser? No. The key authenticates full account access, including spending. Server-side only.
How many orders can I check in one call?
Up to 100 IDs via orders, on status, refill and cancel.
Do I need an API key to start? Create a free account, fund the balance, and the key is in your dashboard. Full reference at /smm-api.