Creator API
Read your saved TikTok creator pool, start discovery scans and download results over HTTP — the same things the Discord bot does, for scripts, spreadsheets and other tools.
Base URL: http://YOUR_SERVER:8787 · All responses are JSON unless you ask for CSV or text.
Quickstart
- In Discord, run
/apikey create name:my-script. The bot replies privately with a key that starts withtts_. Copy it — it is shown only once. - Check it works:
curl -H "Authorization: Bearer tts_YOUR_KEY" http://YOUR_SERVER:8787/v1/me - Get the 10 biggest US creators in your pool:
curl -H "Authorization: Bearer tts_YOUR_KEY" \ "http://YOUR_SERVER:8787/v1/creators?country=US&limit=10" - Start a scan, then check on it with the
idit returns:curl -X POST -H "Authorization: Bearer tts_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"country":"US","count":20,"categories":["gaming:fortnite"]}' \ http://YOUR_SERVER:8787/v1/jobs
API keys
Keys are managed entirely from Discord. Only whitelisted users can make them.
| Command | What it does |
|---|---|
/apikey create name:<label> | Makes a new key and shows it to you once (only you can see the reply). Up to 5 active keys per user. |
/apikey list | Shows your active keys: ID, label, a short hint of the key, when it was made and last used. The super admin sees everyone's keys. |
/apikey revoke key_id:<id> | Stops a key immediately. Use key_id:all to stop every key you own. The super admin can revoke any key by ID. |
/history and /jobs. If you are removed from the whitelist, all your keys stop working at once (they return 403).The bot stores only a fingerprint (SHA-256 hash) of each key, so a lost key can't be recovered — revoke it and make a new one.
Authentication
Send the key on every request to a /v1 route (except /v1/health), in either header:
Authorization: Bearer tts_YOUR_KEY
X-API-Key: tts_YOUR_KEY
Never put the key in the URL. Keep it out of shared code and public repos.
Errors & limits
Every error has the same shape:
{ "error": { "code": "invalid_key", "message": "This API key is wrong or has been revoked." } }
| Status | Code | Meaning |
|---|---|---|
400 | invalid_parameter, invalid_body | A query value or JSON field is wrong. The message says which. |
401 | missing_key, invalid_key | No key sent, or the key is wrong or revoked. |
403 | not_whitelisted, forbidden | The key's owner lost access, or you tried to act on someone else's job. |
404 | not_found | No such creator, job or route. Other users' jobs also return 404. |
409 | queue_unavailable, not_active | The scan queue is full, you already have a queued job, or the job you tried to stop has already finished. |
429 | rate_limited | More than 120 requests per minute on one key. Wait for the number of seconds in the Retry-After header. |
500 | server_error | Something broke on the server. Try again; if it keeps happening, check the bot logs. |
Other limits are the same as in Discord: one scan runs at a time (others wait in a queue of up to 10), each user can have one queued scan, a scan has a 30-minute and per-scan data budget unless unlimited_passes is on, and each user has a daily data allowance that resets at Sydney midnight.
Account
GET/v1/health
Is the API up? No key needed.
{ "ok": true, "version": "1.0" }
GET/v1/me
The key you're using and whether its owner is the super admin.
{
"key": { "id": "3f9a1c2b", "owner_id": "1444906668923027466", "name": "my-script",
"hint": "tts_Ab3d…x9Qz", "created_at": "2026-11-05T09:12:44+00:00",
"last_used_at": "2026-11-05T09:13:02+00:00", "revoked_at": null, "revoked_by": null },
"is_admin": false
}
GET/v1/usage
Your scan data used today and what's left. All sizes are bytes.
{
"usage": { "owner_id": "1444…", "day": "2026-11-05", "browser_bytes": 81234567,
"country_bytes": 1203344, "total_bytes": 82437911 },
"daily_limit_bytes": 5000000000,
"scan_limit_bytes": 2500000000,
"remaining_today_bytes": 4917562089
}
GET/v1/stats
Pool totals, the scan queue and whether the TikTok token is ready (the same numbers as /status).
{
"pool": { "total": 10839, "eligible": 2210, "live_now": 14, "seen_live_7d": 640, "us": 3021,
"by_status": { "new": 10500, "contacted": 120 }, "country_known": 5400,
"country_unknown": 5439, "consistent_live": 310 },
"queue": { "running": "a1b2c3d4e5f6", "waiting": 1 },
"tiktok_token": { "present": true, "age_hours": 3.2 }
}
GET/v1/reference
Valid values for categories, leagues and sort.
{
"categories": [ { "key": "gaming:fortnite", "label": "Fortnite" }, … ],
"leagues": [ "D5", "D4", "D3", "D2", "D1", "C5", … , "A2", "A1" ],
"sorts": [ "followers_desc", "followers_asc", "engagement_desc", … ]
}
Creators
The creator pool is shared — it's the same pool /pool shows.
GET/v1/creators
Search and page through saved creators. All filters are optional and combine with AND.
| Query | Type | Description |
|---|---|---|
search | text | Matches username, display name or bio. |
country | ISO code | Verified account country, e.g. US. |
post_country | ISO code | Where recent posts are mostly from (a hint, not the verified country). |
min_followers, max_followers | number | Follower range. |
live_only | true/false | Only creators confirmed live in the last 15 minutes. |
league | tier | One league tier, e.g. C3. |
status | text | Recruitment state, e.g. new, shortlisted, contacted, do_not_contact. |
niche | text | Matches part of the niche label. |
sort | text | Default followers_desc. See /v1/reference. |
limit | 1–500 | Page size, default 50. |
offset | number | How many to skip, default 0. |
GET /v1/creators?country=US&min_followers=1000&live_only=true&limit=2
{
"total": 14, "limit": 2, "offset": 0,
"items": [ { "username": "examplecreator", "followers": 48200, "region": "US",
"verified_live": true, "live_league": "C2", … }, … ]
}
To get everything, keep adding limit to offset until offset >= total, or use export.
GET/v1/creators/{username}
One creator plus their observed LIVE sessions (newest first, up to 20). The @ is optional and case doesn't matter.
{
"creator": { "username": "examplecreator", … },
"live_sessions": [ { "room_id": "7311…", "first_seen_at": "…", "last_seen_at": "…", "observed_days": 2 } ]
}
GET/v1/creators/export
Every creator matching the same filters as /v1/creators, in one download.
| Query | Description |
|---|---|
format | csv (default, opens in Excel/Sheets), txt (one username per line) or json. |
curl -H "Authorization: Bearer tts_YOUR_KEY" -o us.csv \
"http://YOUR_SERVER:8787/v1/creators/export?country=US&format=csv"
Scans
A scan is a job. Starting one puts it in the same queue as Discord scans; it runs in the background and saves results to the pool. Poll the job to see progress and fetch results when it's done.
Job state goes: queued → running → one of completed, stopped, failed, cancelled (stopped before it started) or interrupted (bot restarted — resume it with /resume in Discord). stopping means a stop was requested and it's finishing the current step.
POST/v1/jobs
Start a scan. Send a JSON body. Every field is optional. Returns 202 with the new job.
kind: "generate" (default) — same as /generate
| Field | Type | Default | Description |
|---|---|---|---|
country | ISO code | "US" | Country the creators stream from. "" or "any" for any. |
count | 1–1000 | 30 | Target number of qualifying creators. You may get fewer if country or activity proof is missing. |
categories | list | all | Category keys, e.g. ["gaming:fortnite","lifestyle:chatting"]. See /v1/reference. |
keywords | list | [] | Extra search keywords. |
leagues | list | [] | Only keep these league tiers, e.g. ["C1","B5"]. |
min_followers | number | 0 | Minimum followers. |
max_followers | number | none | Maximum followers. |
fast | bool | true | Short profile visits; league lookup only when filtering leagues. |
post_country | ISO code | none | Keep only creators whose recent posts are mostly from this country. |
include_unknown_country | bool | false | Also keep creators whose country couldn't be verified. |
include_existing | bool | false | Include creators you've already been given before. |
min_live_sessions | 0–1000 | 0 | Minimum distinct LIVE sessions seen (0 = off). |
min_live_days | 0–window | 0 | Minimum distinct days seen live (0 = off). |
activity_window_days | 1–90 | 14 | How far back the two LIVE checks above look. |
unlimited_passes | bool | false | Keep scanning until the target is reached or you stop it. No time limit. |
kind: "autoscan" — same as /autoscan
Scans every category and ranks the most recruitable creators. Fields: country (default "US"), count (1–100, default 100), include_existing, unlimited_passes. Results carry _score, _grade and _why.
POST /v1/jobs
Content-Type: application/json
{ "country": "US", "count": 25, "categories": ["gaming:fortnite", "gaming:roblox"],
"min_followers": 500, "max_followers": 50000 }
→ 202
{ "id": "a1b2c3d4e5f6", "kind": "generate", "state": "queued", "source": "api", "target": 25, … }
A 409 queue_unavailable means the queue is full or you already have a scan waiting.
GET/v1/jobs
Your recent jobs, newest first — including ones started in Discord. Results aren't included here; use results.
| Query | Description |
|---|---|
limit | 1–100, default 20. |
all | true shows every user's jobs (super admin only; ignored for others). |
{ "items": [ { "id": "a1b2c3d4e5f6", "kind": "generate", "state": "running", … } ] }
GET/v1/jobs/{id}
One job with live progress while it runs. Add ?include_creators=true to include the result rows.
{
"id": "a1b2c3d4e5f6", "kind": "generate", "state": "running", "source": "api",
"target": 25, "created_at": "2026-11-05T09:20:01+00:00",
"progress": { "phase": "Checking profiles", "currently_qualified": 7, … },
"remaining_seconds": 1420.5,
"data": { "used_bytes": 41022331, "limit_bytes": 2500000000, "limit_source": "scan" },
"checkpoint": { "pass": 2, "dropped": { "region": 31, "min_followers": 12 } },
"result_count": 7
}
GET/v1/jobs/{id}/results
The creators a job found. While it runs you get what's qualified so far; once it's finished you get the final list.
| Query | Description |
|---|---|
format | json (default), csv or txt. |
{ "state": "completed", "count": 25, "items": [ { "username": "examplecreator", … } ] }
POST/v1/jobs/{id}/stop
Stop a queued or running job. Work already done is kept. A running job moves to stopping, then stopped; a queued one becomes cancelled. No body needed.
curl -X POST -H "Authorization: Bearer tts_YOUR_KEY" \
http://YOUR_SERVER:8787/v1/jobs/a1b2c3d4e5f6/stop
Objects
Creator
Fields you'll use most. Any number field can be null when it was never observed.
| Field | Description |
|---|---|
username | TikTok handle, lowercase, no @. Profile: https://www.tiktok.com/@username. |
display_name, bio, avatar | Profile details. |
followers, following, likes, video_count | Profile counts. |
region, country_source, country_checked_at | Account country and how/when it was confirmed. |
post_country | Where recent posts mostly come from (a hint). |
language | Detected language code. |
verified_live, verified_live_at | Confirmed live within the last 15 minutes, and when. |
last_live_at, last_post_at | Last time seen live / last post. |
live_league, live_league_rank | TikTok LIVE league tier (D5 lowest … A1 highest) and its rank 1–18. |
avg_views_10, engagement_rate | Average views of recent videos and engagement. |
status, notes | Recruitment state and notes set in Discord. |
created_at, updated_at | When the row was added / last changed (UTC, ISO 8601). |
Job
| Field | Description |
|---|---|
id | 12-character job ID. Works with /run, /jobs and /resume in Discord too. |
kind | generate, autoscan, or a maintenance job from Discord (e.g. proxies). |
state | See Scans. |
source | api or discord. |
target, settings | What the scan was asked for. |
reason, error | Why it stopped or failed, in plain words. |
progress | Live phase and counts while running. |
data | Bytes used and the limit that applies (scan or daily). |
checkpoint | Saved pass count, drop reasons and totals. |
result_count | How many creators the job has right now. |
Full examples
Start a scan, wait for it, and save the results as CSV.
import time, requests
BASE = "http://YOUR_SERVER:8787"
H = {"Authorization": "Bearer tts_YOUR_KEY"}
job = requests.post(f"{BASE}/v1/jobs", headers=H,
json={"country": "US", "count": 20}).json()
print("started", job["id"])
while True:
job = requests.get(f"{BASE}/v1/jobs/{job['id']}", headers=H).json()
print(job["state"], job["result_count"], "found")
if job["state"] not in ("queued", "running", "stopping"):
break
time.sleep(15)
csv = requests.get(f"{BASE}/v1/jobs/{job['id']}/results",
headers=H, params={"format": "csv"})
open("results.csv", "wb").write(csv.content)const BASE = "http://YOUR_SERVER:8787";
const H = { Authorization: "Bearer tts_YOUR_KEY", "Content-Type": "application/json" };
let job = await (await fetch(`${BASE}/v1/jobs`, {
method: "POST", headers: H, body: JSON.stringify({ country: "US", count: 20 }),
})).json();
while (["queued", "running", "stopping"].includes(job.state)) {
await new Promise(r => setTimeout(r, 15000));
job = await (await fetch(`${BASE}/v1/jobs/${job.id}`, { headers: H })).json();
console.log(job.state, job.result_count, "found");
}
const { items } = await (await fetch(`${BASE}/v1/jobs/${job.id}/results`, { headers: H })).json();
console.log(items.map(c => c.username));KEY="tts_YOUR_KEY"; BASE="http://YOUR_SERVER:8787"
# start
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"country":"US","count":20}' $BASE/v1/jobs
# check (repeat until state is completed/stopped/failed)
curl -s -H "Authorization: Bearer $KEY" $BASE/v1/jobs/JOB_ID
# download
curl -s -H "Authorization: Bearer $KEY" -o results.csv "$BASE/v1/jobs/JOB_ID/results?format=csv"Server setup
The API starts automatically with the bot. Configure it with environment variables (on the server: deploy/tiktokbot.env):
| Variable | Default | Description |
|---|---|---|
TIKTOK_API_PORT | 8787 | Port to listen on. 0 turns the API off. |
TIKTOK_API_HOST | 0.0.0.0 | Address to listen on. Use 127.0.0.1 when a reverse proxy sits in front. |
The bot log prints [api] listening on … at startup. Docs are served at /docs (and /).
TIKTOK_API_HOST=127.0.0.1 and put Apache or nginx with a TLS certificate in front, for example with Apache:
ProxyPass / http://127.0.0.1:8787/
ProxyPassReverse / http://127.0.0.1:8787/
If you keep it on 0.0.0.0, open the port in the firewall (e.g. ufw allow 8787/tcp).Keys live in data/api_keys.json (hashes only). Back it up with the rest of data/.