# Response Schema (/docs/response-schema) Response Schema [#response-schema] Every SocialCrawl response — success or error — uses the same envelope. Write one parser, handle every platform. Successful Response [#successful-response] ```json { "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](/docs/computed-fields.md) states the exact rule per field. Envelope Fields [#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](/docs/computed-fields.md) | | `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](#list-endpoints) below and [Pagination](/docs/pagination.md) | | `idempotent_replay` | boolean | Present and `true` only when this response is an idempotency replay. Absent on a normal response. See [Idempotent requests](#idempotent-requests) | Two more fields live **inside `data`**, not at the envelope root: `data.dropped` and `data._warnings[]`. `data.dropped` — Discarded-row counter [#datadropped--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 [#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[]`: ```json { "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 [#error-response] ```json { "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#`), never a per-code sub-path. See [Error Handling](/docs/errors.md) for the full error-code table and refund rules. Response Headers [#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](/docs/caching.md)) | | `X-Idempotent-Replay` | `"true"` on idempotent replays (only present when the response was replayed). See [Idempotent requests](#idempotent-requests) | | `X-RateLimit-Limit` / `-Remaining` / `-Reset` | Request-window headroom, on every authenticated response. See [Rate limits](/docs/rate-limits.md) | | `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 `429`s (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-endpoints] List archetypes (`PostList`, `CommentList`, `SearchResult`) are normalised to a consistent shape regardless of upstream key names: ```json { "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): ```json { "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](/docs/pagination.md). Idempotent requests [#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-6e8b4d2a1f3c ``` Replayed 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](/docs/errors.md) page for the 409/422 outcomes when a key is reused incorrectly. Force a fresh fetch [#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-cache ``` The 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](/docs/caching.md) page.