API reference
Last updated 3 September 2026
Reachara monitors a competitor’s marketing channel by sweeping it on a schedule and storing what it finds: the accounts posting under the hashtags being watched, the videos themselves, and what each hashtag has yielded. This API is direct read access to that store, plus one call that goes out and sweeps.
Everything in it is public channel data. There are no audience demographics, no private metrics, and no live lookups — the API answers from what has been crawled.
Base URL: https://api.reachara.com. Every endpoint is HTTPS-only and returns JSON. TikTok is the only channel today, and it is the default for every platform parameter below.
Authentication
Every request carries an API key as a bearer token:
curl -H "Authorization: Bearer $REACHARA_KEY" \
"https://api.reachara.com/v1/creators?limit=5"Create a key in the dashboard under Settings → API keys. Keys look like rch_live_… and are shown exactly once, at creation — we store only a SHA-256 hash and a short display prefix, so a lost key cannot be recovered and has to be replaced. Revoke a key from the same screen; revocation takes effect on the next request.
A missing header, the wrong scheme, an unknown key and a revoked key all return the same 401 unauthorized. That is deliberate: distinguishing them tells a prober which keys exist.
Rate limits and what a call costs
- At least 600 requests per hour, per key, on a rolling window — higher on Growth and Scale, which is part of what those plans sell. This page is public and cannot know which plan you are on; your account’s actual limit is shown on the API keys screen in your dashboard, and every response carries it. The limit is per key rather than per account, so a batch job on one key cannot starve your production key. Rejected requests do not count towards it — retrying politely cannot extend your own lockout.
- Reads are free. Searching, reading an account, and listing hashtags or your own scans cost nothing; they are bounded by the rate limit instead.
- Sweeps are included in your subscription. Nothing in this API is metered per call. What a plan sets is how many competitor-channel pairs you monitor and how often they refresh — see the pricing page — not a balance that a request draws down.
- Over the limit you get
429 rate_limited. CallGET /v1/usageto see how much of the hour you have used.
Two fields in the responses below are left over from an earlier, credit-metered version of this API and are kept in the shape so existing integrations do not break: credits_charged on a scan, along with the x-credits-charged header, and credits_remaining on GET /v1/usage. Both now always report 0. Do not build billing or quota logic on them.
Endpoints
GET /v1/creators— search the corpus.GET /v1/creators/{handle}— one account, with every video crawled for it.GET /v1/hashtags— which hashtags actually pay off.POST /v1/scans— go and crawl. The only call that leaves the corpus.GET /v1/scans— the scans this account has run.GET /v1/usage— your rate-limit headroom and what you have called.
GET /v1/creators
Search the crawled accounts. All parameters are optional.
platform— defaulttiktok.q— free text, matched case-insensitively against the handle, the display name and the bio. It does not search video captions.min_followers,max_followers— 0 to 100,000,000.min_engagement_rate— a ratio, e.g.0.05. Keeps accounts with at least one crawled video at or above it, rather than an account average.hashtag— restrict to accounts with a crawled video carrying this tag. Case-insensitive, and written without the#.contact—reachablekeeps accounts that published any contact route at all;businesskeeps only those that published a business email address. Omit it for everything.sort—rank(default; the account’s best-scoring crawled video),followers, orrecent— which is when we last saw the account in a sweep, not when it last posted. An unrecognised value falls back torank.limit— 1 to 100, default 25.offset— 0 to 1,000,000, default 0.
Out-of-range numbers are clamped rather than rejected, so limit=5000 returns 100.
{
"total": 1284,
"limit": 25,
"offset": 0,
"creators": [
{
"id": "MS4wLjABAAAA…", // the platform's stable id, not the handle
"platform": "tiktok",
"handle": "gabyalcg",
"nickname": "Gaby",
"bio": "collabs: [email protected]",
"avatar_url": "https://…",
"verified": false,
"profile_url": "https://www.tiktok.com/@gabyalcg",
"stats": { "followers": 48200, "total_likes": 1904000, "video_count": 312 },
"contact": {
"type": "email_business", // email_business | bio_link | email_personal | dm_only
"email": "[email protected]",
"confidence": "high", // high | medium | low
"link": null,
"link_kind": null, // aggregator | own_site
"reachable": true // false means nothing was published
},
"rank_score": 82.4,
"last_seen_at": "2026-09-01T11:22:03.000Z"
}
]
}id is the platform’s own stable id. Key your records on it, not on handle — an account can be renamed, and keying on the handle forks one account into two the day it rebrands.
GET /v1/creators/{handle}
One account and every video we have crawled for it. The handle goes in the path, with or without a leading @; it is matched case-insensitively. Takes an optional platform query parameter.
curl -H "Authorization: Bearer $REACHARA_KEY" \
"https://api.reachara.com/v1/creators/gabyalcg"
{
"creator": { … same shape as above … },
"audience": {
"available": false,
"reason": "Reachara reports only what creators publish publicly. …"
},
"videos": [
{
"id": "7412…",
"url": "https://www.tiktok.com/@gabyalcg/video/7412…",
"caption": "what I eat in a day",
"hashtags": ["whatieatinaday", "highprotein"],
"duration_sec": 34,
"posted_at": "2026-08-27T09:04:00.000Z",
"stats": { "plays": 412000, "likes": 39100, "comments": 812, "shares": 2204, "saves": 5510 },
"metrics": {
"engagement_rate": 0.114,
"weighted_engagement_rate": 0.131,
"amplification": 8.5, // plays ÷ followers; null when not computable
"velocity_per_day": 51500,
"metrics_suspect": false, // true = interactions exceeded plays; exclude before ranking
"relevance": 0.72,
"rank_score": 91.2
},
"why_matched": ["high protein"],
"discovered_via": "whatieatinaday"
}
]
}The audience block is always available: false, and it is returned rather than omitted so the limitation is discoverable from a response. Audience demographics require enumerating follower lists, which is not publicly accessible — so we do not estimate them rather than publish a guess you would target on.
Two fields exist so you can refuse bad data instead of ranking on it. metrics_suspect marks a video whose interactions exceed its play count, which is the platform’s counter lagging rather than a breakout. amplification is null, never 0, when it could not be computed honestly — 0 would read as a measured zero.
An account we have never crawled returns 404 not_found. Run a scan covering its niche first; this API serves the corpus and does not fetch live profiles on demand.
GET /v1/hashtags
What each hashtag has actually produced, sorted by contactable rate — the corpus telling you where its own yield comes from. Parameters: platform, and limit (1 to 100, default 20).
{
"hashtags": [
{
"tag": "whatieatinaday",
"platform": "tiktok",
"times_scanned": 6,
"videos_yielded": 1180,
"creators_yielded": 99,
"contactable_yielded": 94,
"contactable_rate": 0.949,
"last_status": "ok", // ok | empty | blocked | error
"last_scanned_at": "2026-09-02T02:11:40.000Z"
}
]
}blocked is not empty. A challenged page returns zero items and looks identical to a barren tag; reporting them as one value would have you permanently skip tags that were merely rate-limited that day.
POST /v1/scans
Go out and crawl. This is the only call that leaves the corpus, and the only one that takes a JSON body. Send either niche or seed_tags — with neither you get 400 missing_niche.
niche— free text, e.g."calorie tracking app". Used to infer which hashtags to sweep.seed_tags— hashtags to start from. An array, or a comma-separated string. Capped at 24.terms— words a matching video should contain, used for the relevance score. Same array-or-string form, capped at 24.max_tags— how many hashtags to sweep. Capped at 12, which is also the default.rounds— how many expansion passes, 1 to 3, default 2. Each round takes the tags found in the previous one and sweeps the most promising of them.
curl -X POST -H "Authorization: Bearer $REACHARA_KEY" \
-H "Content-Type: application/json" \
-d '{"niche":"calorie tracking app","rounds":2}' \
"https://api.reachara.com/v1/scans"
HTTP/1.1 202 Accepted
x-credits-charged: 0
{
"scan": {
"request_id": "req_…",
"status": "queued",
"niche": "calorie tracking app",
"seed_tags": [],
"max_tags": 12,
"rounds": 2
},
"credits_charged": 0, // legacy field, always 0 — sweeps are included
"poll": "/v1/scans/req_…"
}The per-scan poll URL is not live yet. The field is in the response, but there is no GET /v1/scans/{request_id} endpoint behind it today. Until there is, poll GET /v1/scans and match on request_id. We would rather tell you that than have you build against a 404.
A scan is asynchronous — 202 means queued, not finished. A full sweep typically takes a few minutes; roughly ten seconds per hashtag is the dominant cost.
GET /v1/scans
The scans this account has run, newest first. Scoped to the account behind your key — there is no parameter for reading someone else’s. Takes limit (1 to 50, default 20).
{
"scans": [
{
"id": "9f0c…",
"request_id": "req_…",
"status": "running", // running | succeeded | failed
"niche": "calorie tracking app",
"seed_tags": [],
"terms": [],
"tags_scanned": 7,
"videos_found": 640,
"creators_found": 388,
"contactable_found": 201,
"error": null,
"started_at": "2026-09-02T02:04:11.000Z",
"finished_at": null
}
]
}The counters move while a scan runs, so this is also the progress endpoint. A scan that was partly blocked finishes with real counts and a populated error only if the run itself failed.
GET /v1/usage
Your rate-limit headroom and what you have been calling. Takes no parameters.
{
"credits_remaining": 0, // legacy field, always 0 — nothing is metered
"rate_limit": {
"limit_per_hour": 600,
"used_this_hour": 12,
"scope": "api_key"
},
"requests_last_30d": 4021,
"by_endpoint": [
{ "endpoint": "GET /v1/creators", "requests": 3810, "credits_charged": 0 },
{ "endpoint": "POST /v1/scans", "requests": 14, "credits_charged": 0 }
]
}The number to watch here is used_this_hour against limit_per_hour — that is the only ceiling the API enforces. requests_last_30d and by_endpoint cover every key you created, not just the one you called with; used_this_hour is that one key, because the rate limit is per key.
Errors
Every error has the same shape:
{ "error": { "code": "rate_limited", "message": "…" } }400 invalid_json— the body was not JSON.400 missing_niche— a scan with neithernichenorseed_tags.400 missing_handle— no handle in the path.401 unauthorized— missing, malformed, unknown or revoked key.402 insufficient_credits— still returned by the scan endpoint if a balance check ever fails. Since sweeps are no longer metered you should not see it; handle it as a 402 rather than assuming it cannot happen.402 subscription_past_due— the subscription needs a working payment method.403 account_inactive— the key is valid but its account no longer has access. Create a new key from an active account.404 not_found— no such account in the corpus.429 rate_limited— over 600 requests in the rolling hour for this key.500 internal_error— something broke on our side. The detail is logged rather than returned; quote the code and the time and we can look it up.
Response headers
x-result-count— how many items this response carries, on every list endpoint.x-credits-charged— on a successful scan. Legacy; always 0.cache-control: no-store— responses are never cached for you.access-control-allow-origin: *— the API is callable from a browser. It carries no cookie auth, so there is no cross-site-request surface for a permissive origin to widen. That said, a key in front-end code is a key you have published; keep it on your server.
What this API deliberately does not do
- No audience demographics. See the
audienceblock above. - No live lookups. Reads answer from the crawled corpus. If an account is not in it, scan first.
- No private data. Everything served here was publicly visible on the channel, including any contact details an account published in its own bio. Nothing is inferred, enriched or constructed.
- No webhooks yet. Poll
GET /v1/scansfor a scan’s progress.
Something not working?
Write to [email protected] with the endpoint, the time, and the key prefix (the rch_live_… fragment shown in the dashboard — never the whole key). Every call is logged with its status and duration, so that is usually enough to find it.