Documentation

API documentation

Everything the dashboard does is available over HTTP. Point an SMM panel at one endpoint, or drive the whole platform — catalog, campaigns, logs, invoices — from a script or an AI agent.

On this page

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.

Base URL

Base URL
https://backend.toplistbot.com/api
https://backend.toplistbot.com

Reference 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.

  1. 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.

  2. 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.

  3. 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.

    cURL
    curl -X POST https://backend.toplistbot.com/api/v2 -d "key=YOUR_API_KEY" -d "action=services"
  4. 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.

    cURL
    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"
  5. 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.

    cURL
    curl -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.txt

Starting 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.

Prompt
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.

PathCredentialUse it for
/api/orders/getAllJWTAnything a human signs into
/orders/getAllAPI keyScripts, 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.

cURL
# 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"
Health check
curl https://backend.toplistbot.com/api/v2?key=YOUR_API_KEY

JWT

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

Login
curl -X POST https://backend.toplistbot.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"..."}'
Response
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "token_type": "bearer",
  "expires_in": 3600,
  "user": { "id": 4211, "email": "[email protected]", "tokens": 528.41, "...": "..." }
}
Authenticated request
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.

JavaScript
// 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 formula
cost_in_tokens = (rate * quantity) / 1000

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

POSThttps://backend.toplistbot.com/api/v2
ActionParameters
services
addservice, link, quantity, interval?
statusorders
balance
cancelorders

`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
curl -X POST https://backend.toplistbot.com/api/v2 \
  -d "key=YOUR_API_KEY" \
  -d "action=services"
Response
[
  {
    "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.

ParameterTypeDescription
keyrequiredstringYour API key.
actionrequiredstringMust be `add`.
servicerequiredintegerService id from the services action.
linkrequiredurlThe URL the campaign runs against. Must be a valid URL.
quantityrequiredintegerNumber of actions to run, between 1 and 50000.
intervalintegerActions per hour. Defaults to 15, capped at 4000, and cannot exceed the site's own maximum.
cURL
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"
Response
{
  "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
curl -X POST https://backend.toplistbot.com/api/v2 \
  -d "key=YOUR_API_KEY" \
  -d "action=status" \
  -d "orders=184223"
Response — single order
{
  "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.

Response — multiple orders
{
  "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
curl -X POST https://backend.toplistbot.com/api/v2 \
  -d "key=YOUR_API_KEY" \
  -d "action=balance"
Response
{
  "balance": 528.41,
  "currency": "USD"
}

action=cancel

Stops an order and refunds the unspent remainder to your balance. Completed orders cannot be cancelled.

cURL
curl -X POST https://backend.toplistbot.com/api/v2 \
  -d "key=YOUR_API_KEY" \
  -d "action=cancel" \
  -d "orders=184223,184224"
Response
[
  { "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
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.

cURL + jq
# 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.

ParameterTypeDescription
idrequiredintegerSite id from /orders/getAllWebsites.
amountrequiredintegerVotes to deliver. 0 or more, at most 2,147,483,647.
ownNamerequiredurlThe vote URL. Stored as the order's `url`.
custom_max_per_hourintegerDelivery ceiling, clamped to the site's own maximum.
extra_colstringFree-text field carried on the order.
cURL
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
    }
  ]'
Response
200 OK
Successfully purchased with token balance

A 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.

Body
[
  {
    "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.

422 body
{
  "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.

ParameterTypeDescription
idrequiredintegerThe campaign to edit.
amount_to_dointegerNew total votes. Increasing charges the difference, decreasing refunds it, and there is a 30-second cooldown between changes.
urlurlThe vote URL.
custom_namestringYour own label for the campaign.
custom_max_per_hourintegerDelivery ceiling.
username_profile_idintegerAttach a vote profile.
proxy_profile_idintegerAttach a proxy profile.
http_referralurlReferrer to send with each vote.
extra_colstringFree-text field.
cURL
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
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.

Types to watch
{
  "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`.

ParameterTypeDescription
idintegerThe site id. Pass it as `id` in a checkout line, or as `service` on the SMM endpoint.
namestringDisplay name, and the exact string /orders/getWebsiteDetailsByName matches on.
price_per_1000numberTokens per 1,000 accepted votes. This is the number the cost formula uses.
accept_ratestringPercent of submitted votes that are accepted. Every progress and refund calculation depends on it.
max_per_hourintegerThe site's own delivery ceiling. Both custom_max_per_hour and the SMM interval are clamped to it.
activeinteger1 means orderable. Inactive sites are still returned, so filter them yourself.
vote_reset_timeintegerHours before the same identity may vote again.
speed_changeableinteger1 means the site honours a custom delivery rate.
referer_must_be_setinteger1 means http_referral has to be set on the campaign.
optional_data_possibleinteger1 means the site accepts the campaign's optional_data field.
track_votesinteger1 means per-vote delivery logs are available for campaigns here.
subscribeableinteger1 means subscription cart lines are accepted.
subscription_price_1dnumberTokens per day for a subscription, before the tier discount.
subscription_speedintegerVotes 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.

ParameterTypeDescription
idintegerThe campaign id. Every endpoint under Campaigns takes it as `id`.
vote_website_idintegerThe site this campaign runs on.
websiteobjectThe full site object, joined in. Present on /orders/getAll, absent on /orders/get/.
urlstringThe vote URL — the `ownName` you sent at checkout.
custom_namestringYour own label for the campaign, or null.
amount_to_dointegerAccepted votes purchased. Counts accepted votes, not attempts.
amount_doneintegerVotes submitted so far. A different unit from amount_to_do — see Progress below.
runninginteger1 delivering, 0 paused.
doneinteger1 means closed: cancelled, refunded, and both amounts zeroed. It is not a completion flag.
archiveinteger1 means archived. Archived campaigns are still returned by /orders/getAll.
custom_max_per_hourstringYour delivery ceiling for this campaign, as a string.
max_votes_per_dayintegerDaily vote cap, or null when uncapped.
is_subscriptioninteger1 means a subscription. Subscriptions cannot be edited after purchase.
paused_unpauseddatetimeWhen 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.

TestMeans
1done === 1Cancelled. The unspent remainder was refunded, both amounts were zeroed and archive was set.
2running === 0Paused by you. Resume it with /orders/unpause.
3remaining_accepted === 0Everything purchased has been delivered.
4running === 1Running normally.
5archive === 1Hidden 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.

JavaScript
// 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_1000

The 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.

bash
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.
Validation error
{
  "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.
Free to start

Start Promoting Now!

Verify your email and get 100 free tokens to try out our service. No strings attached.

No credit card
Cancel anytime
24/7 support