On this page
- Overview
- Getting started
- Automate with an agent
- Authentication
- Tokens & pricing
- SMM panel API
- services
- add
- status
- balance
- cancel
- Platform API
- Catalog
- Account
- Two-factor & sign-in
- Preferences & alerts
- Campaigns
- Placing an order
- Editing an order
- Logs & analytics
- Vote & proxy profiles
- Discord tokens
- Billing & payments
- Saved cart
- Internal surfaces
- Response shapes
- Site object
- Campaign object
- Campaign state
- Progress & refunds
- End-to-end example
- Errors
- Rate limits
- Limits & notes
Overview
Toplistbot exposes two HTTP APIs. Both are JSON over HTTPS, both spend the same token balance, and either one is enough to run campaigns without ever opening the dashboard.
SMM panel API
One Perfect Panel compatible endpoint. If your panel already speaks the standard SMM protocol, point it here and it works with no code changes.
Platform API
The REST API behind the dashboard: browse the catalog, create and steer campaigns, read per-vote delivery logs, manage profiles, proxies and invoices.
Base URL
https://backend.toplistbot.com/api
https://backend.toplistbot.comReference below is written against these two hosts. Which one you use decides how you authenticate — see Authentication.
Getting started
From a new account to a running campaign in five steps. Everything below uses the SMM endpoint, which is the quickest way in; the platform API works the same way once you have a JWT.
Create an account
Sign up and verify your email. Verification credits your balance with 100 free tokens, which is enough to run a real campaign before you spend anything.
Copy your API key
Open your dashboard and generate an API key. Treat it like a password — it spends your token balance. You can rotate it at any time, which immediately invalidates the old one.
Find the service you want
List every site you can order on. Each entry has a numeric service id and a rate in tokens per 1,000 actions. Note the id of the site you want to promote on.
cURLcurl -X POST https://backend.toplistbot.com/api/v2 -d "key=YOUR_API_KEY" -d "action=services"Place your first order
Send the service id, the URL your campaign should run against, and how many actions to run. The cost is deducted immediately and the response gives you an order id.
cURLcurl -X POST https://backend.toplistbot.com/api/v2 \ -d "key=YOUR_API_KEY" \ -d "action=add" \ -d "service=9" \ -d "link=https://arena-top100.com/index.php?a=in&u=yourserver" \ -d "quantity=1000"Track delivery
Poll the order id to see how much has been delivered. Once you are happy with the flow, wire the same calls into your own panel or scripts.
cURLcurl -X POST https://backend.toplistbot.com/api/v2 -d "key=YOUR_API_KEY" -d "action=status" -d "orders=184223"
Connecting a Perfect Panel instance
If you run Perfect Panel or compatible SMM panel software, you do not need to write any code — add Toplistbot as a provider with these settings and import the service list.
- API URL
- https://backend.toplistbot.com/api/v2
- API key
- YOUR_API_KEY
- HTTP method
- POST
Start with a small quantity on one site to confirm your link format is accepted before scaling up. A wrong link still costs tokens.
Automate with an AI agent
This page has a plain-text twin written for machines. Give an agent that URL and your API key and it has everything it needs: the full endpoint list, request and response shapes, pricing arithmetic, error codes and worked examples.
Machine-readable reference
One document, no authentication, no JavaScript. Fetch it, paste it into a prompt, or hand the URL to a tool that can browse.
https://toplistbot.com/llms.txtStarting prompt
Paste this into Claude or any agent that can make HTTP requests. Keep the key in an environment variable rather than in the message itself.
Read https://toplistbot.com/llms.txt — it is the complete Toplistbot API reference.
My API key is in the TOPLISTBOT_KEY environment variable. Using the API-key
surface (paths without the /api prefix):
1. list the sites in the catalog that cost under 20 tokens per 1,000 votes
2. tell me my token balance
3. propose a campaign for <my vote URL> that fits a budget of <N> tokens
Do not place the order until I confirm the cost.An API key is the right credential for an agent: it never expires, it is unaffected by two-factor authentication, and rotating it from the dashboard revokes access instantly if you ever need to.
Authentication
There are two credentials, and which one you need depends on the path rather than the endpoint. Almost every endpoint is mounted twice.
The /api prefix decides the credential
The same handler sits behind both paths. Drop the /api prefix and the platform API accepts a long-lived API key; keep it and the endpoint expects a JWT from a login.
| Path | Credential | Use it for |
|---|---|---|
| /api/orders/getAll | JWT | Anything a human signs into |
| /orders/getAll | API key | Scripts, cron jobs, agents |
For automation, prefer the un-prefixed paths. There is no login, no expiry and no session to keep alive — one key does everything, and two-factor authentication never gets in the way.
API key
Send your key as a `key` field on any request — a query parameter, a form field, a JSON field, or an `Authorization: Bearer` header. Generate and rotate it from the dashboard. A GET to /api/v2 returns a status of ok and is a cheap way to check a key is live.
# any of these three carry the key
curl "https://backend.toplistbot.com/orders/getAll?key=YOUR_API_KEY"
curl -X POST https://backend.toplistbot.com/orders/pause -d "key=YOUR_API_KEY" -d "id=184223"
curl https://backend.toplistbot.com/orders/getAll -H "Authorization: Bearer YOUR_API_KEY"curl https://backend.toplistbot.com/api/v2?key=YOUR_API_KEYJWT
Log in to receive a token, then send it as a bearer token on /api routes. Tokens expire, so call /auth/refresh before they do. If the account has two-factor authentication on, login also needs a `two_factor_code`.
curl -X POST https://backend.toplistbot.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"..."}'{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"user": { "id": 4211, "email": "[email protected]", "tokens": 528.41, "...": "..." }
}curl https://backend.toplistbot.com/api/orders/getAll \
-H "Authorization: Bearer YOUR_JWT"Calling from a browser
Every route answers with Access-Control-Allow-Origin set to a wildcard, so a page, a browser extension or a browser-based agent can call the API directly — no proxy of your own required. The warning above still stands: a key you ship to a browser is a key you have published, so this is for your own tools, not for a public page.
// Works from a page, an extension, or a browser-based agent.
const sites = await fetch(
'https://backend.toplistbot.com/orders/getAllWebsites'
).then(r => r.json())Your API key spends real token balance. Keep it server-side: anything shipped to a browser or committed to a repository should be treated as compromised and rotated from the dashboard.
Tokens & pricing
Campaigns are paid for in tokens, bought up front. Every site publishes a rate — the tokens it costs to run 1,000 campaign actions there — returned as `rate` by the services action.
cost_in_tokens = (rate * quantity) / 1000A site with a rate of 13 costs 13 tokens for 1,000 actions, so an order of 500 costs 6.5 tokens. The cost is deducted when the order is accepted, and cancelling refunds the unspent remainder.
The balance and status responses report a currency field of USD for Perfect Panel compatibility, but the value is a token balance, not dollars. Treat the number as tokens.
SMM panel API
One endpoint handles everything. Send an `action` field with each POST to choose the operation; every request also carries your `key`.
https://backend.toplistbot.com/api/v2| Action | Parameters |
|---|---|
services | — |
add | service, link, quantity, interval? |
status | orders |
balance | — |
cancel | orders |
`refill` and `refill_status` are accepted for compatibility and both answer "not implemented". Nothing here is refillable; re-order instead.
action=services
Lists every site you can order on, with its current rate and limits. Use the `service` id in your add calls.
curl -X POST https://backend.toplistbot.com/api/v2 \
-d "key=YOUR_API_KEY" \
-d "action=services"[
{
"service": 9,
"name": "arena-top100.com 1000 upvotes",
"type": "Default",
"category": "Votes",
"rate": 15,
"min": 1,
"max": 50000,
"refill": false,
"cancel": true
}
]`rate` is tokens per 1,000 actions. `min` is 1 and `max` is 50000 for every service.
action=add
Creates a campaign and immediately deducts its cost from your balance.
| Parameter | Type | Description |
|---|---|---|
keyrequired | string | Your API key. |
actionrequired | string | Must be `add`. |
servicerequired | integer | Service id from the services action. |
linkrequired | url | The URL the campaign runs against. Must be a valid URL. |
quantityrequired | integer | Number of actions to run, between 1 and 50000. |
interval | integer | Actions per hour. Defaults to 15, capped at 4000, and cannot exceed the site's own maximum. |
curl -X POST https://backend.toplistbot.com/api/v2 \
-d "key=YOUR_API_KEY" \
-d "action=add" \
-d "service=9" \
-d "link=https://arena-top100.com/index.php?a=in&u=yourserver" \
-d "quantity=1000" \
-d "interval=60"{
"order_id": 184223
}action=status
Returns progress for one or more orders. Pass a single id for a flat object, or a comma-separated list.
curl -X POST https://backend.toplistbot.com/api/v2 \
-d "key=YOUR_API_KEY" \
-d "action=status" \
-d "orders=184223"{
"charge": 13.5,
"start_count": 0,
"status": "Completed",
"remains": 1000,
"currency": "USD"
}With several ids the response is keyed by order id, and unknown or foreign orders return an error entry rather than failing the whole request.
{
"184223": { "charge": 13.5, "start_count": 0, "status": "Completed", "remains": 1000, "currency": "USD" },
"184224": { "error": "Incorrect order ID" }
}Read `remains`, not `status`
`status` is always the literal "Completed". The field exists because every Perfect Panel client demands it, and panels treat any other value as a refill candidate — which this platform does not offer. Progress lives in the numbers: `remains` is the accepted votes still to deliver, so `remains == 0` means the order has finished. `start_count` is what has been delivered so far and `charge` is what it has cost. Both are measured against the site's accept rate, so they count votes you were sold rather than raw attempts.
`status` and `cancel` accept at most 100 order ids per call. Batch rather than looping — one call with 100 ids is far cheaper for both sides than 100 calls.
action=balance
Returns your remaining token balance.
curl -X POST https://backend.toplistbot.com/api/v2 \
-d "key=YOUR_API_KEY" \
-d "action=balance"{
"balance": 528.41,
"currency": "USD"
}action=cancel
Stops an order and refunds the unspent remainder to your balance. Completed orders cannot be cancelled.
curl -X POST https://backend.toplistbot.com/api/v2 \
-d "key=YOUR_API_KEY" \
-d "action=cancel" \
-d "orders=184223,184224"[
{ "order": "184223", "cancel": 1, "refund": 4.5 },
{ "order": "184224", "cancel": { "error": "Incorrect order ID" } }
]Platform API
The same REST API the dashboard uses. Paths below are written in their API-key form, without the /api prefix. Prepend /api and swap the key for a JWT to use the session form; endpoints marked JWT exist only under /api.
Catalog & discovery
Public, no credential needed. getAllWebsites is the endpoint to start from — it carries the id, price, hourly ceiling and accept rate you need to price and shape an order.
- GET
/orders/getAllWebsitesPublicThe full catalog — every site with rates, limits and metadata - GET
/orders/getAllBasicWebsitesDetailsPublic20 random site names, for widgets and autocompletes - POST
/orders/getWebsiteDetailsByNamePublicOne site by its exact name - POST
/products/getSuggestionsPublicSites related to a set of ids - GET
/products/demand?days=30PublicHow much each site has been ordered recently - GET
/products/tokensPublicToken packages you can buy - POST
/products/suggestAPI keyAsk us to list a new site - GET
/api/news/timelinePublicProduct changelog
curl "https://backend.toplistbot.com/orders/getAllWebsites"Ask for less
The full catalog is about 665 KB across 396 sites, and two fields you will never order with account for half of it: a stored popularity blob and the marketing description. Project down to the ordering fields and drop inactive sites and it becomes about 40 KB.
# The whole catalog is ~665 KB across 396 sites.
# Projected to what you actually order with: ~40 KB.
curl -s "https://backend.toplistbot.com/orders/getAllWebsites" \
| jq '[.[]
| select(.active == 1)
| {id, name, price_per_1000, max_per_hour, accept_rate, subscribeable}]'For an AI agent that is the difference between roughly 170,000 tokens and 10,000 — between the first call working and the first call exhausting the context window. Project before you parse.
Account & sessions
Registration needs a browser: it is gated by a Cloudflare challenge. Sign up once at app.toplistbot.com, then automate everything after it.
- POST
/api/auth/registerPublicCreate an account — browser only, gated by a captcha - POST
/api/auth/loginPublicExchange credentials for a JWT - POST
/api/auth/refreshJWTIssue a fresh JWT from an expiring one - POST
/api/auth/logoutJWTInvalidate the current JWT - GET
/api/auth/user-profileJWTThe signed-in account, with balance and API key - GET
/api/userJWTThe same user object, under a shorter path - GET
/api/api_tokenAPI keyResolve an API key to its owner — use it to validate a key - POST
/api/auth/reset-api-keyJWTRotate your API key; the old one dies immediately - POST
/api/auth/fingerprintJWTRecord a browser fingerprint against the account - POST
/api/auth/ipJWTRecord the account's current IP - POST
/api/auth/forgot-passwordPublicEmail a reset link, valid 60 minutes - POST
/api/auth/reset-passwordPublicSet a new password with the emailed token
Two-factor & sign-in
Two-factor protects the password login. It does not apply to API keys, which is why a key is the better credential for unattended work.
- POST
/api/2fa/enableJWTStart enrolment — returns the secret, QR URL and recovery codes - POST
/api/2fa/verifyJWTConfirm a six-digit code and switch two-factor on - POST
/api/2fa/disableJWTSwitch two-factor off - POST
/api/account/verification/requestJWTEmail a verification link to the signed-in address - GET
/api/account/verification/confirm?token=PublicRender the confirmation page — writes nothing - POST
/api/account/verification/confirmPublicCommit the verification - GET
/api/auth/googlePublicStart Google sign-in - GET
/api/auth/google/callbackPublicGoogle sign-in callback - GET
/api/auth/discordPublicStart Discord sign-in - GET
/api/auth/discord/callbackPublicDiscord sign-in callback
Preferences & alerts
Email and notification switches, and the account's alert feed.
- GET
/api/user/email-preferencesJWTMarketing email opt-in state - POST
/api/user/email-preferencesJWTChange it - GET
/api/user/notification-preferencesJWTLive vote toast preference - POST
/api/user/notification-preferencesJWTChange it — must be a real JSON boolean - GET
/api/user/alerts?limit=20JWTAccount alerts, newest first, paged on ?before - POST
/api/user/alerts/readJWTMark an alert read - POST
/api/user/alerts/dismissJWTDismiss an alert - GET
/api/email/unsubscribe?token=PublicOne-click unsubscribe from an emailed token
Campaigns
Create campaigns, steer them while they run, and wind them down. This is the core of the platform API.
- GET
/orders/getAllAPI keyYour campaigns, newest first, with their sites joined in - GET
/orders/get/{id}API keyOne campaign - POST
/orders/checkoutAPI keyCreate campaigns and charge the balance - POST
/orders/updateAPI keyEdit a campaign - POST
/orders/pauseAPI keyPause a running campaign - POST
/orders/unpauseAPI keyResume a paused campaign - POST
/orders/archiveAPI keyArchive a campaign - POST
/orders/unarchiveAPI keyRestore an archived campaign - PATCH
/orders/updateLimitAPI keySet or clear the daily vote cap
POST /orders/checkout
The body is a top-level JSON array of cart lines, not an object. Every line is one campaign. The whole cart is validated before anything is charged, and the charge and the inserts are one transaction — a checkout either happens completely or not at all.
A fixed-quantity line
The ordinary case: deliver a set number of votes to one URL.
| Parameter | Type | Description |
|---|---|---|
idrequired | integer | Site id from /orders/getAllWebsites. |
amountrequired | integer | Votes to deliver. 0 or more, at most 2,147,483,647. |
ownNamerequired | url | The vote URL. Stored as the order's `url`. |
custom_max_per_hour | integer | Delivery ceiling, clamped to the site's own maximum. |
extra_col | string | Free-text field carried on the order. |
curl -X POST "https://backend.toplistbot.com/orders/checkout?key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"id": 9,
"amount": 1000,
"ownName": "https://arena-top100.com/index.php?a=in&u=yourserver",
"custom_max_per_hour": 60
}
]'200 OK
Successfully purchased with token balanceA subscription line
For sites whose `subscribeable` flag is 1. Priced from the site's subscription_price_1d multiplied by the number of days and the tier discount — Weekly is 0.90, Monthly is 0.80, anything else 1.00. The delivered quantity is derived server-side from the site's own subscription_speed, so nothing you send changes it.
[
{
"type": "subscription",
"website": { "id": 9 },
"subscription_days": 30,
"tier": { "name": "Monthly" },
"url": "https://arena-top100.com/index.php?a=in&u=yourserver"
}
]Responses
200Every line was created and the balance was charged. The body is plain text.400The body was not valid JSON.402Not enough tokens. The message names the amount needed, and nothing was charged.422One or more lines are wrong. Nothing was charged.429The identical cart was submitted within the last 60 seconds. Retry after the stated wait.
A 422 names the offending line: errors are keyed items.[index].[field], so a cart with three bad lines takes one round trip to fix rather than three.
{
"errors": {
"items.2.amount": ["Enter 0 or more votes; a negative amount is not allowed."]
}
}POST /orders/update
`id` is required; send only the fields you are changing. Subscription orders cannot be modified.
| Parameter | Type | Description |
|---|---|---|
idrequired | integer | The campaign to edit. |
amount_to_do | integer | New total votes. Increasing charges the difference, decreasing refunds it, and there is a 30-second cooldown between changes. |
url | url | The vote URL. |
custom_name | string | Your own label for the campaign. |
custom_max_per_hour | integer | Delivery ceiling. |
username_profile_id | integer | Attach a vote profile. |
proxy_profile_id | integer | Attach a proxy profile. |
http_referral | url | Referrer to send with each vote. |
extra_col | string | Free-text field. |
curl -X POST "https://backend.toplistbot.com/orders/update?key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"id":184223,"amount_to_do":2000,"custom_name":"EU launch"}'Daily cap
`type: "delete"` clears the cap. `max_votes_per_day` is still required by the validator in that case — send any integer.
curl -X PATCH "https://backend.toplistbot.com/orders/updateLimit?key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"id":184223,"max_votes_per_day":500,"type":"set"}'Logs & analytics
Delivery data, per vote and aggregated. These read a separate logs database and are slower than the rest of the API — poll them at minutes, not seconds.
- GET
/orders/logs/{id}API keyPer-vote delivery log for a campaign - GET
/orders/graph/{id}API keyTime series for one campaign, ready to chart - GET
/api/orders/graph/summaryJWTOne series across every campaign - GET
/orders/grouped/usernames/{id}API keyDeliveries grouped by the username that voted - POST
/orders/averageAPI keyAverage delivery across several campaigns - GET
/api/logs/{id}/filtered-graphJWTFiltered time series
Vote & proxy profiles
A vote profile is a named list of usernames a campaign votes with. A proxy profile is a named country allowlist for the IPs it uses. Attach either to a campaign with username_profile_id or proxy_profile_id on /orders/update.
- GET
/advanced/profile/getAPI keyYour vote profiles - GET
/advanced/profile/get/{id}API keyOne vote profile - POST
/advanced/profile/createAPI keyCreate a vote profile, or overwrite one by id - DELETE
/advanced/profile/delete/{id}API keyDelete a vote profile - GET
/advanced/profile/proxy/getAPI keyYour proxy profiles - GET
/advanced/profile/proxy/get/{id}API keyOne proxy profile - POST
/advanced/profile/proxy/createAPI keyCreate a proxy profile, or overwrite one by id - DELETE
/advanced/profile/proxy/delete/{id}API keyDelete a proxy profile
Discord tokens
For toplists that authenticate voters through Discord. Adding the same token twice is rejected as a duplicate.
- GET
/api/discord-tokensJWTYour Discord tokens - POST
/api/discord-tokensJWTAdd a token - GET
/api/discord-tokens/statsJWTUsage counts across your tokens - GET
/api/discord-tokens/{id}JWTOne token - PATCH
/api/discord-tokens/{id}JWTChange a token or its enabled flag - DELETE
/api/discord-tokens/{id}JWTRemove a token - PUT
/api/discord-tokens/{id}/toggleJWTFlip a token between enabled and disabled
Billing & payments
Buying tokens always ends on a hosted payment page, so topping up cannot be fully headless. Everything after the top-up can be.
- GET
/invoices/getAPI keyBilling history - GET
/api/subscriptions/subscriptionsJWTActive subscriptions - GET
/products/tokensByUserAPI keyToken packages priced for your account - POST
/company/getAPI keyYour billing address - POST
/company/createAPI keySet it — country, region, city, address, postalCode - GET
/api/stripe/checkout?product_id=JWTA Stripe Checkout URL for a token package - GET
/api/stripe/subscription?plan=JWTA Stripe Checkout URL for a plan - GET
/api/stripe/portalJWTA Stripe billing-portal URL - GET
/api/stripe/documentsJWTStripe invoices and receipts - GET
/coinpayments/checkoutAPI keyA crypto checkout URL
Saved cart
The dashboard's cart, persisted server-side so it survives a device change. You do not need it to place orders — /orders/checkout takes the cart inline.
- GET
/api/cartJWTThe saved cart - PUT
/api/cartJWTReplace it - POST
/api/cartJWTReplace it — same as PUT - DELETE
/api/cartJWTEmpty it
Internal surfaces
These exist for Stripe, the job scheduler and the signup anti-abuse challenge. They are authenticated by shared secrets or signatures and are not part of the integration surface — listed here only so the inventory is complete.
- POST
/api/stripe/webhookStripe payment events — signature authenticated - POST
/api/jobs/tickRuns due background jobs — shared-secret authenticated - POST
/api/pow/challengeSignup proof-of-work challenge - POST
/api/logs/updateEmail activity tracker - POST
/api/order/{email}Places an order on another account — admin allowlist only - GET
/reset-password/{token}The old server-rendered password reset page, kept for emailed links - GET
/PublicHealth check
Response shapes
Two objects carry almost everything you will read: a site, returned by the catalog endpoints, and a campaign, returned by the order endpoints. Each has around forty columns; the tables below are the ones an integration actually needs.
Three fields are JSON strings even though they hold numbers — accept_rate and timeout on a site, custom_max_per_hour on a campaign. Coerce them before doing arithmetic, or your progress figures will be string concatenation.
{
"accept_rate": "70", // string, not number
"timeout": "150000", // string, not number
"custom_max_per_hour": "60" // string, not number
}The site object
Returned by /orders/getAllWebsites and /orders/getWebsiteDetailsByName, and joined into each campaign as `website`.
| Parameter | Type | Description |
|---|---|---|
id | integer | The site id. Pass it as `id` in a checkout line, or as `service` on the SMM endpoint. |
name | string | Display name, and the exact string /orders/getWebsiteDetailsByName matches on. |
price_per_1000 | number | Tokens per 1,000 accepted votes. This is the number the cost formula uses. |
accept_rate | string | Percent of submitted votes that are accepted. Every progress and refund calculation depends on it. |
max_per_hour | integer | The site's own delivery ceiling. Both custom_max_per_hour and the SMM interval are clamped to it. |
active | integer | 1 means orderable. Inactive sites are still returned, so filter them yourself. |
vote_reset_time | integer | Hours before the same identity may vote again. |
speed_changeable | integer | 1 means the site honours a custom delivery rate. |
referer_must_be_set | integer | 1 means http_referral has to be set on the campaign. |
optional_data_possible | integer | 1 means the site accepts the campaign's optional_data field. |
track_votes | integer | 1 means per-vote delivery logs are available for campaigns here. |
subscribeable | integer | 1 means subscription cart lines are accepted. |
subscription_price_1d | number | Tokens per day for a subscription, before the tier discount. |
subscription_speed | integer | Votes per hour a subscription delivers. The quantity is derived from this on the server, never from your request. |
The fields omitted here drive the dashboard's own interface. Read them if you like, but they are not part of the integration contract and may change without notice.
The campaign object
Returned by /orders/getAll and /orders/get/. Note what is missing: there is no status field.
| Parameter | Type | Description |
|---|---|---|
id | integer | The campaign id. Every endpoint under Campaigns takes it as `id`. |
vote_website_id | integer | The site this campaign runs on. |
website | object | The full site object, joined in. Present on /orders/getAll, absent on /orders/get/. |
url | string | The vote URL — the `ownName` you sent at checkout. |
custom_name | string | Your own label for the campaign, or null. |
amount_to_do | integer | Accepted votes purchased. Counts accepted votes, not attempts. |
amount_done | integer | Votes submitted so far. A different unit from amount_to_do — see Progress below. |
running | integer | 1 delivering, 0 paused. |
done | integer | 1 means closed: cancelled, refunded, and both amounts zeroed. It is not a completion flag. |
archive | integer | 1 means archived. Archived campaigns are still returned by /orders/getAll. |
custom_max_per_hour | string | Your delivery ceiling for this campaign, as a string. |
max_votes_per_day | integer | Daily vote cap, or null when uncapped. |
is_subscription | integer | 1 means a subscription. Subscriptions cannot be edited after purchase. |
paused_unpaused | datetime | When the campaign was last paused, resumed or resized. This is what starts the 30-second edit cooldown. |
The fields omitted here drive the dashboard's own interface. Read them if you like, but they are not part of the integration contract and may change without notice.
Is it running? Is it finished?
A campaign has no status field, so you derive its state from four columns. Evaluate these in order and take the first match.
| Test | Means |
|---|---|
1done === 1 | Cancelled. The unspent remainder was refunded, both amounts were zeroed and archive was set. |
2running === 0 | Paused by you. Resume it with /orders/unpause. |
3remaining_accepted === 0 | Everything purchased has been delivered. |
4running === 1 | Running normally. |
5archive === 1 | Hidden in the dashboard, but still returned by /orders/getAll. Filter it out if you want to match what the dashboard shows. |
The order matters. Cancelling sets done and archive together, so testing archive first would report a cancelled campaign as merely archived, and testing remaining before running would report a paused campaign as delivering.
Progress and refunds
amount_to_do counts accepted votes; amount_done counts submissions. Only accept_rate percent of submissions are accepted, so the two are in different units and subtracting one from the other directly is wrong.
This is the most common integration mistake, and it fails silently — the numbers stay plausible and the progress bar is simply wrong. At a 70 percent accept rate a finished campaign reads as 70 percent done; at 50 percent, a half-delivered campaign reads as untouched. Convert submissions to accepted votes first, every time.
// accept_rate arrives as a STRING, and it is per-site, not global.
const rate = Number(order.website.accept_rate)
// amount_done counts SUBMISSIONS; amount_to_do counts ACCEPTED votes.
// Convert before comparing them.
const delivered = order.amount_done * rate / 100
const remaining = Math.max(order.amount_to_do - delivered, 0)
const percent = 100 * delivered / order.amount_to_do
// What cancelling right now would put back on your balance:
const refund = (remaining / 1000) * order.website.price_per_1000The same arithmetic prices a cancellation: the refund is the unspent remainder at the site's list price, so you can tell what stopping a campaign is worth before you commit to it.
A campaign end to end
Six calls, one API key, no browser and no login. The same thing through the SMM endpoint is three calls — services, add, status — and does not touch the platform API at all.
KEY=YOUR_API_KEY
BASE=https://backend.toplistbot.com
# 1. What can I order, and what does it cost?
curl -s "$BASE/orders/getAllWebsites" \
| jq '.[] | {id, name, price_per_1000, max_per_hour}'
# 2. What can I afford?
curl -s -X POST "$BASE/api/v2" -d "key=$KEY" -d "action=balance"
# 3. Launch it. cost = price_per_1000 * amount / 1000
curl -s -X POST "$BASE/orders/checkout?key=$KEY" \
-H "Content-Type: application/json" \
-d '[{"id":9,"amount":1000,
"ownName":"https://arena-top100.com/index.php?a=in&u=me",
"custom_max_per_hour":60}]'
# 4. Find the campaign that was just created.
curl -s "$BASE/orders/getAll?key=$KEY" | jq '.[0] | {id, url, amount_to_do, amount_done}'
# 5. Watch it. Poll every few minutes.
curl -s "$BASE/orders/graph/184223?key=$KEY"
# 6. Slow it down, or stop it.
curl -s -X PATCH "$BASE/orders/updateLimit?key=$KEY" \
-H "Content-Type: application/json" -d '{"id":184223,"max_votes_per_day":200,"type":"set"}'
curl -s -X POST "$BASE/orders/pause?key=$KEY" \
-H "Content-Type: application/json" -d '{"id":184223}'Errors
Errors come back with a matching HTTP status. Validation failures return an `errors` object keyed by field name.
400The request could not be read at all — malformed JSON, or an action the SMM endpoint does not know.401Missing or invalid credentials.402Not enough tokens. Nothing was charged.403Authenticated, but not allowed to do this.404No such record — including one that belongs to somebody else.409Conflicts with the account's current state, such as enabling two-factor twice.422The request was understood but failed validation.429Rate limited. The message states how long to wait.500Our fault. Nothing was charged.503A dependency is unavailable. Retry later.
{
"errors": {
"quantity": ["The quantity must be at least 1."]
}
}When the request body was an array, the error keys carry the index of the line that failed.
A few endpoints answer with plain text rather than JSON — /orders/checkout, /orders/pause and /orders/updateLimit among them. Branch on the status code, not on the body shape.
Rate limits
A 429 always states how long to wait. Honour it rather than retrying blind.
- The identical checkout cart is accepted at most once per 60 seconds.
- A campaign's vote total can be changed once per 30 seconds.
- Login attempts are throttled per address and per IP.
- Password reset: 3 per address and 10 per IP per 15 minutes.
- `status` and `cancel` take at most 100 order ids per call.
- Poll campaign progress at minutes, not seconds. Delivery is measured in votes per hour.
Limits & notes
- Order quantity must be between 1 and 50000 actions.
- Interval defaults to 15 per hour and is capped at 4000. Requesting more than a site's own maximum is rejected with a 400 naming the limit.
- The `refill` and `refill_status` actions are not implemented — create a new order instead.
- The `status` field is always the literal "Completed" and is not a progress signal. Use `remains == 0` to decide an order has finished, and `start_count` for what has been delivered.
- Subscriptions cannot be edited after purchase — pause or cancel instead.
- Listing sites set their own rules and change them over time. You are responsible for making sure your use complies with the terms of any site you promote on. We do not promise any particular ranking or placement.
