# Toplistbot API > Toplistbot runs vote and traffic campaigns on game-server toplists and > voting sites. Everything the web dashboard does is available over HTTP, so > an agent or a script can browse the catalog, price a campaign, pay for it > from a token balance, launch it and watch it deliver — without a browser. Human-readable version of this document: https://toplistbot.com/docs - Base URL (JWT surface): `https://backend.toplistbot.com/api` - Base URL (API-key surface): `https://backend.toplistbot.com` - Every response is JSON unless noted. Every request that has a body accepts `application/json` and `application/x-www-form-urlencoded`. - Get an API key at https://app.toplistbot.com — Settings, then API. --- ## 1. The two ways to authenticate **API key (`key`).** A long-lived key belonging to your account. Send it any of three ways: ?key=YOUR_API_KEY (query string) key=YOUR_API_KEY (form field or JSON field) Authorization: Bearer YOUR_API_KEY **JWT (`Bearer`).** A short-lived token from `POST /api/auth/login`. Send it as `Authorization: Bearer `. **Which paths take which.** Most of the platform is mounted twice, at the same path with and without the `/api` prefix: | Prefix | Example | Accepts | |---|---|---| | `/api/...` | `https://backend.toplistbot.com/api/orders/getAll` | JWT | | no prefix | `https://backend.toplistbot.com/orders/getAll` | API key | **This is the important part for automation.** If you drop the `/api` prefix, the whole platform API works with an API key alone — no login, no token refresh, no session to keep alive. Prefer the un-prefixed paths for scripts, cron jobs and agents. Use the `/api` prefix only when you are building something a human signs into. Endpoints marked `public` below need no credential at all. Endpoints marked `jwt only` exist only under `/api` and have no API-key equivalent. Check a key is live: curl "https://backend.toplistbot.com/api/v2?key=YOUR_API_KEY" -> {"status":"ok"} **CORS is open.** Every route answers with `Access-Control-Allow-Origin: *`, so a page, a browser extension or a browser-based agent can call the API directly. No proxy required. 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. --- ## 2. Two APIs, one balance **SMM panel API** — one endpoint, `POST /api/v2`, switched on an `action` field. Perfect Panel compatible, so any SMM panel can point at it unmodified. Simple, and enough on its own for "order N votes and track it". **Platform API** — the REST API the dashboard uses. Everything else: the catalog, invoices, delivery logs, vote profiles, proxies, subscriptions. Both spend the same token balance. --- ## 3. Tokens and pricing Campaigns are paid for in tokens, bought up front. Every site publishes a `rate` (or `price_per_1000`) — the tokens it costs to run 1,000 actions there. cost_in_tokens = (rate * quantity) / 1000 A site at rate 13 costs 13 tokens per 1,000 actions, so 500 actions cost 6.5. The cost is deducted when the order is accepted; cancelling refunds the unspent remainder. `balance` and `status` report `"currency": "USD"` for Perfect Panel compatibility. The number is a **token balance, not dollars**. --- ## 4. SMM panel API — POST /api/v2 Every request carries `key` and `action`. Form-encoded or JSON. ### action=services Lists every orderable site. 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. Use `service` as the id in `action=add`. ### action=add | Field | Type | Required | Notes | |---|---|---|---| | `key` | string | yes | | | `action` | string | yes | `add` | | `service` | integer | yes | id from `action=services` | | `link` | url | yes | the URL the campaign runs against | | `quantity` | integer | yes | 1 – 50000 | | `interval` | integer | no | actions per hour; default 15, capped at 4000, and may not 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} The cost is deducted immediately. If the balance will not cover it the call fails and no order is created. ### action=status `orders` takes one id or a comma-separated list, up to **100 ids** per call. 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 an unknown or foreign id returns an error entry rather than failing the whole call: {"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` — accepted votes still to deliver. **0 means the order is done.** - `start_count` — accepted votes delivered so far. - `charge` — tokens spent so far. `remains` is computed against the site's accept rate, so it counts the votes you were sold, not the raw attempts the bot makes. ### action=balance {"balance":528.41,"currency":"USD"} ### action=cancel Stops orders and refunds the unspent remainder. Up to 100 ids. [{"order":"184223","cancel":1,"refund":4.5}, {"order":"184224","cancel":{"error":"Incorrect order ID"}}] ### action=refill / action=refill_status Accepted for Perfect Panel compatibility; both answer "not implemented". Nothing on this platform is refillable — re-order instead. --- ## 5. Platform API Paths below are written without the `/api` prefix, i.e. the API-key form. Prepend `/api` and swap the key for a JWT to use the session form. ### 5.1 Catalog and discovery (public) | Method | Path | What it does | |---|---|---| | GET | `/orders/getAllWebsites` | Every listed site with rates, limits, categories and metadata. The full catalog. | | GET | `/orders/getAllBasicWebsitesDetails` | 20 random site names. Cheap; for widgets and autocompletes. | | POST | `/orders/getWebsiteDetailsByName` | One site by its exact name. Body: `{"name":"arena-top100.com"}` | | POST | `/products/getSuggestions` | Sites related to a set of ids. Body: `{"siteIds":[9,14],"limit":6}` | | GET | `/products/demand?days=30` | How much each site has been ordered recently. | | GET | `/products/tokens` | Token packages available for purchase. | | POST | `/products/suggest` | Ask us to list a new site. Body: `{"url":"https://..."}`. Needs auth. | | GET | `/api/news/timeline` | Product changelog. `/api` only. | `getAllWebsites` is the endpoint to start from: it carries `id`, `price_per_1000`, `max_per_hour`, `accept_rate`, `subscribeable`, `subscription_price_1d` and `subscription_speed`, which is everything you need to price and shape an order. **Project it before you parse it.** The full response is about 665 KB across 396 sites, and two fields you will never order with are half of that: a stored `popularity` JSON blob (36%) and the marketing `description` (15%). Filtered to active sites and the ordering fields it is about 40 KB — for an agent, the difference between roughly 170k tokens and 10k, which is the difference between the first call working and the first call exhausting the context window. curl -s "$BASE/orders/getAllWebsites" \ | jq '[.[] | select(.active == 1) | {id, name, price_per_1000, max_per_hour, accept_rate, subscribeable}]' ### 5.2 Account and sessions | Method | Path | Auth | What it does | |---|---|---|---| | POST | `/api/auth/register` | public | Create an account. Requires a Cloudflare Turnstile token, so this is a browser flow — sign up at app.toplistbot.com, then automate. | | POST | `/api/auth/login` | public | `{"email","password"}` (plus `two_factor_code` if 2FA is on) → a JWT. | | POST | `/api/auth/refresh` | expiring JWT | A fresh JWT. | | POST | `/api/auth/logout` | jwt | Invalidates the current JWT. | | GET | `/api/auth/user-profile` | jwt | The signed-in account, including `tokens` and `api_token`. | | GET | `/api/user` | jwt | The same user object. | | GET/POST | `/api/api_token` | key | Resolves an API key to its owner. Use it to validate a key. | | POST | `/api/auth/reset-api-key` | jwt | Rotates the API key. The old one stops working immediately. | | POST | `/api/auth/fingerprint` | jwt | Records a browser fingerprint against the account. | | POST | `/api/auth/ip` | jwt | Records the account's current IP. | | POST | `/api/auth/forgot-password` | public | `{"email"}` → emails a reset link, valid 60 minutes. Always answers the same 200 whether or not the address exists. | | POST | `/api/auth/reset-password` | public | `{"token","email","password","password_confirmation"}`. 422 = fields wrong, token still good. 400 = token spent or expired, ask for a new link. | Login: curl -X POST https://backend.toplistbot.com/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"you@example.com","password":"..."}' {"access_token":"eyJ...","token_type":"bearer","expires_in":3600,"user":{...}} ### 5.3 Two-factor, verification, OAuth | Method | Path | Auth | What it does | |---|---|---|---| | POST | `/api/2fa/enable` | jwt | Starts enrolment. Returns `secret`, `qr_code_url` and one-time `recovery_codes`. 2FA is not on yet. | | POST | `/api/2fa/verify` | jwt | `{"code":"123456"}` — confirms the device and switches 2FA on. | | POST | `/api/2fa/disable` | jwt | `{"code":"123456"}` — switches it off. | | POST | `/api/account/verification/request` | jwt | Emails a verification link to the signed-in address. No body. | | GET | `/api/account/verification/confirm?token=` | token | Renders the confirmation page. Writes nothing. | | POST | `/api/account/verification/confirm` | token | Commits the verification. | | GET | `/api/auth/google`, `/api/auth/google/callback` | public | Google sign-in. Browser redirects. | | GET | `/api/auth/discord`, `/api/auth/discord/callback` | public | Discord sign-in. Browser redirects. | Once 2FA is on, `POST /api/auth/login` needs `two_factor_code` as well. **An API key is unaffected by 2FA** — another reason to use the key surface for automation. ### 5.4 Preferences and alerts (jwt only) | Method | Path | What it does | |---|---|---| | GET | `/api/user/email-preferences` | Marketing-email opt-in state. | | POST | `/api/user/email-preferences` | `{"marketing_emails":true|false}` | | GET | `/api/user/notification-preferences` | `{"vote_toasts":true,"available":true}` — `vote_toasts:true` means show them. | | POST | `/api/user/notification-preferences` | `{"vote_toasts":true|false}`. Must be a real JSON boolean; `"true"` is rejected with 422. | | GET | `/api/user/alerts?limit=20&before=` | Account alerts, newest first. Cursor-paginated on `before`. | | POST | `/api/user/alerts/read` | `{"id":123}` | | POST | `/api/user/alerts/dismiss` | `{"id":123}` | | GET/POST | `/api/email/unsubscribe?token=` | One-click unsubscribe. GET renders a form, POST commits. | ### 5.5 Campaigns | Method | Path | What it does | |---|---|---| | GET | `/orders/getAll` | Your campaigns, newest first, up to 500, each with its `website` object joined in. | | GET | `/orders/get/{id}` | One campaign. 404 for an id that is not yours. | | POST | `/orders/checkout` | Creates campaigns and charges the balance. Body is a **top-level JSON array**. | | POST | `/orders/update` | Edits a campaign. | | POST | `/orders/pause` | `{"id":123}` | | POST | `/orders/unpause` | `{"id":123}` | | POST | `/orders/archive` | `{"id":123}` | | POST | `/orders/unarchive` | `{"id":123}` | | PATCH | `/orders/updateLimit` | Daily vote cap. | #### POST /orders/checkout The body is a JSON **array** of cart lines, not an object. Two line shapes. A fixed-quantity line: [{"id": 9, "amount": 1000, "ownName": "https://arena-top100.com/index.php?a=in&u=yourserver", "custom_max_per_hour": 60, "extra_col": ""}] - `id` — the site id from `/orders/getAllWebsites`. - `amount` — how many votes to deliver. 0 or more, at most 2147483647. - `ownName` — the vote URL. Stored as the order's `url`. - `custom_max_per_hour` — delivery ceiling, clamped to the site's own maximum. A subscription line, for sites whose `subscribeable` is 1: [{"type": "subscription", "website": {"id": 9}, "subscription_days": 30, "tier": {"name": "Monthly"}, "url": "https://arena-top100.com/index.php?a=in&u=yourserver"}] - Priced from the site's `subscription_price_1d` × days × tier multiplier (`Weekly` 0.90, `Monthly` 0.80, anything else 1.00). - The delivered quantity is derived server-side from the site's `subscription_speed`; nothing you send changes it. 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=me"}]' 200 Successfully purchased with token balance Responses: | Code | Meaning | |---|---| | 200 | Every line was created and the balance was charged. Body is plain text. | | 400 | The body was not valid JSON. | | 402 | Not enough tokens. The message names the amount needed. | | 422 | One or more lines are wrong. Errors are keyed `items..` so you can fix the exact line. | | 429 | The identical cart was submitted within the last 60 seconds. Retry after the stated wait. | The whole cart is validated before anything is charged, and the charge plus the inserts are one transaction — a checkout either happens completely or not at all. #### POST /orders/update `id` is required; send only the fields you are changing. | Field | Notes | |---|---| | `id` | required, the campaign to edit | | `amount_to_do` | new total votes. Increasing charges the difference; decreasing refunds it. 30-second cooldown between changes (429). | | `url` | the vote URL | | `custom_name` | your label for the campaign | | `custom_max_per_hour` | delivery ceiling | | `username_profile_id` | attach a vote profile (see 5.7) | | `proxy_profile_id` | attach a proxy profile | | `http_referral` | referrer to send | | `extra_col` | free-text field | Subscription orders cannot be modified (403). A decrease can be refused if the votes have already been spent (422). #### PATCH /orders/updateLimit {"id": 123, "max_votes_per_day": 500, "type": "set"} `type: "delete"` clears the cap (`max_votes_per_day` is still required by the validator — send any integer). ### 5.6 Delivery logs and analytics | Method | Path | What it does | |---|---|---| | GET | `/orders/logs/{id}` | Per-vote delivery log for one campaign. | | GET | `/orders/graph/{id}` | Time series for one campaign, ready to chart. | | GET | `/api/orders/graph/summary` | One series across all your campaigns. | | GET | `/orders/grouped/usernames/{id}` | Deliveries grouped by the username that voted. | | POST | `/orders/average` | Average delivery across an array of campaign ids. | | GET | `/api/logs/{id}/filtered-graph` | Filtered time series. `/api` only. | These read a separate logs database, so they are slower than the rest of the API. Poll them at minutes, not seconds. ### 5.7 Vote profiles and 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 used. | Method | Path | Body | |---|---|---| | GET | `/advanced/profile/get` | — | | GET | `/advanced/profile/get/{id}` | — | | POST | `/advanced/profile/create` | `{"name":"main","usernames":["a","b"]}`, plus `"id"` to overwrite an existing profile | | DELETE | `/advanced/profile/delete/{id}` | — | | GET | `/advanced/profile/proxy/get` | — | | GET | `/advanced/profile/proxy/get/{id}` | — | | POST | `/advanced/profile/proxy/create` | `{"name":"eu","country_list":["DE","NL"]}`, plus `"id"` to overwrite | | DELETE | `/advanced/profile/proxy/delete/{id}` | — | Attach either to a campaign with `username_profile_id` / `proxy_profile_id` on `POST /orders/update`. ### 5.8 Discord tokens (jwt only) For campaigns on toplists that authenticate voters through Discord. | Method | Path | Body | |---|---|---| | GET | `/api/discord-tokens` | — | | POST | `/api/discord-tokens` | `{"token":"...","enabled":true}` | | GET | `/api/discord-tokens/stats` | — | | GET | `/api/discord-tokens/{id}` | — | | PUT/PATCH | `/api/discord-tokens/{id}` | `{"token":"...","enabled":false}` | | DELETE | `/api/discord-tokens/{id}` | — | | PUT | `/api/discord-tokens/{id}/toggle` | — | Adding a token twice is rejected as a duplicate. ### 5.9 Billing, tokens and payments | Method | Path | Auth | What it does | |---|---|---|---| | GET | `/invoices/get` | key/jwt | Billing history. | | GET | `/api/subscriptions/subscriptions` | jwt | Active subscriptions. | | GET | `/products/tokens` | public | Token packages, with `id` and `price`. | | GET | `/products/tokensByUser` | key/jwt | Packages priced for your account. | | POST | `/company/get` | key/jwt | Your billing address. | | POST | `/company/create` | key/jwt | Sets it. Required: `country`, `region`, `city`, `address`, `postalCode`. Optional: `companyName`, `taxId`. | | GET | `/api/stripe/checkout?product_id=` | jwt | A Stripe Checkout URL for a token package. | | GET | `/api/stripe/subscription?plan=` | jwt | A Stripe Checkout URL for a plan. | | GET | `/api/stripe/portal` | jwt | A Stripe billing-portal URL. | | GET | `/api/stripe/documents` | jwt | Stripe invoices and receipts. | | GET | `/coinpayments/checkout` | key/jwt | A crypto checkout URL. | Buying tokens always ends in a hosted payment page, so topping up cannot be fully headless. Everything after the top-up can be. ### 5.10 Saved cart (jwt only) The dashboard's cart, persisted server-side so it survives a device change. Not needed to place orders — `POST /orders/checkout` takes the cart inline. | Method | Path | What it does | |---|---|---| | GET | `/api/cart` | The saved cart. | | PUT | `/api/cart` | Replaces it. `{"items":[...]}` | | POST | `/api/cart` | Same as PUT. | | DELETE | `/api/cart` | Empties it. | ### 5.11 Internal and legacy surfaces — do not call These exist for Stripe, the job scheduler, the signup anti-abuse challenge and old links still in people's inboxes. They are authenticated by shared secrets, signatures or one-time tokens and are not part of the integration surface — listed only so the inventory is complete. | Method | Path | What it is | |---|---|---| | POST | `/api/stripe/webhook` | Stripe payment events, signature authenticated | | GET/POST | `/api/jobs/tick` | Runs due background jobs; `Authorization: Bearer $JOB_RUNNER_SECRET` | | POST | `/api/pow/challenge`, `/api/pow/verify`, `/api/pow/claim-trial` | Signup proof-of-work challenge | | GET | `/api/pow/worker` | The worker script the challenge runs | | POST | `/api/logs/update` | Email activity tracker | | POST | `/api/order/{email}` | Places an order on another account. API key **plus** an admin allowlist; not for customer use. | | GET/POST | `/forget-password`, `/reset-password/{token}` | The old server-rendered password-reset pages. Still live because emailed links point at them; the JSON endpoints in 5.2 are the ones to integrate against. | | GET | `/api/login` | Always `401 {"message":"Unauthorized"}` — a framework artefact, not a login endpoint. | | GET | `/` | Health check | --- ## 5.12 Response shapes Two objects carry almost everything you will read. Each has around forty columns; these are the ones an integration needs. The rest drive the dashboard's interface and are not part of the contract. **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 before doing arithmetic. ### The site object Returned by `/orders/getAllWebsites` and `/orders/getWebsiteDetailsByName`, and joined into every campaign as `website`. | Field | Type | Meaning | |---|---|---| | `id` | integer | Pass as `id` in a checkout line, or `service` on the SMM endpoint | | `name` | string | Display name, and what `getWebsiteDetailsByName` matches on | | `price_per_1000` | number | Tokens per 1,000 **accepted** votes | | `accept_rate` | string | Percent of submissions that are accepted. Everything below depends on it | | `max_per_hour` | integer | The site's ceiling; `custom_max_per_hour` and `interval` are clamped to it | | `active` | integer | 1 = orderable. Inactive sites are still returned — filter them | | `vote_reset_time` | integer | Hours before the same identity may vote again | | `speed_changeable` | integer | 1 = a custom delivery rate is honoured | | `referer_must_be_set` | integer | 1 = `http_referral` is required on the campaign | | `optional_data_possible` | integer | 1 = the site accepts `optional_data` | | `track_votes` | integer | 1 = per-vote delivery logs are available | | `subscribeable` | integer | 1 = subscription cart lines accepted | | `subscription_price_1d` | number | Tokens per day, before the tier discount | | `subscription_speed` | integer | Votes/hour a subscription delivers; the quantity is derived from this server-side | ### The campaign object Returned by `/orders/getAll` and `/orders/get/{id}`. **Note what is missing: there is no status field.** | Field | Type | Meaning | |---|---|---| | `id` | integer | The campaign id every Campaigns endpoint takes | | `vote_website_id` | integer | The site it runs on | | `website` | object | The full site object. Present on `getAll`, absent on `get/{id}` | | `url` | string | The vote URL — the `ownName` you sent at checkout | | `custom_name` | string | Your label, or null | | `amount_to_do` | integer | **Accepted** votes purchased | | `amount_done` | integer | Votes **submitted** so far — a different unit | | `running` | integer | 1 delivering, 0 paused | | `done` | integer | 1 = closed (cancelled + refunded, amounts zeroed). NOT a completion flag | | `archive` | integer | 1 = archived. Archived campaigns are still returned by `getAll` | | `custom_max_per_hour` | string | Your delivery ceiling | | `max_votes_per_day` | integer | Daily cap, or null | | `is_subscription` | integer | 1 = subscription; cannot be edited | | `paused_unpaused` | datetime | Last pause/resume/resize. Starts the 30s edit cooldown | ### Deriving campaign state There is no status field. Evaluate in this order and take the first match — cancelling sets `done` AND `archive`, so testing `archive` first would report a cancelled campaign as merely archived. | # | Test | Means | |---|---|---| | 1 | `done === 1` | Cancelled. Remainder refunded, amounts zeroed, archived | | 2 | `running === 0` | Paused by you. Resume with `/orders/unpause` | | 3 | `remaining_accepted === 0` | Everything purchased has been delivered | | 4 | `running === 1` | Running normally | | 5 | `archive === 1` | Hidden in the dashboard, still returned by `getAll` | ### Progress and refunds `amount_to_do` counts **accepted** votes. `amount_done` counts **submissions**, of which only `accept_rate` percent stick. They are in different units and subtracting one from the other directly is wrong. const rate = Number(order.website.accept_rate) // a STRING 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 now would refund: const refund = (remaining / 1000) * order.website.price_per_1000 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% accept rate a finished campaign reads as 70% done; at 50%, a half-delivered campaign reads as untouched. Convert submissions to accepted votes first, every time. --- ## 6. Errors | Code | Meaning | |---|---| | 400 | Malformed request — bad JSON, or an `action` the SMM endpoint does not know. | | 401 | Missing, expired or wrong credential. | | 402 | Not enough tokens. Nothing was charged. | | 403 | Authenticated, but not allowed to do this. | | 404 | No such record — including one that belongs to someone else. | | 409 | Conflicts with the account's current state (e.g. enabling 2FA twice). | | 422 | Validation failed. The body names each field. | | 429 | Rate limited. The message states how long to wait. | | 500 | Our fault. Nothing was charged. | | 503 | A dependency is unavailable. Retry later. | A validation failure looks like this, and array bodies are keyed by index: {"errors": {"quantity": ["The quantity must be at least 1."]}} {"errors": {"items.2.amount": ["Enter 0 or more votes; a negative amount is not allowed."]}} Some endpoints answer with plain text rather than JSON (`/orders/checkout`, `/orders/pause`, `/orders/updateLimit`). Check the status code, not the body shape. --- ## 7. Rate limits and etiquette - The identical checkout cart is accepted at most once per 60 seconds. - `amount_to_do` on a campaign 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. - `action=status` and `action=cancel` accept at most 100 order ids per call — batch rather than looping. - Poll campaign status at minutes, not seconds. Delivery is measured in votes per hour; nothing changes faster than that. - A 429 always states how long to wait. Honour it rather than retrying blind. --- ## 8. Worked example — a campaign, end to end, with one API key KEY=YOUR_API_KEY BASE=https://backend.toplistbot.com # 1. What can I order, and what does it cost? curl -s "$BASE/orders/getAllWebsites?key=$KEY" \ | 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, pause it, or wind it back. 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}' The same thing through the SMM endpoint is three calls — `action=services`, `action=add`, `action=status` — and does not need the platform API at all. Use `/api/v2` when you only want to place and track orders; use the platform API when you want profiles, proxies, per-vote logs or invoices. --- ## 9. Notes and constraints - Quantity per campaign: 1 to 2,147,483,647; the SMM endpoint caps at 50,000. - `interval` (votes per hour) defaults to 15 and is capped at 4,000, and can never exceed the site's own `max_per_hour`. - Nothing is refillable. Re-order instead. - The SMM endpoint's `status` field is always `"Completed"`; use `remains == 0` to decide an order has finished. - Subscriptions cannot be edited after purchase — pause or cancel instead. - Only order against sites and URLs you are entitled to promote. Accounts found voting for third parties without permission are closed. - Treat an API key as a password: it spends real balance. Keep it server-side and rotate it with `POST /api/auth/reset-api-key` if it leaks. Support: https://discord.gg/fr6BYjJbV6