Response Schema
The unified SocialCrawl response envelope, headers, and partial-data warnings
Response Schema
Every SocialCrawl response — success or error — uses the same envelope. Write one parser, handle every platform.
Successful Response
{
"success": true,
"platform": "tiktok",
"endpoint": "/v1/tiktok/profile",
"data": {
"author": { "username": "charlidamelio", "followers": 156800000 },
"computed": {
"engagement_rate": null,
"language": null,
"content_category": null,
"estimated_reach": null
}
},
"credits_used": 1,
"credits_remaining": 4999,
"request_id": "req-XXXXX",
"cached": false
}The computed leaves are null here because this abridged author carries no likes_count and no bio, and because estimated_reach is always null on a profile. Every computed field is a real value or an honest null, never a filled-in guess, so type your parser for T | null on all four. Computed fields states the exact rule per field.
Envelope Fields
| Field | Type | Description |
|---|---|---|
success | boolean | true for successful responses, false for errors |
platform | string | Platform identifier (tiktok, instagram, etc.) — meta for account endpoints like /v1/credits/balance |
endpoint | string | The full endpoint path that was called |
data | object | Platform-specific, normalised payload. Lists are returned as { items, next_cursor?, total? }. The computed sub-object is attached to every Author and Post — see Computed fields |
credits_used | integer | Credits consumed by this request (0 on cache hits and idempotent replays) |
credits_remaining | integer | null | Your current balance. null only when an idempotency replay succeeds but its transient balance lookup fails |
request_id | string | Unique identifier (req-XXXXX) for debugging and log lookup |
cached | boolean | true when served from cache — cache hits cost 0 credits |
pagination | object | List endpoints only. The uniform pagination contract { next_cursor, has_more, page_size } — identical on every platform. See List endpoints below and Pagination |
idempotent_replay | boolean | Present and true only when this response is an idempotency replay. Absent on a normal response. See Idempotent requests |
Two more fields live inside data, not at the envelope root: data.dropped and data._warnings[].
data.dropped — Discarded-row counter
On list endpoints, data.dropped is the count of upstream items omitted because they could not be repaired to the endpoint schema. A healthy list response carries "dropped": 0. It is the only signal that a page came back short, so read it on every page of a drain rather than inferring completeness from items.length.
Note the placement: dropped sits next to items inside data, while pagination sits at the envelope root. Reading response.dropped returns undefined and silently looks like zero.
data._warnings[] — Partial-data channel
When the field-map or computed-field pipeline hits ambiguous upstream data, it attaches a human-readable notice to data._warnings: string[]:
{
"data": {
"author": { "username": "..." },
"computed": { "engagement_rate": 1.0 },
"_warnings": [
"computed.engagement_rate: value exceeded 1.0 (raw: 1.42); clamped"
]
}
}Treat entries as advisory — the response is still valid. Empty arrays are omitted from the envelope, so _warnings is only present when there is something to report.
Error Response
{
"success": false,
"error": {
"type": "INSUFFICIENT_CREDITS",
"message": "Your account has 0 credits remaining. This endpoint requires 1 credits.",
"status": 402,
"doc_url": "https://www.socialcrawl.dev/docs/errors#insufficient-credits"
},
"credits_used": 0,
"credits_remaining": 0,
"request_id": "req-abc123"
}credits_used is the net credits charged for the failed request. Error paths deduct then refund, so this is 0 in effectively every case — read it to confirm a failure cost you nothing instead of inferring it from the refund rules.
doc_url is always the anchor form (/docs/errors#<code-slug>), never a per-code sub-path. See Error Handling for the full error-code table and refund rules.
Response Headers
| Header | Value |
|---|---|
X-Request-Id | Matches request_id in the body — use it to correlate logs |
X-Credits-Used | Credits consumed (0 on cache hits, empty-upstream 404s, 405/409/422, and idempotent replays) |
X-Credits-Remaining | Current balance when known. On an idempotency replay, omitted when the balance lookup fails; body credits_remaining is null instead |
X-Cache | HIT (served from cache, 0 credits) or MISS. Force a MISS with Cache-Control: no-cache (see Caching) |
X-Idempotent-Replay | "true" on idempotent replays (only present when the response was replayed). See Idempotent requests |
X-RateLimit-Limit / -Remaining / -Reset | Request-window headroom, on every authenticated response. See Rate limits |
X-Concurrency-Limit / -Remaining | In-flight headroom, on every authenticated response. Not an alias of X-RateLimit-* |
X-Upstream-Retries | How many upstream retries this call needed. Omitted when there were none |
Retry-After | Seconds to wait. Sent on both 429s (seconds until the window resets for RATE_LIMITED, a static 1 for CONCURRENCY_LIMIT), on a transient 503 (30), and on a 402 served from the credit cooldown (60). Absent otherwise |
Allow | "GET" — only on 405 METHOD_NOT_ALLOWED responses |
List endpoints
List archetypes (PostList, CommentList, SearchResult) are normalised to a consistent shape regardless of upstream key names:
{
"data": {
"items": [
/* ... */
],
"next_cursor": "eyJwYWdlIjoyfQ==",
"total": 1247
}
}The data-level next_cursor and total are legacy per-payload fields, only included when upstream provides them.
Use the top-level pagination block for pagination. Every list response also carries a uniform pagination object at the envelope root — identical on all platforms and all three pagination styles (cursor, page, offset):
{
"data": {
"items": [
/* ... */
],
"dropped": 0
},
"pagination": {
"next_cursor": "sc.eyJ2IjoyLCJjIjoiMTc3NTM5NDk1MDAwMCJ9",
"has_more": true,
"page_size": 30
}
}The loop is the same everywhere: read pagination.next_cursor, send it back as the cursor query parameter, and stop when pagination.has_more is false. You never handle a per-platform input-param name yourself — the API rewrites the sc. cursor to the endpoint's native param (max_cursor, after, etc.) on the way in. For the full drain recipe, see Pagination.
Idempotent requests
Any /v1/* request can be made safely retriable by sending an Idempotency-Key header (UUIDv4 recommended):
GET /v1/tiktok/profile?handle=charlidamelio HTTP/1.1
x-api-key: sc_...
Idempotency-Key: 7a5e1b4c-2d8f-4a3b-9c1e-6e8b4d2a1f3cReplayed calls keep the cached payload immutable except for billing metadata: credits_used becomes 0 and idempotent_replay becomes true. A known current balance refreshes body credits_remaining and the X-Credits-Remaining header; no balance row resolves to 0. On a transient balance lookup failure, body credits_remaining is null and X-Credits-Remaining is omitted. Keys are scoped per account with a 24-hour TTL. See the Error Handling page for the 409/422 outcomes when a key is reused incorrectly.
Force a fresh fetch
Responses from cache-enabled endpoints are cached and the cache is shared, so a repeat call returns instantly and costs 0 credits (X-Cache: HIT). To bypass the cache and force a guaranteed-live fetch for a single call, send the standard Cache-Control: no-cache request header. Registry-marked uncached endpoints skip this cache contract.
GET /v1/tiktok/profile?handle=charlidamelio HTTP/1.1
x-api-key: sc_...
Cache-Control: no-cacheThe request fetches live from the source and is billed at the normal endpoint cost (a forced MISS, so X-Cache: MISS and a non-zero X-Credits-Used). The fresh result is written back, so your next plain call gets a free HIT. Only the no-cache directive triggers it; Cache-Control: no-store on its own does not.
For the freshness windows per data type, the shared-cache model, and guidance on when to bypass the cache, see the Caching page.
