Response schema
The InsightSocial API envelope under schema 2, what sits inside data, pagination, response headers, idempotent replays and fresh fetches.
Every data endpoint answers in one success envelope, and every failure in one error envelope, so a single parser handles every platform. The two share only a few fields: check success first, then read the body as one or the other. The two account endpoints are the exception: GET /v1/credits returns a balance body of its own (it still carries success, credits_used and credits_remaining), and GET /v1/endpoints returns the catalogue in its own shape.
This page describes schema 2, the shape every key gets from October 4, 2026. What changed from the previous shape is on Schema 2.
Successful response
{
"success": true,
"platform": "instagram",
"endpoint": "/v1/instagram/profile",
"schema_version": "2",
"data": {
"author": {
"id": "528817151",
"username": "nasa",
"display_name": "NASA",
"followers": 104294818,
"followers_approximate": null,
"url": "https://www.instagram.com/nasa/",
"external_url": "https://www.nasa.gov",
"last_post_at": "2026-10-03T14:10:18.000Z"
}
},
"unavailable": [],
"credits_used": 10,
"credits_remaining": 9990,
"request_id": "req_d6ad52ab1f9f",
"cached": false,
"idempotent_replay": false,
"charge_reason": "miss",
"free_call": false
}The profile is trimmed here. Every field of each entity is on its unified schema page.
Envelope fields
These are the fields of a successful response. An error carries far fewer; see Error response.
| Field | Type | On every success | Description |
|---|---|---|---|
success | boolean | yes | true. Also on every error, as false: branch on it first. |
platform | string | yes | The platform you called, such as instagram. |
endpoint | string | yes | The path you called, such as /v1/instagram/profile. |
schema_version | string | yes | "2". The contract data follows. An error never carries it. |
data | object | yes | The result. Its shape depends on the endpoint; see Inside data. |
pagination | object | no | List endpoints only. See List endpoints. |
unavailable | string[] | yes | Fields this platform normally fills that this response could not, as paths into data. Empty when nothing is missing. See Data availability. |
credits_used | integer | yes | Credits charged for this call. 0 for replays, free calls, empty results, dry_run=1 and failures. |
credits_remaining | integer | yes | Your balance after this call. |
request_id | string | yes | Our identifier for this call, in the form req_ plus 12 hex characters. |
cached | boolean | yes | true when the result came from the shared cache rather than a live fetch. |
idempotent_replay | boolean | yes | true when this is a replay of an earlier call with the same Idempotency-Key. |
charge_reason | string | yes | Why credits_used has the value it has. See below. |
free_call | boolean | yes | true when the call used one of your free calls, so credits_used is 0. |
charge_reason
| Value | Meaning | Credits |
|---|---|---|
miss | A live fetch | The endpoint's price, or for a metered endpoint what the call used |
shared_cache | Served from the shared cache | 5 |
replay | An idempotent replay of a call you already made | 0 |
no_result | Nothing to charge for, such as an empty result | 0 |
dry_run | A dry_run=1 price quote. No data was fetched | 0 |
The pricing rules are on Credits.
Inside data
data holds one entity, or a page of them, under a key named after the entity:
| Endpoint returns | data holds | Schema |
|---|---|---|
| One post | data.post | Post |
| A list of posts | data.items[].post | Post |
| One profile | data.author | Author and profile |
| A list of profiles | data.items[].author | Author and profile |
| One comment | data.comment | Comment |
| A list of comments | data.items[].comment | Comment |
| A transcript | data.transcript | Transcript |
A list also carries these fields next to items:
data.total: the platform's own total, when it states one (a comment count, for example). It is not the size of this page, and it isnullwhen the platform does not say.data.truncated:truewhen the list is known to be cut short by the platform,falsewhen the platform says it is complete,nullwhen we cannot tell. On a Reddit comment tree it istrueonce the tree reaches Reddit's ten-level depth limit and otherwisenull: Reddit leaves some branches behind "load more" links that the source does not count. To judge how complete a Reddit thread is, compare with the post'sengagement.commentsfrom/v1/reddit/post, which also counts deleted and removed comments. On/v1/instagram/post/likersit istruewhen fewer likers came back than the post has likes: Instagram serves one slice of the list, with no next page.data.ext: what the page says about itself beyond its rows, under the same conventions as an entity'sext(snake_case keys, string ids, ISO times). On/v1/instagram/audio/reels, for example, it carries the sound the reels use (ext.sound, with its title, artist and duration) andext.is_trending. Present only when the platform sends something; absent otherwise, so treat it as optional.
The same rules hold everywhere inside data:
- Ids are strings. Several platforms use ids above 2^53, which a JSON number would corrupt. Never parse an id as a number.
- Times are ISO 8601 in UTC, ending in
Z. nullmeans not known. A count the platform hides isnull, never0.0means zero.content.media_urlsis always an array (ornull), never a bare string.extkeys are snake_case.extholds what one platform carries that the others cannot mean the same way. Treat everyextkey as optional.
Some long-tail endpoints (metrics, and lists with no matching entity) are marked beta on their reference page. They return the source's own structure under our naming rules: snake_case keys, string ids, and ISO times where the source gives a timestamp. Some pass on figures the source itself computes, such as engagement_rate_percentages and likes_per_hour on /v1/instagram/engagement or a confidence on /v1/youtube/video/sponsors, and a source's own date text (a local datetime, a time_ago) stays as the source wrote it. Their fields may change.
profile/full is beta too, but its profile and posts are typed: data.profile is a profile and each data.posts[].post is a post. It pages its posts with cursor like a list. Any other key under its data follows the beta rules above.
The typed entities above carry no analysis of ours: no engagement rate, labels or relevance scores. Compute what you need from the raw counts; see Schema 2 for what was removed and the creator engagement recipe for an engagement rate built from engagement and the author's followers.
One schema
Every key gets schema 2, and it is the only shape served. An error has no schema_version field. See Schema 2 for what changed from the previous shape.
Error response
An error is a different, smaller body. It has no platform, endpoint, schema_version, data, unavailable, cached or charge_reason, so read success before anything else:
{
"success": false,
"error": {
"type": "UNSUPPORTED_PARAMETER",
"message": "`relevance` is not supported: it asks for analysis, which is not part of schema 2. Remove it and retry. Nothing was charged.",
"param": "relevance"
},
"request_id": "req_80d197121a1e",
"credits_used": 0,
"credits_remaining": null
}| Field | Description |
|---|---|
error.type | A stable string to branch on. |
error.message | A readable explanation, often naming what to fix. Do not parse it. |
error.param | The query parameter at fault, when there is one. |
request_id | Quote it to support. |
credits_used | Always 0: failed calls are never charged. |
credits_remaining | Your balance when we know it, otherwise null (for example on authentication failures, or on a request refused before we read your balance). |
That is the whole body: success, error (type, message, and param when one parameter is at fault), request_id, credits_used and credits_remaining.
const body = await res.json();
if (!body.success) {
// { error: { type, message, param? }, request_id, credits_used, credits_remaining }
throw new Error(`${body.error.type}: ${body.error.message} (${body.request_id})`);
}
// Only now: body.schema_version, body.data, body.unavailable, body.paginationNot every error is in your Activity Log. A request refused before it reaches a platform writes no log row: 400 UNSUPPORTED_PARAMETER, 400 CURSOR_INVALID and CURSOR_EXPIRED, authentication and rate-limit errors, idempotency conflicts and 402 INSUFFICIENT_CREDITS. dry_run=1 quotes are not logged either. Other errors, including the 400 parameter errors a platform returns, are logged with their status.
The full list of types is on Errors.
Response headers
| Header | Value |
|---|---|
X-Request-Id | Same as request_id in the body. |
X-Credits-Used | Credits charged for this call. |
X-Credits-Remaining | Your balance after the call. Omitted when it is unknown, never sent as a placeholder 0. |
Retry-After | Seconds to wait. Sent on 429, on 409 IDEMPOTENCY_IN_PROGRESS and on some 503 responses. |
Cache-Control | no-store. Do not cache API responses in a shared proxy. |
CORS headers are also set, and X-Request-Id, X-Credits-Used, X-Credits-Remaining and Retry-After are exposed to browser code. Keys still belong on your server; see Authentication.
List endpoints
List responses put their rows in data.items and carry a pagination object at the root of the envelope:
{
"success": true,
"platform": "twitter",
"endpoint": "/v1/twitter/user/tweets",
"schema_version": "2",
"data": {
"items": [
{ "post": { "id": "2106502020849127865", "kind": null, "author": { "id": null, "username": "NASA" } } }
],
"total": null,
"truncated": null
},
"pagination": {
"next_cursor": "v2c.M-GEP84JXNZPqhQ0NlEOubhbuFGGiCK9…",
"has_more": true,
"page_size": 19
},
"unavailable": ["items[].post.author.id"],
"credits_used": 10,
"credits_remaining": 9980,
"request_id": "req_161a29ec13ca",
"cached": false,
"idempotent_replay": false,
"charge_reason": "miss",
"free_call": false
}Here unavailable says that no row on this page carries an author id, though the platform has one. The rows are still valid; plan for author.id being null on this endpoint.
| Field | Description |
|---|---|
next_cursor | Send it back as the cursor query parameter to get the next page. Starts with v2c.; pass it unchanged. null on the last page. |
has_more | false on the last page. Stop there. |
page_size | Rows on this page. |
stopped_at | Only on creator post lists called with since or stop_at_id. Says which boundary ended the walk: since, known_id or end. |
The cursor is only valid as cursor, with the same endpoint and parameters, for 24 hours. The full loop is on Pagination.
Idempotent requests
Send an Idempotency-Key header to make a call safe to retry. Use a fresh UUID for each logical operation.
curl "https://api.insightsocial.app/v1/tiktok/profile?handle=khaby.lame" \
-H "x-api-key: $INSIGHTSOCIAL_API_KEY" \
-H "Idempotency-Key: c3f9d27e-81b4-4e06-a5d2-94be0f6c1a78"- A replay returns
idempotent_replay: true,charge_reason: "replay"andcredits_used: 0. - If the original call with the same key is still running, you get
409 IDEMPOTENCY_IN_PROGRESSwithRetry-After. Wait, then send the same key again. - Keys are scoped to your account. Reusing a key for a different request, or with a different schema, answers
409 IDEMPOTENCY_KEY_REUSED. - A key must be at most 255 characters of letters, digits,
.,_,:or-. A key that does not fit is ignored, and the call runs as an ordinary request.
Force a fresh fetch
Repeat calls can be served from the shared cache (cached: true) at 2 credits. To skip it and fetch live, send Cache-Control: no-cache, or add fresh=1 to the query:
curl "https://api.insightsocial.app/v1/tiktok/profile?handle=khaby.lame" \
-H "x-api-key: $INSIGHTSOCIAL_API_KEY" \
-H "Cache-Control: no-cache"A fresh fetch is always charged at the normal price. See Credits for the shared cache.