# Riken Scraper: full documentation
> TikTok creator data over one HTTP API. Base URL: https://scraper.riken.me.
# Introduction
URL: https://scraper.riken.me/docs
Live TikTok creator data over plain HTTP. One key, one header, JSON back.
Riken Scraper gives you TikTok profiles, profile videos and keyword search as JSON. Every answer is fetched live when you ask for it, and you pay only for answers you get.
## Make your first call
### Get a key
Create an API key in the [dashboard](https://scraper.riken.me/dashboard). Keys start with `sk_`. Keep yours out of source control.
```bash
export RIKEN_API_KEY=sk_...
```
### Send it in a header
Every request carries the key in `x-api-key` (or `Authorization: Bearer`). See [Authentication](https://scraper.riken.me/docs/authentication).
### Call an endpoint
```bash
curl -s --max-time 200 "https://scraper.riken.me/v1/tiktok/profile?handle=example_creator" \
-H "x-api-key: $RIKEN_API_KEY"
```
```json
{
"success": true,
"credits_charged": 1,
"credits_remaining": 999,
"user": { "uniqueId": "example_creator", "nickname": "Example Creator" },
"stats": { "followerCount": 52300, "videoCount": 214 }
}
```
## Base URL
```
https://scraper.riken.me
```
Every route is a `GET` under `/v1/...` or `/v3/...` and every answer is JSON. The full contract is also published as [OpenAPI](https://scraper.riken.me/docs/openapi.json).
## Endpoints
| endpoint | path | status | cost |
|---|---|---|---|
| [Profile](https://scraper.riken.me/docs/v1/tiktok/profile) | `GET /v1/tiktok/profile` | live | 1 credit |
| Profile videos | `GET /v3/tiktok/profile/videos` | live, page 1 only in beta | 1 credit |
| [Keyword search](https://scraper.riken.me/docs/v1/tiktok/search/keyword) | `GET /v1/tiktok/search/keyword` | live | 1 credit per page |
| Followers | `GET /v1/tiktok/user/followers` | coming | 1 credit per page |
| Following | `GET /v1/tiktok/user/following` | coming | 1 credit per page |
| Audience | `GET /v1/tiktok/user/audience` | coming | 20 credits |
The sidebar lists every endpoint with its own page: parameters, code in six languages, a live **Try it** box and example answers per status.
## Before you ship
- Set your HTTP client timeout above 180 seconds. A request can be held that long before you get `504 timeout`. See [Capacity and Retry-After](https://scraper.riken.me/docs/capacity).
- Branch on the `error` code, never on `message`. See [Errors](https://scraper.riken.me/docs/errors).
- Send an `Idempotency-Key` on retries so a retry never costs twice. See [Retries and idempotency](https://scraper.riken.me/docs/idempotency).
## For AI tools
- [llms.txt](https://scraper.riken.me/docs/llms.txt) lists every page. [llms-full.txt](https://scraper.riken.me/docs/llms-full.txt) is the whole site in one file.
- Each page has a **Copy for AI** button that copies it as Markdown.
- The same endpoints work as tools in Claude, Cursor and other agents through the [MCP server](https://scraper.riken.me/docs/mcp).
---
# Authentication
URL: https://scraper.riken.me/docs/authentication
Send your API key on every request, in one of two headers.
Every request needs your API key. Send it in either header:
```http
x-api-key: sk_...
```
```http
Authorization: Bearer sk_...
```
If a request carries both, `x-api-key` is used and `Authorization` is ignored.
## Examples
```bash
curl -s "https://scraper.riken.me/v1/tiktok/profile?handle=example_creator" \
-H "x-api-key: $RIKEN_API_KEY"
```
```bash
curl -s "https://scraper.riken.me/v1/tiktok/profile?handle=example_creator" \
-H "Authorization: Bearer $RIKEN_API_KEY"
```
## When the key is wrong
A missing, unknown or revoked key gets `401 unauthorized`. It is never charged.
```json
{"success":false,"error":"unauthorized","errorStatus":401,"message":"missing or invalid API key: send x-api-key or Authorization: Bearer","credits_charged":0}
```
## Keeping keys safe
- Call the API from your server, not from a browser or mobile app. Anyone who can read the key can spend its credits.
- Each key has its own credit balance, its own [idempotency](https://scraper.riken.me/docs/idempotency) replays and its own [free-answer limit](https://scraper.riken.me/docs/free-answer-limit).
- Revoke a key in the [dashboard](https://scraper.riken.me/dashboard) the moment it leaks. Revocation takes effect on the next request.
The **Try it** box on endpoint pages keeps your key in that browser tab only and sends it only to `https://scraper.riken.me`. Live requests from it spend credits like any other.
---
# Response envelope
URL: https://scraper.riken.me/docs/response-envelope
The keys every answer has, success or error.
## Success
Every successful answer has three keys next to the route's own data:
```json
{
"success": true,
"credits_charged": 1,
"credits_remaining": 999
}
```
| key | meaning |
|---|---|
| `success` | `true` |
| `credits_charged` | what this request cost |
| `credits_remaining` | what your key can still spend after this request |
Credit amounts are exact decimals in the JSON, never rounded floats. Parse them as decimals if you do money math.
## Error
Every error has the same shape, and `errorStatus` always equals the HTTP status:
```json
{
"success": false,
"error": "timeout",
"errorStatus": 504,
"message": "no answer within the time limit; not charged",
"credits_charged": 0
}
```
| key | meaning |
|---|---|
| `error` | a stable code. Branch on this. |
| `errorStatus` | the HTTP status, repeated |
| `message` | for people. It may be reworded at any time. |
| `credits_charged` | always `0` on an error |
| `retry_after_s` | optional: the same number as the `Retry-After` header |
| `credits_remaining` | optional: on `402`, and on free answers that report it |
| `needed_credits` | optional: on `402`, what the request costs |
Every code is listed on [Errors](https://scraper.riken.me/docs/errors).
---
# Credits and pricing
URL: https://scraper.riken.me/docs/credits
What each call costs, and exactly which answers are charged.
You buy credits and each answer spends some. Every endpoint page shows its cost at the top.
## Cost per endpoint
| endpoint | cost |
|---|---|
| Profile | 1 credit |
| Profile videos | 1 credit |
| Keyword search | 1 credit per page |
| Followers | 1 credit per page |
| Following | 1 credit per page. A hidden following list is free. |
| Audience | 20 credits, charged only when 30 or more followers are placed |
| Job results and export | free |
Followers, following and audience are not open yet. An endpoint without a price answers `503 route_not_priced` and never serves for free.
## Charged or free
You pay only for an answer you receive. Exactly these are charged:
| answer | status | charged |
|---|---|---|
| a success | 200 | yes |
| profile of an account that does not exist (`account_deactivated: true`) | 200 | yes |
| profile videos or followers of a private account (`account_private: true`) | 200 | yes |
Everything else is free: every `4xx` and `5xx`, timeouts, overload refusals and bad requests. A request you cancel by closing the connection is not charged, even if the answer was ready.
## Reading your balance
Every answer carries `credits_remaining`. A request your key cannot cover gets `402 insufficient_credits` with `needed_credits`, and is not charged:
```json
{"success":false,"error":"insufficient_credits","errorStatus":402,"message":"this key does not have enough credits for this request","credits_charged":0,"credits_remaining":0,"needed_credits":1}
```
Top up in the [dashboard](https://scraper.riken.me/dashboard).
---
# Errors
URL: https://scraper.riken.me/docs/errors
Every error code, what it means and what to do. Errors are never charged.
Every error body on the TikTok routes has the same [shape](https://scraper.riken.me/docs/response-envelope#error). Branch on `error`: it is stable. `message` is for people and may change.
```json
{
"success": false,
"error": "",
"errorStatus": 400,
"message": "",
"credits_charged": 0
}
```
## All codes
The TikTok routes (`/v1/tiktok/...`, `/v3/tiktok/...`):
| status | error | routes | meaning | what to do | charged |
|---|---|---|---|---|---|
| 400 | `bad_request` | all | A parameter is missing or malformed: no handle, a character TikTok does not allow, a query over 200 bytes, an Idempotency-Key over 255 characters. | Fix the request. | free |
| 400 | `unsupported_parameter` | all | A parameter value we cannot honour yet, a user_id not in our index, or a user_id whose handle has changed. | Drop the parameter, or pass handle. | free |
| 400 | `invalid_cursor` | search | The cursor is not from a previous answer to the same query with the same API key. | Start again without a cursor. | free |
| 400 | `cursor_expired` | search | The cursor is older than 24 hours, or the search session expired. | Start again from page 1. | free |
| 401 | `unauthorized` | all | Missing, unknown or revoked key. | Send x-api-key. | free |
| 402 | `insufficient_credits` | all | The key cannot cover this request. The body has needed_credits and credits_remaining. | Top up. | free |
| 404 | `not_found` | profile videos, followers, following, audience | The account does not exist (account_deactivated: true). | Check the handle. | free |
| 409 | `idempotency_conflict` | all | A request with this Idempotency-Key and parameters is still running. Retry-After: 1. | Retry after it finishes. | free |
| 409 | `cursor_region_changed` | search | Page 2+ could only be searched from another country than page 1. | Start again from page 1. | free |
| 409 | `cursor_exit_gone` | search | The search session behind this cursor is gone. | Start again from page 1. | free |
| 429 | `over_capacity` | all | No capacity to answer within the time limit, or the server is restarting. Has Retry-After. | Wait Retry-After seconds and retry. | free |
| 429 | `rate_limited` | profile, profile videos, followers, following | The free-answer limit: this answer would have been free, and the key has had 100 free answers in 10 minutes. Has Retry-After. | Wait, or fix the handles you send. | free |
| 500 | `internal` | all | Our fault. | Retry. Report the X-Request-Id. | free |
| 501 | `not_yet_available` | profile videos | max_cursor asks for page 2+, which is not available in beta. | Call without max_cursor. | free |
| 502 | `upstream_failed` | all | TikTok's answer failed or could not be read, including accounts TikTok withholds. | Retry later. | free |
| 503 | `route_not_priced` | all | The route has no price yet, so it does not serve. | The route is not open yet. | free |
| 503 | `search_unavailable` | search | No search capacity right now. Retry-After: 60. | Retry after 60 seconds. | free |
| 504 | `timeout` | all | No answer within the time limit: 180 seconds, 300 for audience. | Retry. | free |
## Job routes
`/v1/jobs`, `/v1/search` and export answer errors in their own shape, with the code under `error.code`:
```json
{
"error": {
"code": "invalid_unit",
"message": "",
"field": "keywords"
}
}
```
| status | error.code | routes | meaning | what to do | charged |
|---|---|---|---|---|---|
| 400 | `bad_request` | jobs | The body or a parameter is malformed, or has an unknown field. | Fix the request. | free |
| 400 | `invalid_unit` | submit a job, search | A keyword or handle cannot be used as given. error.field says which list. | Fix that unit. | free |
| 400 | `cursor_expired` | get a job | A paging cursor from before the id-order change. | Start again without a cursor. | free |
| 401 | `unauthorized` | jobs | Missing, unknown or revoked key. | Send x-api-key. | free |
| 402 | `insufficient_credits` | submit a job, search | The key cannot hold the job's credits. error has needed_credits and available_credits. | Top up, or submit fewer units. | free |
| 404 | `not_found` | jobs | No such job, another key's job, or no export requested yet. | Check the job id and key. | free |
| 409 | `idempotency_conflict` | submit a job, search | This Idempotency-Key was used with a different request. error.job_id is that job. | Use a new Idempotency-Key. | free |
| 409 | `job_not_finished` | export | The job is still queued or running, or its results are still being saved. | Poll the job until finished_at is set. | free |
| 429 | `rate_limited` | jobs | The key's request rate on the job routes. Has Retry-After. | Wait Retry-After seconds. | free |
| 429 | `concurrency_limit` | submit a job, search | The key already has its maximum of jobs queued or running. Retry-After: 10. | Wait for a job to finish, or cancel one. | free |
| 500 | `internal` | jobs | Our fault. | Retry. | free |
| 503 | `export_unavailable` | export | Export is switched off on this server. | Page the job instead, or retry later. | free |
## Not found on profile videos and followers
The `404` of profile videos and followers carries a few extra keys:
```json
{
"success": false,
"error": "not_found",
"errorStatus": 404,
"message": "Account deactivated",
"account_deactivated": true,
"handle": "no_such_user_123",
"credits_charged": 0,
"credits_remaining": 998
}
```
A profile of an account that does not exist is different: it is a `200` with `account_deactivated: true`, and it is charged. See [Credits and pricing](https://scraper.riken.me/docs/credits#charged-or-free).
## Retrying
| status | retry? |
|---|---|
| `400`, `401`, `402`, `404` | no: fix the request, the key or the balance first |
| `409` | yes, after `Retry-After`, or start the search again from page 1 |
| `429` | yes, after `Retry-After` seconds |
| `500`, `502`, `504` | yes, with backoff. Report the `X-Request-Id` if it keeps happening. |
| `501`, `503 route_not_priced` | no: the feature is not open yet |
| `503 search_unavailable` | yes, after 60 seconds |
Send an [Idempotency-Key](https://scraper.riken.me/docs/idempotency) on every retry.
---
# Retries and idempotency
URL: https://scraper.riken.me/docs/idempotency
Retry safely with an Idempotency-Key. A replayed answer is never charged twice.
Send an `Idempotency-Key` header with any string up to 255 characters. Use a new key for each logical request and the same key for its retries.
```bash
curl -s "https://scraper.riken.me/v1/tiktok/profile?handle=example_creator" \
-H "x-api-key: $RIKEN_API_KEY" \
-H "Idempotency-Key: profile-example_creator-2026-11-11"
```
## How it behaves
| case | result |
|---|---|
| same key, same parameters, within 24 hours | the stored answer comes back with `Idempotent-Replay: true`. Not charged again. |
| same key, other parameters | a new request, charged on its own. The key is bound to the parameters. |
| same key while the first request still runs | `409 idempotency_conflict` with `Retry-After: 1`. Wait and retry. |
| the first answer was an error or free | nothing was stored, so the retry runs again |
| another API key, same Idempotency-Key | unrelated. Replays never cross API keys. |
Only charged answers are stored.
## A retry loop
```python
import time, uuid, requests
def get(url, key, params):
idem = str(uuid.uuid4())
for attempt in range(5):
r = requests.get(url, params=params, timeout=200,
headers={"x-api-key": key, "Idempotency-Key": idem})
if r.status_code in (409, 429, 503) or r.status_code >= 500:
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
continue
return r
return r
```
---
# Capacity and Retry-After
URL: https://scraper.riken.me/docs/capacity
No per-key rate limit. When we are full you get 429 with Retry-After, free.
There is no per-key rate limit and no concurrency cap. Requests from all keys are served first in, first out, as capacity allows.
## When we are full
If we cannot answer within the time limit, you get `429 over_capacity` straight away. It has a `Retry-After` header and the same number in `retry_after_s`. Nothing is charged.
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12
```
```json
{"success":false,"error":"over_capacity","errorStatus":429,"message":"no capacity to answer within the time limit; retry after retry_after_s seconds","credits_charged":0,"retry_after_s":12}
```
Wait that many seconds and retry. The value is our estimate of when capacity frees up. During a restart, or when too many requests are already held open, it is 30 seconds.
## Timeouts
We hold a request open for up to 180 seconds. Past that you get `504 timeout`, free. A client that gives up sooner sees its own timeout instead of our answer.
| client | setting |
|---|---|
| curl | `--max-time 200` |
| Python `requests` | `timeout=200` |
| Node.js `fetch` | `signal: AbortSignal.timeout(200_000)` |
| Go `net/http` | `http.Client{Timeout: 200 * time.Second}` |
## Cold starts
An endpoint with no capacity running must start some first. The first request after a quiet period can take over a minute.
## Related limits
- [Free-answer limit](https://scraper.riken.me/docs/free-answer-limit): 100 free answers per 10 minutes per key.
- Keyword search can answer `503 search_unavailable` with `Retry-After: 60`.
---
# Free-answer limit
URL: https://scraper.riken.me/docs/free-answer-limit
100 free answers per 10 minutes per key. Charged answers are never limited.
A few answers are free even though they are answers:
- profile videos, followers or following of an account that does not exist (`404`);
- following of an account that hides whom it follows (`200`, `following_hidden: true`);
- a `user_id` we have not indexed yet (`400 unsupported_parameter`);
- a `user_id` whose handle has changed since we indexed it (`400 unsupported_parameter`).
Each API key gets **100 free answers per 10 minutes**, in a sliding window. Past that, an answer that would have been free becomes `429 rate_limited` with `Retry-After`. That is free too.
Charged answers are never limited. The limit only stops a key from probing made-up handles at no cost.
## If you hit it
- Check the handles you send. A list with many dead or mistyped handles hits this limit first.
- Prefer `handle` over `user_id`. A `user_id` works only for accounts in our index.
- Wait `Retry-After` seconds and continue.
---
# Response headers
URL: https://scraper.riken.me/docs/response-headers
Headers we send back and when.
| header | when |
|---|---|
| `X-Request-Id` | every answer. Quote it when you report a problem. |
| `Retry-After` | on `429`, `503 search_unavailable` and `409 idempotency_conflict`: seconds to wait |
| `Idempotent-Replay: true` | a stored answer replayed for an [Idempotency-Key](https://scraper.riken.me/docs/idempotency) |
| `X-Search-Region` | keyword search answers: `US` or `CA`, where the page was searched from |
Job exports also answer `202` with `Location` and `Retry-After: 5` while they build. See [Jobs and export](https://scraper.riken.me/docs/jobs-and-export).
---
# Latency
URL: https://scraper.riken.me/docs/latency
What our own load tests measured. A guide, not a promise.
Measured on production code on 30 and 31 October 2026, from the client side.
| endpoint | p50 | p95 | test |
|---|---|---|---|
| Profile | 1.14 s | 4.51 s | 40 requests per second |
| Profile and profile videos, 60/40 mix | 1.63 s | 6.87 s | 56 requests per second |
| Keyword search, pages 1 to 3 | 3.9 s | 8.9 s | 90 pages, one request at a time |
| Audience | about 37 s | | 3 creators |
## Caveats
- Under load past our running capacity, profile p95 rose to 23.6 s while more capacity started.
- Search was measured one request at a time. Its p95 includes a 2 s gap between pages.
- An endpoint with nothing running must start first, which took about 70 s for search.
- Followers and following are not live, so they are not measured.
Plan for the tail: set your client timeout above 180 seconds and run requests in parallel rather than in a long serial loop.
---
# Migrating from another API
URL: https://scraper.riken.me/docs/migrating
Same paths, parameters and body keys. Change the base URL and the key.
Our TikTok endpoints use the same paths, query parameters and response keys as the TikTok data APIs most teams already call. If your code uses those paths today, you switch by changing two settings.
| | before | after |
|---|---|---|
| base URL | your current provider | `https://scraper.riken.me` |
| auth header | `x-api-key: ` | `x-api-key: ` (or `Authorization: Bearer `) |
| paths | `/v1/tiktok/profile` and the rest | the same |
| body keys | `success`, `credits_charged`, `credits_remaining`, route keys | the same |
```bash
curl -s "https://scraper.riken.me/v1/tiktok/profile?handle=example_creator" \
-H "x-api-key: $RIKEN_API_KEY"
```
## What behaves differently
- **No cache.** Every answer is fetched live. `cache_max_age` is accepted if it is one of `1d`, `3d`, `7d`, `14d`, `30d`, and ignored. There are no `cached` or `cached_at` keys.
- `trim` is accepted and ignored. `region` is ignored on the profile endpoints. On keyword search, `CA` searches from Canada only; any other two letters keep the default order, US then Canada. A value that is not two letters is a free `400`.
- **No silent fallbacks.** A parameter we cannot honour gets `400 unsupported_parameter`, free. We never return unfiltered results for a filter we ignore.
- `user_id` works only for accounts in our index. Otherwise pass `handle`.
- **Stable error codes.** Every error has an `error` code you can branch on. See [Errors](https://scraper.riken.me/docs/errors).
- We send `429` and `503` with `Retry-After`. See [Capacity and Retry-After](https://scraper.riken.me/docs/capacity).
- Profile videos returns page 1 only during the beta.
---
# MCP server
URL: https://scraper.riken.me/docs/mcp
Use the API as tools in Claude, Cursor, VS Code and other agents. Same key, same prices.
The MCP server exposes each endpoint as a tool. Each tool call is one HTTP call to `https://scraper.riken.me` with your own key, and returns the API's JSON answer unchanged. Prices are the same as the API's, and errors are free.
## Tools
| tool | endpoint | cost |
|---|---|---|
| `tiktok_profile` | `GET /v1/tiktok/profile` | 1 credit |
| `tiktok_profile_videos` | `GET /v3/tiktok/profile/videos` (page 1 in beta) | 1 credit |
| `tiktok_search_keyword` | `GET /v1/tiktok/search/keyword` | 1 credit per page |
| `tiktok_user_followers` | `GET /v1/tiktok/user/followers` | 1 credit per page, when it opens |
| `tiktok_user_audience` | `GET /v1/tiktok/user/audience` | 20 credits, when it opens |
Arguments are the endpoint's parameters: `handle` or `user_id`, and `query` and `cursor` for search. Every tool also takes an optional `idempotency_key`.
Each result has two text blocks: the API's JSON body, byte for byte, and one line of HTTP facts such as `http_status=200 search_region=US request_id=...`. A non-2xx answer is a tool error (`isError: true`) carrying the API's error body, so the model sees the `error` code. The server never retries on its own.
## Connect
The hosted endpoint `https://scraper.riken.me/mcp` is planned, with your API key in a header. Until it opens, run the server locally over stdio (the second tab of each client).
Remote:
```bash
claude mcp add --transport http riken-scraper https://scraper.riken.me/mcp \
--header "x-api-key: $RIKEN_API_KEY"
```
Local (stdio):
```bash
claude mcp add riken-scraper \
-e CREATOR_API_BASE_URL=https://scraper.riken.me \
-e CREATOR_API_KEY=$RIKEN_API_KEY \
-- /absolute/path/to/mcpserver
```
Then run `/mcp` in Claude Code to check it is connected. If long calls are cut off, raise `MCP_TOOL_TIMEOUT` (milliseconds).
Add to `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`) and restart Claude Desktop:
```json
{
"mcpServers": {
"riken-scraper": {
"command": "/absolute/path/to/mcpserver",
"env": {
"CREATOR_API_BASE_URL": "https://scraper.riken.me",
"CREATOR_API_KEY": "sk_..."
}
}
}
}
```
In `~/.cursor/mcp.json` or `.cursor/mcp.json`:
```json
{
"mcpServers": {
"riken-scraper": {
"url": "https://scraper.riken.me/mcp",
"headers": { "x-api-key": "sk_..." }
}
}
}
```
Local (stdio):
```json
{
"mcpServers": {
"riken-scraper": {
"command": "/absolute/path/to/mcpserver",
"env": { "CREATOR_API_BASE_URL": "https://scraper.riken.me", "CREATOR_API_KEY": "sk_..." }
}
}
}
```
In `.vscode/mcp.json`:
```json
{
"servers": {
"riken-scraper": {
"type": "http",
"url": "https://scraper.riken.me/mcp",
"headers": { "x-api-key": "${input:riken-scraper-key}" }
}
},
"inputs": [
{ "id": "riken-scraper-key", "type": "promptString", "description": "Riken Scraper API key", "password": true }
]
}
```
## Configuration of the local server
| flag | environment | meaning |
|---|---|---|
| `-base-url` | `CREATOR_API_BASE_URL` | the API base URL, `https://scraper.riken.me` |
| `-api-key` | `CREATOR_API_KEY` | your API key (stdio only). Prefer the environment. |
| `-http ADDR` | | serve streamable HTTP at `http://ADDR/mcp` instead of stdio |
In HTTP mode every request must carry the caller's own key as `x-api-key` or `Authorization: Bearer`. The server's own key is never used, so one caller can never spend another's credits.
## Timeouts
The API holds a request up to 180 seconds. The server waits 200 seconds for an answer (6 minutes for audience). Some clients give up on a tool call sooner, so raise their tool timeout if long calls are cut off.
---
# Profile
URL: https://scraper.riken.me/docs/v1/tiktok/profile
`GET https://scraper.riken.me/v1/tiktok/profile`
- Status: Live
- Cost: 1 credit. Errors are free.
One TikTok account: its user record and its counts. `stats` holds TikTok's
rounded counts and `statsV2` the exact counts as strings. `itemList` is
always empty, as on ScrapeCreators.
An account that does not exist is a **200, charged** answer with
`account_deactivated: true`, as ScrapeCreators answers it. A private
account is the normal body with `user.privateAccount: true`, charged.
When TikTok's profile page fails and we fall back to the embed, some
`user` settings and counts can be `null`.
Latency (our load test, 31 Oct 2026, 40 requests/s on one box): p50 1.14 s, p95 4.51 s.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
| `Idempotency-Key` | string | no | Any string up to 255 characters. Makes a retry safe for 24 hours; bound to this API key and the request's parameters. |
## Query parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `handle` | string | no | The TikTok handle. Required unless `user_id` is given. A leading `@` is removed; case does not matter. Letters, digits, `_` and `.` only, at most 64 characters. With both `handle` and `user_id`, `user_id` is ignored. | `dagestantrainingcamp` |
| `user_id` | string | no | TikTok's numeric user id, at most 20 digits, used only when `handle` is absent. Works only for accounts in our index: an unknown id is `400 unsupported_parameter` ("pass handle"), and an id whose handle has changed is `400 unsupported_parameter` too. Both free, both count toward the free-answer limit. | `6918108151024829445` |
| `cache_max_age` | `1d` \| `3d` \| `7d` \| `14d` \| `30d` | no | Accepted and ignored; we always fetch live. Any other value is `400 bad_request`. | |
| `trim` | boolean | no | Accepted and ignored. Must be `true` or `false` (else `400 bad_request`). | |
| `region` | string | no | Accepted and ignored. Not validated on this route (any value passes). | |
## Example
```bash
curl -s --max-time 400 -X GET "https://scraper.riken.me/v1/tiktok/profile?handle=dagestantrainingcamp&user_id=6918108151024829445" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | charged | The profile, or the not-found answer. Charged either way. |
| 400 | free | A parameter is missing, malformed, or one we cannot honour yet. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 402 | free | The key cannot cover this request. `needed_credits` is the price. Free. |
| 409 | free | A request with this Idempotency-Key and parameters is still running. Free; retry after `Retry-After` (1 s). |
| 429 | free | `over_capacity`: no capacity within the time limit, or the server is restarting (30 s). `rate_limited`: the free-answer limit (this answer would have been free). Free; wait `Retry-After` seconds. |
| 500 | free | Our fault. Free; retry and quote `X-Request-Id`. |
| 502 | free | TikTok's answer failed or could not be read (including accounts TikTok withholds from logged-out views). Free. |
| 503 | free | The route has no price yet, so it does not serve. Free. |
| 504 | free | No answer within the time limit (180 s; audience 300 s). Free. |
## Response examples
### 200
A profile (our shaper on our fixture):
```json
{
"credits_charged": 1,
"credits_remaining": 999,
"itemList": [],
"stats": {
"diggCount": 0,
"followerCount": 8740,
"followingCount": 125,
"friendCount": 41,
"heart": 275500,
"heartCount": 275500,
"videoCount": 48
},
"statsV2": {
"diggCount": "0",
"followerCount": "8740",
"followingCount": "125",
"friendCount": "41",
"heart": "275546",
"heartCount": "275546",
"videoCount": "48"
},
"success": true,
"user": {
"UserStoryStatus": 0,
"avatarLarger": "https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/23acee9247d9576ff8a4626add13fbfd~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=9640&refresh_token=7231d9a6&x-expires=1793563200&x-signature=c41FazTETsYo8P%2BTmuPfZjEZAFI%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast8",
"avatarMedium": "https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/23acee9247d9576ff8a4626add13fbfd~tplv-tiktokx-cropcenter:720:720.jpeg?dr=9640&refresh_token=69603e15&x-expires=1793563200&x-signature=scJlakKQv0P9I46Wo9%2FV5pfatIw%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast8",
"avatarThumb": "https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/23acee9247d9576ff8a4626add13fbfd~tplv-tiktokx-cropcenter:100:100.jpeg?dr=9640&refresh_token=95abdb08&x-expires=1793563200&x-signature=8OwnKAnW8MdgUe9z%2FBger1%2FN%2FZQ%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast8",
"bioLink": {
"link": "Dagestantrainingcamp.com",
"risk": 3
},
"canExpPlaylist": true,
"commentSetting": 0,
"commerceUserInfo": {
"commerceUser": false
},
"createTime": 1610747711,
"downloadSetting": 0,
"duetSetting": 0,
"eventList": [],
"followingVisibility": 1,
"ftc": false,
"id": "6918108151024829445",
"isADVirtual": false,
"isEmbedBanned": false,
"isOrganization": 0,
"language": "fr",
"nickNameModifyTime": 1777223133,
"nickname": "Dagestan Training Camp",
"nowInvitationCardUrl": "",
"openFavorite": false,
"privateAccount": false,
"profileEmbedPermission": 1,
"profileTab": {
"showMusicTab": false,
"showPlayListTab": false,
"showQuestionTab": false
},
"recommendReason": "",
"relation": 0,
"roomId": "",
"secUid": "MS4wLjABAAAAynBXrV7vRUXrvgYYr-msCZbjkFi9Xh-AiK6DkKzopQIeqh7Ppoqv81VHPZfD5xf6",
"secret": false,
"shortDramaCreator": {},
"shortId": "",
"signature": "🥊 MMA & Wrestling Training Camp in Dagestan\n⛰️For all level (beginner to professional)\n\nCheck our website ⬇️",
"stitchSetting": 0,
"suggestAccountBind": false,
"ttSeller": false,
"uniqueId": "dagestantrainingcamp",
"uniqueIdModifyTime": 0,
"verified": false
}
}
```
The account does not exist (charged):
```json
{
"success": true,
"credits_remaining": 48175,
"credits_charged": 1,
"account_deactivated": true,
"handle": "zq_nosuchuser_scdiff_7731",
"message": "Account doesn't exist"
}
```
### 400
bad_request:
```json
{
"success": false,
"error": "bad_request",
"errorStatus": 400,
"message": "handle is required",
"credits_charged": 0
}
```
unsupported_parameter:
```json
{
"success": false,
"error": "unsupported_parameter",
"errorStatus": 400,
"message": "user_id 6918108151024829445 is not in our index yet; pass handle",
"credits_charged": 0
}
```
### 401
```json
{
"success": false,
"error": "unauthorized",
"errorStatus": 401,
"message": "missing or invalid API key: send x-api-key or Authorization: Bearer",
"credits_charged": 0
}
```
### 402
```json
{
"success": false,
"error": "insufficient_credits",
"errorStatus": 402,
"message": "this key does not have enough credits for this request",
"credits_charged": 0,
"credits_remaining": 0,
"needed_credits": 1
}
```
### 409
```json
{
"success": false,
"error": "idempotency_conflict",
"errorStatus": 409,
"message": "a request with this Idempotency-Key is still in progress; retry after it finishes",
"credits_charged": 0,
"retry_after_s": 1
}
```
### 429
Over capacity (Retry-After 12):
```json
{
"success": false,
"error": "over_capacity",
"errorStatus": 429,
"message": "no capacity to answer within the time limit; retry after retry_after_s seconds",
"credits_charged": 0,
"retry_after_s": 12
}
```
rate_limited:
```json
{
"success": false,
"error": "rate_limited",
"errorStatus": 429,
"message": "this answer would be free (unknown user_id, moved handle or missing account), and this key has had 100 free answers in 10m0s; retry after retry_after_s seconds; not charged. Answers that are charged are never limited",
"credits_charged": 0,
"retry_after_s": 42
}
```
### 500
```json
{
"success": false,
"error": "internal",
"errorStatus": 500,
"message": "internal error",
"credits_charged": 0
}
```
### 502
```json
{
"success": false,
"error": "upstream_failed",
"errorStatus": 502,
"message": "the upstream request failed; not charged",
"credits_charged": 0
}
```
### 503
The route has no price yet:
```json
{
"success": false,
"error": "route_not_priced",
"errorStatus": 503,
"message": "this route is not available yet",
"credits_charged": 0
}
```
### 504
```json
{
"success": false,
"error": "timeout",
"errorStatus": 504,
"message": "no answer within the time limit; not charged",
"credits_charged": 0
}
```
---
# Profile videos
URL: https://scraper.riken.me/docs/v3/tiktok/profile/videos
`GET https://scraper.riken.me/v3/tiktok/profile/videos`
- Status: Beta
- Cost: 1 credit. Errors are free.
An account's newest videos.
**Beta: page 1 only.** `has_more` is always `0`. Asking for a later page
with `max_cursor` returns `501 not_yet_available`, free. On page 1
`statistics` has `play_count` only; like, comment, share and save counts
are `null`. The count per page is what TikTok's embed gives (12 and 13 in
our fixtures) and is not fixed.
An account that does not exist is a **404, free** (counts toward the
free-answer limit). A private account is a 200, **charged**,
`{"handle", "account_private": true}`.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
| `Idempotency-Key` | string | no | Any string up to 255 characters. Makes a retry safe for 24 hours; bound to this API key and the request's parameters. |
## Query parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `handle` | string | no | The TikTok handle. Required unless `user_id` is given. A leading `@` is removed; case does not matter. Letters, digits, `_` and `.` only, at most 64 characters. With both `handle` and `user_id`, `user_id` is ignored. | `dagestantrainingcamp` |
| `user_id` | string | no | TikTok's numeric user id, at most 20 digits, used only when `handle` is absent. Works only for accounts in our index: an unknown id is `400 unsupported_parameter` ("pass handle"), and an id whose handle has changed is `400 unsupported_parameter` too. Both free, both count toward the free-answer limit. | `6918108151024829445` |
| `max_cursor` | string | no | Absent, empty or `0` means page 1. Any other value is `501 not_yet_available` in beta. | `0` |
| `sort_by` | `latest` \| `popular` | no | `latest` or absent. `popular` is `400 unsupported_parameter`; any other value is `400 bad_request`. | |
| `include_pinned` | boolean | no | `true` or absent. `false` is `400 unsupported_parameter`: pinned posts are always included. | |
| `region` | string | no | Two letters, accepted and ignored. Not two letters is `400 bad_request`. | |
| `trim` | string | no | Accepted and ignored. Not validated on this route. | |
## Example
```bash
curl -s --max-time 400 -X GET "https://scraper.riken.me/v3/tiktok/profile/videos?handle=dagestantrainingcamp&user_id=6918108151024829445&max_cursor=0" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | charged | Page 1 of the videos, or the private-account answer. Charged. |
| 400 | free | A parameter is missing, malformed, or one we cannot honour yet. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 402 | free | The key cannot cover this request. `needed_credits` is the price. Free. |
| 404 | free | The account does not exist. Free; counts toward the free-answer limit. |
| 409 | free | A request with this Idempotency-Key and parameters is still running. Free; retry after `Retry-After` (1 s). |
| 429 | free | `over_capacity`: no capacity within the time limit, or the server is restarting (30 s). `rate_limited`: the free-answer limit (this answer would have been free). Free; wait `Retry-After` seconds. |
| 500 | free | Our fault. Free; retry and quote `X-Request-Id`. |
| 501 | free | `max_cursor` asks for page 2 or later, not available in beta. Free. |
| 502 | free | TikTok's answer failed or could not be read (including accounts TikTok withholds from logged-out views). Free. |
| 503 | free | The route has no price yet, so it does not serve. Free. |
| 504 | free | No answer within the time limit (180 s; audience 300 s). Free. |
## Response examples
### 200
Page 1 (our shaper on our fixture, list cut to one item):
```json
{
"aweme_list": [
{
"added_sound_music_info": null,
"aigc_info": null,
"anchors": null,
"anchors_extras": null,
"animated_image_info": null,
"author": {
"avatar_168x168": null,
"avatar_300x300": null,
"avatar_larger": {
"height": null,
"uri": null,
"url_list": [
"https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/23acee9247d9576ff8a4626add13fbfd~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=9640&refresh_token=7231d9a6&x-expires=1793563200&x-signature=c41FazTETsYo8P%2BTmuPfZjEZAFI%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast8"
],
"url_prefix": null,
"width": null
},
"avatar_medium": {
"height": null,
"uri": null,
"url_list": [
"https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/23acee9247d9576ff8a4626add13fbfd~tplv-tiktokx-cropcenter:720:720.jpeg?dr=9640&refresh_token=69603e15&x-expires=1793563200&x-signature=scJlakKQv0P9I46Wo9%2FV5pfatIw%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast8"
],
"url_prefix": null,
"width": null
},
"avatar_thumb": {
"height": null,
"uri": null,
"url_list": [
"https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/23acee9247d9576ff8a4626add13fbfd~tplv-tiktokx-cropcenter:100:100.jpeg?dr=9640&refresh_token=95abdb08&x-expires=1793563200&x-signature=8OwnKAnW8MdgUe9z%2FBger1%2FN%2FZQ%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast8"
],
"url_prefix": null,
"width": null
},
"aweme_count": 48,
"commerce_user_level": null,
"custom_verify": null,
"enterprise_verify_reason": null,
"favoriting_count": 0,
"follower_count": 8740,
"following_count": 125,
"hide_search": null,
"ins_id": null,
"is_star": null,
"language": "fr",
"nickname": "Dagestan Training Camp",
"sec_uid": "MS4wLjABAAAAynBXrV7vRUXrvgYYr-msCZbjkFi9Xh-AiK6DkKzopQIeqh7Ppoqv81VHPZfD5xf6",
"secret": 0,
"short_id": "",
"signature": "🥊 MMA & Wrestling Training Camp in Dagestan\n⛰️For all level (beginner to professional)\n\nCheck our website ⬇️",
"total_favorited": 275500,
"twitter_id": null,
"uid": "6918108151024829445",
"unique_id": "dagestantrainingcamp",
"verification_type": 0,
"with_commerce_entry": null,
"youtube_channel_id": null
},
"author_user_id": 6918108151024829000,
"aweme_acl": null,
"aweme_id": "7574554126068075798",
"aweme_type": null,
"banners": null,
"behind_the_song_music_ids": null,
"behind_the_song_video_music_ids": null,
"bodydance_score": null,
"branded_content_accounts": null,
"c2pa_info": null,
"cc_template_info": null,
"cha_list": null,
"challenge_position": null,
"cmt_swt": null,
"collect_stat": null,
"comment_config": null,
"comment_topbar_info": null,
"commerce_config_data": null,
"commerce_info": null,
"content_desc": null,
"content_desc_extra": null,
"content_model": null,
"content_original_type": null,
"content_size_type": null,
"content_type": null,
"cover_labels": null,
"create_time": 1763588312,
"create_time_utc": "2025-11-19T21:38:32.000Z",
"creation_info": null,
"desc": "Visite de la salle à Khabib, c’est ici que vous allez passer vos camps d’entraînement avec nous 🏔️ #mma #islammakhachev #khabib_nurmagomedov #dagestan #trainingcamp ",
"desc_language": null,
"disable_search_trending_bar": null,
"distance": null,
"distribute_type": null,
"follow_up_item_id_groups": null,
"follow_up_publish_from_id": null,
"geofencing": null,
"geofencing_regions": null,
"green_screen_materials": null,
"group_id": "7574554126068075798",
"group_id_list": null,
"has_danmaku": null,
"has_promote_entry": null,
"has_vs_entry": null,
"have_dashboard": null,
"hybrid_label": null,
"image_infos": null,
"interact_permission": null,
"interaction_stickers": null,
"is_ad": null,
"is_ads": null,
"is_description_translatable": null,
"is_eligible_for_commission": null,
"is_hash_tag": null,
"is_nff_or_nr": null,
"is_on_this_day": null,
"is_paid_partnership": null,
"is_pgcshow": null,
"is_preview": null,
"is_relieve": null,
"is_text_sticker_translatable": null,
"is_title_translatable": null,
"is_top": null,
"is_vr": null,
"item_comment_settings": null,
"item_duet": null,
"item_react": null,
"item_stitch": null,
"label_top": null,
"label_top_text": null,
"long_video": null,
"main_arch_common": null,
"mask_infos": null,
"meme_reg_info": null,
"misc_info": null,
"muf_comment_info_v2": null,
"music": {
"author": null,
"duration": null,
"id_str": null,
"is_original_sound": null,
"owner_handle": null,
"title": null
},
"music_begin_time_in_ms": null,
"music_end_time_in_ms": null,
"music_selected_from": null,
"music_title_style": null,
"music_volume": null,
"need_trim_step": null,
"need_vs_entry": null,
"nickname_position": null,
"no_selected_music": null,
"operator_boost_info": null,
"origin_comment_ids": null,
"origin_volume": null,
"original_client_text": null,
"picked_users": null,
"playlist_blocked": null,
"poi_re_tag_signal": null,
"position": null,
"prevent_download": null,
"products_info": null,
"promote_capcut_toggle": null,
"promote_icon_text": null,
"promote_toast": null,
"promote_toast_key": null,
"question_list": null,
"quick_reply_emojis": null,
"rate": null,
"reference_tts_voice_ids": null,
"reference_voice_filter_ids": null,
"region": null,
"risk_infos": null,
"search_highlight": null,
"share_info": null,
"share_url": "https://www.tiktok.com/@dagestantrainingcamp/video/7574554126068075798",
"shoot_tab_name": null,
"shop_product_url": null,
"social_interaction_blob": null,
"solaria_profile": null,
"sort_label": null,
"standard_component_info": null,
"statistics": {
"aweme_id": "7574554126068075798",
"collect_count": null,
"comment_count": null,
"digg_count": null,
"download_count": null,
"forward_count": null,
"lose_comment_count": null,
"lose_count": null,
"play_count": 42300,
"repost_count": null,
"share_count": null,
"whatsapp_share_count": null
},
"status": {
"allow_comment": null,
"allow_share": null,
"aweme_id": "7574554126068075798",
"is_delete": null,
"is_prohibited": null,
"private_status": 0,
"self_see": null
},
"support_danmaku": null,
"text_extra": null,
"text_sticker_major_lang": null,
"title_language": null,
"ttec_suggest_words": null,
"tts_voice_ids": null,
"uniqid_position": null,
"upvote_info": null,
"upvote_preload": null,
"url": "https://www.tiktok.com/@dagestantrainingcamp/video/7574554126068075798",
"used_full_song": null,
"user_digged": null,
"video": {
"CoverTsp": null,
"ai_dynamic_cover": null,
"ai_dynamic_cover_bak": null,
"animated_cover": null,
"big_thumbs": null,
"bit_rate": null,
"bit_rate_audio": null,
"cdn_url_expired": null,
"cover": {
"height": null,
"uri": null,
"url_list": [
"https://p16-common-sign.tiktokcdn-us.com/tos-no1a-p-0037-no/oMOEFHNkmQAjGzJuRaOgefGGAgEqzBDApQORqF~tplv-tiktokx-origin.image?dr=9636&x-expires=1793563200&x-signature=v5fG%2BDuW6t9y66Go9nYmPf%2BnXvM%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast8"
],
"url_prefix": null,
"width": null
},
"duration": null,
"dynamic_cover": {
"height": null,
"uri": null,
"url_list": [
"https://p16-common-sign.tiktokcdn-us.com/tos-no1a-p-0037-no/oURBgFFEEuNfAqHzOpC3AAkewKQEQhRODjOGEg~tplv-tiktokx-origin.image?dr=9636&x-expires=1793563200&x-signature=SF0ary6TU0RymVZVVO5%2FuRdbF0M%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast8"
],
"url_prefix": null,
"width": null
},
"has_watermark": null,
"height": 1024,
"is_bytevc1": null,
"is_callback": null,
"meta": null,
"misc_download_addrs": null,
"need_set_token": null,
"origin_cover": {
"height": null,
"uri": null,
"url_list": [
"https://p19-common-sign.tiktokcdn-us.com/tos-no1a-p-0037-no/owkOOjHzDQwOAeqgARFCk0RNGBf4pdFQEjguEH~tplv-tiktokx-origin.image?dr=9636&x-expires=1793563200&x-signature=yU0DtuLP405xCZlGLmGW7I02GQM%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast8"
],
"url_prefix": null,
"width": null
},
"play_addr": {
"height": null,
"uri": null,
"url_list": [
"https://v45.tiktokcdn-us.com/951e1b268a588f6919ae0e92d73dae0b/6ae7a75b/video/tos/useast2a/tos-useast2a-ve-68c810-euttp/ooIjRIDeHeMgcEIjb4PSnALAAkhGcBALAiVf4D/?a=1233&bti=ODszNWYuMDE6&&bt=1113&ft=aEKfuqT0mIoPD12pyq9I3wUBI0AbMeF~O5&mime_type=video_mp4&rc=ZjQ8ODc3aWdoODozZ2U3PEBpMzZ2d285cjY2NzMzbzczNUBjNWNjM19gNTExXy5iLzE0YSMtZWxgMmRjZGNhLS1kMTFzcw%3D%3D&vvpl=1&l=20261030205450AE8867C13A6CC8260364&btag=e00095000"
],
"url_prefix": null,
"width": null
},
"ratio": "540p",
"source_HDR_type": null,
"tags": null,
"width": 576
},
"video_control": null,
"video_labels": null,
"video_text": null,
"visual_search_info": null,
"voice_filter_ids": null,
"with_promotional_music": null,
"without_watermark": null
}
],
"credits_charged": 1,
"credits_remaining": 999,
"has_more": 0,
"max_cursor": 1763503506000,
"status_code": 0,
"success": true
}
```
A private account (charged; ScrapeCreators' recorded body, ours has the same keys):
```json
{
"success": true,
"credits_remaining": 48177,
"credits_charged": 1,
"handle": "yukaaa_s",
"account_private": true
}
```
### 400
bad_request:
```json
{
"success": false,
"error": "bad_request",
"errorStatus": 400,
"message": "handle is required",
"credits_charged": 0
}
```
unsupported_parameter:
```json
{
"success": false,
"error": "unsupported_parameter",
"errorStatus": 400,
"message": "user_id 6918108151024829445 is not in our index yet; pass handle",
"credits_charged": 0
}
```
### 401
```json
{
"success": false,
"error": "unauthorized",
"errorStatus": 401,
"message": "missing or invalid API key: send x-api-key or Authorization: Bearer",
"credits_charged": 0
}
```
### 402
```json
{
"success": false,
"error": "insufficient_credits",
"errorStatus": 402,
"message": "this key does not have enough credits for this request",
"credits_charged": 0,
"credits_remaining": 0,
"needed_credits": 1
}
```
### 404
ScrapeCreators' recorded body; ours has the same keys:
```json
{
"success": false,
"credits_remaining": 48176,
"credits_charged": 0,
"account_deactivated": true,
"handle": "zq_nosuchuser_scdiff_7731",
"message": "Account deactivated",
"error": "not_found",
"errorStatus": 404
}
```
### 409
```json
{
"success": false,
"error": "idempotency_conflict",
"errorStatus": 409,
"message": "a request with this Idempotency-Key is still in progress; retry after it finishes",
"credits_charged": 0,
"retry_after_s": 1
}
```
### 429
Over capacity (Retry-After 12):
```json
{
"success": false,
"error": "over_capacity",
"errorStatus": 429,
"message": "no capacity to answer within the time limit; retry after retry_after_s seconds",
"credits_charged": 0,
"retry_after_s": 12
}
```
rate_limited:
```json
{
"success": false,
"error": "rate_limited",
"errorStatus": 429,
"message": "this answer would be free (unknown user_id, moved handle or missing account), and this key has had 100 free answers in 10m0s; retry after retry_after_s seconds; not charged. Answers that are charged are never limited",
"credits_charged": 0,
"retry_after_s": 42
}
```
### 500
```json
{
"success": false,
"error": "internal",
"errorStatus": 500,
"message": "internal error",
"credits_charged": 0
}
```
### 501
```json
{
"success": false,
"error": "not_yet_available",
"errorStatus": 501,
"message": "pages after the first are not available yet; call without max_cursor for page 1",
"credits_charged": 0
}
```
### 502
```json
{
"success": false,
"error": "upstream_failed",
"errorStatus": 502,
"message": "the upstream request failed; not charged",
"credits_charged": 0
}
```
### 503
The route has no price yet:
```json
{
"success": false,
"error": "route_not_priced",
"errorStatus": 503,
"message": "this route is not available yet",
"credits_charged": 0
}
```
### 504
```json
{
"success": false,
"error": "timeout",
"errorStatus": 504,
"message": "no answer within the time limit; not charged",
"credits_charged": 0
}
```
---
# Keyword search
URL: https://scraper.riken.me/docs/v1/tiktok/search/keyword
`GET https://scraper.riken.me/v1/tiktok/search/keyword`
- Status: Live
- Cost: 1 credit per page. Errors are free.
TikTok's Top results for a keyword, one page per call. Send back `cursor`
for the next page; on the last page `has_more` is `0` and there is no
`cursor`.
**Cursors** are bound to your API key, the query and the region page 1
came from, and expire after 24 hours. Another key or query:
`400 invalid_cursor`. Older than 24 h (or TikTok expired the session):
`400 cursor_expired`. Page 1's IP session gone: `409 cursor_exit_gone`.
A later page only available from another country: `409 cursor_region_changed`.
In every case, start again from page 1 without a cursor. A cursor is an
integer of at most 2^53 - 1.
**Region.** We search from the US, and from Canada when US capacity is
short. `X-Search-Region` says which (`US` or `CA`); all pages of one
search come from the same region. `region=CA` searches from Canada only.
Page size is what TikTok's Top tab returns (median 23 on page 1 on 31 Oct 2026).
Latency (31 Oct 2026, 90 pages, pages 1 to 3): p50 3.9 s, p95 8.9 s.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
| `Idempotency-Key` | string | no | Any string up to 255 characters. Makes a retry safe for 24 hours; bound to this API key and the request's parameters. |
## Query parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `query` | string | yes | The keyword. Trimmed. At most 200 bytes, valid UTF-8. | `protein` |
| `cursor` | string | no | The `cursor` of the previous page. Absent, empty or `0` means page 1. | `4503599627370495` |
| `date_posted` | `all-time` \| `yesterday` \| `this-week` \| `this-month` \| `last-3-months` \| `last-6-months` | no | `all-time` or absent. `yesterday`, `this-week`, `this-month`, `last-3-months` and `last-6-months` are `400 unsupported_parameter`; anything else `400 bad_request`. | |
| `sort_by` | `relevance` \| `most-liked` \| `date-posted` | no | `relevance` or absent. `most-liked` and `date-posted` are `400 unsupported_parameter`; anything else `400 bad_request`. | |
| `region` | string | no | Two letters, or absent. `CA` (any case) searches from Canada only; any other two letters use the default order (US, then Canada). Not two letters is `400 bad_request`. | `CA` |
## Example
```bash
curl -s --max-time 400 -X GET "https://scraper.riken.me/v1/tiktok/search/keyword?query=protein&cursor=4503599627370495®ion=CA" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | charged | One page of results. Charged. |
| 400 | free | A bad parameter, or a cursor that cannot be used. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 402 | free | The key cannot cover this request. `needed_credits` is the price. Free. |
| 409 | free | An idempotency conflict, or a cursor whose search session cannot continue. Free. |
| 429 | free | No capacity to answer within the time limit, or the server is restarting. Free. Wait `Retry-After` seconds. |
| 500 | free | Our fault. Free; retry and quote `X-Request-Id`. |
| 502 | free | TikTok's answer failed or could not be read (including accounts TikTok withholds from logged-out views). Free. |
| 503 | free | No search IP can serve now (`search_unavailable`, `Retry-After: 60`), or the route is not priced. Free. |
| 504 | free | No answer within the time limit (180 s; audience 300 s). Free. |
## Response examples
### 200
Page 1 (our shaper on our fixture, list cut to one item; the cursor is a stand-in):
```json
{
"credits_charged": 1,
"credits_remaining": 999,
"cursor": 4503599627370495,
"has_more": 1,
"search_item_list": [
{
"aweme_info": {
"added_sound_music_info": null,
"aigc_info": null,
"anchors": null,
"anchors_extras": null,
"animated_image_info": null,
"author": {
"avatar_168x168": null,
"avatar_300x300": null,
"avatar_larger": {
"height": null,
"uri": null,
"url_list": [
"https://c/al.jpg"
],
"url_prefix": null,
"width": null
},
"avatar_medium": {
"height": null,
"uri": null,
"url_list": [
"https://c/am.jpg"
],
"url_prefix": null,
"width": null
},
"avatar_thumb": {
"height": null,
"uri": null,
"url_list": [
"https://c/at.jpg"
],
"url_prefix": null,
"width": null
},
"aweme_count": 2017,
"commerce_user_level": null,
"custom_verify": null,
"enterprise_verify_reason": null,
"follower_count": 4100000,
"following_count": 74,
"hide_search": null,
"ins_id": null,
"is_star": null,
"nickname": "Dave Portnoy",
"sec_uid": "MS4wLjABAAAAINC_ElRR",
"secret": 0,
"short_id": null,
"total_favorited": 190400000,
"uid": "6659752019493208069",
"unique_id": "stoolpresidente",
"verification_type": 1,
"with_commerce_entry": null
},
"author_user_id": 6659752019493208000,
"aweme_acl": null,
"aweme_id": "7334621391758642478",
"aweme_type": null,
"banners": null,
"behind_the_song_music_ids": null,
"behind_the_song_video_music_ids": null,
"bodydance_score": null,
"branded_content_accounts": null,
"c2pa_info": null,
"cc_template_info": null,
"cha_list": [
{
"cha_name": "gym",
"cid": "1",
"desc": ""
}
],
"challenge_position": null,
"cmt_swt": null,
"collect_stat": null,
"comment_config": null,
"comment_topbar_info": null,
"commerce_config_data": null,
"commerce_info": null,
"content_desc": null,
"content_desc_extra": null,
"content_model": null,
"content_original_type": null,
"content_size_type": null,
"content_type": null,
"cover_labels": null,
"create_time": 1707840000,
"create_time_utc": "2024-02-13T16:00:00.000Z",
"creation_info": null,
"desc": "leg day #gym",
"desc_language": null,
"disable_search_trending_bar": null,
"distance": null,
"distribute_type": null,
"follow_up_item_id_groups": null,
"follow_up_publish_from_id": null,
"geofencing": null,
"geofencing_regions": null,
"green_screen_materials": null,
"group_id": "7334621391758642478",
"group_id_list": null,
"has_danmaku": null,
"has_promote_entry": null,
"has_vs_entry": null,
"have_dashboard": null,
"hybrid_label": null,
"image_infos": null,
"interact_permission": null,
"interaction_stickers": null,
"is_ad": false,
"is_ads": false,
"is_description_translatable": null,
"is_eligible_for_commission": null,
"is_hash_tag": null,
"is_nff_or_nr": null,
"is_on_this_day": null,
"is_paid_partnership": null,
"is_pgcshow": null,
"is_preview": null,
"is_relieve": null,
"is_text_sticker_translatable": null,
"is_title_translatable": null,
"is_top": null,
"is_vr": null,
"item_comment_settings": null,
"item_duet": null,
"item_react": null,
"item_stitch": null,
"label_top": null,
"label_top_text": null,
"long_video": null,
"main_arch_common": null,
"mask_infos": null,
"meme_reg_info": null,
"misc_info": null,
"muf_comment_info_v2": null,
"music": {
"author": "Dave Portnoy",
"duration": 89,
"id_str": "7470969632736709422",
"is_original_sound": true,
"owner_handle": null,
"title": "original sound - stoolpresidente"
},
"music_begin_time_in_ms": null,
"music_end_time_in_ms": null,
"music_selected_from": null,
"music_title_style": null,
"music_volume": null,
"need_trim_step": null,
"need_vs_entry": null,
"nickname_position": null,
"no_selected_music": null,
"operator_boost_info": null,
"origin_comment_ids": null,
"origin_volume": null,
"original_client_text": null,
"picked_users": null,
"playlist_blocked": null,
"poi_re_tag_signal": null,
"position": null,
"prevent_download": null,
"products_info": null,
"promote_capcut_toggle": null,
"promote_icon_text": null,
"promote_toast": null,
"promote_toast_key": null,
"question_list": null,
"quick_reply_emojis": null,
"rate": null,
"reference_tts_voice_ids": null,
"reference_voice_filter_ids": null,
"region": "US",
"risk_infos": null,
"search_desc": null,
"search_highlight": null,
"share_info": null,
"share_url": "https://www.tiktok.com/@stoolpresidente/video/7334621391758642478",
"shoot_tab_name": null,
"shop_product_url": null,
"social_interaction_blob": null,
"solaria_profile": null,
"sort_label": null,
"standard_component_info": null,
"statistics": {
"aweme_id": "7334621391758642478",
"collect_count": 274,
"comment_count": 152,
"digg_count": 18651,
"download_count": null,
"forward_count": null,
"lose_comment_count": null,
"lose_count": null,
"play_count": 141567,
"repost_count": null,
"share_count": 237,
"whatsapp_share_count": null
},
"status": {
"allow_comment": null,
"allow_share": null,
"aweme_id": "7334621391758642478",
"is_delete": null,
"is_prohibited": null,
"private_status": 0,
"self_see": null
},
"suggest_words": null,
"support_danmaku": null,
"text_extra": [
{
"end": 12,
"hashtag_id": "1",
"hashtag_name": "gym",
"sec_uid": "",
"start": 8,
"type": 1,
"user_id": ""
}
],
"text_sticker_major_lang": null,
"title_language": null,
"ttec_suggest_words": null,
"tts_voice_ids": null,
"uniqid_position": null,
"upvote_info": null,
"upvote_preload": null,
"url": "https://www.tiktok.com/@stoolpresidente/video/7334621391758642478",
"used_full_song": null,
"user_digged": null,
"video": {
"CoverTsp": null,
"ai_dynamic_cover": null,
"ai_dynamic_cover_bak": null,
"animated_cover": null,
"big_thumbs": null,
"bit_rate": null,
"bit_rate_audio": null,
"cdn_url_expired": null,
"cover": {
"height": null,
"uri": null,
"url_list": [
"https://c/1.jpg"
],
"url_prefix": null,
"width": null
},
"download_addr": {
"height": null,
"uri": null,
"url_list": [
"https://c/d.mp4"
],
"url_prefix": null,
"width": null
},
"duration": 89000,
"dynamic_cover": {
"height": null,
"uri": null,
"url_list": [
"https://c/3.jpg"
],
"url_prefix": null,
"width": null
},
"has_watermark": null,
"height": 1024,
"is_bytevc1": null,
"is_callback": null,
"meta": null,
"need_set_token": null,
"origin_cover": {
"height": null,
"uri": null,
"url_list": [
"https://c/2.jpg"
],
"url_prefix": null,
"width": null
},
"play_addr": {
"height": null,
"uri": null,
"url_list": [
"https://c/p.mp4"
],
"url_prefix": null,
"width": null
},
"play_addr_bytevc1": null,
"play_addr_h264": null,
"ratio": "540p",
"source_HDR_type": null,
"tags": null,
"width": 576
},
"video_control": null,
"video_labels": null,
"video_text": null,
"visual_search_info": null,
"voice_filter_ids": null,
"with_promotional_music": null,
"without_watermark": null
}
}
],
"success": true
}
```
### 400
bad_request:
```json
{
"success": false,
"error": "bad_request",
"errorStatus": 400,
"message": "query is required",
"credits_charged": 0
}
```
unsupported_parameter:
```json
{
"success": false,
"error": "unsupported_parameter",
"errorStatus": 400,
"message": "sort_by \"most-liked\" is not supported yet; use relevance or leave it out",
"credits_charged": 0
}
```
invalid_cursor:
```json
{
"success": false,
"error": "invalid_cursor",
"errorStatus": 400,
"message": "this cursor is not valid for this query; pass a cursor from a previous response to the same query, or none for page 1",
"credits_charged": 0
}
```
cursor_expired:
```json
{
"success": false,
"error": "cursor_expired",
"errorStatus": 400,
"message": "this cursor has expired; start again from page 1 without a cursor; not charged",
"credits_charged": 0
}
```
### 401
```json
{
"success": false,
"error": "unauthorized",
"errorStatus": 401,
"message": "missing or invalid API key: send x-api-key or Authorization: Bearer",
"credits_charged": 0
}
```
### 402
```json
{
"success": false,
"error": "insufficient_credits",
"errorStatus": 402,
"message": "this key does not have enough credits for this request",
"credits_charged": 0,
"credits_remaining": 0,
"needed_credits": 1
}
```
### 409
idempotency_conflict:
```json
{
"success": false,
"error": "idempotency_conflict",
"errorStatus": 409,
"message": "a request with this Idempotency-Key is still in progress; retry after it finishes",
"credits_charged": 0,
"retry_after_s": 1
}
```
cursor_region_changed:
```json
{
"success": false,
"error": "cursor_region_changed",
"errorStatus": 409,
"message": "this page could only be searched from another region than page 1; start again from page 1 without a cursor; not charged",
"credits_charged": 0
}
```
cursor_exit_gone:
```json
{
"success": false,
"error": "cursor_exit_gone",
"errorStatus": 409,
"message": "the search session behind this cursor is gone; start again from page 1 without a cursor; not charged",
"credits_charged": 0
}
```
### 429
Over capacity (Retry-After 12):
```json
{
"success": false,
"error": "over_capacity",
"errorStatus": 429,
"message": "no capacity to answer within the time limit; retry after retry_after_s seconds",
"credits_charged": 0,
"retry_after_s": 12
}
```
### 500
```json
{
"success": false,
"error": "internal",
"errorStatus": 500,
"message": "internal error",
"credits_charged": 0
}
```
### 502
```json
{
"success": false,
"error": "upstream_failed",
"errorStatus": 502,
"message": "the upstream request failed; not charged",
"credits_charged": 0
}
```
### 503
search_unavailable:
```json
{
"success": false,
"error": "search_unavailable",
"errorStatus": 503,
"message": "no search capacity right now; retry later; not charged",
"credits_charged": 0,
"retry_after_s": 60
}
```
The route has no price yet:
```json
{
"success": false,
"error": "route_not_priced",
"errorStatus": 503,
"message": "this route is not available yet",
"credits_charged": 0
}
```
### 504
```json
{
"success": false,
"error": "timeout",
"errorStatus": 504,
"message": "no answer within the time limit; not charged",
"credits_charged": 0
}
```
---
# Followers
URL: https://scraper.riken.me/docs/v1/tiktok/user/followers
`GET https://scraper.riken.me/v1/tiktok/user/followers`
- Status: Coming soon
- Cost: 1 credit per page (price not final). Errors are free.
One page of an account's followers, newest first, usually 50. Send back
`min_time` for the next page; stop when `has_more` is `false`.
Each follower carries every key of ScrapeCreators' item; `region`,
`create_time` and app-only flags are `null` (TikTok's web list does not
send them). Rarely the second half of a page fails: you then get 30,
charged, with a `min_time` that resumes right after them.
An account that does not exist is a 404, free. A private account is a
200, **charged**, `{"handle", "account_private": true}`.
**Coming in beta.** Built but not priced for customers: answers
`503 route_not_priced` until a price row exists.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
| `Idempotency-Key` | string | no | Any string up to 255 characters. Makes a retry safe for 24 hours; bound to this API key and the request's parameters. |
## Query parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `handle` | string | no | The TikTok handle. Required unless `user_id` is given. A leading `@` is removed; case does not matter. Letters, digits, `_` and `.` only, at most 64 characters. With both `handle` and `user_id`, `user_id` is ignored. | `dagestantrainingcamp` |
| `user_id` | string | no | TikTok's numeric user id, at most 20 digits, used only when `handle` is absent. Works only for accounts in our index: an unknown id is `400 unsupported_parameter` ("pass handle"), and an id whose handle has changed is `400 unsupported_parameter` too. Both free, both count toward the free-answer limit. | `6918108151024829445` |
| `min_time` | integer | no | The `min_time` of the previous page (TikTok's own cursor, unix seconds). Absent, empty or `0` means page 1. | `1792981730` |
| `trim` | boolean | no | Accepted and ignored. Must be `true` or `false` (else `400 bad_request`). | |
## Example
```bash
curl -s --max-time 400 -X GET "https://scraper.riken.me/v1/tiktok/user/followers?handle=dagestantrainingcamp&user_id=6918108151024829445&min_time=1792981730" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | charged | One page of followers, or the private-account answer. Charged. |
| 400 | free | A parameter is missing, malformed, or one we cannot honour yet. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 402 | free | The key cannot cover this request. `needed_credits` is the price. Free. |
| 404 | free | The account does not exist. Free; counts toward the free-answer limit. |
| 409 | free | A request with this Idempotency-Key and parameters is still running. Free; retry after `Retry-After` (1 s). |
| 429 | free | `over_capacity`: no capacity within the time limit, or the server is restarting (30 s). `rate_limited`: the free-answer limit (this answer would have been free). Free; wait `Retry-After` seconds. |
| 500 | free | Our fault. Free; retry and quote `X-Request-Id`. |
| 502 | free | TikTok's answer failed or could not be read (including accounts TikTok withholds from logged-out views). Free. |
| 503 | free | The route has no price yet, so it does not serve. Free. |
| 504 | free | No answer within the time limit (180 s; audience 300 s). Free. |
## Response examples
### 200
A page (our shaper on our hand-written fixture, list cut to one item):
```json
{
"credits_charged": 1,
"credits_remaining": 999,
"extra": {
"fatal_item_ids": [],
"logid": "20261030171146081F76B937404FB3139A",
"now": 1793380307000
},
"followers": [
{
"accept_private_policy": null,
"account_labels": null,
"account_region": null,
"ad_cover_url": null,
"advance_feature_item_order": null,
"advanced_feature_info": null,
"authority_status": null,
"avatar_168x168": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:100:100.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_300x300": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:720:720.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_larger": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:1080:1080.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_medium": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:720:720.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_thumb": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:100:100.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"aweme_count": 5,
"bind_phone": null,
"bold_fields": null,
"can_message_follow_status_list": null,
"can_set_geofencing": null,
"cha_list": null,
"comment_filter_status": null,
"comment_setting": 0,
"commerce_user_level": null,
"cover_url": null,
"create_time": null,
"custom_verify": null,
"cv_level": null,
"download_prompt_ts": null,
"download_setting": 3,
"duet_setting": 3,
"enabled_filter_all_comments": null,
"enterprise_verify_reason": null,
"events": null,
"fake_data_info": null,
"favoriting_count": 28191,
"follow_status": null,
"follower_count": 12,
"follower_status": null,
"followers_detail": null,
"following_count": 2359,
"friends_status": null,
"geofencing": null,
"has_email": null,
"has_insights": null,
"has_orders": null,
"hide_search": null,
"homepage_bottom_toast": null,
"is_ad_fake": false,
"is_block": null,
"is_discipline_member": null,
"is_mute": null,
"is_mute_lives": null,
"is_mute_non_story_post": null,
"is_mute_story": null,
"is_mute_thought": null,
"is_phone_binded": null,
"is_star": null,
"item_list": null,
"language": null,
"live_agreement": null,
"live_commerce": null,
"live_verify": null,
"mention_status": null,
"mutual_relation_avatars": null,
"need_points": null,
"need_recommend": null,
"nickname": "grams",
"original_musician": null,
"platform_sync_info": null,
"prevent_download": null,
"react_setting": null,
"region": null,
"relative_users": null,
"reply_with_video_flag": null,
"search_highlight": null,
"sec_uid": "MS4wLjABAAAAexampleexampleexampleexampleexampleexample1",
"secret": 0,
"share_info": null,
"share_qrcode_uri": null,
"shield_comment_notice": null,
"shield_digg_notice": null,
"shield_edit_field_info": null,
"shield_follow_notice": null,
"short_id": null,
"show_image_bubble": null,
"signature": "hi",
"special_account": null,
"special_lock": null,
"status": null,
"stitch_setting": 3,
"total_favorited": 4,
"type_label": null,
"uid": "6761309637569709061",
"unique_id": "gramsworld",
"unique_id_modify_time": null,
"user_canceled": null,
"user_mode": null,
"user_period": null,
"user_profile_guide": null,
"user_rate": null,
"user_spark_info": null,
"user_tags": null,
"verification_type": null,
"verify_info": null,
"video_icon": null,
"white_cover_url": null,
"with_commerce_entry": null,
"with_shop_entry": null
}
],
"has_more": true,
"log_pb": {
"impr_id": "20261030171146081F76B937404FB3139A"
},
"max_time": 1793380036,
"min_time": 1792981730,
"myself_user_id": "0",
"next_page_token": "eyJtYXhfY3Vyc29yIjoxNzkzMzgwMDM2LCJtaW5fY3Vyc29yIjoxNzkyOTgxNzMwfQ==",
"offset": 0,
"status_code": 0,
"status_msg": "",
"success": true,
"total": 221779
}
```
A private account (charged):
```json
{
"success": true,
"credits_charged": 1,
"credits_remaining": 998,
"handle": "example.private",
"account_private": true
}
```
### 400
bad_request:
```json
{
"success": false,
"error": "bad_request",
"errorStatus": 400,
"message": "handle is required",
"credits_charged": 0
}
```
unsupported_parameter:
```json
{
"success": false,
"error": "unsupported_parameter",
"errorStatus": 400,
"message": "user_id 6918108151024829445 is not in our index yet; pass handle",
"credits_charged": 0
}
```
### 401
```json
{
"success": false,
"error": "unauthorized",
"errorStatus": 401,
"message": "missing or invalid API key: send x-api-key or Authorization: Bearer",
"credits_charged": 0
}
```
### 402
```json
{
"success": false,
"error": "insufficient_credits",
"errorStatus": 402,
"message": "this key does not have enough credits for this request",
"credits_charged": 0,
"credits_remaining": 0,
"needed_credits": 1
}
```
### 404
ScrapeCreators' recorded body; ours has the same keys:
```json
{
"success": false,
"credits_remaining": 48176,
"credits_charged": 0,
"account_deactivated": true,
"handle": "zq_nosuchuser_scdiff_7731",
"message": "Account deactivated",
"error": "not_found",
"errorStatus": 404
}
```
### 409
```json
{
"success": false,
"error": "idempotency_conflict",
"errorStatus": 409,
"message": "a request with this Idempotency-Key is still in progress; retry after it finishes",
"credits_charged": 0,
"retry_after_s": 1
}
```
### 429
Over capacity (Retry-After 12):
```json
{
"success": false,
"error": "over_capacity",
"errorStatus": 429,
"message": "no capacity to answer within the time limit; retry after retry_after_s seconds",
"credits_charged": 0,
"retry_after_s": 12
}
```
rate_limited:
```json
{
"success": false,
"error": "rate_limited",
"errorStatus": 429,
"message": "this answer would be free (unknown user_id, moved handle or missing account), and this key has had 100 free answers in 10m0s; retry after retry_after_s seconds; not charged. Answers that are charged are never limited",
"credits_charged": 0,
"retry_after_s": 42
}
```
### 500
```json
{
"success": false,
"error": "internal",
"errorStatus": 500,
"message": "internal error",
"credits_charged": 0
}
```
### 502
```json
{
"success": false,
"error": "upstream_failed",
"errorStatus": 502,
"message": "the upstream request failed; not charged",
"credits_charged": 0
}
```
### 503
The route has no price yet:
```json
{
"success": false,
"error": "route_not_priced",
"errorStatus": 503,
"message": "this route is not available yet",
"credits_charged": 0
}
```
### 504
```json
{
"success": false,
"error": "timeout",
"errorStatus": 504,
"message": "no answer within the time limit; not charged",
"credits_charged": 0
}
```
---
# Following
URL: https://scraper.riken.me/docs/v1/tiktok/user/following
`GET https://scraper.riken.me/v1/tiktok/user/following`
- Status: Coming soon
- Cost: 1 credit per page (price not final). Errors are free.
One page of the accounts an account follows, newest follow first, usually
50. Send back `min_time` for the next page; stop when `has_more` is `false`.
`rec_has_more` always equals `has_more`.
**A hidden list is free:** 200, `credits_charged: 0`, the page shape with
no accounts, `following_hidden: true` and `total: null`. It counts toward
the free-answer limit.
An account that does not exist is a 404, free. A private account is a
200, **charged**, `{"handle", "account_private": true}`.
**Coming.** Built but not priced: answers `503 route_not_priced` until a
price row exists.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
| `Idempotency-Key` | string | no | Any string up to 255 characters. Makes a retry safe for 24 hours; bound to this API key and the request's parameters. |
## Query parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `handle` | string | no | The TikTok handle. Required unless `user_id` is given. A leading `@` is removed; case does not matter. Letters, digits, `_` and `.` only, at most 64 characters. With both `handle` and `user_id`, `user_id` is ignored. | `dagestantrainingcamp` |
| `user_id` | string | no | TikTok's numeric user id, at most 20 digits, used only when `handle` is absent. Works only for accounts in our index: an unknown id is `400 unsupported_parameter` ("pass handle"), and an id whose handle has changed is `400 unsupported_parameter` too. Both free, both count toward the free-answer limit. | `6918108151024829445` |
| `min_time` | integer | no | The `min_time` of the previous page (TikTok's own cursor, unix seconds). Absent, empty or `0` means page 1. | `1792981730` |
| `trim` | boolean | no | Accepted and ignored. Must be `true` or `false` (else `400 bad_request`). | |
## Example
```bash
curl -s --max-time 400 -X GET "https://scraper.riken.me/v1/tiktok/user/following?handle=dagestantrainingcamp&user_id=6918108151024829445&min_time=1792981730" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | depends | A page (charged), a hidden list (free, `credits_charged` 0), or the private-account answer (charged). |
| 400 | free | A parameter is missing, malformed, or one we cannot honour yet. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 402 | free | The key cannot cover this request. `needed_credits` is the price. Free. |
| 404 | free | The account does not exist. Free; counts toward the free-answer limit. |
| 409 | free | A request with this Idempotency-Key and parameters is still running. Free; retry after `Retry-After` (1 s). |
| 429 | free | `over_capacity`: no capacity within the time limit, or the server is restarting (30 s). `rate_limited`: the free-answer limit (this answer would have been free). Free; wait `Retry-After` seconds. |
| 500 | free | Our fault. Free; retry and quote `X-Request-Id`. |
| 502 | free | TikTok's answer failed or could not be read (including accounts TikTok withholds from logged-out views). Free. |
| 503 | free | The route has no price yet, so it does not serve. Free. |
| 504 | free | No answer within the time limit (180 s; audience 300 s). Free. |
## Response examples
### 200
A page (our shaper on our hand-written fixture, list cut to one item):
```json
{
"credits_charged": 1,
"credits_remaining": 999,
"extra": {
"fatal_item_ids": [],
"logid": "20261030171146081F76B937404FB3139A",
"now": 1793380307000
},
"followings": [
{
"accept_private_policy": null,
"account_labels": null,
"account_region": null,
"ad_cover_url": null,
"advance_feature_item_order": null,
"advanced_feature_info": null,
"authority_status": null,
"avatar_168x168": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:100:100.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_300x300": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:720:720.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_larger": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:1080:1080.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_medium": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:720:720.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_thumb": {
"data_size": 0,
"height": 720,
"uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"url_list": [
"https://p16-common-sign.tiktokcdn-eu.com/tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2~tplv-tiktokx-cropcenter:100:100.jpeg?x-expires=1793466000"
],
"url_prefix": null,
"width": 720
},
"avatar_uri": "tos-maliva-avt-0068/d6a1705a030406c01fb474ce6cc6f0c2",
"aweme_count": 5,
"bind_phone": null,
"bold_fields": null,
"can_message_follow_status_list": null,
"can_set_geofencing": null,
"cha_list": null,
"comment_filter_status": null,
"comment_setting": 0,
"commerce_user_level": null,
"cover_url": null,
"create_time": null,
"custom_verify": null,
"cv_level": null,
"download_prompt_ts": null,
"download_setting": 3,
"duet_setting": 3,
"enabled_filter_all_comments": null,
"enterprise_verify_reason": null,
"events": null,
"fake_data_info": null,
"favoriting_count": 28191,
"follow_status": null,
"follower_count": 12,
"follower_status": null,
"followers_detail": null,
"following_count": 2359,
"friends_status": null,
"geofencing": null,
"has_email": null,
"has_insights": null,
"has_orders": null,
"hide_search": null,
"homepage_bottom_toast": null,
"is_ad_fake": false,
"is_block": null,
"is_discipline_member": null,
"is_mute": null,
"is_mute_lives": null,
"is_mute_non_story_post": null,
"is_mute_story": null,
"is_mute_thought": null,
"is_phone_binded": null,
"is_star": null,
"item_list": null,
"language": null,
"live_agreement": null,
"live_commerce": null,
"live_verify": null,
"mention_status": null,
"mutual_relation_avatars": null,
"need_points": null,
"need_recommend": null,
"nickname": "grams",
"original_musician": null,
"platform_sync_info": null,
"prevent_download": null,
"react_setting": null,
"region": null,
"relative_users": null,
"reply_with_video_flag": null,
"search_highlight": null,
"sec_uid": "MS4wLjABAAAAexampleexampleexampleexampleexampleexample1",
"secret": 0,
"share_info": null,
"share_qrcode_uri": null,
"shield_comment_notice": null,
"shield_digg_notice": null,
"shield_edit_field_info": null,
"shield_follow_notice": null,
"short_id": null,
"show_image_bubble": null,
"signature": "hi",
"special_account": null,
"special_lock": null,
"status": null,
"stitch_setting": 3,
"total_favorited": 4,
"type_label": null,
"uid": "6761309637569709061",
"unique_id": "gramsworld",
"unique_id_modify_time": null,
"user_canceled": null,
"user_mode": null,
"user_period": null,
"user_profile_guide": null,
"user_rate": null,
"user_spark_info": null,
"user_tags": null,
"verification_type": null,
"verify_info": null,
"video_icon": null,
"white_cover_url": null,
"with_commerce_entry": null,
"with_shop_entry": null
}
],
"has_more": true,
"log_pb": {
"impr_id": "20261030171146081F76B937404FB3139A"
},
"max_time": 1793380036,
"min_time": 1792981730,
"myself_user_id": "0",
"next_page_token": "eyJtYXhfY3Vyc29yIjoxNzkzMzgwMDM2LCJtaW5fY3Vyc29yIjoxNzkyOTgxNzMwfQ==",
"offset": 0,
"rec_has_more": true,
"status_code": 0,
"status_msg": "",
"success": true,
"total": 221779
}
```
A hidden following list (free):
```json
{
"success": true,
"credits_charged": 0,
"credits_remaining": 1000,
"extra": {
"fatal_item_ids": [],
"logid": "",
"now": null
},
"followings": [],
"following_hidden": true,
"handle": "example.hidden",
"has_more": false,
"log_pb": {
"impr_id": ""
},
"max_time": 0,
"min_time": 0,
"myself_user_id": "0",
"next_page_token": "eyJtYXhfY3Vyc29yIjowLCJtaW5fY3Vyc29yIjowfQ==",
"offset": 0,
"rec_has_more": false,
"status_code": 0,
"status_msg": "",
"total": null
}
```
### 400
bad_request:
```json
{
"success": false,
"error": "bad_request",
"errorStatus": 400,
"message": "handle is required",
"credits_charged": 0
}
```
unsupported_parameter:
```json
{
"success": false,
"error": "unsupported_parameter",
"errorStatus": 400,
"message": "user_id 6918108151024829445 is not in our index yet; pass handle",
"credits_charged": 0
}
```
### 401
```json
{
"success": false,
"error": "unauthorized",
"errorStatus": 401,
"message": "missing or invalid API key: send x-api-key or Authorization: Bearer",
"credits_charged": 0
}
```
### 402
```json
{
"success": false,
"error": "insufficient_credits",
"errorStatus": 402,
"message": "this key does not have enough credits for this request",
"credits_charged": 0,
"credits_remaining": 0,
"needed_credits": 1
}
```
### 404
ScrapeCreators' recorded body; ours has the same keys:
```json
{
"success": false,
"credits_remaining": 48176,
"credits_charged": 0,
"account_deactivated": true,
"handle": "zq_nosuchuser_scdiff_7731",
"message": "Account deactivated",
"error": "not_found",
"errorStatus": 404
}
```
### 409
```json
{
"success": false,
"error": "idempotency_conflict",
"errorStatus": 409,
"message": "a request with this Idempotency-Key is still in progress; retry after it finishes",
"credits_charged": 0,
"retry_after_s": 1
}
```
### 429
Over capacity (Retry-After 12):
```json
{
"success": false,
"error": "over_capacity",
"errorStatus": 429,
"message": "no capacity to answer within the time limit; retry after retry_after_s seconds",
"credits_charged": 0,
"retry_after_s": 12
}
```
rate_limited:
```json
{
"success": false,
"error": "rate_limited",
"errorStatus": 429,
"message": "this answer would be free (unknown user_id, moved handle or missing account), and this key has had 100 free answers in 10m0s; retry after retry_after_s seconds; not charged. Answers that are charged are never limited",
"credits_charged": 0,
"retry_after_s": 42
}
```
### 500
```json
{
"success": false,
"error": "internal",
"errorStatus": 500,
"message": "internal error",
"credits_charged": 0
}
```
### 502
```json
{
"success": false,
"error": "upstream_failed",
"errorStatus": 502,
"message": "the upstream request failed; not charged",
"credits_charged": 0
}
```
### 503
The route has no price yet:
```json
{
"success": false,
"error": "route_not_priced",
"errorStatus": 503,
"message": "this route is not available yet",
"credits_charged": 0
}
```
### 504
```json
{
"success": false,
"error": "timeout",
"errorStatus": 504,
"message": "no answer within the time limit; not charged",
"credits_charged": 0
}
```
---
# Audience
URL: https://scraper.riken.me/docs/v1/tiktok/user/audience
`GET https://scraper.riken.me/v1/tiktok/user/audience`
- Status: Coming soon
- Cost: 20 credits (price not final). Errors are free.
The countries of an account's followers, estimated from a sample: we read
the newest 720 followers and place up to 150 by the country their latest
video was posted from. **Estimates, subject to change.**
Usually 35 to 40 seconds; up to **5 minutes**. Set your client timeout to
at least 6 minutes.
**Charged only when 30 or more followers are placed.** 1 to 29 placed:
200, free, `X-Audience-Confidence: low`. None placed: 200,
`audienceLocations: []`, free. Private account: 200, free,
`{"account_private": true, "handle"}`. Some free answers are kept per
account (24 h; private 6 h) and served again with `X-Audience-Cached`.
Limits: 2 audience requests in flight per key (a third is
`429 rate_limited`, `Retry-After: 30`); past 100 free answers in 10
minutes, audience requests from the key are refused **before** they run.
**Coming in beta.** Answers `503 route_not_priced` until priced.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
| `Idempotency-Key` | string | no | Any string up to 255 characters. Makes a retry safe for 24 hours; bound to this API key and the request's parameters. |
## Query parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `handle` | string | yes | The TikTok handle; a leading `@` is removed. The only parameter, as on ScrapeCreators. | `karlaakat` |
## Example
```bash
curl -s --max-time 400 -X GET "https://scraper.riken.me/v1/tiktok/user/audience?handle=karlaakat" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | depends | The country breakdown (charged at 30+ placed, else free), or the private answer (free). |
| 400 | free | A parameter is missing, malformed, or one we cannot honour yet. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 402 | free | The key cannot cover this request. `needed_credits` is the price. Free. |
| 404 | free | The account does not exist. Free; counts toward the free-answer limit. |
| 409 | free | A request with this Idempotency-Key and parameters is still running. Free; retry after `Retry-After` (1 s). |
| 429 | free | Over capacity (`over_capacity`), or the key's audience limits (`rate_limited`). Free. |
| 500 | free | Our fault. Free; retry and quote `X-Request-Id`. |
| 502 | free | TikTok's answer failed or could not be read (including accounts TikTok withholds from logged-out views). Free. |
| 503 | free | The route has no price yet, so it does not serve. Free. |
| 504 | free | No answer within the time limit (180 s; audience 300 s). Free. |
## Response examples
### 200
Our own live answer for karlaakat (smoke, 31 Oct 2026), cut to 3 of 22 countries:
```json
{
"audienceLocations": [
{
"count": 115,
"country": "United States",
"countryCode": "US",
"percentage": "70.55%"
},
{
"count": 19,
"country": "Mexico",
"countryCode": "MX",
"percentage": "11.66%"
},
{
"count": 3,
"country": "Chile",
"countryCode": "CL",
"percentage": "1.84%"
}
],
"credits_charged": 1,
"credits_remaining": 999,
"success": true
}
```
A private account (free):
```json
{
"success": true,
"credits_charged": 0,
"credits_remaining": 1000,
"account_private": true,
"handle": "example.private"
}
```
### 400
bad_request:
```json
{
"success": false,
"error": "bad_request",
"errorStatus": 400,
"message": "handle is required",
"credits_charged": 0
}
```
unsupported_parameter:
```json
{
"success": false,
"error": "unsupported_parameter",
"errorStatus": 400,
"message": "user_id 6918108151024829445 is not in our index yet; pass handle",
"credits_charged": 0
}
```
### 401
```json
{
"success": false,
"error": "unauthorized",
"errorStatus": 401,
"message": "missing or invalid API key: send x-api-key or Authorization: Bearer",
"credits_charged": 0
}
```
### 402
```json
{
"success": false,
"error": "insufficient_credits",
"errorStatus": 402,
"message": "this key does not have enough credits for this request",
"credits_charged": 0,
"credits_remaining": 0,
"needed_credits": 1
}
```
### 404
ScrapeCreators' recorded body; ours has the same keys:
```json
{
"success": false,
"credits_remaining": 48176,
"credits_charged": 0,
"account_deactivated": true,
"handle": "zq_nosuchuser_scdiff_7731",
"message": "Account deactivated",
"error": "not_found",
"errorStatus": 404
}
```
### 409
```json
{
"success": false,
"error": "idempotency_conflict",
"errorStatus": 409,
"message": "a request with this Idempotency-Key is still in progress; retry after it finishes",
"credits_charged": 0,
"retry_after_s": 1
}
```
### 429
Over capacity (Retry-After 12):
```json
{
"success": false,
"error": "over_capacity",
"errorStatus": 429,
"message": "no capacity to answer within the time limit; retry after retry_after_s seconds",
"credits_charged": 0,
"retry_after_s": 12
}
```
rate_limited:
```json
{
"success": false,
"error": "rate_limited",
"errorStatus": 429,
"message": "this key already has 2 audience requests in progress; retry after retry_after_s seconds; not charged",
"credits_charged": 0,
"retry_after_s": 30
}
```
### 500
```json
{
"success": false,
"error": "internal",
"errorStatus": 500,
"message": "internal error",
"credits_charged": 0
}
```
### 502
```json
{
"success": false,
"error": "upstream_failed",
"errorStatus": 502,
"message": "the upstream request failed; not charged",
"credits_charged": 0
}
```
### 503
The route has no price yet:
```json
{
"success": false,
"error": "route_not_priced",
"errorStatus": 503,
"message": "this route is not available yet",
"credits_charged": 0
}
```
### 504
```json
{
"success": false,
"error": "timeout",
"errorStatus": 504,
"message": "no answer within the time limit; not charged",
"credits_charged": 0
}
```
---
# Jobs and export
URL: https://scraper.riken.me/docs/jobs-and-export
Read a job's results page by page, or download them all as gzipped NDJSON. Free.
A large job (a walk over thousands of creators, say) has more results than you want to page through. Export them instead: one request starts the export, polling it returns download links.
## Flow
```
POST /v1/jobs/{id}/export -> 202, Location, Retry-After: 5 (state: pending)
GET /v1/jobs/{id}/export -> 202 while pending or running
GET /v1/jobs/{id}/export -> 200, state: done, parts[] with URLs
GET -> the gzipped NDJSON file (no API key)
```
The job must be finished first: `done`, `failed` or `canceled`, with `finished_at` set. A job still `queued` or `running` gets `409 job_not_finished`.
## Start an export
```bash
curl -s -X POST "https://scraper.riken.me/v1/jobs/685/export" -H "x-api-key: $RIKEN_API_KEY"
```
```json
{"job_id": 685, "state": "pending", "format": "ndjson.gz", "poll_url": "/v1/jobs/685/export",
"parts": [], "rows_total": 0, "bytes_total": 0, "credits_charged": "0", "attempts": 0}
```
**Idempotent.** POST again and you get the same export, not rebuilt and not charged. It is rebuilt only if the last export failed or the job finished again after it was built.
## Poll and download
```bash
curl -s "https://scraper.riken.me/v1/jobs/685/export" -H "x-api-key: $RIKEN_API_KEY"
```
```json
{"job_id": 685, "state": "done", "format": "ndjson.gz",
"sections": {"creators": 9720, "media": 299818, "walks": 9731},
"parts": [
{"section": "creators", "part": 0, "rows": 9720, "bytes": 1350211, "url": "https://..."},
{"section": "media", "part": 0, "rows": 50000, "bytes": 6012345, "url": "https://..."}
],
"rows_total": 319269, "urls_expire_at": "2026-11-05T23:41:00Z", "credits_charged": "0"}
```
The URL is the credential, so send no API key with it:
```bash
curl -s -o media-0000.ndjson.gz ""
gunzip -c media-0000.ndjson.gz | head -1 | jq .
```
| field | meaning |
|---|---|
| `state` | `pending`, `running`, `done`, `failed`, or `stale` (the job finished again after this export; POST to rebuild) |
| `sections` | every list this kind of job has, with its row count. Present once `done`. |
| `parts[]` | one file each: `section`, `part`, `rows`, `bytes` (gzipped), `url` |
| `urls_expire_at` | when the URLs stop working. GET the export again for fresh ones. |
| `failure_reason` | why an attempt failed |
## What is in a file
- One section per file, up to 50,000 rows each. A section with no rows has no file.
- One JSON object per line, identical to the object `GET /v1/jobs/{id}` returns in that list, so one parser reads both.
- Stored as gzip (`Content-Type: application/gzip`), so your client never unzips it silently.
- The whole export is read in one database snapshot: no row skipped or repeated, and no count changes between parts.
| job kind | sections |
|---|---|
| `tiktok.walk`, `instagram.fetch` | `creators`, `media`, `walks` |
| `tiktok.search` | `creators`, `media`, `units` |
| `tiktok.audience` | `creators`, `media`, `audience` |
## Paging instead
`GET /v1/jobs/{id}?results=true` pages 500 rows at a time, free.
- Creators and media come in id order, so every page costs the same however deep you go.
- Send `sections=` to read one list only, for example `sections=walks` with `walk_cursor`.
- Page a job only after it is done. While it runs, a new row can land behind your cursor.
- An old cursor gets `400 cursor_expired`: start again without one.
## Errors
| status | `error` | when |
|---|---|---|
| 404 | `not_found` | no such job, another key's job, or no export requested yet (GET) |
| 409 | `job_not_finished` | the job is still running or saving results |
| 503 | `export_unavailable` | export is switched off on this server |
Only the key that submitted a job can read or export it.
---
# List jobs
URL: https://scraper.riken.me/docs/v1/jobs-get
`GET https://scraper.riken.me/v1/jobs`
- Status: Live
- Cost: Free. Errors are free.
This key's jobs, newest first, without chunks or results. Free.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
## Query parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `state` | string | no | Comma-separated or repeated filter. | `running,queued` |
| `kind` | string | no | Comma-separated or repeated filter, one of the job kinds. | `tiktok.walk` |
| `limit` | integer | no | Rows per page (per list). 0 or less means the default; above 500 is capped at 500. | `500` |
| `cursor` | string | no | The `next_cursor` of the previous page. | |
## Example
```bash
curl -s --max-time 400 -X GET "https://scraper.riken.me/v1/jobs?state=running%2Cqueued&kind=tiktok.walk&limit=500" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | free | A page of jobs. Free. |
| 400 | free | A malformed body, parameter or cursor. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 429 | free | The key's request rate. Free. |
| 500 | free | Our fault. Free. |
## Response examples
### 200
The real final state of job 533 (docs/results/2026-10-31-search-inpage-deployed/job533.json):
```json
{
"jobs": [
{
"id": 533,
"kind": "tiktok.search",
"state": "failed",
"priority": 30,
"progress": {
"units_done": 0,
"units_total": 20
},
"credits": {
"held_micro": 20000000,
"spent_micro": 0,
"held": "20.000000",
"spent": "0.000000"
},
"error": "ttsearch: exit status 1: *** IN-HOUSE SEARCH RETURNED NOTHING ***",
"created_at": "2026-10-31T02:37:56.843367Z",
"started_at": "2026-10-31T02:37:56.860944Z",
"finished_at": "2026-10-31T02:38:55.516451Z"
}
]
}
```
### 400
bad_request:
```json
{
"error": {
"code": "bad_request",
"message": "kind must be one of instagram.fetch, instagram.search, tiktok.audience, tiktok.screen, tiktok.search, tiktok.walk, got \"tiktok.profile\""
}
}
```
invalid_unit:
```json
{
"error": {
"code": "invalid_unit",
"message": "unit \"#gym\" starts with \"#\"; drop the \"#\" to search the word",
"field": "keywords"
}
}
```
### 401
```json
{
"error": {
"code": "unauthorized",
"message": "invalid API key"
}
}
```
### 429
```json
{
"error": {
"code": "rate_limited",
"message": "rate limit exceeded for this key",
"retry_after_seconds": 1
}
}
```
### 500
```json
{
"error": {
"code": "internal",
"message": "internal error"
}
}
```
---
# Submit a job
URL: https://scraper.riken.me/docs/v1/jobs-post
`POST https://scraper.riken.me/v1/jobs`
- Status: Live
- Cost: 1 credit per unit attempted (keyword or handle). Errors are free.
Queue a bulk job and hold its credits. Answers `202` at once; poll
`poll_url`. Handles lose a leading `@`; units are trimmed and deduplicated
case-insensitively; at most 50,000 units per job.
**Billing is per unit attempted, not per result found**, at the job kind's
price pinned at submit. The whole hold is taken now; unused credit comes
back when the job finishes or is cancelled.
`Idempotency-Key`: the same key with the same request returns the same job
(`Idempotency-Replayed: true`, `idempotent_replay: true`), no second hold.
The same key with a different request is `409 idempotency_conflict`.
Unknown body fields are a 400. Per-key rate limit and a per-key cap on
queued plus running jobs apply (`429 rate_limited`, `429 concurrency_limit`).
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
| `Idempotency-Key` | string | no | Any string up to 255 characters. Makes a retry safe for 24 hours; bound to this API key and the request's parameters. |
## Request body
JSON, required.
A TikTok walk with an early-stop rule:
```json
{
"kind": "tiktok.walk",
"handles": [
"lucymfit",
"@dishes.easyrecieps"
],
"options": {
"max_videos": 120,
"early_stop": {
"min_reels_per_week": 3,
"full_weeks": 4,
"ignore_current_week": true,
"reel_kinds": [
"video"
],
"views": {
"min_views_per_video": 40000,
"min_share_clearing": 0.8,
"band_min_age_days": 10,
"band_max_age_days": 40,
"min_band_videos": 5,
"exclude_pinned": true
}
}
}
}
```
TikTok keyword search from Canada only:
```json
{
"kind": "tiktok.search",
"keywords": [
"protein",
"meal prep"
],
"options": {
"region": "CA"
}
}
```
Instagram profile and feed fetch:
```json
{
"kind": "instagram.fetch",
"handles": [
"example.creator"
]
}
```
TikTok audience share with a 45% US+CA cutoff:
```json
{
"kind": "tiktok.audience",
"handles": [
"karlaakat"
],
"options": {
"us_ca_share_min": 0.45
}
}
```
## Example
```bash
curl -s --max-time 400 -X POST "https://scraper.riken.me/v1/jobs" \
-H "x-api-key: $RIKEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"tiktok.walk","handles":["lucymfit","@dishes.easyrecieps"],"options":{"max_videos":120,"early_stop":{"min_reels_per_week":3,"full_weeks":4,"ignore_current_week":true,"reel_kinds":["video"],"views":{"min_views_per_video":40000,"min_share_clearing":0.8,"band_min_age_days":10,"band_max_age_days":40,"min_band_videos":5,"exclude_pinned":true}}}}'
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 202 | depends | Accepted (or replayed for the same Idempotency-Key). Credits are held, not yet spent. |
| 400 | free | A malformed body, parameter or cursor. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 402 | free | The key cannot cover the job's hold. Micro fields are the exact integers; the credit strings are the same amounts for people. Free. |
| 409 | free | This Idempotency-Key was already used for a different request. Free. |
| 429 | free | The key's request rate (`rate_limited`) or its cap on queued plus running jobs (`concurrency_limit`, Retry-After 10). Free. |
| 500 | free | Our fault. Free. |
## Response examples
### 202
Illustrative (shape from cmd/apiserver/jobs.go writeAccepted):
```json
{
"job_id": 685,
"job": {
"id": 685,
"kind": "tiktok.walk",
"state": "queued",
"priority": 120,
"progress": {
"units_done": 0,
"units_total": 2
},
"credits": {
"held_micro": 2000000,
"spent_micro": 0,
"held": "2.000000",
"spent": "0.000000"
},
"created_at": "2026-11-05T10:00:00Z"
},
"poll_url": "/v1/jobs/685"
}
```
### 400
bad_request:
```json
{
"error": {
"code": "bad_request",
"message": "kind must be one of instagram.fetch, instagram.search, tiktok.audience, tiktok.screen, tiktok.search, tiktok.walk, got \"tiktok.profile\""
}
}
```
invalid_unit:
```json
{
"error": {
"code": "invalid_unit",
"message": "unit \"#gym\" starts with \"#\"; drop the \"#\" to search the word",
"field": "keywords"
}
}
```
### 401
```json
{
"error": {
"code": "unauthorized",
"message": "invalid API key"
}
}
```
### 402
```json
{
"error": {
"code": "insufficient_credits",
"message": "this job needs 20.000000 credits for 20 units of tiktok.walk; the key has 3.000000. Billing is per unit attempted, not per result found.",
"needed_micro": 20000000,
"available_micro": 3000000,
"needed_credits": "20.000000",
"available_credits": "3.000000"
}
}
```
### 409
```json
{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key \"batch-7\" was already used for a different request (job 685, kind \"tiktok.walk\"). Use a new key, or resend the original request.",
"job_id": 685
}
}
```
### 429
rate_limited:
```json
{
"error": {
"code": "rate_limited",
"message": "rate limit exceeded for this key",
"retry_after_seconds": 1
}
}
```
concurrency_limit:
```json
{
"error": {
"code": "concurrency_limit",
"message": "this key already has 4 jobs queued or running; its limit is 4. Wait for one to finish, or cancel one.",
"retry_after_seconds": 10
}
}
```
### 500
```json
{
"error": {
"code": "internal",
"message": "internal error"
}
}
```
---
# Submit a job and wait for it
URL: https://scraper.riken.me/docs/v1/search
`POST https://scraper.riken.me/v1/search`
- Status: Live
- Cost: 1 credit per unit attempted (keyword or handle). Errors are free.
The same body as `POST /v1/jobs`, run ahead of queued work and held open
until it finishes or the server's synchronous cap (`CS_SEARCH_TIMEOUT`,
default 30 s) passes. Finished: `200` with the job and the first page of
results. Still running: `202` with `poll_url`; the job is queued and paid
for, so poll rather than resubmit. At most `CS_SEARCH_MAX_UNITS` units
(default 200). `priority` is not settable here (400).
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
| `Idempotency-Key` | string | no | Any string up to 255 characters. Makes a retry safe for 24 hours; bound to this API key and the request's parameters. |
## Query parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `limit` | integer | no | Rows per list in the finished answer. | |
## Request body
JSON, required.
```json
{
"kind": "tiktok.search",
"keywords": [
"protein"
]
}
```
## Example
```bash
curl -s --max-time 400 -X POST "https://scraper.riken.me/v1/search" \
-H "x-api-key: $RIKEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"tiktok.search","keywords":["protein"]}'
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | charged | The finished job with the first page of each list. Units already billed at submit. |
| 202 | depends | Still running past the synchronous cap. Poll `poll_url`; do not resubmit. |
| 400 | free | A malformed body, parameter or cursor. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 402 | free | The key cannot cover the job's hold. Micro fields are the exact integers; the credit strings are the same amounts for people. Free. |
| 409 | free | This Idempotency-Key was already used for a different request. Free. |
| 429 | free | The key's request rate (`rate_limited`) or its cap on queued plus running jobs (`concurrency_limit`, Retry-After 10). Free. |
| 500 | free | Our fault. Free. |
## Response examples
### 200
Illustrative (shape from cmd/apiserver/search.go):
```json
{
"id": 701,
"kind": "tiktok.search",
"state": "done",
"priority": 10,
"progress": {
"units_done": 1,
"units_total": 1,
"chunks": {
"queued": 0,
"running": 0,
"done": 1,
"failed": 0
},
"units_fetched": 1,
"last_progress_at": "2026-11-06T09:00:12Z"
},
"credits": {
"held_micro": 0,
"spent_micro": 1000000,
"held": "0.000000",
"spent": "1.000000"
},
"created_at": "2026-11-06T09:00:00Z",
"started_at": "2026-11-06T09:00:01Z",
"finished_at": "2026-11-06T09:00:12Z",
"results": {
"creators": [
{
"id": 42,
"platform": "tiktok",
"handle": "example.creator",
"followers": 120000,
"first_seen": "2026-09-18T10:00:00Z",
"last_seen": "2026-11-06T09:00:12Z"
}
],
"media": [
{
"external_id": "7334621391758642478",
"creator_handle": "example.creator",
"caption": "leg day #gym",
"plays": 141567,
"posted_at": "2024-02-13T16:00:00Z",
"keyword": "protein",
"region": "US"
}
],
"units": [
{
"unit": "protein",
"outcome": "answered",
"region": "US",
"tries": 1,
"billed": true
}
]
}
}
```
### 202
```json
{
"job_id": 702,
"job": {
"id": 702,
"kind": "tiktok.search",
"state": "running",
"priority": 10,
"progress": {
"units_done": 0,
"units_total": 150
},
"credits": {
"held_micro": 150000000,
"spent_micro": 0,
"held": "150.000000",
"spent": "0.000000"
},
"created_at": "2026-11-06T09:00:00Z",
"started_at": "2026-11-06T09:00:01Z"
},
"poll_url": "/v1/jobs/702",
"message": "still running after 30s; the job is queued and paid for -- poll poll_url rather than resubmitting"
}
```
### 400
bad_request:
```json
{
"error": {
"code": "bad_request",
"message": "kind must be one of instagram.fetch, instagram.search, tiktok.audience, tiktok.screen, tiktok.search, tiktok.walk, got \"tiktok.profile\""
}
}
```
invalid_unit:
```json
{
"error": {
"code": "invalid_unit",
"message": "unit \"#gym\" starts with \"#\"; drop the \"#\" to search the word",
"field": "keywords"
}
}
```
### 401
```json
{
"error": {
"code": "unauthorized",
"message": "invalid API key"
}
}
```
### 402
```json
{
"error": {
"code": "insufficient_credits",
"message": "this job needs 20.000000 credits for 20 units of tiktok.walk; the key has 3.000000. Billing is per unit attempted, not per result found.",
"needed_micro": 20000000,
"available_micro": 3000000,
"needed_credits": "20.000000",
"available_credits": "3.000000"
}
}
```
### 409
```json
{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key \"batch-7\" was already used for a different request (job 685, kind \"tiktok.walk\"). Use a new key, or resend the original request.",
"job_id": 685
}
}
```
### 429
rate_limited:
```json
{
"error": {
"code": "rate_limited",
"message": "rate limit exceeded for this key",
"retry_after_seconds": 1
}
}
```
concurrency_limit:
```json
{
"error": {
"code": "concurrency_limit",
"message": "this key already has 4 jobs queued or running; its limit is 4. Wait for one to finish, or cancel one.",
"retry_after_seconds": 10
}
}
```
### 500
```json
{
"error": {
"code": "internal",
"message": "internal error"
}
}
```
---
# Cancel a job
URL: https://scraper.riken.me/docs/v1/jobs/id/cancel
`POST https://scraper.riken.me/v1/jobs/{id}/cancel`
- Status: Live
- Cost: Free. Errors are free.
Stop a `queued` or `running` job and refund what it has not spent. Work
already under way finishes and is billed; nothing is deleted. A job in
any other state is `400 bad_request`. Free.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
## Path parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `id` | integer | yes | The job id. Another key's job answers as if it did not exist. | `685` |
## Example
```bash
curl -s --max-time 400 -X POST "https://scraper.riken.me/v1/jobs/685/cancel" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | free | The job after the cancel and refund. |
| 400 | free | A malformed body, parameter or cursor. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 404 | free | No such job, another key's job, or (GET export only) no export requested yet. Free. |
| 429 | free | The key's request rate. Free. |
| 500 | free | Our fault. Free. |
## Response examples
### 200
Illustrative:
```json
{
"id": 686,
"kind": "instagram.fetch",
"state": "canceled",
"priority": 100,
"progress": {
"units_done": 50,
"units_total": 200
},
"credits": {
"held_micro": 0,
"spent_micro": 50000000,
"held": "0.000000",
"spent": "50.000000"
},
"created_at": "2026-11-05T10:00:00Z",
"started_at": "2026-11-05T10:00:02Z"
}
```
### 400
bad_request:
```json
{
"error": {
"code": "bad_request",
"message": "kind must be one of instagram.fetch, instagram.search, tiktok.audience, tiktok.screen, tiktok.search, tiktok.walk, got \"tiktok.profile\""
}
}
```
invalid_unit:
```json
{
"error": {
"code": "invalid_unit",
"message": "unit \"#gym\" starts with \"#\"; drop the \"#\" to search the word",
"field": "keywords"
}
}
```
### 401
```json
{
"error": {
"code": "unauthorized",
"message": "invalid API key"
}
}
```
### 404
not_found:
```json
{
"error": {
"code": "not_found",
"message": "no job 685"
}
}
```
no_export:
```json
{
"error": {
"code": "not_found",
"message": "no export of job 685; POST /v1/jobs/685/export to start one"
}
}
```
### 429
```json
{
"error": {
"code": "rate_limited",
"message": "rate limit exceeded for this key",
"retry_after_seconds": 1
}
}
```
### 500
```json
{
"error": {
"code": "internal",
"message": "internal error"
}
}
```
---
# Poll and download an export
URL: https://scraper.riken.me/docs/v1/jobs/id/export-get
`GET https://scraper.riken.me/v1/jobs/{id}/export`
- Status: Live
- Cost: Free. Errors are free.
The export's state; once `done`, one presigned URL per part. Download each
with a plain GET and no API key: the URL is the credential. Every GET
signs fresh URLs; `urls_expire_at` is when these stop working (24 h at
most, sooner when the signing session ends).
Sections by job kind: `tiktok.walk` and `instagram.fetch`: creators,
media, walks. `tiktok.search`: creators, media (the hits), units.
`tiktok.audience`: creators, media, audience. Up to 50,000 rows per file;
each line is byte-for-byte the object `GET /v1/jobs/{id}` returns in that list.
`stale`: the job finished again after this export was built; no URLs,
POST to rebuild.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
## Path parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `id` | integer | yes | The job id. Another key's job answers as if it did not exist. | `685` |
## Example
```bash
curl -s --max-time 400 -X GET "https://scraper.riken.me/v1/jobs/685/export" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | free | The export is `done` (with URLs), `failed`, or `stale`. Free. |
| 202 | free | The export is `pending` or `running`. Poll `poll_url` after `Retry-After` (5 s). Free. |
| 400 | free | A malformed body, parameter or cursor. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 404 | free | No such job, another key's job, or (GET export only) no export requested yet. Free. |
| 409 | free | The job is still queued or running, or canceled/failed with results still being saved (no `finished_at` yet). Free. |
| 429 | free | The key's request rate. Free. |
| 500 | free | Our fault. Free. |
| 503 | free | Export is switched off on this server. Free. |
## Response examples
### 200
Job 685's real row counts; byte sizes and times illustrative (docs/customer/routes/job-export.md):
```json
{
"job_id": 685,
"state": "done",
"format": "ndjson.gz",
"poll_url": "/v1/jobs/685/export",
"sections": {
"creators": 9720,
"media": 299818,
"walks": 9731
},
"parts": [
{
"section": "creators",
"part": 0,
"rows": 9720,
"bytes": 1350211,
"url": "https://cs-niche-video.s3.us-west-2.amazonaws.com/scraper/exports/key-7/job-685/attempt-1/creators-0000.ndjson.gz?X-Amz-..."
},
{
"section": "media",
"part": 0,
"rows": 50000,
"bytes": 6012345,
"url": "https://..."
},
{
"section": "media",
"part": 1,
"rows": 50000,
"bytes": 5998812,
"url": "https://..."
}
],
"rows_total": 319269,
"bytes_total": 41234567,
"urls_expire_at": "2026-11-05T23:41:00Z",
"credits_charged": "0",
"attempts": 1,
"requested_at": "2026-11-05T18:02:11Z",
"started_at": "2026-11-05T18:02:11Z",
"finished_at": "2026-11-05T18:04:40Z"
}
```
Illustrative:
```json
{
"job_id": 685,
"state": "failed",
"format": "ndjson.gz",
"poll_url": "/v1/jobs/685/export",
"parts": [],
"rows_total": 0,
"bytes_total": 0,
"credits_charged": "0",
"failure_reason": "storage unavailable",
"attempts": 3,
"requested_at": "2026-11-05T18:02:11Z",
"started_at": "2026-11-05T18:03:20Z",
"finished_at": "2026-11-05T18:03:25Z"
}
```
### 202
docs/customer/routes/job-export.md:
```json
{
"job_id": 685,
"state": "pending",
"format": "ndjson.gz",
"poll_url": "/v1/jobs/685/export",
"parts": [],
"rows_total": 0,
"bytes_total": 0,
"credits_charged": "0",
"attempts": 0,
"requested_at": "2026-11-05T18:02:11Z"
}
```
### 400
bad_request:
```json
{
"error": {
"code": "bad_request",
"message": "kind must be one of instagram.fetch, instagram.search, tiktok.audience, tiktok.screen, tiktok.search, tiktok.walk, got \"tiktok.profile\""
}
}
```
invalid_unit:
```json
{
"error": {
"code": "invalid_unit",
"message": "unit \"#gym\" starts with \"#\"; drop the \"#\" to search the word",
"field": "keywords"
}
}
```
### 401
```json
{
"error": {
"code": "unauthorized",
"message": "invalid API key"
}
}
```
### 404
not_found:
```json
{
"error": {
"code": "not_found",
"message": "no job 685"
}
}
```
no_export:
```json
{
"error": {
"code": "not_found",
"message": "no export of job 685; POST /v1/jobs/685/export to start one"
}
}
```
### 409
```json
{
"error": {
"code": "job_not_finished",
"message": "job 685 is running; export it once it is done"
}
}
```
### 429
```json
{
"error": {
"code": "rate_limited",
"message": "rate limit exceeded for this key",
"retry_after_seconds": 1
}
}
```
### 500
```json
{
"error": {
"code": "internal",
"message": "internal error"
}
}
```
### 503
```json
{
"error": {
"code": "export_unavailable",
"message": "bulk export is not configured on this server"
}
}
```
---
# Start an export
URL: https://scraper.riken.me/docs/v1/jobs/id/export-post
`POST https://scraper.riken.me/v1/jobs/{id}/export`
- Status: Live
- Cost: Free. Errors are free.
Write all of a finished job's results as gzipped NDJSON files, read in one
database snapshot. Free; owner key only. The job must be `done`,
`failed` or `canceled` with `finished_at` set, else `409 job_not_finished`.
Idempotent: POST again and you get the same export, not rebuilt. It is
rebuilt only when the last export failed or the job finished again after
it was built. `202` while it builds, with `Location` and `Retry-After: 5`.
## Headers
| name | type | required | description |
|---|---|---|---|
| `x-api-key` | string | one of the auth headers | Your API key. If both headers are sent, `x-api-key` wins. |
| `Authorization` | string | one of the auth headers | `Authorization: Bearer `, the same key. |
## Path parameters
| name | type | required | description | example |
|---|---|---|---|---|
| `id` | integer | yes | The job id. Another key's job answers as if it did not exist. | `685` |
## Example
```bash
curl -s --max-time 400 -X POST "https://scraper.riken.me/v1/jobs/685/export" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | free | The export is `done` (with URLs), `failed`, or `stale`. Free. |
| 202 | free | The export is `pending` or `running`. Poll `poll_url` after `Retry-After` (5 s). Free. |
| 400 | free | A malformed body, parameter or cursor. Free. |
| 401 | free | Missing, unknown or revoked key. Free. |
| 404 | free | No such job, another key's job, or (GET export only) no export requested yet. Free. |
| 409 | free | The job is still queued or running, or canceled/failed with results still being saved (no `finished_at` yet). Free. |
| 429 | free | The key's request rate. Free. |
| 500 | free | Our fault. Free. |
| 503 | free | Export is switched off on this server. Free. |
## Response examples
### 200
Job 685's real row counts; byte sizes and times illustrative (docs/customer/routes/job-export.md):
```json
{
"job_id": 685,
"state": "done",
"format": "ndjson.gz",
"poll_url": "/v1/jobs/685/export",
"sections": {
"creators": 9720,
"media": 299818,
"walks": 9731
},
"parts": [
{
"section": "creators",
"part": 0,
"rows": 9720,
"bytes": 1350211,
"url": "https://cs-niche-video.s3.us-west-2.amazonaws.com/scraper/exports/key-7/job-685/attempt-1/creators-0000.ndjson.gz?X-Amz-..."
},
{
"section": "media",
"part": 0,
"rows": 50000,
"bytes": 6012345,
"url": "https://..."
},
{
"section": "media",
"part": 1,
"rows": 50000,
"bytes": 5998812,
"url": "https://..."
}
],
"rows_total": 319269,
"bytes_total": 41234567,
"urls_expire_at": "2026-11-05T23:41:00Z",
"credits_charged": "0",
"attempts": 1,
"requested_at": "2026-11-05T18:02:11Z",
"started_at": "2026-11-05T18:02:11Z",
"finished_at": "2026-11-05T18:04:40Z"
}
```
Illustrative:
```json
{
"job_id": 685,
"state": "failed",
"format": "ndjson.gz",
"poll_url": "/v1/jobs/685/export",
"parts": [],
"rows_total": 0,
"bytes_total": 0,
"credits_charged": "0",
"failure_reason": "storage unavailable",
"attempts": 3,
"requested_at": "2026-11-05T18:02:11Z",
"started_at": "2026-11-05T18:03:20Z",
"finished_at": "2026-11-05T18:03:25Z"
}
```
### 202
docs/customer/routes/job-export.md:
```json
{
"job_id": 685,
"state": "pending",
"format": "ndjson.gz",
"poll_url": "/v1/jobs/685/export",
"parts": [],
"rows_total": 0,
"bytes_total": 0,
"credits_charged": "0",
"attempts": 0,
"requested_at": "2026-11-05T18:02:11Z"
}
```
### 400
bad_request:
```json
{
"error": {
"code": "bad_request",
"message": "kind must be one of instagram.fetch, instagram.search, tiktok.audience, tiktok.screen, tiktok.search, tiktok.walk, got \"tiktok.profile\""
}
}
```
invalid_unit:
```json
{
"error": {
"code": "invalid_unit",
"message": "unit \"#gym\" starts with \"#\"; drop the \"#\" to search the word",
"field": "keywords"
}
}
```
### 401
```json
{
"error": {
"code": "unauthorized",
"message": "invalid API key"
}
}
```
### 404
not_found:
```json
{
"error": {
"code": "not_found",
"message": "no job 685"
}
}
```
no_export:
```json
{
"error": {
"code": "not_found",
"message": "no export of job 685; POST /v1/jobs/685/export to start one"
}
}
```
### 409
```json
{
"error": {
"code": "job_not_finished",
"message": "job 685 is running; export it once it is done"
}
}
```
### 429
```json
{
"error": {
"code": "rate_limited",
"message": "rate limit exceeded for this key",
"retry_after_seconds": 1
}
}
```
### 500
```json
{
"error": {
"code": "internal",
"message": "internal error"
}
}
```
### 503
```json
{
"error": {
"code": "export_unavailable",
"message": "bulk export is not configured on this server"
}
}
```
---
# Liveness
URL: https://scraper.riken.me/docs/healthz
`GET https://scraper.riken.me/healthz`
- Status: Live
- Cost: Free. Errors are free.
No auth. `503` with `status: degraded` when the database is unreachable.
## Example
```bash
curl -s --max-time 400 -X GET "https://scraper.riken.me/healthz" \
-H "x-api-key: $RIKEN_API_KEY"
```
## Status codes
| status | charged | meaning |
|---|---|---|
| 200 | free | Up. |
| 503 | free | The process is up and the database is not. |
## Response examples
### 200
```json
{
"status": "ok",
"version": "6bced82",
"schema_version": 28,
"uptime_seconds": 3600.5
}
```
### 503
```json
{
"status": "degraded",
"schema_version": 0,
"uptime_seconds": 12.1,
"db_error": "database unreachable"
}
```
---
# Changelog
URL: https://scraper.riken.me/docs/changelog
Changes to endpoints, response bodies and limits.
## November 2026
- Docs site published in preview.
- Job results: creators and media page in id order. Old cursors answer `400 cursor_expired`.
- Job export: `POST` and `GET /v1/jobs/{id}/export`, gzipped NDJSON, free.
## October 2026
- Profile, profile videos (page 1) and keyword search open in beta.
---