SMM Provider API: The Complete Integration Guide for Resellers

How to integrate an SMM provider API properly: endpoint reference, catalog sync, idempotency, polling inside 240 requests per minute, and the bugs that cost money.

Reselling51 min read

An SMM provider API is one HTTP endpoint that accepts a form encoded POST, reads an action parameter, and answers with JSON. That is the entire surface. Seven actions, one key, no OAuth handshake, no webhooks, no SDK you have to audit. A competent developer gets an order placed in about twenty minutes.

The twenty minutes is not the hard part. The hard part is month two, when your storefront has 4,000 open orders, a package service is being sold for three cents because your unit price math was wrong, your poller is eating 429s in a tight loop, and a customer is asking why order 4412 shows "In progress" in your interface and "Canceled" upstream. None of that is in the endpoint documentation, because the endpoint documentation is four tables long and takes ten minutes to read.

This guide is the other part. It covers the engineering that sits around those seven actions: how to sync a catalog whose prices move hourly, how to stay idempotent when the provider gives you no idempotency key, how to design a poller that scales past a few thousand open orders inside a fixed request budget, how to classify errors so your retry logic does not compound a failure, and how to reconcile what you charged against what you were charged. Panel Follows is used as the concrete example throughout, because its reseller API is the standard reseller API shape and every number in it is checkable on the API reference page. If you are integrating with a different provider using the same shape, almost everything here transfers with a changed base URL.

One thing to be honest about before you write a line of code. What this API moves is purchased engagement, not a real audience. It can conflict with the terms of service of the platform it targets, drop is a real behavior and not a bug, and no amount of good engineering changes either fact. Build the integration so it tells your customer the truth quickly, rather than one that hides a failure until they notice it themselves.

What an SMM provider API is and what it gives you that a dashboard does not

An SMM provider API is a machine interface to a provider catalog and order book: it lets your own software list services, place orders, poll their status, request refills, cancel where permitted, and read your balance, without a human ever opening the provider dashboard. That is the definition, and it is worth being precise about it because plenty of marketing uses the word "API" to mean "we will send you a spreadsheet".

The dashboard and the API are not two views of the same thing. They differ in three ways that matter to an integrator.

First, the API has no interface guard rails. Panel Follows blocks a duplicate order in its own order form when the same service and the same link already have a running order. Through the API, that check is not applied. Every validation you want, you write.

Second, the API is where your margin lives. Manual order entry costs a person roughly one to two minutes per order once you count reading the request, picking the service, pasting the link and checking the result. At 500 orders a month that is between eight and seventeen hours of somebody's time. The API makes that number zero and makes 5,000 orders cost the same to process as 500.

Third, the API is the only way to build a product that is actually yours. A dashboard makes you a buyer. An API makes you an operator: you put your own catalog in front of your own customers, apply your own pricing, run your own retry logic, and keep your own record of every order. That is the difference between reselling and having a business, and it is why the reseller panel treats the API as the primary product rather than a bonus feature.

There is a fourth benefit that only shows up after a few months: the API gives you data. Every order you place through it leaves a row in your own database with a service ID, a charge, a start count, a completion time and a final status. After 2,000 orders you can answer questions your provider cannot answer for you, like which of the six Instagram follower services you carry actually completes inside four hours, and which one silently goes partial 15 percent of the time. That data is the raw material for every good decision you make later.

SMM provider API endpoint reference: seven actions and what each returns

The whole SMM provider API surface is one URL and seven values of action. Panel Follows exposes it at POST https://panelfollows.com/api/v2, with a Turkish variant at /api/v2/tr that returns service names, category names, statuses and error messages in Turkish. The same key works on both, and the technical fields stay English on both, so you can serve a Turkish speaking customer base without maintaining a translation table of your own.

The transport rules are short:

  • Method and encoding. POST with application/x-www-form-urlencoded. Responses are JSON. A JSON body or a GET query string is also accepted, which is handy for a browser test, but form encoded POST is what to build against.
  • Authentication. A 64 character hex key, sent as a key field in the request body on every call. There is no header auth, no bearer token and no session. Because the key travels in the body, it is a server side secret and can never appear in browser code.
  • Rate limits. 240 requests per minute per key and 300 per minute per IP. Exceeding either returns HTTP 429 with an error payload.
  • Key rotation. Regenerating the key in the panel invalidates the old one immediately. There is no grace period and no dual key window, so rotate when you can redeploy, not mid shift.

Here is the full action table.

action Required parameters Optional Returns
services key, action none Array of services with service, name, type, category, rate, min, max, refill, cancel, dripfeed
add key, action, service, link, quantity runs, interval, plus type specific fields { "order": 23501 }
status key, action, order or orders none charge, start_count, status, remains, currency
balance key, action none Balance and currency
refill key, action, order or orders none { "refill": 55 }
refill_status key, action, refill or refills none Refill progress
cancel key, action, orders none [{ "order": 123, "cancel": 1 }]

Note what is not in that table, because the absences shape your architecture more than the presences do.

Missing capability Consequence for your integration
No webhooks or callbacks You must poll. Every status change reaches you only when you ask for it
No idempotency key on add A timed out request may or may not have created an order. You solve this yourself
No pagination on services The catalog arrives as one large array. Budget memory and parse time for 3,500+ entries
No per request cost preview The authoritative charge appears on the status response after the order exists, not before
No status code convention beyond 429 A perfectly successful looking 200 can carry { "error": "..." }. Check the error key first, always
No sandbox or test mode Every call you make runs against production with real balance

That last row deserves its own sentence. There is no free test environment. Your integration test suite either runs against a real balance with real minimum quantities, or it runs against a mock you wrote yourself. Both approaches appear later in this guide, and you want both.

Your first three calls to an SMM provider API: balance, services, add

Make these three calls, in this order, before you write any application code. They take ten minutes and they answer every question about whether your credentials, your encoding and your assumptions are right.

Call one, prove the key works. balance is the cheapest possible authenticated request and it has no side effects.

curl -X POST https://panelfollows.com/api/v2 \
  -d "key=YOUR_API_KEY" \
  -d "action=balance"

A working key returns your balance and currency. The currency is always USD on the API regardless of the display currency you picked in the panel, which is one less thing to normalize. If the key is wrong you get the error envelope instead, and this is the moment to write the response handler you will use everywhere:

def call(action, **params):
    r = session.post(ENDPOINT, data={"key": KEY, "action": action, **params}, timeout=30)
    if r.status_code == 429:
        raise RateLimited(retry_after=backoff.next())
    body = r.json()
    if isinstance(body, dict) and "error" in body:
        raise ProviderError(body["error"], action=action, params=redact(params))
    return body

Three things in that function are deliberate. The error check happens before any success parsing, because a 200 response can carry an error. The 429 is separated from every other failure, because it is the only one that should trigger a wait and retry with no further thought. And redact(params) strips the key before the exception message can reach a log line.

Call two, pull the catalog. services returns every active service in one array.

curl -X POST https://panelfollows.com/api/v2 \
  -d "key=YOUR_API_KEY" \
  -d "action=services" > catalog.json

Open that file and read it before you write the importer. Look at how many entries there are, look at the type values you did not expect, and count how many have max set to 1. Those three observations will save you a week. The live human readable version of the same catalog sits on the service catalog page, which is a useful cross check when a field surprises you.

Call three, place one real order. Use the smallest quantity the service allows, on a link you own.

curl -X POST https://panelfollows.com/api/v2 \
  -d "key=YOUR_API_KEY" \
  -d "action=add" \
  -d "service=1" \
  -d "link=https://instagram.com/your_own_test_account" \
  -d "quantity=100"

The success response is { "order": 23501 } and nothing else. No charge, no status, no estimated delivery. Store that order ID immediately, in the same transaction that recorded your intent to order, because that integer is the only handle you will ever have on this order. Then poll it with status every few minutes for an hour and write down what you see. You now know what a normal order looks like on this provider, which is the baseline you need before you can recognize an abnormal one.

Catalog sync: cache the list, never cache the price

Service IDs on a mature SMM provider API are stable and prices are not, so catalog sync is two problems with two different refresh policies. Panel Follows syncs its own rates against upstream cost every hour. The rate you fetched at 09:00 can be wrong at 09:30, while the service ID you fetched in January is still the same service in August.

The failure this causes is undramatic, which is exactly why it goes unnoticed. Your storefront quotes 1.20 USD per 1,000. Upstream cost moves, the provider rate becomes 1.35, and your customer pays 1.20 while you are charged 1.35. Nothing errors. Nothing alerts. You simply lose 0.15 on every order of that service until somebody reconciles the month and asks why the margin on Instagram views went negative.

Here is a sane storage and refresh policy, field by field.

Field from services Store as Refresh cadence Why
service Primary key, integer Never changes Your entire order history hangs off this
name Text, plus your own display name Daily Provider renames happen and break your search if you mirror blindly
type Enum you branch on Daily Controls which extra add parameters are required
category Text, plus your own grouping Daily Provider categories are built for the provider, not for your shop
rate Decimal, with a fetched-at timestamp Hourly, or before every quote This is the number that moves
min and max Integers, validated before submit Daily A quantity outside the range is a wasted round trip
refill Boolean Daily Gates whether you may offer a refill button at all
cancel Boolean Daily Gates whether you may offer cancellation
dripfeed Boolean Daily Gates whether your drip feed fields render

Two practices make this reliable. First, never delete a service row when it disappears from the services response. Mark it inactive instead. Orders in your history still point at it, and a service that vanishes for six hours during an upstream outage should not orphan 300 rows in your database.

Second, keep a price history table with one row per service per observed rate change. It costs almost nothing and it answers the two questions that come up constantly: was this service always this expensive, and when exactly did our margin on it change. For the full commercial treatment of what those numbers do to your business, wholesale SMM provider pricing covers the margin side properly.

A minimum viable sync job looks like this: fetch services nightly, upsert every row, mark missing IDs inactive, log every rate change to history, and separately re-read the rate for any service a customer is actively quoting before you submit their order. That second half is the part people skip, and it is the half that protects the money.

The flat priced package trap that sells a 22 USD package for 3 cents

Rates on an SMM provider API are per 1,000 units for standard services, but package style services with a max of 1 are flat priced, meaning the rate is the price of the entire package and not a per 1,000 figure. Applying the universal per 1,000 formula to those services divides your price by 1,000 and sells a 22 USD package for about 3 cents. This is the most expensive integration bug in this industry and it is completely silent.

The arithmetic makes the size of the hole obvious.

Service shape rate max Quantity ordered Naive rate x qty / 1000 Correct price Loss per order
Standard followers 1.40 100000 2,000 2.80 2.80 0.00
Standard views 0.09 500000 50,000 4.50 4.50 0.00
Package, comments bundle 22.00 1 1 0.022 22.00 21.98
Package, growth bundle 8.50 1 1 0.0085 8.50 8.49
Package, custom comments 14.00 1 1 0.014 14.00 13.99

Sell forty of those in a week and you have handed away roughly 600 USD while your dashboard showed forty successful orders. The provider charged you correctly. Your storefront simply did not charge your customer.

The fix is one function, defined in exactly one place, used by every price calculation you have:

def unit_price(service, quantity):
    # A max of 1 marks a flat priced package: rate is the whole package price.
    if int(service["max"]) == 1:
        return Decimal(service["rate"])
    return Decimal(service["rate"]) * Decimal(quantity) / Decimal(1000)

Two rules make it stick. Never inline the per 1,000 formula anywhere else in the codebase, not even in a report or an admin screen, because the copy you forgot is the one that runs in production. And add a test that fails when a service with max of 1 produces a price below one tenth of its rate, because that assertion catches the regression the day somebody decides to simplify the pricing helper.

A related trap sits on the other side of the same coin. A rounding function that rounds to two decimals can turn a genuinely cheap per unit price into zero, which is how a service ends up free. Round at the total, never at the unit, and assert that no computed price is 0 for a non zero quantity. To see how this looks from the buyer side of the panel, the walkthrough in how to use Panel Follows shows what the price box displays for package services.

See live pricing in the panel

Unit prices for follower, like, view and engagement services are listed live. Registration is free and you can browse the list before adding any balance.

An SMM provider API add call is not one shape. The type field on each service tells you which extra parameters that service requires, and an integration that assumes every order is link plus quantity will fail on every specialized service with a data error while you debug the wrong layer entirely.

Read type from the services response, store it, and branch on it at order time. These are the branches on Panel Follows.

type Extra add parameters Shape of the value
Standard none link and quantity only
Custom comments comments Newline separated list of comment texts
Comment likes username The account whose comment receives the likes
Comment replies username, comments Account plus newline separated replies
Mentions username Target account
Mentions from followers username Source account whose followers get mentioned
Mentions custom list usernames List of accounts to mention
Mentions hashtag hashtag One hashtag
Mentions hashtags usernames, hashtags Both lists
Mentions from media likers media A media URL
Poll answer_number Integer index of the answer to vote for
Group invites groups List of groups
SEO keywords Keyword list
Subscriptions username, posts, min, max Account, post count, and per post quantity range

Model this as a table of required fields keyed by type, not as a chain of if statements. When the provider adds a type you add a row rather than a branch, and your order form can render itself from the same table.

Drip feed is the other place the add call surprises people, and it surprises them financially. With runs and interval set, quantity becomes the amount delivered per run, not the total. It multiplies.

Interpretation quantity runs interval Total delivered Total charged
What the buyer thinks they bought 1,000 5 60 1,000 1x
What actually happens 1,000 5 60 5,000 5x
What they should have entered 200 5 60 1,000 1x

The charge follows the total, so a customer who wanted 1,000 followers spread over five hours and typed 1,000 into a drip feed form gets charged for 5,000. If you are selling this to third parties, compute and display the total quantity and the total price in the form before submit, and validate that quantity multiplied by runs sits inside the service max. Also check the dripfeed boolean before you render the fields at all: a service without the flag rejects runs and interval, and there is no way to force it.

One operational note about interval. It is expressed in minutes, and setting it shorter than the service start time causes runs to pile up on an order that has not begun delivering. A 60 minute interval is a safe default for most services and worth enforcing as a floor in your own form.

The order lifecycle as a state machine you can implement

Model an SMM provider API order as an explicit state machine with terminal and non terminal states, because the alternative, storing the provider status string and rendering it directly, means your interface changes meaning whenever the provider adds a status. Panel Follows returns the standard status set, and every one of them maps onto exactly one of three behaviors in your system: keep polling, stop and settle, or stop and escalate.

Provider status Meaning Terminal Your system should
Pending Accepted, not yet picked up by fulfilment No Poll. Escalate if still Pending past your service specific threshold
Processing Being prepared upstream No Poll. Same threshold logic as Pending
In progress Delivering, remains is decreasing No Poll. Show progress from start_count and remains
Awaiting Queued behind a condition upstream No Poll at a longer interval. Do not alarm the customer yet
Completed Full quantity delivered Yes Settle the charge, stop polling, start the refill eligibility clock
Partial Some delivered, remainder refunded automatically Yes Settle at the actual charge, refund the difference to your customer, notify
Canceled Not delivered, balance refunded Yes Refund your customer in full, stop polling, record the reason if you have one

There is a fourth practical state your system needs that the provider does not have: Submitting. That is the window between your decision to place an order and your receipt of an order ID. It exists for a few hundred milliseconds normally and for thirty seconds when something is wrong, and if you do not model it explicitly you will discover it later as a duplicate order.

The full flow, written as a sequence you can implement:

  1. Write an order row in your database with state submitting, your own reference, the service ID, the normalized link, the quantity and the rate you quoted, all in one transaction, before any network call.
  2. Call add. On success, store the returned provider order ID and move the row to pending.
  3. On a timeout or a network error, do not retry the add. Move the row to submit_unknown and let the reconciler resolve it.
  4. Poll status in batches. Update charge, start_count, remains and the mapped state on every poll.
  5. On a non terminal state, keep polling on your interval schedule.
  6. On a terminal state, settle: write the final charge to your ledger, release or refund the customer hold, and stop polling.
  7. On partial or canceled, run the refund path, which is the path you will test least and need most.
  8. After a terminal completed, if the service carries the refill flag, mark the order refill eligible and start the 24 hour cooldown timer that governs how often a refill can be requested.

Two thresholds deserve to be per service rather than global. The stuck-in-Pending alarm should be based on the observed start time distribution of that specific service, not a flat two hours, because a service that normally starts in six hours will page you every night otherwise. And the poll interval should widen with order age: every two minutes for the first hour, every ten minutes for the first day, hourly after that.

The add action on a standard SMM provider API has no idempotency key, so a request that times out leaves you genuinely unable to tell whether an order was created. Retrying blindly creates two orders and charges you twice. Not retrying loses an order your customer already paid for. Both outcomes are wrong, and the correct answer is to make the ambiguity resolvable rather than to guess at it.

The pattern that works has three parts.

Part one, write your intent first. Before the HTTP call, commit a row containing your own reference, the service, the normalized link, the quantity, the quoted price and a submitting state. Now a crash between the call and the response leaves evidence behind.

Part two, never retry add automatically. Move the row to submit_unknown and hand it to a reconciler. This is uncomfortable to write, because every instinct says retry, and every retry on this endpoint is a coin flip with your own money.

Part three, reconcile by observation. The reconciler runs a minute later and asks a question the API can actually answer: did my balance move by roughly the expected charge. Snapshot the balance before the call, compare after, and you have your answer without guessing.

def place(row):
    before = call("balance")                    # snapshot
    row.mark("submitting", balance_before=before["balance"])
    try:
        resp = call("add", service=row.service_id, link=row.link, quantity=row.quantity)
        row.attach_provider_id(resp["order"])
        row.mark("pending")
    except (Timeout, ConnectionError):
        row.mark("submit_unknown")              # never retry here
        reconcile_later(row)

def reconcile(row):
    after = call("balance")
    moved = Decimal(row.balance_before) - Decimal(after["balance"])
    if moved >= row.expected_charge * Decimal("0.95"):
        row.mark("orphan_created")              # an order exists; find it or credit the customer
    else:
        row.mark("safe_to_resubmit")            # nothing was charged; the retry is now safe

The 0.95 tolerance is there because other orders may settle between your two balance reads. On a busy account, use a short lock around the submit path so the balance delta is attributable to one order, or accept that the reconciler is advisory and needs a human to confirm above a certain value.

The other duplication hazard runs in the opposite direction: the duplicate link lock. The in panel duplicate block, which refuses a second order on the same service and same link while one is running, is not applied to API orders. The upstream lock still applies and it is stricter, because it locks the link regardless of which service you ordered. When it fires, the order is cancelled and the balance is refunded automatically. Your customer sees an order that appears and then cancels itself thirty seconds later, which reads as a broken shop even though no money was lost.

Guard it on your side. Before every add, check your own order table for a non terminal order on the same normalized link, and refuse or queue rather than submit. Normalize the link first, because instagram.com/user, www.instagram.com/user/ and https://instagram.com/user?hl=en are one target to the upstream lock and three different strings to your database. Store the normalized form in an indexed column and check against that, not against whatever the customer pasted.

Building a status poller that fits inside 240 requests per minute

A batched status poller is the only design that scales on an SMM provider API, and the arithmetic decides everything: at 240 requests per minute per key, one request per order caps you at 240 orders per minute, while batching 100 order IDs per request caps you at 24,000. Panel Follows accepts a comma separated orders list on the status action, and batches of 50 to 100 IDs are the practical sweet spot.

Work the budget out properly. Assume you want every open order refreshed within a target window, and that you spend 200 of your 240 requests per minute on polling.

Open orders Batch size Requests per full sweep Sweeps per minute at 200 req/min Refresh interval achieved
500 100 5 40 About 1.5 seconds
2,000 100 20 10 About 6 seconds
10,000 100 100 2 About 30 seconds
25,000 100 250 0.8 About 75 seconds
25,000 50 500 0.4 About 150 seconds

Two conclusions fall out of that table. First, you do not need anything clever until you are past roughly 10,000 simultaneously open orders, which is a real business. Second, unbatched polling breaks at a few hundred orders, which is a hobby. The gap between those two numbers is entirely a function of whether somebody wrote a loop or a batcher.

Leave headroom deliberately. The 40 requests per minute you held back cover order placement, balance checks, refills and cancels, because a poller that consumes the whole quota starves the calls that actually make money.

Then stop polling things that do not need it. A tiered schedule cuts real request volume by an order of magnitude:

priority queue ordered by next_poll_at:
  order age < 1 hour     -> repoll in 120 s
  order age < 24 hours   -> repoll in 600 s
  order age < 7 days     -> repoll in 3600 s
  terminal state         -> never repoll

The implementation is a single worker that pops the due orders, chunks them into batches of 100, issues one status call per chunk, and writes the results back. Keep it single threaded per key. Parallel workers sharing one key turn a comfortable 200 requests per minute into a 429 storm, because each worker only knows about its own rate.

On 429, back off exponentially with jitter: wait 1 second, then 2, then 4, then 8, capped at 60, with a random 0 to 30 percent added to each wait. The jitter matters more than the exponent. Without it, every worker that got a 429 at the same moment retries at the same moment and reproduces the burst exactly. Treat a sustained 429 rate as an alarm rather than a routine condition. If you are hitting the limit consistently, your poll schedule is wrong, not your backoff.

One last piece of realism. Poll results are the only order events you will ever receive, so everything downstream depends on them: customer notifications, refunds, refill eligibility, ledger settlement. That makes the poller the most critical process you run. Give it a heartbeat metric, alert when a sweep has not completed in three times its expected duration, and make sure it restarts cleanly. A poller that dies quietly on a Friday evening means a book of orders nobody settles until Monday.

Resell the same services at your own price

Send orders from your own site through the reseller API and set your own margin. You can also run a child panel under your own brand.

Error taxonomy: retryable, not retryable, and needs a human

Classify every SMM provider API error into exactly one of three buckets before you write any retry logic, because the default behavior of most HTTP clients, retry on failure, is correct for one bucket and actively harmful for the other two. Errors arrive in the same envelope as successes, as { "error": "Incorrect data" }, with no distinguishing status code beyond the 429.

Condition Typical cause Bucket What to do
HTTP 429 Over 240 requests per minute per key, or 300 per IP Retryable Exponential backoff with jitter, resume, alarm if sustained
Timeout on add Network, not the provider Ambiguous Never auto retry. Mark submit_unknown and reconcile by balance
Timeout on status, balance, services Network Retryable Safe to retry immediately, these are read only
Incorrect data Bad parameter: quantity out of range, missing type specific field, malformed link Not retryable Fix the request. Log the redacted payload so you can see which field
Insufficient balance Balance below the order charge Needs a human Pause the submit queue, alert, top up. Retrying only wastes quota
Refill not available for this service Service refill flag is false Not retryable Do not offer refill for that service at all
Cancel not available for this service Service cancel flag is false Not retryable Route the customer to a support path instead
Account is banned Provider account state Needs a human Stop all traffic on that key immediately and contact support
Account is suspended Provider account state, or a child panel balance exhausted Needs a human Stop traffic and resolve the account condition
Order cancelled and refunded seconds after add Upstream duplicate link lock on that target Not retryable Enforce your own link lock. Resubmitting trips it again
Unexpected JSON shape Provider changed something Needs a human Alert with the raw body. Do not silently swallow it

The taxonomy has one rule that catches most bugs: anything caused by your own request is not retryable. A retry of an invalid request is still an invalid request. All you achieve is spending quota and filling logs while the customer waits.

The "needs a human" bucket deserves a real escalation path, not a log line. An insufficient balance error in particular is a business emergency dressed as an error string: every order in your queue is now failing, your customers are paying you for things you are not buying, and every minute of it is refund liability. Pause the submit queue on the first occurrence rather than the fiftieth, and page somebody.

One subtlety worth building for. When you get an error string you have never seen before, the correct behavior is to fail that one order, not to stop the world. Provider error text changes without notice, and an unrecognized string should fail one order loudly rather than crash the worker that is processing 2,000 others.

Refill and cancel are gated by per service flags, so gate your product the same way

Refill and cancel are per service capabilities on an SMM provider API, exposed as the refill and cancel booleans on the services response, and there is no override. Calling refill on a service without the flag returns an error, and calling cancel on one without it does the same. Your product has to respect the same gate, because a refill button that fails is worse than no refill button.

On Panel Follows both actions go straight to the provider with no admin approval, from the panel and from the API alike. That is a meaningful design choice: a refill request submitted at 2am by your automation is processed the same way as one a human submits at 2pm. The exception is a manual, non automated service, where an admin approves.

The mechanics you need to encode:

  • Refill has a 24 hour cooldown per order. A second request inside the window is rejected. Store last_refill_requested_at on your order row and enforce the cooldown in your own interface, so your customer sees "available in 6 hours" rather than an error.
  • refill returns a refill ID, in the shape { "refill": 55 }. That ID is a separate object with its own lifecycle. Poll it with refill_status and store it against the order, because a customer asking whether their refill worked is asking about the refill ID, not the order ID.
  • Cancel refunds automatically. Where the service supports it, a cancelled order returns the balance, and a partial order refunds the undelivered remainder without you asking. Your ledger has to expect money arriving back.
  • Store both flags at import time. Deciding whether to show a refill button by attempting the call and catching the error is the wrong shape. You already have the boolean.

There is a commercial dimension here that is easy to miss while thinking in code. Services with a refill flag carry a refill window, and services without one carry no drop guarantee at all, which means no refund for drops. If your storefront presents both kinds identically, you are manufacturing your own support queue. Surface the flag as a product attribute, in the listing and on the order confirmation, in plain language. The mechanics of drop and refill from the customer side are covered in why followers drop and how refills work, which is worth linking from your own help pages rather than rewriting badly.

A refill window is a promise about a window, not a promise about permanence. Do not let your interface copy inflate it into a guarantee, because your customers will hold you to whatever your interface said, not to whatever the provider actually offered.

Reconciling charges, partials and automatic refunds against your own ledger

Reconcile every order against the provider charge field rather than against the price you quoted, because those two numbers diverge routinely and the difference is your actual margin. The status response returns charge and currency, and that charge is the authoritative amount deducted from your balance. Your quoted price is a forecast; the charge is the fact.

Three mechanisms cause divergence, and all three are normal rather than exceptional.

The first is price drift. The rate moved between your quote and your submission. On a service that was 1.20 and became 1.26, a 5,000 unit order costs you 6.30 instead of the 6.00 you planned. Small, constant, and invisible unless you compare.

The second is partial delivery. A partial order delivers some of the quantity and refunds the remainder automatically. If your customer paid you for 5,000 and 3,200 arrived, the provider charged you for 3,200 and you owe your customer the difference. Your system has to compute and execute that refund, because nobody upstream knows your retail price.

The third is cancellation refunds. A cancelled order, whether cancelled by you or by the upstream duplicate link lock, returns the money to your balance. That is an unsolicited credit arriving in your account, and any reconciliation logic that assumes balance only decreases will drift.

A ledger design that survives all three keeps four amounts on every order row.

Amount Written when Source Used for
quoted_price At quote time Your pricing function What the customer was shown and agreed to
customer_paid At checkout Your payment system Revenue
provider_charge On first status response Provider charge field Cost of goods sold
settled_at On terminal state Your settlement job Marks the row as final and reconcilable

Run a daily job that sums provider_charge for orders settled that day, compares it against the actual movement in your provider balance over the same period, and alerts when the two differ by more than a small tolerance. This job is boring, it takes an afternoon to write, and it is the only thing that catches a class of bug where money quietly leaves without a matching order. I have seen a reconciliation job find a 300 USD gap that turned out to be a retry loop placing a second copy of an order whose response had timed out, running for eleven days before anyone noticed.

Keep the tolerance tight enough to be useful. If you allow a 5 percent daily variance on a 2,000 USD spend, you have permitted 100 USD of undetected leakage per day. A 0.5 percent tolerance with a manual review of every breach is a better trade, and the breaches become rare within a couple of weeks of fixing the first three.

One more habit worth adopting from day one: store the raw JSON of the first and last status response on each order. It costs a few kilobytes per order and it turns every future dispute from an argument into a lookup. When a client asks what the start count was before delivery began, you have the number rather than a theory.

Secrets, logging and the observability you will want at 3am

An API key that travels in the request body has one specific security property that changes how you handle it: it is invisible to any proxy or gateway that logs URLs, and it is visible to anything that logs request bodies. Most default logging configurations log bodies. That is the whole threat model in two sentences.

The rules that follow from it:

  1. Server side only, always. The key never reaches browser JavaScript, a mobile app bundle, or any client you do not control. If your storefront needs to place an order, it calls your backend, and your backend calls the provider.
  2. Redact at the serialization boundary, not at the log call. Write one function that renders request parameters for logging, strip key inside it, and use it everywhere. Relying on every developer to remember at every log line is a plan that fails on the day someone adds a debug line at 3am.
  3. One key per environment, and preferably per worker role. Regenerating the key invalidates the old one immediately with no grace period, so a shared key means every service using it dies at the same instant during a rotation. If you must share, at least make the rotation a deliberate, scheduled deploy rather than a fix attempted during an incident.
  4. Store it in a secret manager or an environment variable, never in the repository. This is obvious and it is still the most common way keys leak, usually through a committed .env file or a screenshot in a support chat.
  5. Rotate on a schedule and on any suspicion. The key is 64 hex characters and there is no expiry, which means a leaked key works forever until you rotate it.

Observability is the other half. When an order stalls, the question is always the same: what did we send, what came back, and when. Three log streams answer it.

The request log records action, redacted parameters, HTTP status, latency and response size for every call. The order event log records every state transition with a timestamp and the poll response that caused it, which is what lets you reconstruct an order's history months later. The money log is your ledger, and it should be append only.

The metrics worth alerting on are short:

  • Requests per minute against the 240 per key limit, with a warning at 80 percent.
  • 429 rate, with an alert on any sustained occurrence rather than on a threshold.
  • Poller sweep duration and time since last completed sweep.
  • Orders stuck in a non terminal state past their service specific threshold, as a count.
  • Provider balance, with an alert at your reorder point rather than at zero.

That last one is worth more than it looks. Running out of balance turns every order into an error, and errors caused by an empty balance are indistinguishable at the customer level from a broken integration. Alert at the point where you have four hours of typical spend left, not at the point where you have none. If you are running orders on behalf of clients rather than a public shop, the operational shape is a little different and the agency workflow is written around that use.

Staging, going live, and a day one to day thirty plan

There is no sandbox on a standard SMM provider API, so your staging strategy is two layers: a recorded mock for automated tests, and a small real balance for the tests that must be real. Build the mock from actual captured responses rather than from the documentation, because the documentation describes the intent and the capture describes the behavior.

The mock covers everything deterministic: response parsing, the error key check, type branching, price calculation, state transitions, batching logic and backoff timing. Feed it recorded payloads including the ugly ones, a 429 body, an Incorrect data response, a partial order status, a cancel response with a per order error. Those are the paths your unit tests need and they cost nothing to run.

The real balance covers everything the mock cannot prove: that your form encoding is right, that the provider accepts your parameter names, that a real order actually starts, that a refill request on a real drop actually returns a refill ID. Budget 20 to 50 USD for this. Place orders on accounts you own, at the service minimum, and keep every response.

Here is a realistic thirty day plan for a team of one or two.

Days Goal Concrete deliverable Done when
1 Prove access balance and services calls succeed from your server Catalog JSON saved and inspected
2 to 3 Catalog import Services table with all nine fields, plus price history Nightly sync job runs and logs rate changes
4 to 5 Pricing Single unit_price function with the flat package rule and its test Test suite fails on a mispriced package
6 to 8 Order placement add for standard type, intent row written before the call One real order placed and stored end to end
9 to 11 Poller Batched status with tiered schedule and 429 backoff 100 test orders tracked to terminal state
12 to 14 State machine Full mapping including partial and canceled, with refund path Forced partial refunds a customer correctly
15 to 17 Type branching Required field table, form rendering, validation before submit One order placed on a specialized type
18 to 20 Refill and cancel Flag gated actions, refill ID storage, 24 hour cooldown Refill requested on a real drop and tracked
21 to 23 Error handling Full taxonomy, escalation for the human bucket, alerting Balance exhaustion pauses the queue automatically
24 to 26 Reconciliation Daily charge versus balance movement job Job runs clean for three consecutive days
27 to 28 Observability Request log, order event log, five metrics, alert routing An alert fires in a drill and reaches a person
29 to 30 Soft launch Real customers on a subset of services 50 real orders through with no manual intervention

Two notes on sequencing. Do not open the storefront before the reconciliation job runs, because launching without it means discovering your pricing bugs from your bank balance. And soft launch on a narrow catalog, twenty services you understand, rather than all 3,500+, because the first week of real traffic is for finding out what you got wrong and a narrow surface makes that legible.

Panel Follows requires no reseller package, no membership fee and no minimum volume to reach any of this: the API, mass order, drip feed, subscriptions and the child panel program all sit on an ordinary free account, so day one costs whatever you decide to deposit and nothing else.

Wiring a second SMM provider API behind one abstraction layer, and when not to

A provider abstraction layer is worth building the day you have a concrete reason to route an order to a different source, and not one day earlier. That is the honest version of the advice, and it contradicts the usual engineering instinct to abstract early. Premature abstraction on an SMM provider API is expensive in a specific way: you end up designing for the lowest common denominator of two APIs, and you lose the features that made your primary provider worth using.

When it is genuinely warranted, the shape is a narrow interface with four methods and a router.

interface SupplySource:
    list_services() -> [ServiceSpec]        # normalized: id, type, unit_price_fn, flags
    place(order: OrderIntent) -> ExternalRef
    fetch_status(refs: [ExternalRef]) -> [OrderState]
    request_refill(ref: ExternalRef) -> RefillRef | Unsupported

Everything provider specific lives behind that boundary: the action parameter names, the form encoding, the flat package rule, the type branching, the error string mapping. Your application never sees a provider error string. It sees your own enum.

The routing layer then decides, per order, which source to use. Sensible inputs are your own measured completion rate for that service on that source over the last 30 days, current unit cost, whether the source supports the refill flag your customer was promised, and a circuit breaker that removes a source after a run of failures and probes it back gradually.

Now the counter case, because it matters more than the design.

Most resellers should not build this. A second provider doubles your catalog mapping work, doubles your error taxonomy, doubles your reconciliation surface and roughly doubles the ways an order can go wrong, all in exchange for redundancy you may never need. If your primary provider has been stable for six months, the abstraction layer is a project you are doing instead of getting customers.

Two providers can be worse than one. Every added hop in the supply chain adds latency and adds a party who cannot answer a question about your order. Routing a customer's order to whichever source is cheapest this hour means their refill behavior, start time and drop profile change between orders on the same service, which is precisely the inconsistency your customers will complain about. Buying from a shorter chain is usually a bigger win than routing across a longer one.

The failure it protects against is often not the failure you get. People build failover for provider downtime. The failures that actually hurt are a service quietly degrading, a rate moving against you, and an account state error, and none of those are solved by a second base URL.

The reasonable middle path is to build the interface, implement exactly one source behind it, and keep the second integration warm as documentation rather than as running code. You get the decoupling benefit and none of the operational cost, and the day you actually need a second source you are a week away rather than a quarter. How to evaluate that second source when the day comes is the subject of how to find an SMM provider, and if you are wiring this up in order to run your own branded panel on top, how to become an SMM provider covers what gets built above the API layer.

The five integration mistakes that cost real money

Every expensive SMM provider API bug I have seen falls into one of five patterns, and every one of them is silent: the code does not throw, the dashboard looks normal, and the loss shows up in a monthly reconciliation or a customer complaint. Here they are with the cost and the guard.

Mistake How it presents Typical cost The guard
Per 1,000 formula applied to a flat priced package Orders succeed, revenue looks tiny 8 to 22 USD per order, unbounded One unit_price function with the max of 1 branch, plus a test
Drip feed quantity treated as the total Customer charged 5x what they expected 4x the order value per incident, plus a refund Compute total from quantity multiplied by runs and show it before submit
Quoting from a cached rate Margin silently negative on drifted services Small per order, continuous Re-read the rate before submit, alert on negative margin
Retrying add after a timeout Duplicate orders, double charges Full order value per duplicate Intent row, submit_unknown state, reconcile by balance movement
Not checking the error key on a 200 Orders recorded as placed that never existed Full order value, plus a support incident Check error before parsing any success shape

There is a sixth that costs time rather than money and belongs in the same list: retry storms. A worker that treats 429 as a generic failure and retries immediately turns a brief rate limit into a sustained one, and because the retries are also counted, the loop is self sustaining until somebody kills the process. Backoff with jitter is not a nicety here, it is the difference between a thirty second blip and an hour of downtime.

The pattern behind all six is worth naming, because it generalizes to whatever the next bug is. This API fails by succeeding. It returns 200 with an error inside, it accepts a wrong price without complaint, it accepts a drip feed order that costs five times what the customer intended, and it accepts your duplicate. Nothing in the transport layer tells you that anything is wrong. Every assertion has to be yours, which means your integration needs more validation than an API with proper status codes would require, not less.

Write the assertions as tests that run in CI, not as checks you perform manually before a deploy. A test that fails when a package price drops below a floor, when a drip feed total exceeds the service max, when a computed price is zero, or when an order row reaches pending without a provider ID, costs an hour to write once and catches the same class of bug forever.

Open your account and order in minutes

Signing up is free and takes two steps. Top up with card, bank transfer or crypto, place your order and track delivery from the dashboard.

Frequently asked questions

What is an SMM provider API?

An SMM provider API is a machine interface that lets your own software list a provider catalog, place orders, poll order status, request refills, cancel orders where permitted and read your balance, without a human opening the provider dashboard. On Panel Follows it is one endpoint, POST https://panelfollows.com/api/v2, that accepts form encoded parameters including a key and an action, and returns JSON. Seven actions cover the whole surface: services, add, status, balance, refill, refill_status and cancel. Most panel software connects to this shape with nothing more than a changed base URL and key.

How do I connect my SMM panel to a provider API?

Generate an API key in the provider panel, store it as a server side secret, and point your panel software at the provider endpoint with that key. In practice the sequence is: call balance to confirm authentication, call services to import the catalog, map the provider service IDs onto your own catalog rows with their type, rate, min, max and the refill, cancel and dripfeed flags, then implement add and batched status polling. Most standard panel software has a provider settings screen where the endpoint URL and key are the only two fields you fill in.

Why does the API charge more than the price I quoted?

Because provider rates track upstream cost on an hourly sync, so the rate can change between the moment you quote a customer and the moment you submit the order. The authoritative amount is the charge field on the status response, not the rate you cached. Re-read the rate immediately before submitting any order, keep a small buffer in your retail markup to absorb drift, and run a daily job that compares the sum of provider charges against the movement in your provider balance. If those two numbers disagree by more than a fraction of a percent, something in your pricing or retry logic is wrong.

How do I avoid placing duplicate orders through the API?

Write an intent row in your own database before the HTTP call, never retry add automatically after a timeout, and reconcile the ambiguous cases by checking whether your balance moved. A standard SMM provider API has no idempotency key, so a timed out add may or may not have created an order and a blind retry can charge you twice. Separately, guard against same link orders on your side: the in panel duplicate block is not applied to API orders, and the upstream lock is stricter because it locks the link regardless of which service you ordered.

What is the rate limit on the Panel Follows API?

240 requests per minute per key and 300 requests per minute per IP. Exceeding either returns HTTP 429 with an error payload rather than a silent drop. A polling loop that makes one request per order hits the per key limit long before the per IP one, which is why batched status calls with a comma separated orders list are the correct pattern. Batches of 50 to 100 IDs keep several thousand open orders in sync comfortably inside the budget. Reserve roughly 40 requests per minute for order placement and balance checks so polling cannot starve them.

How should I poll order status without hitting the rate limit?

Batch the order IDs and tier the schedule by order age. Send 50 to 100 IDs per status call, poll orders under one hour old every two minutes, orders under a day old every ten minutes, older open orders hourly, and terminal orders never. With 100 IDs per request and 200 requests per minute spent on polling, 10,000 open orders refresh roughly every 30 seconds. Run one poller per key rather than parallel workers, because workers that share a key each track only their own rate and collectively produce 429s.

Why is my order canceled and refunded immediately after I place it?

Almost always because the upstream duplicate link lock fired: another order on the same link was still running, and that lock applies regardless of which service you ordered. When it triggers, the order is cancelled and the balance is refunded automatically, which is why nothing appears to be lost even though the customer sees a failure. Resubmitting will trip the same lock. Fix it by enforcing your own link lock before every add, checking your order table for a non terminal order on the same normalized link, and queueing rather than submitting when one exists.

What does a type of "package" mean for pricing?

A package style service has a max of 1 and is flat priced, so the rate is the price of the whole package rather than a price per 1,000 units. Applying the standard per 1,000 formula to it divides the price by 1,000 and sells a 22 USD package for about 3 cents. Branch on max being 1 in a single shared pricing function, never inline the per 1,000 formula anywhere else, and add a test that fails when a package price falls below a floor. This is the single most expensive silent bug in SMM panel integrations.

Do refill and cancel work on every service?

No. Both are per service capabilities, exposed as refill and cancel booleans on the services response, and there is no way to force either one. Calling refill on a service without the flag returns an error. Store both flags at import time and gate your own interface on them, so a customer never sees a button that cannot work. Refill also carries a 24 hour cooldown per order, and returns a refill ID in the shape { "refill": 55 } that you track separately with refill_status. Services without a refill flag carry no drop guarantee at all.

Do I need a special account or a reseller plan to get API access?

Not on Panel Follows. The API, mass order, drip feed, subscription services and the child panel program all sit on an ordinary free account, with no reseller package, no membership fee and no minimum volume. The list price is the reseller price, so there is no separate wholesale tier hidden behind a sales call. You generate a key in the panel, deposit whatever balance you want to start with, and the same catalog and rates you see in the interface are the ones the API returns.

What to build first

If you only have a week, build these four things in this order and skip everything else: the shared unit_price function with the flat package rule, the intent row written before every add, the batched status poller with tiered intervals and 429 backoff, and the daily reconciliation job. Those four cover the mistakes that cost real money. Everything else in this guide makes the integration better; those four keep it from losing you money while you sleep.

The order matters. Pricing first, because a pricing bug loses money on every single order from the moment you launch. Then the intent row, because duplicate charges are the second most expensive failure and the hardest to detect after the fact. Then the poller, because without it you have no events at all and your customers find out about failures before you do. Then reconciliation, because it is the only mechanism that tells you the other three are working.

What you build on top of that is a business decision rather than an engineering one. Some resellers stop at an internal tool that places orders for their agency clients. Some build a public storefront. Some take the white label child panel route, put their own brand on their own domain, connect their own payment accounts and keep their markup entirely on their side of the transaction. The API is the same in all three cases, which is the point: the integration you write in month one is still the integration you are running in year two.

A last word on expectations. A good integration does not make purchased engagement into a real audience, does not remove drop risk, and does not put you outside the terms of service of the platforms involved. What it does is make the failures visible fast, settle the money correctly, and let one person run a volume of orders that would otherwise need five. That is a genuinely valuable thing to build, and it is worth building carefully. When you are ready to start, create an account, generate a key, and make the balance call.

You have read the guide, now run it

Create your free account, top up with card, crypto or bank transfer and place your first order within minutes.