Insights
Follower, like and posting velocity from the snapshot series
/data-api/v2/insights/tiktok/{handle}/growth- Scope
- insights:read
- Freshness
- catalog read
- Group
- Insights
- Platforms
- 1 of 7
What this endpoint answers
The snapshot history for one TikTok account, with a velocity for each counter. Every reading carries the precision it was actually measured at, and velocity is computed only from exact readings, because TikTok rounded its own counters until 2026-08-24 and a series that ignores that reports rounding as growth.
This is a catalog read: it is served from the CRM Solid database in milliseconds, costs one budget unit, and reports how old the reading is in meta.cache_age_s. It needs the insights:read scope (Insights): Engagement rates, top posts, viral patterns, cohort benchmarks.
Good to know
- Until 2026-08-24 11:00 UTC the crawler read TikTok's rendered counters - one decimal place and a unit, so 12.3M - and truncated the like total to a signed int32 on top of that. From that hour it reads statsV2, which is an exact int64. The cutover was verified hour by hour against production: 461 of 461 readings rounded in the 10:00 hour, 0 of 251 in the 11:00 hour.
- followers_step, hearts_step and videos_step are the rounding unit each reading was measured at. 1 means exact; 100,000 means the true value was anywhere within 50,000 of what is shown. A change smaller than the step is not a measurement.
- Because the rounding was proportional, which counters survive the older window depends on account size, not on date. video_count is exact for 16,123 of 16,190 accounts, so posting velocity is measurable across the whole history for almost everyone. follower_count is exact for 8,357 pre-cutover and heart_count for only 4,158.
- A worked example of why this matters. mohsinbahim reads 628500 flat for five days to 2026-08-23, then 628516 on the 24th, then drifts down to 628510. A naive series calls that a five-day plateau and a jump. The account is in fact slowly losing followers and the jump is the rounding coming off. This endpoint excludes those points and returns the decline.
- Velocity spans the first exact reading to the last rather than summing consecutive deltas, because a hole in the series is a missed snapshot and not a period of zero change.
- measurable is false, and every figure null, when a counter has fewer than two exact readings in the window. Null is an absent measurement; it is never a change of zero.
- Depth varies enormously. About 2,900 accounts hold 40 or more snapshots and 13,080 hold fewer than 10. GET /insights/tiktok/{handle}/coverage says which kind an account is before you read it.
- Reads scraper.tt_snapshots directly rather than the daily rollup, because the rollup inherited the same rounding: 97.9 percent of pre-cutover hearts_eod rows above 1M are multiples of 100,000.
Parameters
Every value the call accepts, with the example the spec ships so the request runs as written.
| Name | In | Required | Example | What it does |
|---|---|---|---|---|
handle | path | required | mohsinbahim | TikTok handle, without the @. |
days | query | optional | 30 | Window to read, 1 to 365 days. |
Call it
Authenticate with a bearer token or the x-api-key header. Keys are server-to-server credentials. Never embed one in front-end code - call the API from your own backend and forward the result.
curl "https://crmsolid.com/data-api/v2/insights/tiktok/mohsinbahim/growth" \
-H "Authorization: Bearer psk_live_..."
const res = await fetch("https://crmsolid.com/data-api/v2/insights/tiktok/mohsinbahim/growth", {
headers: {
Authorization: `Bearer ${process.env.CRM_SOLID_DATA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}
const { data, meta } = await res.json();
import os
import requests
res = requests.get(
"https://crmsolid.com/data-api/v2/insights/tiktok/mohsinbahim/growth",
headers={"Authorization": f"Bearer {os.environ['CRM_SOLID_DATA_API_KEY']}"},
timeout=30,
)
res.raise_for_status()
payload = res.json()
data, meta = payload["data"], payload["meta"]
Keys look like psk_live_... for production keys, psk_test_... for test keys and are minted in the panel.
What comes back
Success is { data, meta }. Failure is { error: { code, message, request_id } }. The body below is the spec's own example: the values in it are illustrative readings, not live numbers.
- Handle
- mohsinbahim
- User id
- 6743139527352910850
- Current followers
- 628,510
- Current hearts
- 14,516,013
- Current videos
- 1,334
- Current captured at
- 8/30/2026, 10:39:28 PM
- Crawled at
- 8/30/2026, 10:39:28 PM
- Data age s
- 50,713
- Exact counts from
- 8/24/2026, 11:00:00 AM
- Window days
- 30
- Series
- 2 items
- Growth followers measurable
- yes
- Growth followers change
- -6
- Growth followers per day
- -0.99
- Growth followers change pct
- -0.001
- Growth followers measured from
- 8/24/2026, 8:34:41 PM
- Growth followers measured to
- 8/30/2026, 10:39:28 PM
- Growth followers points used
- 7
- Growth followers points excluded rounded
- 22
- Growth followers note
- Measured across 7 exact readings. 22 earlier points were excluded because TikTok had rounded the follower count at capt…
- Growth hearts measurable
- yes
- Growth hearts change
- 0
- Growth hearts per day
- 0
- Growth hearts change pct
- 0
- Growth hearts measured from
- 8/24/2026, 8:34:41 PM
- Growth hearts measured to
- 8/30/2026, 10:39:28 PM
- Growth hearts points used
- 7
- Growth hearts points excluded rounded
- 22
{
"data": {
"handle": "mohsinbahim",
"user_id": "6743139527352910850",
"current": {
"followers": 628510,
"hearts": 14516013,
"videos": 1334,
"captured_at": "2026-08-30T22:39:28.441Z"
},
"crawled_at": "2026-08-30T22:39:28.448Z",
"data_age_s": 50713,
"exact_counts_from": "2026-08-24T11:00:00.000Z",
"window_days": 30,
"series": [
{
"captured_at": "2026-08-23T19:36:22.782Z",
"followers": 628500,
"followers_step": 100,
"hearts": 14500000,
"hearts_step": 100000,
"videos": 1334,
"videos_step": 1
},
{
"captured_at": "2026-08-30T22:39:28.441Z",
"followers": 628510,
"followers_step": 1,
"hearts": 14516013,
"hearts_step": 1,
"videos": 1334,
"videos_step": 1
}
],
"growth": {
"followers": {
"measurable": true,
"change": -6,
"per_day": -0.99,
"change_pct": -0.001,
"measured_from": "2026-08-24T20:34:41.317Z",
"measured_to": "2026-08-30T22:39:28.441Z",
"points_used": 7,
"points_excluded_rounded": 22,
"note": "Measured across 7 exact readings. 22 earlier points were excluded because TikTok had rounded the follower count at capture time; including them would have produced a step change that is rounding coming off, not movement."
},
"hearts": {
"measurable": true,
"change": 0,
"per_day": 0,
"change_pct": 0,
"measured_from": "2026-08-24T20:34:41.317Z",
"measured_to": "2026-08-30T22:39:28.441Z",
"points_used": 7,
"points_excluded_rounded": 22,
"note": "Measured across 7 exact readings. 22 earlier points were excluded because TikTok had rounded the like total at capture time; including them would have produced a step change that is rounding coming off, not movement."
},
"videos": {
"measurable": true,
"change": 0,
"per_day": 0,
"change_pct": 0,
"measured_from": "2026-08-01T21:12:44.000Z",
"measured_to": "2026-08-30T22:39:28.441Z",
"points_used": 29,
"points_excluded_rounded": 0,
"note": "Measured across all 29 readings in the window; every one was exact."
}
}
},
"meta": {
"request_id": "req_9f2c41a8b3d5",
"generated_at": "2026-08-23T09:14:02.317Z",
"took_ms": 42
}
}
The meta block
request_idstringrequiredUnique id for this request. Quote it in a support ticket.
generated_atstringrequiredServer time the response was produced.
took_msintegerrequiredMilliseconds spent server-side.
pagePageoptionalsourcestringoptionalWhich backend served the payload, for endpoints with more than one.
cache_age_sintegeroptionalAge of the underlying data in seconds. 0 for live reads.
When it fails
GET /insights/tiktok/{handle}/growth documents 8 failure statuses. Branch on error.code, which is stable and enumerated; message is prose and may change.
- 401Unauthorized
unauthorized | invalid_keyNo key was presented, or the key is unknown, revoked or expired.
- 402PaymentRequired
payment_required | subscription_inactiveThe key is valid but the plan behind it cannot serve the call: the included requests are spent and overage is switched off, capped or unfunded (payment_required), or the billing period lapsed and was not renewed (subscription_inactive). Retrying does not help; paying does. The X-Plan-* headers on this response say how far past the line you are.
Carries X-Plan, X-Plan-Limit, X-Plan-Overage, X-Plan-Period-End, X-Plan-Remaining.
- 403Forbidden
forbidden_scope | forbidden_ipThe key is valid but not allowed to make this call: it lacks the scope, or the request came from an address outside the key's allowlist.
- 404NotFound
not_foundThe addressed resource does not exist.
- 422InvalidRequest
invalid_requestA parameter is malformed, out of range or mutually exclusive with another. `details` names the offending fields.
- 429RateLimited
rate_limited | quota_exceededEither the burst ceiling for the current minute or the daily quota is spent. Distinguish with the code: rate_limited clears within the minute, quota_exceeded does not clear until 00:00 UTC.
Carries Retry-After, X-Quota-Limit, X-Quota-Remaining, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Request-Id.
- 500InternalError
internal_errorSomething failed on our side. Internals are never leaked; quote the request id.
- 503Unavailable
upstream_timeout | upstream_error | not_configuredThe request could not be served right now. BRANCH ON error.code, not on the status: 'upstream_timeout' means a source was too slow (this is what a catalog query hitting its 15-second statement timeout returns, so it is reachable from any endpoint that reads the corpus, not only the live-scrape ones) and the same call is worth retrying with backoff - narrowing it with a smaller limit, a filtered scope or a less popular account makes it far less likely; 'upstream_error' means a source was unreachable, so back off further; 'not_configured' means the capability has no backing service in this deployment, and retrying will never help.
Every response carries X-Request-Id and meta.request_id. Quote it in support requests.
Coverage and limits
This endpoint covers TikTok. Rate limits come from the tier on your key.
| Tier | Requests a minute | Requests a day | Live reads a minute |
|---|---|---|---|
| free | 30 | 1,000 | 5 |
| standard | 120 | 25,000 | 20 |
| pro | 600 | 250,000 | 60 |
| unlimited | 6,000 | 10,000,000 | 600 |
This call only draws on the ordinary per-minute and per-day columns. Every response reports where you stand in X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and the X-Plan headers.
Start calling it
A key takes a minute to mint in the panel, no card. The reference covers authentication, the envelope, scopes, rate limits and every error code in one page.
Reference path: /data-api/reference/insights-tiktok-growth