# SocialTap: full documentation > Public social media data through one API key and one JSON schema. Every SocialTap docs page and endpoint reference in one Markdown file, generated from the product catalog. The short index is https://staging.socialtap.dev/llms.txt. **Instructions for AI agents** 1. **Authenticate.** Send the API key in the `x-api-key` header of every request to `https://staging.socialtap.dev`. Read it from an environment variable such as `SOCIALTAP_API_KEY`, never hard-code it, and call the API from server-side code only. Without a key, ask the user for one (keys: https://staging.socialtap.dev/signup/); never invent one. 2. **No key yet?** `POST https://staging.socialtap.dev/v1/agent_signup` with `{"email": ""}` returns a test key that works at once and a live key that stays pending until your user confirms the emailed link (it spends nothing until then). Ask the user for their email address first, build with the test key, and tell them to confirm. Details: [account and keys](https://staging.socialtap.dev/docs/account/index.md). 3. **Pick an endpoint.** Every endpoint is `GET https://staging.socialtap.dev/v1//` with query parameters; the endpoint list ([all endpoints](https://staging.socialtap.dev/apis/index.md), or [catalog.json](https://staging.socialtap.dev/catalog.json) for machines) gives each one's parameters and price per successful request. Read the endpoint's Markdown docs page before writing code, and don't rely on memory: endpoints and parameters change. Every endpoint of one kind of data (ads, comments, posts, profiles and transcripts) returns the same schema on every platform; `null` means unknown, never 0 or "". 4. **Read the response.** A success is HTTP 200 with `{data, meta}`: `meta.credits_charged` is what the request cost, `meta.credits_remaining` the balance left, `meta.request_id` identifies the request for support. 5. **Paginate.** List endpoints return `{data, page, meta}`. While `page.has_more` is true, repeat the same request with `cursor` set to `page.next_cursor`, keeping every other parameter. An empty page with `page.has_more` true is not the end. Cursors expire (`page.next_cursor_expires_at`): don't store them. Leave `limit` out: a smaller page costs the same. 6. **Handle errors.** Errors are non-2xx responses with `{"error": {code, message, request_id, status, retryable, param, remediation, doc_url, details, charged}}`. Branch on `error.code`, never on `error.message`. Retry only when `error.retryable` is true, with exponential backoff, first waiting the seconds in the `retry-after` header (also `error.details.retry_after`) when it is sent; otherwise do what `error.remediation` says. `insufficient_credits` means the balance is too low: stop and tell the user. Every code is documented at `https://staging.socialtap.dev/docs/errors/#` (`error.doc_url`). Failed requests cost nothing. 7. **Spend credits deliberately.** Each endpoint states its price per successful request (per page for lists); fetch only what the task needs. Add `max_age=` to accept a stored response up to that old: cache hits are free. 8. **Test first.** Keys starting with `st_test_` are test keys: they answer every endpoint from recorded responses, free and deterministic. Build and test the integration with one, then switch to a live key. Magic inputs make an endpoint return any error code it can return ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)), so test your error handling too. 9. **Many items or long lists.** Use background jobs, `POST /v1/jobs`, instead of a loop of single requests: one job runs many requests and walks whole lists. 10. **Notifications.** Instead of polling, register a webhook endpoint with `POST /v1/webhook_endpoints`: deliveries are signed (Standard Webhooks); verify the signature on the raw body before trusting one. 11. **Retry POSTs safely.** Send a new `idempotency-key` (a UUID) with every POST and reuse it when you retry: the work runs once, and a retry answers with the current state. 12. **Balance.** `GET /v1/account` returns the balance, free. 13. **MCP.** Agents with MCP can use the remote server at `https://staging.socialtap.dev/mcp` (Streamable HTTP, the same API key as `authorization: Bearer ` or in the `x-api-key` header), with the tools `get_post`, `get_profile`, `get_transcript`, `list_comments`, `search`, `find_endpoint`, `get_account`, `create_job` and `get_job`; see [the agent guide](https://staging.socialtap.dev/docs/agents/index.md). 14. **Verify.** Before building on an endpoint, run its example request (on its docs page) and check that `data` is not empty. Report the endpoint, `meta.credits_charged` and any `error.code` you saw. --- Source: https://staging.socialtap.dev/docs/ # Quickstart Docs Make your first request in a minute: one API key, one HTTP call, one JSON shape for every platform. ## 1. Your API key Every request is authenticated with an API key sent in the `x-api-key` header. Keys belong to an account and spend that account's credit balance. [Create an API key from your dashboard](https://staging.socialtap.dev/signup/) Keep the key out of source control, for example in an environment variable: Shell: ```shell export SOCIALTAP_API_KEY="your-api-key" ``` ## 2. Make a request Endpoints are plain HTTPS `GET` requests with query parameters. This calls the [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md) with its documented example input: cURL: ```shell curl -G 'https://staging.socialtap.dev/v1/youtube/transcript' \ --data-urlencode 'url=dQw4w9WgXcQ' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` TypeScript: ```typescript const params = new URLSearchParams({ url: "dQw4w9WgXcQ", }); const res = await fetch(`https://staging.socialtap.dev/v1/youtube/transcript?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const { data, meta } = body; console.log(data, meta.credits_charged); ``` Python: ```python import os import requests res = requests.get( "https://staging.socialtap.dev/v1/youtube/transcript", params={"url": "dQw4w9WgXcQ"}, headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]}, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") data, meta = body["data"], body["meta"] print(data, meta["credits_charged"]) ``` Go: ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" ) func main() { q := url.Values{} q.Set("url", "dQw4w9WgXcQ") req, err := http.NewRequest("GET", "https://staging.socialtap.dev/v1/youtube/transcript?"+q.Encode(), nil) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { Data json.RawMessage `json:"data"` Meta map[string]any `json:"meta"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if body.Error != nil { log.Fatalf("%s: %s", body.Error.Code, body.Error.Message) } fmt.Println(string(body.Data), body.Meta["credits_charged"]) } ``` ## Response A successful response is a JSON object with two keys: `data`, the item in its resource's [shared schema](https://staging.socialtap.dev/resources/index.md), and `meta`, which describes how the request was served. [List endpoints](https://staging.socialtap.dev/docs/pagination/index.md) add a third key with what you need to fetch the next page. 200 OK: ```json { "data": { "available_languages": [ "en", "de-DE", "ja" ], "duration_ms": 213000, "id": "dQw4w9WgXcQ", "is_auto_generated": false, "language": "en", "platform": "youtube", "segments": [ { "end_ms": 3040, "start_ms": 1360, "text": "[♪♪♪]" }, { "end_ms": 21880, "start_ms": 18640, "text": "♪ We're no strangers to love ♪" }, { "end_ms": 26960, "start_ms": 22640, "text": "♪ You know the rules and so do I ♪" } ], "source": "captions", "text": "[♪♪♪] ♪ We're no strangers to love ♪ ♪ You know the rules and so do I ♪ ♪ A full commitment's what I'm thinking of ♪ ♪ You wouldn't get this from any other guy ♪ ♪ I just wanna tell you how I'm feeling ♪ ♪ Gotta make you understand ♪ ♪ Never gonna give you up ♪ ♪ Never gonna let …", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "youtube", "source": "captions", "is_cached": false, "credits_charged": 1, "latency_ms": 511, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "video", "id": "dQw4w9WgXcQ", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display. | Field | Type | Description | | --- | --- | --- | | `meta.request_id` | `string` | Unique request ID; include it when contacting support. | | `meta.platform` | `string` | Platform ID of the endpoint (youtube, tiktok, …). | | `meta.source` | `string` | Which upstream strategy produced the data. | | `meta.is_cached` | `boolean` | True when served from the cache (free); see max_age. | | `meta.credits_charged` | `integer` | Credits charged for this request (0 for errors and cache hits). | | `meta.latency_ms` | `integer` | Time SocialTap spent on the request, in milliseconds. | | `meta.fetched_at` | `string` (date-time) | When the data was fetched from the platform, RFC 3339 UTC (older than the request for cache hits). | | `meta.input` | `object` or `null` | What the request's url or handle resolved to: kind, platform ID and canonical URL, so you can confirm what was fetched; null for searches. | | `meta.input.kind` | `string` | What the input points to (profile, video, post, …). New values may be added; treat unknown values as other. | | `meta.input.id` | `string` or `null` | Platform-native ID of the item; null when it is only known after the fetch and the request failed. | | `meta.input.url` | `string` or `null` | Canonical URL of the item; null when unknown. | | `meta.credits_remaining` | `integer` or `null` | Account balance after this request (the x-credits-remaining header); for a busy account an estimate that may not yet subtract concurrent requests (about a second of usage), never below the real balance; null when SocialTap could not read it. | | `meta.client_request_id` | `string` or `null` | Your x-client-request-id, echoed; null when you sent none. | | `meta.warnings` | `object[]` | Things to act on that did not fail the request: a deprecated endpoint, partial data. \[\] when none. | | `meta.warnings[].code` | `string` | Machine-readable warning code (see the warnings list). New codes may be added. | | `meta.warnings[].message` | `string` | What to do about it. | | `meta.warnings[].doc_url` | `string` | Where to read more (the changelog entry, the endpoint's docs). | | `meta.mode` | `string` or `null` | live, or test for a test key's answer: a recorded response (fixture), free and deterministic; null when SocialTap could not tell.
One of `live`, `test` | ## Errors A failed request returns a non-2xx status and an `error` object with a stable `code`: 400 response: ```json { "error": { "code": "invalid_input", "message": "unknown parameter \"link\"; did you mean \"url\"? This endpoint accepts: url, lang, max_age, fields.", "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "status": 400, "retryable": false, "param": "link", "remediation": "Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names.", "doc_url": "https://staging.socialtap.dev/docs/errors/#invalid_input", "details": { "accepted_params": [ "url", "lang", "max_age", "fields" ], "suggestion": "url" }, "charged": false } } ``` Branch on `code`, never on `message`. The [error reference](https://staging.socialtap.dev/docs/errors/index.md) lists every code and whether it is safe to retry. ## Next steps - [Authentication](https://staging.socialtap.dev/docs/authentication/index.md): headers you send and receive. - [Credits and caching](https://staging.socialtap.dev/docs/credits/index.md): how requests are priced, and how to get free cache hits. - [Playground](https://staging.socialtap.dev/playground/): call any endpoint from the browser. - [API reference](https://staging.socialtap.dev/docs/reference/index.md): every operation on one page, with an interactive version. - [Build with AI agents](https://staging.socialtap.dev/docs/agents/index.md): a prompt to paste into your coding agent, an Agent Skill and Markdown docs. ## All endpoints ### [Facebook](https://staging.socialtap.dev/apis/facebook/index.md) - [Facebook Ad Library Search API](https://staging.socialtap.dev/apis/facebook/search_ads/index.md) `GET /v1/facebook/search_ads` - [Facebook Video Transcript API](https://staging.socialtap.dev/apis/facebook/transcript/index.md) `GET /v1/facebook/transcript` ### [Instagram](https://staging.socialtap.dev/apis/instagram/index.md) - [Instagram Post API](https://staging.socialtap.dev/apis/instagram/post/index.md) `GET /v1/instagram/post` - [Instagram Reels Transcript API](https://staging.socialtap.dev/apis/instagram/transcript/index.md) `GET /v1/instagram/transcript` ### [TikTok](https://staging.socialtap.dev/apis/tiktok/index.md) - [TikTok Profile API](https://staging.socialtap.dev/apis/tiktok/profile/index.md) `GET /v1/tiktok/profile` - [TikTok Transcript API](https://staging.socialtap.dev/apis/tiktok/transcript/index.md) `GET /v1/tiktok/transcript` ### [YouTube](https://staging.socialtap.dev/apis/youtube/index.md) - [YouTube Comments API](https://staging.socialtap.dev/apis/youtube/comments/index.md) `GET /v1/youtube/comments` - [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md) `GET /v1/youtube/transcript` --- Source: https://staging.socialtap.dev/docs/authentication/ # Authentication Docs Authenticate every request with your API key in a header. One key works for every endpoint and platform. Send your API key in the `x-api-key` header of every request: Shell: ```shell curl -H "x-api-key: $SOCIALTAP_API_KEY" "https://staging.socialtap.dev/v1/youtube/transcript?..." ``` The key is a secret. Call SocialTap from your server or backend jobs, never from code that ships to browsers or apps, and rotate the key if it leaks. ## Keys, scopes and modes Get a key on the [signup page](https://staging.socialtap.dev/signup/); AI agents can sign up through the API (see [account and API keys](https://staging.socialtap.dev/docs/account/index.md)). Keys starting with `st_live_` are live: real requests, billed. Keys starting with `st_test_` are test keys: every endpoint answers from recorded responses, free (see [test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). Each key has scopes (what it may call) and can have a spend limit, an expiry and an IP allowlist; a key that may not make a request gets [`forbidden`](https://staging.socialtap.dev/docs/errors/index.md#forbidden) (HTTP 403) with the reason. Manage keys in the [dashboard](https://staging.socialtap.dev/dashboard/) or with the API keys routes. ## Request headers | Header | Type | Description | | --- | --- | --- | | `x-api-key` | `string` | Your API key. | | `authorization` | `string` | Bearer \: the MCP server also takes the key this way (what MCP clients send); the REST API reads x-api-key. | | `x-client-request-id` | `string` | Your own ID for the request (1–128 printable ASCII characters): echoed back and logged with ours, so support can find a request whose response never reached you. | | `traceparent` | `string` | W3C Trace Context of your trace: recorded with the request (its trace ID) so a trace can be matched to ours; SocialTap starts its own trace. | | `idempotency-key` | `string` | On any POST: your unique key for the request (1–255 printable ASCII characters; a UUID is ideal), kept 24 hours. Retrying with the same key and body never repeats the work: the answer is the first request's result, in its current state. | | `prefer` | `string` | RFC 7240. respond-async on any endpoint request runs it as a one-request job: the answer is 202 with the job (Location: its URL) instead of the result. | | `cache-control` | `string` | max-age=N accepts a cached response up to N seconds old, like the max_age parameter (max_age wins when both are sent). Other directives are ignored. | ## Response headers | Header | Type | Description | | --- | --- | --- | | `x-request-id` | `string` | Unique request ID; include it when contacting support. | | `x-credits-charged` | `int` | Credits charged for this request (0 for errors and cache hits). | | `x-credits-remaining` | `int` | Account balance after this request; for a busy account an estimate that may not yet subtract concurrent requests (about a second of usage), never below the real balance. | | `retry-after` | `int` | Seconds to wait before retrying; sent only with the codes whose retry_after is set in the error codes list (never on credit errors). | | `allow` | `string` | Methods the endpoint accepts (method_not_allowed only). | | `age` | `int` | Seconds since the data was fetched (cache hits only). | | `x-client-request-id` | `string` | Your x-client-request-id, echoed (also meta.client_request_id). | | `ratelimit-policy` | `string` | The account's runaway-protection ceiling as an IETF RateLimit-Policy field: "account";q=\;w=1 (requests per second). Not a plan limit. | | `ratelimit` | `string` | What is left of the ceiling (IETF RateLimit field): "account";r=\;t=\. | | `deprecation` | `string` | RFC 9745: the endpoint is deprecated, as of this date (@\; may be in the future). Also in meta.warnings. | | `sunset` | `string` | RFC 8594: when a deprecated endpoint stops answering (HTTP-date); from then on it answers endpoint_retired. | | `link` | `string` | RFC 8288 links: rel="next" is the next page of a list (while page.has_more), the same request with cursor set; rel="deprecation" is a deprecated endpoint's notice. | | `location` | `string` | On 202 answers: the job's URL (GET it, free, for its status). | | `preference-applied` | `string` | RFC 7240: respond-async when Prefer: respond-async turned the request into a job (202). | | `idempotent-replayed` | `bool` | true when the answer replays an earlier request with the same Idempotency-Key (it shows the resource's current state). | `x-credits-charged` and `x-credits-remaining` are on every response to an authenticated request, successful or not: errors show a charge of 0 and your unchanged balance. A request rejected for its key has neither, because there is no account to report on. ## Missing or invalid keys A request without a key, or with an unknown or revoked one, fails with [`unauthorized`](https://staging.socialtap.dev/docs/errors/index.md#unauthorized) (HTTP 401), carries no credit headers and costs nothing. ## Rate limits Plans do not limit request rate: you pay for credits (see [Pricing](https://staging.socialtap.dev/pricing/index.md)), and you can send requests as fast and as many in parallel as your balance covers. Each account has a high safety ceiling on requests per second that only stops runaway clients, such as a retry loop without backoff. Above it, requests fail with [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) (HTTP 429) and cost nothing; wait the number of seconds in the `retry-after` header, then retry. If your sustained traffic needs more, ask support and the ceiling is raised for your account. --- Source: https://staging.socialtap.dev/docs/requests/ # Requests Docs How every endpoint takes its input, the parameter names shared by all of them, and how unknown or mistyped parameters are answered. Every endpoint follows the same conventions, so once you have called one you can call them all. ## One identifying parameter Each endpoint takes exactly one parameter that says what to fetch, named for what you have in hand: - **`url`** for an item and an item's lists (a video's transcript, a post's comments): a canonical link, a short or share link, a mobile link or the platform's own ID. - **`handle`** for a profile and a profile's lists: a profile link, `@handle`, a username or the platform's own ID. - **`query`** for searches (endpoints named `search_…`). Each endpoint page lists the kinds of link its parameter takes. A link of another kind, or from another platform, fails with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) at no charge, and the error names the endpoint that takes it: `error.details` says what the link is (`url_kind`, `url_platform`) and which endpoints take it (`endpoints`). The links SocialTap recognizes: | Platform | Kind of link | Example | | --- | --- | --- | | Facebook | video | `https://www.facebook.com/reel/1803909017420449` | | | hashtag | `https://www.facebook.com/hashtag/cooking` | | | post | `https://www.facebook.com/NASA/posts/pfbid02abc` | | | profile | `https://www.facebook.com/NASA` | | Instagram | video | `https://www.instagram.com/reel/De1S3KpCXf0/` | | | post | `https://www.instagram.com/p/C0AbCdEfGhI/` | | | hashtag | `https://www.instagram.com/explore/tags/cooking/` | | | profile | `https://www.instagram.com/natgeo/` | | TikTok | video | `https://www.tiktok.com/@scout2015/video/6718335390845095173` | | | post | `https://www.tiktok.com/@nasa/photo/7310000000000000000` | | | hashtag | `https://www.tiktok.com/tag/cooking` | | | profile | `https://www.tiktok.com/@scout2015` | | YouTube | video | `https://www.youtube.com/watch?v=dQw4w9WgXcQ` | | | video | `https://www.youtube.com/shorts/dQw4w9WgXcQ` | | | video | `https://youtu.be/dQw4w9WgXcQ` | | | playlist | `https://www.youtube.com/playlist?list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI` | | | hashtag | `https://www.youtube.com/hashtag/cooking` | | | profile | `https://www.youtube.com/@RickAstleyYT` | The response says what your input resolved to in meta.input: the kind of item, its ID on the platform and its canonical URL. A short link shows the item it pointed to, so you can check you fetched what you meant. ## Shared parameters Parameters that mean the same thing on every endpoint have one name, one format and one validation. An endpoint offers the ones that apply to it (its page lists them), and never under another name: the last column lists names other APIs use that are answered with a suggestion instead. | Parameter | Type | Values | Meaning | Not | | --- | --- | --- | --- | --- | | `lang` | `string`
bcp47 | — | Preferred language: a BCP-47 tag such as en or pt-BR (case-insensitive; _ is read as -). | `hl`, `lang_code`, `language`, `language_code`, `locale` | | `country` | `string`
iso3166-1-alpha-2 | — | Region: an ISO 3166-1 alpha-2 country code such as US or BR (case-insensitive). | `country_code`, `geo`, `gl`, `market`, `region` | | `sort` | `string` | `relevance`, `newest`, `oldest`, `popular`
each endpoint offers a subset | Order of the results. | `order`, `order_by`, `orderby`, `sort_by`, `sortby` | | `since` | `string`
date-time | — | Only items at or after this time: RFC 3339 (2026-10-01T12:00:00Z) or a date (2026-10-01, midnight UTC). On newest-first lists, paging stops when it is reached, so later pages are not fetched or charged.
Only on lists ordered by time. | `after`, `date_from`, `from`, `from_date`, `min_date`, `start`, `start_date`, `start_time` | | `until` | `string`
date-time | — | Only items before this time (exclusive): RFC 3339 or a date (midnight UTC).
Only on lists ordered by time. | `before`, `date_to`, `end`, `end_date`, `end_time`, `max_date`, `to`, `to_date` | Every endpoint also accepts the common parameters (max_age, fields; see [Credits and caching](https://staging.socialtap.dev/docs/credits/index.md#param-max_age)), and list endpoints limit and cursor (see [Pagination](https://staging.socialtap.dev/docs/pagination/index.md)). ## Unknown parameters are rejected A parameter the endpoint does not accept fails with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input), free, before anything is fetched. A typo can't silently change nothing, and junk parameters can't make a request look new to the cache. The error names the parameter in `error.param`, the one you most likely meant in `details.suggestion` (`username` is answered with `handle`, `lnag` with `lang`), and every accepted parameter in `details.accepted_params`. ## Selecting fields `fields` returns only the fields you name, to cut payloads and tokens: comma-separated paths in the notation of the resource's [schema](https://staging.socialtap.dev/resources/index.md) (`id,text`, `metrics.view_count`, `segments[].text`; a parent path such as `metrics` keeps the whole object). It applies to the item, or to each item of a list page; `page` and `meta` always come back whole. Fields you leave out are absent, so `null` still only ever means unknown. An unknown path fails with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) and lists every valid path in `details.fields`. Selection happens after the cache: it changes neither the price nor whether a request is a cache hit, and requests that differ only in `fields` share one cached result. ## Freshness Responses are fetched fresh unless you opt into the cache with max_age (see [Credits and caching](https://staging.socialtap.dev/docs/credits/index.md#param-max_age)). The standard `cache-control: max-age=N` request header works too; when you send both, max_age wins. Values above the longest a result is kept are reduced to it. ## Request IDs and tracing Every response carries our `x-request-id`; quote it when you contact support. You can also send your own ID for the request in `x-client-request-id` (1 to 128 printable ASCII characters): it is echoed back in the same header and in meta.client_request_id, and logged with ours, so a request whose response never reached you can still be found. A malformed one fails with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) before anything runs. A W3C `traceparent` is accepted too: its trace ID is recorded with the request so your trace can be matched to ours (SocialTap starts its own trace; a malformed `traceparent` is ignored). ## Rate limit headers Plans don't limit request rate, but every account has a high runaway-protection ceiling (see [Rate limits](https://staging.socialtap.dev/docs/authentication/index.md#rate-limits)). Every response states it in the standard IETF fields: `ratelimit-policy` is the ceiling in requests per second (`"account";q=1000;w=1`), and `ratelimit` what is left of it (`"account";r=997;t=1`: remaining requests, and seconds until the full ceiling is available again). Above it, requests fail with [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) and a `retry-after`. ## Warnings meta.warnings lists things to act on that did not fail the request, such as an endpoint that will be retired. It is always present, and empty when there is nothing to say. Each warning has a `code`, a `message` and a `doc_url`: | Warning | Meaning | | --- | --- | | `deprecated_endpoint` | The endpoint is deprecated: it stops answering on its sunset date, after which it returns endpoint_retired. The message names the date and the replacement. | --- Source: https://staging.socialtap.dev/docs/credits/ # Credits and caching Docs How requests are priced, reserved and charged, how to read what you paid, and how to get free cache hits. SocialTap is prepaid: you buy credits, and each successful request spends some of them. One balance covers every endpoint on every platform. - Failed requests are free: errors cost 0 credits. - Cache hits are free: responses served from the cache cost 0 credits. - Credits never expire. - One credit balance for every endpoint and platform. ## How a request is charged 1. **Reserve.** Before calling the platform, SocialTap checks that your balance covers the endpoint's minimum price and sets that minimum aside, nothing more. If the balance is below the minimum, the request fails immediately with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits). 2. **Fetch.** Endpoints try their sources cheapest first and stop at the first one that works. The source that produced the result is reported as `meta.source` (see sources). Right before a source that costs more than what is set aside (a price that grows with the size of the item), SocialTap sets aside more of your balance: the exact price when the size of the item is known in advance, otherwise as much as your balance allows, and it then processes only as much of the item as that pays for. A source your balance can't pay for is skipped. 3. **Charge.** The price of the result is charged and the rest released. If the result would cost more than your balance covers, the request fails with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) instead and costs nothing. A failed request releases everything. What you paid is in `meta.credits_charged` and in the `x-credits-charged` header. `x-credits-remaining` is your account balance right after this request was charged; other requests of yours still in flight may lower it further, so with requests in parallel treat it as an upper bound. Both headers are on every response to an authenticated request, errors included (with a charge of 0); a [`unauthorized`](https://staging.socialtap.dev/docs/errors/index.md#unauthorized) response has neither. ### Reservations A request starts only if your balance covers the endpoint's minimum, and only that minimum is set aside when it starts. More is set aside just before costlier work, and only what that work needs. Requests running in parallel therefore don't each lock up the most they could cost: your balance has to cover their minimums, plus the actual price of whatever costlier work they reach. Prices that grow with the size of the item have a ceiling, because items above a size limit are refused; that ceiling is the most one request can cost. | Endpoint | Balance needed to start | Most one request can cost | | --- | --- | --- | | [Facebook Ad Library Search API](https://staging.socialtap.dev/apis/facebook/search_ads/index.md) | 1 credit | 1 credit | | [Facebook Video Transcript API](https://staging.socialtap.dev/apis/facebook/transcript/index.md) | 1 credit | 122 credits | | [Instagram Post API](https://staging.socialtap.dev/apis/instagram/post/index.md) | 1 credit | 1 credit | | [Instagram Reels Transcript API](https://staging.socialtap.dev/apis/instagram/transcript/index.md) | 1 credit | 122 credits | | [TikTok Profile API](https://staging.socialtap.dev/apis/tiktok/profile/index.md) | 1 credit | 1 credit | | [TikTok Transcript API](https://staging.socialtap.dev/apis/tiktok/transcript/index.md) | 1 credit | 122 credits | | [YouTube Comments API](https://staging.socialtap.dev/apis/youtube/comments/index.md) | 1 credit | 1 credit | | [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md) | 1 credit | 122 credits | ## Prices A price is either flat ("1 credit") or a base plus a per-unit amount, for sources whose cost grows with the size of the item; the unit is part of the price. | Endpoint | Source | Price per successful request | Per 1,000 requests | | --- | --- | --- | --- | | [Facebook Ad Library Search API](https://staging.socialtap.dev/apis/facebook/search_ads/index.md) | Platform API `api` | 1 credit | $0.50–$0.80 | | [Facebook Video Transcript API](https://staging.socialtap.dev/apis/facebook/transcript/index.md) | Captions `captions` | 1 credit | $0.50–$0.80 | | [Facebook Video Transcript API](https://staging.socialtap.dev/apis/facebook/transcript/index.md) | Speech-to-text `asr` | 2 credits + 2 per started minute of media
up to 122 credits (60 started minutes of media) | $1.00–$1.60 + $1.00–$1.60 per started minute of media | | [Instagram Post API](https://staging.socialtap.dev/apis/instagram/post/index.md) | Public page `page` | 1 credit | $0.50–$0.80 | | [Instagram Reels Transcript API](https://staging.socialtap.dev/apis/instagram/transcript/index.md) | Captions `captions` | 1 credit | $0.50–$0.80 | | [Instagram Reels Transcript API](https://staging.socialtap.dev/apis/instagram/transcript/index.md) | Speech-to-text `asr` | 2 credits + 2 per started minute of media
up to 122 credits (60 started minutes of media) | $1.00–$1.60 + $1.00–$1.60 per started minute of media | | [TikTok Profile API](https://staging.socialtap.dev/apis/tiktok/profile/index.md) | Public page `page` | 1 credit | $0.50–$0.80 | | [TikTok Transcript API](https://staging.socialtap.dev/apis/tiktok/transcript/index.md) | Captions `captions` | 1 credit | $0.50–$0.80 | | [TikTok Transcript API](https://staging.socialtap.dev/apis/tiktok/transcript/index.md) | Speech-to-text `asr` | 2 credits + 2 per started minute of media
up to 122 credits (60 started minutes of media) | $1.00–$1.60 + $1.00–$1.60 per started minute of media | | [YouTube Comments API](https://staging.socialtap.dev/apis/youtube/comments/index.md) | Platform API `api` | 1 credit | $0.50–$0.80 | | [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md) | Captions `captions` | 1 credit | $0.50–$0.80 | | [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md) | Speech-to-text `asr` | 2 credits + 2 per started minute of media
up to 122 credits (60 started minutes of media) | $1.00–$1.60 + $1.00–$1.60 per started minute of media | ### Sources | meta.source | Source | What it means | | --- | --- | --- | | `captions` | Captions | The platform's own caption track (uploaded or auto-generated). | | `asr` | Speech-to-text | Transcribed by SocialTap from the media's audio when the platform has no captions. | | `api` | Platform API | The platform's public, logged-out API. | | `page` | Public page | Data embedded in the platform's public web page. | ## Packs | Credits | Price | Per 1,000 credits | | --- | --- | --- | | 25,000 | $20 | $0.80 | | 100,000 | $70 | $0.70 | | 500,000 | $300 | $0.60 | | 2,000,000 | $1,000 | $0.50 | | Operation | Description | | --- | --- | | `GET /status.json`
Live health of every endpoint | Public, no API key. \`status\` is health from the scheduled canaries: an endpoint is degraded after repeated failed runs and healthy again after several passes; unknown until first checked, and again when the canary has not run recently. \`success_rate\` is the share of customer requests in the last \`success_rate_window_minutes\` that got an answer, measured where every request ends (failures on SocialTap's side, including capacity refusals, count against it; requests refused for credits or rate do not count), rounded down; null when there was too little traffic. Cached for 60 seconds. | | `GET /v1/account`
Get the account, balance and calling key | The account's balance and plan, the low-balance alert threshold, and the calling key: its mode, scopes, spend limit (with what it used this interval) and expiry. Free; never cached. A pending agent key may call it (and nothing else) to learn when its owner confirmed. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key sees the fixed test balance. | | `PATCH /v1/account`
Update account settings | Sets low_balance_threshold: credits.low (an event and an email to the owner) fires once each time a request takes the balance from above it to at or below it; credits.depleted fires when the balance reaches 0. Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key's change is validated and answered but not stored: test mode never changes the live account. | | `POST /v1/agent_signup`
Sign up as an agent: a test key now, live on confirm | For AI agents: send your human's email address and get a test key that works at once (recorded responses, free) and a live key that stays pending until the human confirms by email, which grants the free credits. Agents never get spending power without a human. An address that already has an account is refused: its owner creates keys in the dashboard. \*\*Auth:\*\* none. | | `GET /v1/api_keys`
List API keys | The account's keys, newest first, revoked ones included; never their secrets. Free. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key sees and manages test keys only. | | `POST /v1/api_keys`
Create an API key | Creates a key with the given scopes, spend limit, expiry and IP allowlist. The secret is in this answer only: store it now. A key never gets a scope its creator lacks. Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key sees and manages test keys only. Send an \`idempotency-key\` header to retry safely: the same key and body within 1 day never repeat the work. | | `DELETE /v1/api_keys/{id}`
Revoke an API key | Revokes a key for good (a key may revoke itself). It stops working within a minute everywhere; revoking it again changes nothing. Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key sees and manages test keys only. | | `GET /v1/api_keys/{id}`
Get an API key | One key and its spend this interval; never its secret. Free. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key sees and manages test keys only. | | `PATCH /v1/api_keys/{id}`
Update an API key | Changes a key's name, scopes, spend limit, expiry or IP allowlist; absent fields keep their value. Takes effect within a minute everywhere. Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key sees and manages test keys only. | | `POST /v1/billing/checkout`
Buy a credit pack (Stripe Checkout) | Creates a Stripe Checkout session for a credit pack; open the returned url to pay. Credits are added when payment completes. | | `GET /v1/credit_transactions`
List credit transactions (the ledger) | Every change to the balance, newest first, cursor-paged: grants (free credits), purchases, hourly usage roll-ups and adjustments. Per-request detail is in GET /v1/usage. Free. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key gets an empty report: test requests are not metered. | | `GET /v1/events`
List events, newest first | Every event of the account, newest first, whether or not a webhook endpoint received it: what webhooks would have delivered. Kept \`events_retention_s\` (30 days). Free. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key sees test events only (a test job's job.\* events and webhook.test); credits.\* and api_key.\* events are live only. | | `GET /v1/events/{id}`
Get an event | One event, as webhooks deliver it. Free. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key sees test events only (a test job's job.\* events and webhook.test); credits.\* and api_key.\* events are live only. | | `POST /v1/events/{id}/redeliver`
Deliver an event again | Queues a new delivery of the event (same \`webhook-id\`, so receivers deduplicate it) to one endpoint, or to every enabled endpoint subscribed to its type. The answer lists the deliveries queued (one page). Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key sees test events only (a test job's job.\* events and webhook.test); credits.\* and api_key.\* events are live only. Send an \`idempotency-key\` header to retry safely: the same key and body within 1 day never repeat the work. | | `GET /v1/jobs`
List jobs, newest first | The account's jobs, newest first, cursor-paged. Records are kept \`records_retention_s\` (30 days) after a job ends. Free. \*\*Auth:\*\* an API key with the \`jobs\` scope, or the dashboard session. \*\*Test mode:\*\* A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. | | `POST /v1/jobs`
Run many requests (or one long one) as a job | Runs up to \`max_requests\` (1000) endpoint requests in the background: each item is exactly the request its endpoint answers synchronously, at the same price (failed items are free), with the same caches. \`max_pages\` walks a list endpoint server-side with its cursor. The job reserves \`max_credits\` (default: the sum of every item's maximum price) at creation, against the balance and the calling key's spend limit, charges each item as it ends and releases the rest when the job ends. With \`wait\`, the answer is 200 with the first page of results when the job ends in time (a synchronous batch), else 202 with the job; poll \`GET /v1/jobs/{id}\` (free) or subscribe to \`job.completed\`. \*\*Auth:\*\* an API key with the \`jobs\` scope, or the dashboard session. \*\*Test mode:\*\* A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. Send an \`idempotency-key\` header to retry safely: the same key and body within 1 day never repeat the work. | | `GET /v1/jobs/{id}`
Get a job (free to poll) | The job's status, per-state request counts and credits. Free: poll it, or long-poll with \`wait\`. \*\*Auth:\*\* an API key with the \`jobs\` scope, or the dashboard session. \*\*Test mode:\*\* A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. | | `POST /v1/jobs/{id}/cancel`
Cancel a job | Stops the job: items not started become \`canceled\` (free); items already running finish and are charged. The job is \`canceling\` until they do, then \`canceled\`. Canceling an ended job changes nothing. Free. \*\*Auth:\*\* an API key with the \`jobs\` scope, or the dashboard session. \*\*Test mode:\*\* A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. Send an \`idempotency-key\` header to retry safely: the same key and body within 1 day never repeat the work. | | `GET /v1/jobs/{id}/results`
List a job's results as they complete | The job's results in the order they completed, cursor-paged: one per page an item fetched (a single-item endpoint has one page), and one with the item's final status when it ended without a result (canceled, expired). \`result\` is exactly the synchronous response's envelope, \`error\` exactly its error object. Results of a running job can be read as they arrive: keep the last \`page.next_cursor\` and continue from it. Kept \`results_retention_s\` (7 days) after the job ends. Free. \*\*Auth:\*\* an API key with the \`jobs\` scope, or the dashboard session. \*\*Test mode:\*\* A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. | | `GET /v1/jobs/{id}/results.jsonl`
Download every result of a job (JSON Lines) | The job's \`results_url\`: every result as JSON Lines (one JobResult object per line, in completion order), streamed. The URL is signed: it needs no API key and works until \`results_url_expires_at\`; GET the job again for a fresh one. Free. \*\*Auth:\*\* none: the signed URL is the credential. | | `DELETE /v1/session`
Sign out | Ends the session and clears its cookie. \*\*Auth:\*\* the dashboard session cookie. | | `GET /v1/session`
Get the dashboard session | The signed-in account of the session cookie (the dashboard calls it first). \*\*Auth:\*\* the dashboard session cookie. | | `POST /v1/session`
Confirm an emailed link: start a session | Exchanges an emailed link's token (single use) for a dashboard session: an HttpOnly, SameSite=Strict cookie on the API origin. Confirming a sign-up creates the account, grants the free credits and returns its first live and test keys; confirming an agent signup activates the agent's live key and grants the free credits. \*\*Auth:\*\* none. | | `POST /v1/signup`
Email a sign-up or sign-in link | Sends a single-use link to the address; opening it and confirming creates the account (with free credits and live and test keys) or signs in to an existing one. The answer is the same whether or not the address has an account. Used by the signup page; agents use POST /v1/agent_signup. \*\*Auth:\*\* none. | | `GET /v1/usage`
Usage report: requests and credits over time | Requests, successes, failures, cache hits and credits charged per time bucket, optionally grouped by endpoint, key and outcome. Buckets are listed oldest first, cursor-paged. Data is complete to within about 5 minutes; credits reconcile with the ledger. Free. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key gets an empty report: test requests are not metered. | | `GET /v1/webhook_endpoints`
List webhook endpoints | The account's webhook endpoints, newest first; never their secrets. Free. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `POST /v1/webhook_endpoints`
Register a webhook endpoint | Registers an https URL to receive the events it subscribes to, signed per Standard Webhooks. The answer is the only time \`secret\` is shown: store it to verify signatures. At most \`max_endpoints\` (16) per account and mode. Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. Send an \`idempotency-key\` header to retry safely: the same key and body within 1 day never repeat the work. | | `DELETE /v1/webhook_endpoints/{id}`
Delete a webhook endpoint | Deletes the endpoint; pending deliveries to it stop. Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `GET /v1/webhook_endpoints/{id}`
Get a webhook endpoint | One webhook endpoint (never its secret). Free. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `PATCH /v1/webhook_endpoints/{id}`
Update a webhook endpoint | Changes the URL, events, description or status; absent fields keep their value. Setting \`status\` to \`enabled\` re-enables an endpoint SocialTap disabled (see \`disabled_reason\`). Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `GET /v1/webhook_endpoints/{id}/deliveries`
List an endpoint's delivery attempts | Every delivery of an event to this endpoint, newest first: its last attempt's status code, duration and the start of the response body, and when the next attempt is due. Free. \*\*Auth:\*\* an API key with the \`account:read\` scope, or the dashboard session. \*\*Test mode:\*\* A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `POST /v1/webhook_endpoints/{id}/rotate_secret`
Rotate a webhook endpoint's signing secret | Issues a new secret, shown once in the answer. The previous one keeps signing too (a second \`v1,\` signature) for \`previous_secret_expires_in\` seconds, so receivers can switch without dropping a delivery. Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. Send an \`idempotency-key\` header to retry safely: the same key and body within 1 day never repeat the work. | | `POST /v1/webhook_endpoints/{id}/test`
Send a webhook.test event to an endpoint | Emits a \`webhook.test\` event delivered to this endpoint only (signed and retried like any event). The answer is the event; its delivery appears in the endpoint's deliveries. Free. \*\*Auth:\*\* an API key with the \`admin\` scope, or the dashboard session. \*\*Test mode:\*\* A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. Send an \`idempotency-key\` header to retry safely: the same key and body within 1 day never repeat the work. | ## Caching Every endpoint accepts these options: | Parameter | Type | Description | | --- | --- | --- | | `max_age` | `integer`
min 0 | Accept a cached response up to this many seconds old. Cache hits cost 0 credits. Omit to always fetch fresh. The Cache-Control: max-age=N request header is equivalent; max_age wins when both are sent. | | `fields` | `string` | Return only these fields of each item: comma-separated paths in the schema's notation (id,text,metrics.view_count,segments\[\].text; a parent path keeps the whole object). Unknown paths are rejected. Applied after the cache, so it changes neither the price nor cache hits; page and meta are always returned whole. | With `max_age=N`, SocialTap may answer from a stored response that is at most N seconds old. Cached responses have `meta.is_cached` set to `true` and are free. Use it whenever slightly stale data is fine, for example when several of your users ask for the same item. --- Source: https://staging.socialtap.dev/docs/errors/ # Errors Docs Every error code, its HTTP status, whether a later retry can succeed, and what to do about it. Errors return a non-2xx status and a JSON body with one `error` object. Everything a program or an agent needs to react is in the body, so a client that only sees the JSON can still decide what to do: 400 response: ```json { "error": { "code": "invalid_input", "message": "unknown parameter \"link\"; did you mean \"url\"? This endpoint accepts: url, lang, max_age, fields.", "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "status": 400, "retryable": false, "param": "link", "remediation": "Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names.", "doc_url": "https://staging.socialtap.dev/docs/errors/#invalid_input", "details": { "accepted_params": [ "url", "lang", "max_age", "fields" ], "suggestion": "url" }, "charged": false } } ``` `error.code` values are stable; `error.message` wording may change. Every field is always present: | Field | Type | Description | | --- | --- | --- | | `error.code` | `string` | Machine-readable error code (see the error codes list); stable. | | `error.message` | `string` | What went wrong and how to fix it, for humans and agents; wording may change. | | `error.request_id` | `string` | Unique request ID; include it when contacting support. | | `error.status` | `integer` | The HTTP status of the response, repeated for clients that only see the body. | | `error.retryable` | `boolean` | True when repeating the same request later may succeed (after details.retry_after when present). | | `error.param` | `string` or `null` | The query parameter or header the error is about; null when it is not about one input. | | `error.remediation` | `string` | What to do about this code, one sentence. | | `error.doc_url` | `string` | URL of this code's documentation. | | `error.details` | `object` | The code's dynamic values (the error codes list documents each code's keys); {} when none. | | `error.charged` | `boolean` | Whether the request was charged: always false, failed requests are free. | - **`error.param`** names the query parameter or header the error is about, so you can fix the call without parsing the message. - **`error.details`** holds the code's dynamic values: the parameter you most likely meant, the endpoint a link belongs to, the credits a request needed. The table below lists each code's keys. Keys are only ever added. - **`error.charged`** is always false: failed requests are free (see [Credits](https://staging.socialtap.dev/docs/credits/index.md)), so a retry costs nothing. ## Error codes **Retry** says whether sending the same request again later can get a different answer. **What to do** is the recommended reaction. Codes marked **Reserved** are documented ahead of the feature that returns them; nothing returns them yet. | Code | HTTP | Retry | Meaning | What to do | details | | --- | --- | --- | --- | --- | --- | | `invalid_input` | 400 | No | The request parameters are missing or malformed, or the URL is not a supported link for this endpoint. | Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names. | `suggestion` `string`: For an unknown parameter: the accepted parameter it most likely means; absent when nothing is close.`accepted_params` `string[]`: For an unknown parameter: every query parameter this endpoint accepts.`url_kind` `string`: For a link of the wrong kind: what the link points to (profile, video, post, …).`url_platform` `string`: For a link of the wrong kind: the platform the link belongs to.`accepted_url_kinds` `string[]`: For a link of the wrong kind: the kinds of link this endpoint takes.`endpoints` `string[]`: For a link of the wrong kind: IDs of the endpoints that take it; empty when none does yet.`fields` `string[]`: For an unknown fields path: every path fields accepts on this endpoint. | | `invalid_cursor` | 400 | No | The cursor is malformed, expired, belongs to another endpoint, or the other parameters differ from the request that issued it. | Repeat the original request without cursor to start the list again. | `reason` `string`: Why: malformed, expired, wrong_endpoint, params_changed or pin_lost (the upstream exit a cursor-bound list was pinned to is gone; new values may be added). | | `unauthorized` | 401 | No | The x-api-key header is missing, unknown or revoked (or, on the dashboard's routes, the session cookie is missing or expired). | Send a valid API key in the x-api-key header; without one, get a key at details.signup_url (AI agents: POST details.agent_signup_url). | `signup_url` `string`: Where a human gets an API key (self-serve signup).`agent_signup_url` `string`: POST an email address here to get a test key at once and a live key once the human confirms. | | `forbidden` | 403 | No | The API key is valid but may not make this request: its account is suspended, its scopes do not include the route, it is waiting for its owner's email confirmation, it is past its expiry, or the request came from an IP its allowlist does not include. | Do what details.reason says: if suspended, contact support; otherwise use a key whose scopes cover the route (details.required_scope), have the owner confirm the emailed link, create a new key, or call from an allowed IP. | `reason` `string`: Why: suspended (an operator suspended the account), scope (the key lacks details.required_scope), key_pending (an agent signup's live key before its owner confirms), key_expired, ip_not_allowed, test_mode (a test key asked for something live, such as a live key) or csrf (a dashboard request from another site). New values may be added.`required_scope` `string`: For reason scope: the scope the route needs (endpoints, jobs, account:read or admin). | | `insufficient_credits` | 402 | No | The account does not have enough credits for this request. | Buy a credit pack, then repeat the request; do not retry automatically. | `credits_required` `integer`: Credits this request needed at the point it stopped (to start, or to finish a longer item).`credits_available` `integer`: Credits the account could spend at that moment (in-flight requests hold part of the balance).`max_credits` `integer`: For a job's item: the job's max_credits, when that cap (not the balance) stopped the item. | | `spend_limit_reached` | 402 | No | The API key reached its spend limit for the current period; the account may still have credits. | Wait until details.resets_at or raise the key's spend limit (PATCH /v1/api_keys/{id}); do not retry automatically. | `api_key_id` `string`: The key whose limit was reached.`spend_limit` `integer`: The limit: most credits the key may spend per interval.`interval` `string`: day or month (UTC calendar).`used` `integer`: Credits the key spent this interval, plus what its requests in flight hold.`credits_required` `integer`: Credits this request needed.`resets_at` `string`: When the interval ends and the limit is free again, RFC 3339 UTC. | | `rate_limited` | 429 | Yes, later | The account exceeded its runaway-protection ceiling: plans do not limit request rate, but a high per-account safety ceiling stops broken client loops. Support raises it on request. | Wait for the number of seconds in the retry-after header, then retry; ask support to raise the ceiling if your sustained traffic needs more. | `retry_after` `integer`: Seconds to wait before retrying; equals the retry-after header. | | `method_not_allowed` | 405 | No | The endpoint exists but does not accept this HTTP method. | Use the method in the allow header (GET for every data endpoint). | — | | `endpoint_retired` | 410 | No | The endpoint passed its sunset date and no longer answers. | Call the replacement endpoint (details.replacement); the changelog describes the migration. | `sunset_at` `string`: When the endpoint stopped answering, RFC 3339 UTC.`replacement` `string`: ID of the endpoint to use instead; absent when there is none. | | `idempotency_key_reused` | 422 | No | The Idempotency-Key was already used with a different request (another route, or another body). | Send a new Idempotency-Key for a different request; reuse a key only to retry the identical request. | — | | `idempotency_key_in_progress` | 409 | Yes, later | A request with this Idempotency-Key is still running. | Retry after the number of seconds in the retry-after header to get that request's result. | `retry_after` `integer`: Seconds to wait before retrying; equals the retry-after header. | | `job_not_found` | 404 | No | No job with this ID exists for the account: unknown, or past its retention. | Check the job ID; finished jobs are kept for a limited time. | — | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | — | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | — | | `no_transcript` | 404 | No | The media exists but has no transcript: no caption track and no speech to transcribe. | Do not retry; try another language with lang if the platform offers one. | — | | `upstream_blocked` | 503 | Yes, later | The platform refused every attempt. | Retry later with backoff; failed requests are free. | — | | `upstream_error` | 502 | Yes, later | The platform failed or returned something unexpected. | Retry with backoff; failed requests are free. | — | | `overloaded` | 503 | Yes, later | SocialTap was momentarily at capacity and did not start the request: nothing was fetched or charged. | Retry after the number of seconds in the retry-after header; failed requests are free. | `retry_after` `integer`: Seconds to wait before retrying; equals the retry-after header. | | `internal` | 500 | Yes, later | Unexpected SocialTap error. | Retry with backoff; contact support with the request_id if it persists. | — | ## Retrying - **Retry: yes.** Retry with exponential backoff and a cap on attempts. For [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited), wait at least the number of seconds in the `retry-after` header (also in `error.details`) first, then retry. It is not a plan limit but the per-account safety ceiling against runaway clients (see [Rate limits](https://staging.socialtap.dev/docs/authentication/index.md#rate-limits)). - **Retry: no.** The code describes your request or the item, and the same request gets the same answer on every attempt: fix the input, top up your balance, or treat the item as unavailable, as the table says. - `retry-after` is only ever sent with codes you may retry. Credit errors such as [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) never carry it: buy credits, then repeat the request. --- Source: https://staging.socialtap.dev/docs/pagination/ # Pagination Docs How list endpoints return items in pages, how to fetch the next page with a cursor, and how pages are priced. Some endpoints return a list of items, a page at a time. Every list endpoint pages the same way, so you write the loop once. ## The response A successful list response has three keys: `data`, an array of items in the resource's [shared schema](https://staging.socialtap.dev/resources/index.md); `page`, which says whether more items exist and how to get them; and `meta`, as on every response: Shape of a list response: ```json { "data": [ "…" ], "page": { "has_more": true, "next_cursor": "…", "next_cursor_expires_at": "…", "end_reason": "…" }, "meta": { "request_id": "…", "platform": "…", "source": "…", "is_cached": true, "credits_charged": 0, "latency_ms": 0, "fetched_at": "…", "input": "…", "credits_remaining": 0, "client_request_id": "…", "warnings": "…", "mode": "…" } } ``` 200 OK (Facebook Ad Library Search API): ```json { "data": [ { "advertiser": { "avatar_url": "https://scontent-atl3-2.xx.fbcdn.net/v/t39.35426-6/739782028_1533198008291919_1079542453788497101_n.jpg?stp=dst-jpg_s60x60_tt6&_nc_cat=104&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-2.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&…", "display_name": "American Health Support Community", "id": "966175593248497", "is_verified": null, "url": "https://www.facebook.com/61588018263996", "username": null }, "approximate_fields": [ "first_shown_at" ], "audience_size": null, "country_codes": [], "creatives": [ { "body": "My aunt called me a creep for putting a camera in Nana’s living room.\n\nNana called me practical.\n\nThen the camera caught something worse than a fall.\n\nI need to be clear about one thing before anyone decides what kind of grandson I am.\n\nNana asked for the camera.\n\nNot me.\n\nNot my…", "cta_text": "Learn more", "link_description": null, "link_url": "https://shop.pipitea.com/hbt/kd/sp-nm", "media": [ { "alt_text": null, "duration_ms": null, "expires_at": null, "height": null, "thumbnail_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/742001725_2100792434176297_9060645049521596104_n.jpg?stp=dst-jpg_s600x600_tt6&_nc_cat=100&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBE…", "type": "image", "url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/742501415_1706621370582971_4085631001896338504_n.jpg?_nc_cat=103&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&oe=SCRUBBED", "width": null } ], "title": "Natural Relief in Weeks" } ], "first_shown_at": "2026-07-18T07:00:00Z", "id": "2619351891814847", "impressions": null, "is_active": true, "is_political": false, "languages": [], "last_shown_at": null, "paid_by": null, "platform": "facebook", "publisher_platforms": [ "facebook", "instagram", "audience_network" ], "reach": null, "spend": null, "type": "image", "url": "https://www.facebook.com/ads/library/?id=2619351891814847" }, { "advertiser": { "avatar_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/712623693_1599994994434095_1320701223312676781_n.jpg?stp=dst-jpg_s60x60_tt6&_nc_cat=110&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&…", "display_name": "American Health Support Community", "id": "966175593248497", "is_verified": null, "url": "https://www.facebook.com/61588018263996", "username": null }, "approximate_fields": [ "first_shown_at" ], "audience_size": null, "country_codes": [], "creatives": [ { "body": "I almost looked past the woman who saved my husband's life. She was seventy-nine, sitting alone at the booth next to ours at the Cracker Barrel off Highway 31. A glass of milk in front of her. She kept stirring it. She wasn't eating.\n\nIf I'd known, the night I leaned over and ask…", "cta_text": "Learn more", "link_description": "\"I've tried everything to support my kidney function naturally. Fish oil, CoQ10, cutting protein, stressing about every meal. Nothing moved the needle enough to satisfy my doctor. Hibiscus tea is the first thing that actually showed up in my numbers. It's been 3 months and my eGF…", "link_url": "https://shop.pipitea.com/hbt/kd/sp", "media": [ { "alt_text": null, "duration_ms": null, "expires_at": null, "height": null, "thumbnail_url": "https://scontent-atl3-2.xx.fbcdn.net/v/t39.35426-6/711729685_1717035472767079_186136568392095276_n.jpg?stp=dst-jpg_s600x600_tt6&_nc_cat=104&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-2.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED…", "type": "image", "url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/713729791_868879862958547_7409366389733397444_n.jpg?_nc_cat=107&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&oe=SCRUBBED", "width": null } ], "title": "Natural Relief in Weeks" } ], "first_shown_at": "2026-06-01T07:00:00Z", "id": "1923645134954720", "impressions": null, "is_active": true, "is_political": false, "languages": [], "last_shown_at": null, "paid_by": null, "platform": "facebook", "publisher_platforms": [ "facebook", "instagram", "audience_network" ], "reach": null, "spend": null, "type": "image", "url": "https://www.facebook.com/ads/library/?id=1923645134954720" }, { "advertiser": { "avatar_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/712025330_1303417705276292_736671271727941526_n.jpg?stp=dst-jpg_s60x60_tt6&_nc_cat=103&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&o…", "display_name": "American Health Support Community", "id": "966175593248497", "is_verified": null, "url": "https://www.facebook.com/61588018263996", "username": null }, "approximate_fields": [ "first_shown_at" ], "audience_size": null, "country_codes": [], "creatives": [ { "body": "I almost looked past the woman who saved my husband's life. She was seventy-nine, sitting alone at the booth next to ours at the Cracker Barrel off Highway 31. A glass of milk in front of her. She kept stirring it. She wasn't eating.\n\nIf I'd known, the night I leaned over and ask…", "cta_text": "Learn more", "link_description": "\"I've tried everything to support my kidney function naturally. Fish oil, CoQ10, cutting protein, stressing about every meal. Nothing moved the needle enough to satisfy my doctor. Hibiscus tea is the first thing that actually showed up in my numbers. It's been 3 months and my eGF…", "link_url": "https://shop.pipitea.com/hbt/kd/sp", "media": [ { "alt_text": null, "duration_ms": null, "expires_at": null, "height": null, "thumbnail_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/711737708_4485707625086258_2353150069319301753_n.jpg?stp=dst-jpg_s600x600_tt6&_nc_cat=109&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBE…", "type": "image", "url": "https://scontent-atl3-2.xx.fbcdn.net/v/t39.35426-6/710755472_911440771966686_1736050619245168045_n.jpg?_nc_cat=111&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-2.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&oe=SCRUBBED", "width": null } ], "title": "Natural Relief in Weeks" } ], "first_shown_at": "2026-06-01T07:00:00Z", "id": "2140497416496021", "impressions": null, "is_active": true, "is_political": false, "languages": [], "last_shown_at": null, "paid_by": null, "platform": "facebook", "publisher_platforms": [ "facebook", "instagram", "audience_network" ], "reach": null, "spend": null, "type": "image", "url": "https://www.facebook.com/ads/library/?id=2140497416496021" } ], "page": { "has_more": true, "next_cursor": "c1.EXAMPLE_CURSOR", "next_cursor_expires_at": "2026-10-26T12:00:00Z", "end_reason": null }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "facebook", "source": "api", "is_cached": false, "credits_charged": 1, "latency_ms": 0, "fetched_at": "2026-10-25T12:00:00Z", "input": null, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display. | Field | Type | Description | | --- | --- | --- | | `page.has_more` | `boolean` | True when another page exists; then next_cursor is set. An empty page with has_more true is not the end. | | `page.next_cursor` | `string` or `null` | Pass as cursor, with the same other parameters, to get the next page; null at the end. | | `page.next_cursor_expires_at` | `string` (date-time) or `null` | When next_cursor stops working, RFC 3339 UTC; null at the end. Don't store cursors. | | `page.end_reason` | `string` or `null` | Why there are no more pages; null while has_more is true. end: the list ended, or reached since/until. platform_limit: the platform stops serving further pages although the list goes on. max_pages: SocialTap's cap on pages in one walk. New values may be added. A short or empty page is not the end: only has_more false is.
One of `end`, `platform_limit`, `max_pages` | There are no totals: platforms don't report them reliably, and counting would cost extra requests. ## Parameters Every list endpoint accepts these on top of its own parameters and the [common options](https://staging.socialtap.dev/docs/credits/index.md#caching): | Parameter | Type | Description | | --- | --- | --- | | `limit` | `integer`
min 1 | Items per page, 1 to the endpoint's page size (the default and maximum; larger values are reduced). Reduces payload, not price: each page costs the same. | | `cursor` | `string` | page.next_cursor from the previous response. Send the same other parameters; omit to start from the first page. | ## Getting the next page 1. Send the first request without `cursor`. 2. If `page.has_more` is `true`, send **the same request again** with `cursor` set to `page.next_cursor` from the previous response. 3. Stop when `page.has_more` is `false`. Keep every other parameter the same: only `limit` and the common options such as `max_age` may change between pages. **A short or empty page is not the end**: a page can hold fewer items than `limit`, or none, and still have `page.has_more` set to `true`; keep going. Only `page.has_more` `false` ends the list. Every list page with a next page also carries the next request ready to send, in the standard `link` header (`rel="next"`): your request with the cursor filled in. Generic HTTP clients can follow it. ## Why a list ended When `page.has_more` is `false`, `page.end_reason` says why, so fewer items than you expected is never a mystery: - `end`: the list really ended, or it reached the time window you asked for with `since`/`until`. - `platform_limit`: the platform stops serving further pages although the list goes on (many platforms cap how far logged-out visitors can scroll). Repeating the walk later will not get further. - `max_pages`: the walk reached SocialTap's cap on pages per list. ## Time windows List endpoints ordered by time accept `since` (inclusive) and `until` (exclusive), as an RFC 3339 time or a date (see [Requests](https://staging.socialtap.dev/docs/requests/index.md#param-since)). On a newest-first list, paging stops as soon as an item older than `since` appears: the last page has `page.has_more` `false` with `page.end_reason` `end`, and the older pages are neither fetched nor charged. `until` filters out newer items; the pages that hold them are still read, so they are charged. JavaScript: ```javascript const items = []; let cursor = null; do { const params = new URLSearchParams({ ...yourParams, ...(cursor ? { cursor: cursor } : {}) }); const res = await fetch(`https://staging.socialtap.dev/v1//?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY }, }); if (!res.ok) throw new Error((await res.json()).error.code); const body = await res.json(); items.push(...body.data); cursor = body.page.next_cursor; } while (cursor); ``` ## Cursors Cursors are opaque and short-lived: don't parse, build or store them. Each one stops working at `page.next_cursor_expires_at`. A cursor that is malformed, expired, from another endpoint, or sent with different parameters fails with [`invalid_cursor`](https://staging.socialtap.dev/docs/errors/index.md#invalid_cursor) (HTTP 400) and costs nothing; repeat the original request without a cursor to start again. ## Order and page size Each list endpoint states the order of its items and its page size: the default and the largest `limit` it accepts (larger values are reduced). One request reads one page from the platform, so a list of N items takes about N ÷ page size requests when you leave `limit` out. | Endpoint | Order | Page size | Cursor lifetime | Price per page | | --- | --- | --- | --- | --- | | [Facebook Ad Library Search API](https://staging.socialtap.dev/apis/facebook/search_ads/index.md) | most impressions first | up to 10 | 1 hour | 1 credit | | [YouTube Comments API](https://staging.socialtap.dev/apis/youtube/comments/index.md) | newest first | up to 20 | 1 hour | 1 credit | ## Pricing A list endpoint's price is **per page**: every successful page costs the same, however many items it holds and whatever `limit` you send (a smaller limit reduces the payload, not the price). Empty pages are charged like any other successful page. Failed requests, including [`invalid_cursor`](https://staging.socialtap.dev/docs/errors/index.md#invalid_cursor), are free, and so are pages served from the cache. See [Credits and caching](https://staging.socialtap.dev/docs/credits/index.md). ### A smaller limit costs more A `limit` below the page size does not split one charge into parts. The request returns the first `limit` items of a platform page, and the next request, with its cursor, serves the rest of that same platform page: it is fetched again and charged as a page each time. Walking a page in parts therefore costs one page per part, and because the platform page is read again for every part, items can shift between those requests if the platform adds or removes items in the meantime, so an item may repeat or be skipped. **Omitting `limit` is the cheapest way to walk a list:** every request returns a full page, and each platform page is read and charged once. Use a smaller `limit` only when you need fewer items than a page holds and will stop there. ## Errors On top of the errors every endpoint can return, list endpoints have these: | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `invalid_cursor` | 400 | No | The cursor is malformed, expired, belongs to another endpoint, or the other parameters differ from the request that issued it. | Repeat the original request without cursor to start the list again. | A rejected cursor costs nothing; repeat the original request without `cursor` to start again. --- Source: https://staging.socialtap.dev/docs/agents/ # Build with AI agents Docs Everything a coding agent needs to integrate the API correctly on the first try: a prompt to paste, the rules to follow, an Agent Skill and Markdown docs. SocialTap is built to be integrated by AI coding agents such as Claude Code, Cursor or Codex. Every docs page is also Markdown, the whole documentation is one file, and the rules below are generated from the same catalog as the API, so they are always current. ## Paste this into your agent Give your agent this prompt to integrate SocialTap into your project. It sends the agent to the docs first and ends with a real request, so you see it working: Prompt: ``` Integrate the SocialTap API (https://staging.socialtap.dev) into this project. 1. Read https://staging.socialtap.dev/llms.txt first, then the Markdown docs page of the endpoint you need (linked there). Use only endpoints and parameters documented there. 2. Send the API key in the x-api-key header, read from the SOCIALTAP_API_KEY environment variable. Never hard-code it; if it is not set, ask me for it. 3. Branch on error.code: retry only the retryable codes the docs list, with backoff. On list endpoints follow page.next_cursor while page.has_more is true. 4. Verify with one real request before building on it, for example: curl -G 'https://staging.socialtap.dev/v1/youtube/transcript' \ --data-urlencode 'url=dQw4w9WgXcQ' \ -H "x-api-key: $SOCIALTAP_API_KEY" and check that data is not empty. 5. Tell me which endpoint you used, what meta.credits_charged was, and any error code you saw. ``` ## Instructions for AI agents The same rules open [llms.txt](https://staging.socialtap.dev/llms.txt) and the Agent Skill: 1. **Authenticate.** Send the API key in the `x-api-key` header of every request to `https://staging.socialtap.dev`. Read it from an environment variable such as `SOCIALTAP_API_KEY`, never hard-code it, and call the API from server-side code only. Without a key, ask the user for one (keys: [https://staging.socialtap.dev/signup/](https://staging.socialtap.dev/signup/)); never invent one. 2. **No key yet?** `POST https://staging.socialtap.dev/v1/agent_signup` with `{"email": ""}` returns a test key that works at once and a live key that stays pending until your user confirms the emailed link (it spends nothing until then). Ask the user for their email address first, build with the test key, and tell them to confirm. Details: [account and keys](https://staging.socialtap.dev/docs/account/index.md). 3. **Pick an endpoint.** Every endpoint is `GET https://staging.socialtap.dev/v1//` with query parameters; the endpoint list ([all endpoints](https://staging.socialtap.dev/apis/index.md), or [catalog.json](https://staging.socialtap.dev/catalog.json) for machines) gives each one's parameters and price per successful request. Read the endpoint's Markdown docs page before writing code, and don't rely on memory: endpoints and parameters change. Every endpoint of one kind of data (ads, comments, posts, profiles and transcripts) returns the same schema on every platform; `null` means unknown, never 0 or "". 4. **Read the response.** A success is HTTP 200 with `{data, meta}`: `meta.credits_charged` is what the request cost, `meta.credits_remaining` the balance left, `meta.request_id` identifies the request for support. 5. **Paginate.** List endpoints return `{data, page, meta}`. While `page.has_more` is true, repeat the same request with `cursor` set to `page.next_cursor`, keeping every other parameter. An empty page with `page.has_more` true is not the end. Cursors expire (`page.next_cursor_expires_at`): don't store them. Leave `limit` out: a smaller page costs the same. 6. **Handle errors.** Errors are non-2xx responses with `{"error": {code, message, request_id, status, retryable, param, remediation, doc_url, details, charged}}`. Branch on `error.code`, never on `error.message`. Retry only when `error.retryable` is true, with exponential backoff, first waiting the seconds in the `retry-after` header (also `error.details.retry_after`) when it is sent; otherwise do what `error.remediation` says. `insufficient_credits` means the balance is too low: stop and tell the user. Every code is documented at `https://staging.socialtap.dev/docs/errors/#` (`error.doc_url`). Failed requests cost nothing. 7. **Spend credits deliberately.** Each endpoint states its price per successful request (per page for lists); fetch only what the task needs. Add `max_age=` to accept a stored response up to that old: cache hits are free. 8. **Test first.** Keys starting with `st_test_` are test keys: they answer every endpoint from recorded responses, free and deterministic. Build and test the integration with one, then switch to a live key. Magic inputs make an endpoint return any error code it can return ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)), so test your error handling too. 9. **Many items or long lists.** Use background jobs, `POST /v1/jobs`, instead of a loop of single requests: one job runs many requests and walks whole lists. 10. **Notifications.** Instead of polling, register a webhook endpoint with `POST /v1/webhook_endpoints`: deliveries are signed (Standard Webhooks); verify the signature on the raw body before trusting one. 11. **Retry POSTs safely.** Send a new `idempotency-key` (a UUID) with every POST and reuse it when you retry: the work runs once, and a retry answers with the current state. 12. **Balance.** `GET /v1/account` returns the balance, free. 13. **MCP.** Agents with MCP can use the remote server at `https://staging.socialtap.dev/mcp` (Streamable HTTP, the same API key as `authorization: Bearer ` or in the `x-api-key` header), with the tools `get_post`, `get_profile`, `get_transcript`, `list_comments`, `search`, `find_endpoint`, `get_account`, `create_job` and `get_job`; see [the agent guide](https://staging.socialtap.dev/docs/agents/index.md). 14. **Verify.** Before building on an endpoint, run its example request (on its docs page) and check that `data` is not empty. Report the endpoint, `meta.credits_charged` and any `error.code` you saw. ## Which endpoint | Data | Request | Price | Docs | | --- | --- | --- | --- | | Facebook · Ads (beta) | `GET /v1/facebook/search_ads` | 1 credit (api) per page | [docs](https://staging.socialtap.dev/apis/facebook/search_ads/index.md) | | Facebook · Transcript (beta) | `GET /v1/facebook/transcript` | 1 credit (captions); 2 credits + 2 per started minute of media (asr) per request | [docs](https://staging.socialtap.dev/apis/facebook/transcript/index.md) | | Instagram · Post (beta) | `GET /v1/instagram/post` | 1 credit (page) per request | [docs](https://staging.socialtap.dev/apis/instagram/post/index.md) | | Instagram · Transcript (beta) | `GET /v1/instagram/transcript` | 1 credit (captions); 2 credits + 2 per started minute of media (asr) per request | [docs](https://staging.socialtap.dev/apis/instagram/transcript/index.md) | | TikTok · Profile (beta) | `GET /v1/tiktok/profile` | 1 credit (page) per request | [docs](https://staging.socialtap.dev/apis/tiktok/profile/index.md) | | TikTok · Transcript (beta) | `GET /v1/tiktok/transcript` | 1 credit (captions); 2 credits + 2 per started minute of media (asr) per request | [docs](https://staging.socialtap.dev/apis/tiktok/transcript/index.md) | | YouTube · Comments (beta) | `GET /v1/youtube/comments` | 1 credit (api) per page | [docs](https://staging.socialtap.dev/apis/youtube/comments/index.md) | | YouTube · Transcript (beta) | `GET /v1/youtube/transcript` | 1 credit (captions); 2 credits + 2 per started minute of media (asr) per request | [docs](https://staging.socialtap.dev/apis/youtube/transcript/index.md) | ## Docs for agents - [llms.txt](https://staging.socialtap.dev/llms.txt): the instructions above and a link to every page, as Markdown. - [llms-full.txt](https://staging.socialtap.dev/llms-full.txt): every guide and endpoint reference in one Markdown file. - Every page has a Markdown twin: add `index.md` to its URL, or request the page with `Accept: text/markdown`. Each page also has "Copy page as Markdown" and "Open in Claude / ChatGPT" buttons. - The [API reference](https://staging.socialtap.dev/docs/reference/index.md): every operation on one page, readable without JavaScript. - [openapi.json](https://staging.socialtap.dev/openapi.json): OpenAPI 3.1 with examples, the exact errors of each operation and `x-socialtap-*` extensions for pricing, pagination and retries. ## Agent Skill The skill is published at [`https://staging.socialtap.dev/.well-known/agent-skills/socialtap/SKILL.md`](https://staging.socialtap.dev/.well-known/agent-skills/socialtap/SKILL.md) and listed in the [Agent Skills discovery index](https://staging.socialtap.dev/.well-known/agent-skills/index.json). It follows the open [Agent Skills](https://agentskills.io/specification) format, so any agent that loads skills can use it. For Claude Code, add it to a project: Shell: ```shell mkdir -p .claude/skills/socialtap && curl -fsSL https://staging.socialtap.dev/.well-known/agent-skills/socialtap/SKILL.md -o .claude/skills/socialtap/SKILL.md ``` **Show the generated SKILL.md** SKILL.md: ````markdown --- name: socialtap description: "Fetch public social media data (ads, comments, posts, profiles and transcripts) from Facebook, Instagram, TikTok and YouTube with the SocialTap API: one API key, one JSON schema per kind of data, pay per successful request. Use when the user asks to get, scrape, download or analyze ads, comments, posts, profiles and transcripts from Facebook, Instagram, TikTok and YouTube, or to build code that does." metadata: homepage: "https://staging.socialtap.dev" docs: "https://staging.socialtap.dev/llms.txt" --- # SocialTap SocialTap returns public social media data (transcripts today; profiles, posts and comments next) from every major platform through one API key, one credit balance and one response schema per resource. ## How to use the API 1. **Authenticate.** Send the API key in the `x-api-key` header of every request to `https://staging.socialtap.dev`. Read it from an environment variable such as `SOCIALTAP_API_KEY`, never hard-code it, and call the API from server-side code only. Without a key, ask the user for one (keys: https://staging.socialtap.dev/signup/); never invent one. 2. **No key yet?** `POST https://staging.socialtap.dev/v1/agent_signup` with `{"email": ""}` returns a test key that works at once and a live key that stays pending until your user confirms the emailed link (it spends nothing until then). Ask the user for their email address first, build with the test key, and tell them to confirm. Details: [account and keys](https://staging.socialtap.dev/docs/account/index.md). 3. **Pick an endpoint.** Every endpoint is `GET https://staging.socialtap.dev/v1//` with query parameters; the endpoint list ([all endpoints](https://staging.socialtap.dev/apis/index.md), or [catalog.json](https://staging.socialtap.dev/catalog.json) for machines) gives each one's parameters and price per successful request. Read the endpoint's Markdown docs page before writing code, and don't rely on memory: endpoints and parameters change. Every endpoint of one kind of data (ads, comments, posts, profiles and transcripts) returns the same schema on every platform; `null` means unknown, never 0 or "". 4. **Read the response.** A success is HTTP 200 with `{data, meta}`: `meta.credits_charged` is what the request cost, `meta.credits_remaining` the balance left, `meta.request_id` identifies the request for support. 5. **Paginate.** List endpoints return `{data, page, meta}`. While `page.has_more` is true, repeat the same request with `cursor` set to `page.next_cursor`, keeping every other parameter. An empty page with `page.has_more` true is not the end. Cursors expire (`page.next_cursor_expires_at`): don't store them. Leave `limit` out: a smaller page costs the same. 6. **Handle errors.** Errors are non-2xx responses with `{"error": {code, message, request_id, status, retryable, param, remediation, doc_url, details, charged}}`. Branch on `error.code`, never on `error.message`. Retry only when `error.retryable` is true, with exponential backoff, first waiting the seconds in the `retry-after` header (also `error.details.retry_after`) when it is sent; otherwise do what `error.remediation` says. `insufficient_credits` means the balance is too low: stop and tell the user. Every code is documented at `https://staging.socialtap.dev/docs/errors/#` (`error.doc_url`). Failed requests cost nothing. 7. **Spend credits deliberately.** Each endpoint states its price per successful request (per page for lists); fetch only what the task needs. Add `max_age=` to accept a stored response up to that old: cache hits are free. 8. **Test first.** Keys starting with `st_test_` are test keys: they answer every endpoint from recorded responses, free and deterministic. Build and test the integration with one, then switch to a live key. Magic inputs make an endpoint return any error code it can return ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)), so test your error handling too. 9. **Many items or long lists.** Use background jobs, `POST /v1/jobs`, instead of a loop of single requests: one job runs many requests and walks whole lists. 10. **Notifications.** Instead of polling, register a webhook endpoint with `POST /v1/webhook_endpoints`: deliveries are signed (Standard Webhooks); verify the signature on the raw body before trusting one. 11. **Retry POSTs safely.** Send a new `idempotency-key` (a UUID) with every POST and reuse it when you retry: the work runs once, and a retry answers with the current state. 12. **Balance.** `GET /v1/account` returns the balance, free. 13. **MCP.** Agents with MCP can use the remote server at `https://staging.socialtap.dev/mcp` (Streamable HTTP, the same API key as `authorization: Bearer ` or in the `x-api-key` header), with the tools `get_post`, `get_profile`, `get_transcript`, `list_comments`, `search`, `find_endpoint`, `get_account`, `create_job` and `get_job`; see [the agent guide](https://staging.socialtap.dev/docs/agents/index.md). 14. **Verify.** Before building on an endpoint, run its example request (on its docs page) and check that `data` is not empty. Report the endpoint, `meta.credits_charged` and any `error.code` you saw. ## Endpoints | Data | Request | Price | Docs | | --- | --- | --- | --- | | Facebook · Ads (beta) | `GET /v1/facebook/search_ads` | 1 credit (api) per page | [docs](https://staging.socialtap.dev/apis/facebook/search_ads/index.md) | | Facebook · Transcript (beta) | `GET /v1/facebook/transcript` | 1 credit (captions); 2 credits + 2 per started minute of media (asr) per request | [docs](https://staging.socialtap.dev/apis/facebook/transcript/index.md) | | Instagram · Post (beta) | `GET /v1/instagram/post` | 1 credit (page) per request | [docs](https://staging.socialtap.dev/apis/instagram/post/index.md) | | Instagram · Transcript (beta) | `GET /v1/instagram/transcript` | 1 credit (captions); 2 credits + 2 per started minute of media (asr) per request | [docs](https://staging.socialtap.dev/apis/instagram/transcript/index.md) | | TikTok · Profile (beta) | `GET /v1/tiktok/profile` | 1 credit (page) per request | [docs](https://staging.socialtap.dev/apis/tiktok/profile/index.md) | | TikTok · Transcript (beta) | `GET /v1/tiktok/transcript` | 1 credit (captions); 2 credits + 2 per started minute of media (asr) per request | [docs](https://staging.socialtap.dev/apis/tiktok/transcript/index.md) | | YouTube · Comments (beta) | `GET /v1/youtube/comments` | 1 credit (api) per page | [docs](https://staging.socialtap.dev/apis/youtube/comments/index.md) | | YouTube · Transcript (beta) | `GET /v1/youtube/transcript` | 1 credit (captions); 2 credits + 2 per started minute of media (asr) per request | [docs](https://staging.socialtap.dev/apis/youtube/transcript/index.md) | ## Example ```shell curl -G 'https://staging.socialtap.dev/v1/youtube/transcript' \ --data-urlencode 'url=dQw4w9WgXcQ' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` ## More - Docs index for agents: https://staging.socialtap.dev/llms.txt; everything in one file: https://staging.socialtap.dev/llms-full.txt - API reference: https://staging.socialtap.dev/docs/reference/index.md; OpenAPI: https://staging.socialtap.dev/openapi.json - Error codes: https://staging.socialtap.dev/docs/errors/index.md ```` ## MCP server The remote MCP server at `https://staging.socialtap.dev/mcp` (Streamable HTTP) takes your API key as `authorization: Bearer ` or in the `x-api-key` header, and each tool call costs what the same API request costs. Set `SOCIALTAP_API_KEY` in your environment, then add the server: Claude Code: ```shell claude mcp add --transport http socialtap https://staging.socialtap.dev/mcp --header "authorization: Bearer $SOCIALTAP_API_KEY" ``` Claude Code (.mcp.json): ```json { "mcpServers": { "socialtap": { "type": "http", "url": "https://staging.socialtap.dev/mcp", "headers": { "authorization": "Bearer ${SOCIALTAP_API_KEY}" } } } } ``` Cursor (mcp.json): ```json { "mcpServers": { "socialtap": { "url": "https://staging.socialtap.dev/mcp", "headers": { "authorization": "Bearer ${env:SOCIALTAP_API_KEY}" } } } } ``` Codex: ```shell codex mcp add socialtap --url https://staging.socialtap.dev/mcp --bearer-token-env-var SOCIALTAP_API_KEY ``` Codex (config.toml): ```toml [mcp_servers.socialtap] url = "https://staging.socialtap.dev/mcp" bearer_token_env_var = "SOCIALTAP_API_KEY" ``` Its tools cover every endpoint, grouped by resource: pass `platform` and the link, handle or query. Results are concise JSON (`detail: "full"` returns every field), state `credits_charged` and `credits_remaining`, and are cut at about 20,000 tokens with a `next_cursor` to continue. Listing the tools and `find_endpoint` work without a key. | Tool | What it does | | --- | --- | | `get_post` | Get a public post on Instagram. | | `get_profile` | Get a public profile on TikTok. | | `get_transcript` | Get the transcript of a public video or post on Facebook, Instagram, TikTok or YouTube. | | `list_comments` | List the comments of a public post on YouTube, one page per call. | | `search` | Search public ads on Facebook by query. | | `find_endpoint` | Find which SocialTap tool and endpoint serves a need ("TikTok comments", "YouTube transcript") or a link: returns the best matches with the tool and arguments to call, the price, the parameters and an example result. | | `get_account` | Get the account, balance and calling key. | | `create_job` | Run many requests (or one long one) as a job. | | `get_job` | Get a job (free to poll). | --- Source: https://staging.socialtap.dev/docs/account/ # Account, API keys and signup Docs Sign up, manage API keys (scopes, spend limits, expiry, IP allowlists, live and test modes), read usage and the credit ledger, and get balance alerts. Everything about your account is an API route, free, and also available in the [dashboard](https://staging.socialtap.dev/dashboard/). ## Sign up **People:** enter your email address on the [signup page](https://staging.socialtap.dev/signup/). We email a single-use link; confirming it creates your account with **100 free credits** (once per account, no card) and two keys: a live key and a test key, shown once. The same link signs in to an existing account. **AI agents:** an agent without a key signs up with its human's email address: Shell: ```shell curl -X POST https://staging.socialtap.dev/v1/agent_signup \ -H 'content-type: application/json' \ -d '{"email": "you@example.com", "name": "my-agent"}' ``` The answer has two keys. The **test key** works at once: every endpoint answers from recorded responses, free (see [test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). The **live key** is *pending*: it spends nothing and can only call `GET /v1/account` until the human opens the confirmation email and confirms, which activates it and grants the free credits. Agents never get spending power without a human. Poll `GET /v1/account` with the live key: `api_key.status` turns from `pending` to `active`. An address that already has an account is refused: its owner creates a key for the agent in the dashboard. Signup is throttled per IP address and per email address, and disposable email domains are refused. ## Routes | Route | What it does | Auth | | --- | --- | --- | | `GET /v1/account`
Get the account, balance and calling key | The account's balance and plan, the low-balance alert threshold, and the calling key: its mode, scopes, spend limit (with what it used this interval) and expiry. Free; never cached. A pending agent key may call it (and nothing else) to learn when its owner confirmed. | key with account:read, or the session | | `PATCH /v1/account`
Update account settings | Sets low_balance_threshold: credits.low (an event and an email to the owner) fires once each time a request takes the balance from above it to at or below it; credits.depleted fires when the balance reaches 0. Free. | key with admin, or the session | | `POST /v1/agent_signup`
Sign up as an agent: a test key now, live on confirm | For AI agents: send your human's email address and get a test key that works at once (recorded responses, free) and a live key that stays pending until the human confirms by email, which grants the free credits. Agents never get spending power without a human. An address that already has an account is refused: its owner creates keys in the dashboard. | none | | `POST /v1/api_keys`
Create an API key | Creates a key with the given scopes, spend limit, expiry and IP allowlist. The secret is in this answer only: store it now. A key never gets a scope its creator lacks. Free. | key with admin, or the session | | `GET /v1/api_keys/{id}`
Get an API key | One key and its spend this interval; never its secret. Free. | key with account:read, or the session | | `GET /v1/api_keys`
List API keys | The account's keys, newest first, revoked ones included; never their secrets. Free. | key with account:read, or the session | | `DELETE /v1/api_keys/{id}`
Revoke an API key | Revokes a key for good (a key may revoke itself). It stops working within a minute everywhere; revoking it again changes nothing. Free. | key with admin, or the session | | `PATCH /v1/api_keys/{id}`
Update an API key | Changes a key's name, scopes, spend limit, expiry or IP allowlist; absent fields keep their value. Takes effect within a minute everywhere. Free. | key with admin, or the session | | `GET /v1/credit_transactions`
List credit transactions (the ledger) | Every change to the balance, newest first, cursor-paged: grants (free credits), purchases, hourly usage roll-ups and adjustments. Per-request detail is in GET /v1/usage. Free. | key with account:read, or the session | | `POST /v1/session`
Confirm an emailed link: start a session | Exchanges an emailed link's token (single use) for a dashboard session: an HttpOnly, SameSite=Strict cookie on the API origin. Confirming a sign-up creates the account, grants the free credits and returns its first live and test keys; confirming an agent signup activates the agent's live key and grants the free credits. | none | | `DELETE /v1/session`
Sign out | Ends the session and clears its cookie. | dashboard session | | `GET /v1/session`
Get the dashboard session | The signed-in account of the session cookie (the dashboard calls it first). | dashboard session | | `POST /v1/signup`
Email a sign-up or sign-in link | Sends a single-use link to the address; opening it and confirming creates the account (with free credits and live and test keys) or signs in to an existing one. The answer is the same whether or not the address has an account. Used by the signup page; agents use POST /v1/agent_signup. | none | | `GET /v1/usage`
Usage report: requests and credits over time | Requests, successes, failures, cache hits and credits charged per time bucket, optionally grouped by endpoint, key and outcome. Buckets are listed oldest first, cursor-paged. Data is complete to within about 5 minutes; credits reconcile with the ledger. Free. | key with account:read, or the session | ## API keys Send the key in the `x-api-key` header. A key has: - a **mode**: live keys start with `st_live_` and make real, billed requests; test keys start with `st_test_` and answer from recorded responses, free. The mode is stored with the key; the prefix is only a hint for people. - **scopes**, what it may call. A new key gets every scope but `admin` unless you choose, and a key never gets a scope its creator lacks. Without the scope a route needs, the answer is [`forbidden`](https://staging.socialtap.dev/docs/errors/index.md#forbidden) with `details.required_scope`. - an optional **spend limit**: at most that many credits per UTC day or month. Past it, requests fail with [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) (HTTP 402, never retried automatically) until `details.resets_at`, even when the account still has credits. Concurrent requests can never take a key past its limit. - an optional **expiry** and **IP allowlist** (addresses or CIDR ranges, IPv4 or IPv6). An expired key, or a call from another address, is refused with [`forbidden`](https://staging.socialtap.dev/docs/errors/index.md#forbidden) (`details.reason` says which). The secret is shown once, when the key is created; the API only ever shows its prefix afterwards. Changes and revocations reach every server within a minute. | Scope | Routes that need it | | --- | --- | | `endpoints` | the routes of its own feature (see above) | | `jobs` | `POST /v1/jobs/{id}/cancel`, `POST /v1/jobs`, `GET /v1/jobs/{id}`, `GET /v1/jobs`, `GET /v1/jobs/{id}/results` | | `account:read` | `GET /v1/account`, `GET /v1/api_keys/{id}`, `GET /v1/api_keys`, `GET /v1/credit_transactions`, `GET /v1/events/{id}`, `GET /v1/events`, `DELETE /v1/session`, `GET /v1/session`, `GET /v1/usage`, `GET /v1/webhook_endpoints/{id}/deliveries`, `GET /v1/webhook_endpoints/{id}`, `GET /v1/webhook_endpoints` | | `admin` | `PATCH /v1/account`, `POST /v1/api_keys`, `DELETE /v1/api_keys/{id}`, `PATCH /v1/api_keys/{id}`, `POST /v1/events/{id}/redeliver`, `POST /v1/webhook_endpoints`, `DELETE /v1/webhook_endpoints/{id}`, `POST /v1/webhook_endpoints/{id}/rotate_secret`, `POST /v1/webhook_endpoints/{id}/test`, `PATCH /v1/webhook_endpoints/{id}` | ## Usage and the ledger `GET /v1/usage` reports requests, successes, failures, cache hits and credits per hour or day, grouped by endpoint, key or outcome. `GET /v1/credit_transactions` lists every change to the balance: free credits, purchases, hourly usage and adjustments, with the balance after each. ## Balance alerts Set `low_balance_threshold` with `PATCH /v1/account`. When a request takes the balance from above it to at or below it, the account gets a `credits.low` event and an email to its owner, once per crossing. `credits.depleted` fires when the balance reaches 0, and `api_key.spend_limit_reached` the first time in an interval that a key is refused for its spend limit. ## The dashboard session Confirming an emailed link starts a dashboard session: a random secret in a `Secure`, `HttpOnly`, `SameSite=Strict` cookie with the `__Host-` prefix on this origin; sign in again when it ends. Only its hash is stored. The session can read and manage the account and keys, never call data endpoints. Every change it makes must come from this origin (the API checks the request's `Origin`), so another site can neither read nor drive it. Links are single use and expire after minutes; the confirm page asks you to press a button, so mail scanners that open links can't use them up. --- Source: https://staging.socialtap.dev/docs/jobs/ # Jobs and batches Docs Run many requests, or one long list walk, in the background: same prices and caches as single requests, with results you can stream, download or receive by webhook. A job runs up to 1,000 endpoint requests for you in the background. Use one when a task needs many items ("these 300 videos"), a whole list ("every comment"), or a request that can take a while, such as a long transcript. Each item is exactly the request its endpoint answers synchronously: same parameters, same price, same caches, and its result is the same envelope, so the code that reads a single response reads a job's results too. Failed items cost nothing. | Route | What it does | Key scope | | --- | --- | --- | | `POST /v1/jobs` | Run many requests (or one long one) as a job | `jobs` | | `GET /v1/jobs` | List jobs, newest first | `jobs` | | `GET /v1/jobs/{id}` | Get a job (free to poll) | `jobs` | | `GET /v1/jobs/{id}/results` | List a job's results as they complete | `jobs` | | `GET /v1/jobs/{id}/results.jsonl` | Download every result of a job (JSON Lines) | none (signed URL) | | `POST /v1/jobs/{id}/cancel` | Cancel a job | `jobs` | ## Create a job Send the requests with `POST /v1/jobs`. Each one names an endpoint by ID (the `x-socialtap-endpoint` of its operation in the [API reference](https://staging.socialtap.dev/docs/reference/index.md)) and its parameters, as you would send them to the endpoint, plus your own `custom_id`: Shell: ```shell curl -X POST 'https://staging.socialtap.dev/v1/jobs?wait=60' \ -H "x-api-key: $SOCIALTAP_API_KEY" \ -H "idempotency-key: $(uuidgen)" \ -H 'content-type: application/json' \ --data '{ "requests": [ { "custom_id": "row-1", "endpoint": "youtube.transcript", "params": { "url": "dQw4w9WgXcQ" } } ] }' ``` TypeScript: ```typescript const res = await fetch("https://staging.socialtap.dev/v1/jobs?wait=60", { method: "POST", headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "", "idempotency-key": crypto.randomUUID(), "content-type": "application/json" }, body: JSON.stringify({ "requests": [ { "custom_id": "row-1", "endpoint": "youtube.transcript", "params": { "url": "dQw4w9WgXcQ" } } ] }), }); const body = await res.json(); // 200: body.results holds the first page of results; 202: still running, poll body.data.id. console.log(res.status, body.data.status, body.data.request_counts); ``` | Field | Type | Description | | --- | --- | --- | | `requests` | `object[]` | The requests to run, 1 to max_requests; custom_id must be unique within the job. | | `requests[].custom_id` | `string` | Your ID for the item (letters, digits, _ and -), echoed on its results. | | `requests[].endpoint` | `string` | The endpoint ID (youtube.transcript). | | `requests[].params` | `object` | The endpoint's query params, as you would send them to it (strings, numbers or booleans). | | `requests[].max_pages?` | `integer` or `null` | List endpoints only: walk up to this many pages with the cursor (default 1). Each page is a result and costs a page's price. | | `max_age?` | `integer` or `null` | Accept cached results up to this many seconds old for every item (free cache hits), like the endpoints' max_age. | | `max_credits?` | `integer` or `null` | Most the job may spend; reserved at creation. Default: the sum of every item's maximum price (times its max_pages). Items that would go over it are canceled. | | `webhook_url?` | `string` or `null` | Also deliver this job's job.\* events to this URL: the URL of one of the account's webhook endpoints (its secret signs them). | | `metadata?` | `object` | Up to metadata_max_keys string pairs of your own, returned with the job and in its events. | Every item is checked before anything runs: an unknown endpoint or parameter, a missing required parameter, a duplicate `custom_id` or a list option on a single-item endpoint fails the whole request with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input), and its `param` names the field, such as `requests[3].params.url`. Nothing is charged. **Synchronous batches.** With `wait` (up to 1 minute), the request waits for the job to end. If it ends in time, the answer is `200` with the job and the first page of its results in `results` (continue from `results_page.next_cursor`). Otherwise it is `202` with the job as it is, and its URL in the `location` header. **Any endpoint, in the background.** A single endpoint request with the header `Prefer: respond-async` becomes a one-request job: the answer is `202` with the job instead of the result. Its one item's `custom_id` is `request`. ## Walk whole lists On a list endpoint, `max_pages` (up to 1,000 per item) walks the list for you with its cursor: every page is one result with its `page_index`, priced as one page. The walk stops at the end of the list, at `max_pages`, or when the job's credits run out. A job holds at most 10,000 pages in total. ## The job Poll `GET /v1/jobs/{id}`: it is free, and `wait` long-polls until the job ends. Or subscribe a [webhook](https://staging.socialtap.dev/docs/webhooks/index.md) to `job.completed`. | Field | Type | Description | | --- | --- | --- | | `object` | `string` | Always job.
One of `job` | | `id` | `string` | The job ID (job_…). | | `status` | `string` | queued: nothing started yet. running. canceling: canceled, running items finishing. completed: every item ended (check request_counts: there is no job-level failure). canceled. expired: the deadline passed; unfinished items expired.
One of `queued`, `running`, `canceling`, `completed`, `canceled`, `expired` | | `request_counts` | `object` | | | `request_counts.total` | `integer` | | | `request_counts.queued` | `integer` | Not started. | | `request_counts.running` | `integer` | Started (fetching, waiting for a retry or walking pages). | | `request_counts.succeeded` | `integer` | | | `request_counts.failed` | `integer` | | | `request_counts.canceled` | `integer` | | | `request_counts.expired` | `integer` | | | `credits` | `object` | | | `credits.max` | `integer` | max_credits: the most the job may spend. | | `credits.reserved` | `integer` | Credits still held for items not ended (0 once the job ends). | | `credits.charged` | `integer` | Credits charged so far (items' prices; failed items are free). | | `max_age` | `integer` or `null` | The job's max_age; null when it accepts no cached results. | | `metadata` | `object` | Your metadata; {} when none. | | `webhook_url` | `string` or `null` | The job's own webhook URL; null when none. | | `created_at` | `string` (date-time) | | | `started_at` | `string` (date-time) or `null` | When the first item started; null before. | | `finished_at` | `string` (date-time) or `null` | When the job ended; null while it runs. | | `cancel_requested_at` | `string` (date-time) or `null` | | | `expires_at` | `string` (date-time) | The deadline: items not ended by then expire. | | `results_url` | `string` or `null` | Every result as JSON Lines (one JobResult per line), signed: works without the API key until results_url_expires_at. Null until the job ends and after results_expire_at. | | `results_url_expires_at` | `string` (date-time) or `null` | | | `results_expire_at` | `string` (date-time) or `null` | When results are deleted; null while the job runs. | A job is `queued` until its first item starts, `running` while items run, and `completed` once every item has ended: there is no failed job, items fail one by one, so check `request_counts`. A canceled job is `canceling` until its running items finish, then `canceled`. A job that reaches `expires_at` (its deadline, 1 day after creation) ends `expired`: items that had not started expire, running ones finish. ## Results `GET /v1/jobs/{id}/results` lists results in the order they completed, cursor-paged, while the job runs and after. Keep the last `page.next_cursor` to read only what arrived since. Each result is one page of one item: | Field | Type | Description | | --- | --- | --- | | `object` | `string` | Always job_result.
One of `job_result` | | `custom_id` | `string` | | | `endpoint` | `string` | | | `page_index` | `integer` | 0 for a single-item endpoint and a list's first page; n for the list's page n+1. | | `status` | `string` | The page's outcome; an item's status is its last result's. canceled and expired results carry no result (error says why when it is not the job's cancel or deadline).
One of `succeeded`, `failed`, `canceled`, `expired` | | `status_code` | `integer` or `null` | The HTTP status the synchronous request answered; null when it did not run. | | `result` | `object` or `null` | Exactly the synchronous success envelope; null unless succeeded. | | `result.data` | `any` | The endpoint's data: its resource, or a list page's items. | | `result.page?` | `object` or `null` | List endpoints only. | | `result.page.has_more` | `boolean` | True when another page exists; then next_cursor is set. An empty page with has_more true is not the end. | | `result.page.next_cursor` | `string` or `null` | Pass as cursor, with the same other parameters, to get the next page; null at the end. | | `result.page.next_cursor_expires_at` | `string` (date-time) or `null` | When next_cursor stops working, RFC 3339 UTC; null at the end. Don't store cursors. | | `result.page.end_reason` | `string` or `null` | Why there are no more pages; null while has_more is true. end: the list ended, or reached since/until. platform_limit: the platform stops serving further pages although the list goes on. max_pages: SocialTap's cap on pages in one walk. New values may be added. A short or empty page is not the end: only has_more false is.
One of `end`, `platform_limit`, `max_pages` | | `result.meta` | `object` | | | `result.meta.request_id` | `string` | Unique request ID; include it when contacting support. | | `result.meta.platform` | `string` | Platform ID of the endpoint (youtube, tiktok, …). | | `result.meta.source` | `string` | Which upstream strategy produced the data. | | `result.meta.is_cached` | `boolean` | True when served from the cache (free); see max_age. | | `result.meta.credits_charged` | `integer` | Credits charged for this request (0 for errors and cache hits). | | `result.meta.latency_ms` | `integer` | Time SocialTap spent on the request, in milliseconds. | | `result.meta.fetched_at` | `string` (date-time) | When the data was fetched from the platform, RFC 3339 UTC (older than the request for cache hits). | | `result.meta.input` | `object` or `null` | What the request's url or handle resolved to: kind, platform ID and canonical URL, so you can confirm what was fetched; null for searches. | | `result.meta.credits_remaining` | `integer` or `null` | Account balance after this request (the x-credits-remaining header); for a busy account an estimate that may not yet subtract concurrent requests (about a second of usage), never below the real balance; null when SocialTap could not read it. | | `result.meta.client_request_id` | `string` or `null` | Your x-client-request-id, echoed; null when you sent none. | | `result.meta.warnings` | `object[]` | Things to act on that did not fail the request: a deprecated endpoint, partial data. \[\] when none. | | `result.meta.mode` | `string` or `null` | live, or test for a test key's answer: a recorded response (fixture), free and deterministic; null when SocialTap could not tell.
One of `live`, `test` | | `error` | `object` or `null` | Exactly the synchronous error object; null on success and for the job's own cancel or deadline. | | `error.code` | `string` | Machine-readable error code (see the error codes list); stable. | | `error.message` | `string` | What went wrong and how to fix it, for humans and agents; wording may change. | | `error.request_id` | `string` | Unique request ID; include it when contacting support. | | `error.status` | `integer` | The HTTP status of the response, repeated for clients that only see the body. | | `error.retryable` | `boolean` | True when repeating the same request later may succeed (after details.retry_after when present). | | `error.param` | `string` or `null` | The query parameter or header the error is about; null when it is not about one input. | | `error.remediation` | `string` | What to do about this code, one sentence. | | `error.doc_url` | `string` | URL of this code's documentation. | | `error.details` | `object` | The code's dynamic values (the error codes list documents each code's keys); {} when none. | | `error.charged` | `boolean` | Whether the request was charged: always false, failed requests are free. | | `completed_at` | `string` (date-time) | | - `result` is exactly the synchronous response's envelope (`data`, `meta`, and `page` on list endpoints). - `error` is exactly the synchronous error object: branch on `error.code` as you would for a single request. - An item that ended without a fetched page (canceled, expired) has one result with that status and no `result`. - `?status=failed` lists the failures, to resubmit them in a new job. Once the job ends, `results_url` is a signed link to every result as JSON Lines (one result per line), valid 1 hour and usable without an API key: hand it to a download tool or another service. Get the job again for a fresh link. Results are kept 7 days after the job ends and job records 30 days; after that the job answers [`job_not_found`](https://staging.socialtap.dev/docs/errors/index.md#job_not_found). ## Credits 1. **Reserve.** Creating a job sets `max_credits` aside: by default the most its items can cost (each item's maximum price, times its `max_pages`). Lower it to cap what the job may spend. If your balance can't cover it, the request fails with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) and nothing runs; if the key has a spend limit, `max_credits` must also fit in what the limit leaves this interval, else [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached). 2. **Charge.** Each page is charged its own price as it ends, exactly like a single request: cache hits and failures are free. A page that the remaining budget can't pay for does not run: its item is `canceled` with an [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) error whose `details.max_credits` says the job's cap stopped it, or [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) when the key's spend limit did. Pages count against the key that created the job, exactly as its own requests do. 3. **Release.** When the job ends, whatever is still set aside goes back to your balance. `credits.charged` is what the job cost. ## Keys and test mode Creating a job needs a key with the `jobs` scope, and `endpoints` too: a job's items are endpoint requests. A test key's jobs run every item in [test mode](https://staging.socialtap.dev/docs/test-mode/index.md): each result is exactly the recorded answer the endpoint gives that test key, free, and nothing is set aside or charged. Test jobs are separate from live ones: a test key lists and reads its own, a live key never sees them. ## Cancel `POST /v1/jobs/{id}/cancel` stops a job: items that haven't started are `canceled` (free), items already running finish and are charged. Canceling a job that has ended changes nothing. ## Limits and fairness A job runs up to 10 items at a time, and all of an account's jobs together up to 25, so a large job never crowds out your own synchronous requests. When SocialTap is busy, a job's items wait for capacity instead of failing; an item whose platform fails is retried up to 3 times before it fails. The unfinished jobs of an account hold at most 50,000 pages; above that, creating a job answers [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited): wait for running jobs to end. ## Retry safely Send an `idempotency-key` with every create: if the connection drops, repeat the request with the same key and body and you get the job the first request created, never a second one. See [idempotency](https://staging.socialtap.dev/docs/idempotency/index.md). --- Source: https://staging.socialtap.dev/docs/test-mode/ # Test mode Docs Test keys answer every endpoint from recorded responses, free and deterministic, and magic inputs trigger every error code, so you can build and test an integration without spending credits. A key that starts with `st_test_` is a **test key**. It works like a live key, with three differences: 1. **Recorded answers.** Every endpoint answers from its recorded responses (the same recordings the docs examples come from), never from the platform. Responses have the live schema; `meta.mode` is `test`. 2. **Free and deterministic.** Nothing is charged: `meta.credits_charged` is 0, and the balance a test key sees is always 10,000 credits. The same request always gets the same answer. 3. **No platform traffic.** Test requests are not metered in usage reports. Get one with the [signup page](https://staging.socialtap.dev/signup/) (every new account has one) or, as an AI agent, from the agent signup in [account and API keys](https://staging.socialtap.dev/docs/account/index.md). ## Which answer a request gets - A **recorded input**: each endpoint's page lists the inputs it has recordings for. Sending one returns that recording, success or error. - A **magic input** (below): returns that error code. - **Anything else**: returns the endpoint's docs example. Unknown parameters, `fields` and deprecation work as in live mode. Lists are paged over the recorded items: cursors start with `t1.`, and any other cursor is [`invalid_cursor`](https://staging.socialtap.dev/docs/errors/index.md#invalid_cursor). ## Magic inputs Send one of these values as the endpoint's identifying parameter to get the error code, with the status, body, `details` and (where the code has one) the `retry-after` header that live mode sends. Each endpoint accepts the magic inputs of the codes it can return; its page lists them. | Error code | Status | `handle` | `query` | `url` | | --- | --- | --- | --- | --- | | [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | 400 | `socialtap_test_invalid_input` | `socialtap_test_invalid_input` | `https://socialtap.dev/test/invalid_input` | | [`invalid_cursor`](https://staging.socialtap.dev/docs/errors/index.md#invalid_cursor) | 400 | `socialtap_test_invalid_cursor` | `socialtap_test_invalid_cursor` | `https://socialtap.dev/test/invalid_cursor` | | [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) | 402 | `socialtap_test_insufficient_credits` | `socialtap_test_insufficient_credits` | `https://socialtap.dev/test/insufficient_credits` | | [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) | 402 | `socialtap_test_spend_limit_reached` | `socialtap_test_spend_limit_reached` | `https://socialtap.dev/test/spend_limit_reached` | | [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) | 429 | `socialtap_test_rate_limited` | `socialtap_test_rate_limited` | `https://socialtap.dev/test/rate_limited` | | [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | 404 | `socialtap_test_not_found` | `socialtap_test_not_found` | `https://socialtap.dev/test/not_found` | | [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | 403 | `socialtap_test_private_or_unavailable` | `socialtap_test_private_or_unavailable` | `https://socialtap.dev/test/private_or_unavailable` | | [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | 404 | `socialtap_test_no_transcript` | `socialtap_test_no_transcript` | `https://socialtap.dev/test/no_transcript` | | [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | 503 | `socialtap_test_upstream_blocked` | `socialtap_test_upstream_blocked` | `https://socialtap.dev/test/upstream_blocked` | | [`upstream_error`](https://staging.socialtap.dev/docs/errors/index.md#upstream_error) | 502 | `socialtap_test_upstream_error` | `socialtap_test_upstream_error` | `https://socialtap.dev/test/upstream_error` | | [`overloaded`](https://staging.socialtap.dev/docs/errors/index.md#overloaded) | 503 | `socialtap_test_overloaded` | `socialtap_test_overloaded` | `https://socialtap.dev/test/overloaded` | | [`internal`](https://staging.socialtap.dev/docs/errors/index.md#internal) | 500 | `socialtap_test_internal` | `socialtap_test_internal` | `https://socialtap.dev/test/internal` | Errors about the key itself ([`unauthorized`](https://staging.socialtap.dev/docs/errors/index.md#unauthorized), [`forbidden`](https://staging.socialtap.dev/docs/errors/index.md#forbidden)) are reached the same way as with a live key: send no key, or a key without the scope. ## Account routes in test mode | Route | In test mode | | --- | --- | | `GET /v1/account` | A test key sees the fixed test balance. | | `PATCH /v1/account` | A test key's change is validated and answered but not stored: test mode never changes the live account. | | `POST /v1/api_keys` | A test key sees and manages test keys only. | | `GET /v1/api_keys/{id}` | A test key sees and manages test keys only. | | `GET /v1/api_keys` | A test key sees and manages test keys only. | | `DELETE /v1/api_keys/{id}` | A test key sees and manages test keys only. | | `PATCH /v1/api_keys/{id}` | A test key sees and manages test keys only. | | `POST /v1/billing/checkout` | A test key gets a placeholder session (no Stripe call, nothing to pay). | | `GET /v1/credit_transactions` | A test key gets an empty report: test requests are not metered. | | `GET /v1/events/{id}` | A test key sees test events only (a test job's job.\* events and webhook.test); credits.\* and api_key.\* events are live only. | | `GET /v1/events` | A test key sees test events only (a test job's job.\* events and webhook.test); credits.\* and api_key.\* events are live only. | | `POST /v1/events/{id}/redeliver` | A test key sees test events only (a test job's job.\* events and webhook.test); credits.\* and api_key.\* events are live only. | | `POST /v1/jobs/{id}/cancel` | A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. | | `POST /v1/jobs` | A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. | | `GET /v1/jobs/{id}` | A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. | | `GET /v1/jobs` | A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. | | `GET /v1/jobs/{id}/results` | A test key's job runs every item in test mode: each gets its endpoint's test-mode answer (recorded responses, magic inputs), free; nothing is held or charged. A test key sees test jobs only. | | `GET /v1/usage` | A test key gets an empty report: test requests are not metered. | | `POST /v1/webhook_endpoints` | A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `DELETE /v1/webhook_endpoints/{id}` | A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `GET /v1/webhook_endpoints/{id}/deliveries` | A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `GET /v1/webhook_endpoints/{id}` | A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `GET /v1/webhook_endpoints` | A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `POST /v1/webhook_endpoints/{id}/rotate_secret` | A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `POST /v1/webhook_endpoints/{id}/test` | A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | | `PATCH /v1/webhook_endpoints/{id}` | A test key manages the account's test webhook endpoints, separate from live ones; they receive test events only (a test job's job.\* events and webhook.test), signed and retried like live ones. | --- Source: https://staging.socialtap.dev/docs/webhooks/ # Webhooks Docs Receive signed notifications when jobs end or credits run low: Standard Webhooks signatures, retries for days, delivery logs and secret rotation. Instead of polling, register an HTTPS endpoint and SocialTap sends it an [event](https://staging.socialtap.dev/docs/events/index.md) when something happens: a job ends, your credits run low. Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com/) exactly, so its verification libraries work too. | Route | What it does | Key scope | | --- | --- | --- | | `POST /v1/webhook_endpoints` | Register a webhook endpoint | `admin` | | `GET /v1/webhook_endpoints` | List webhook endpoints | `account:read` | | `GET /v1/webhook_endpoints/{id}` | Get a webhook endpoint | `account:read` | | `PATCH /v1/webhook_endpoints/{id}` | Update a webhook endpoint | `admin` | | `DELETE /v1/webhook_endpoints/{id}` | Delete a webhook endpoint | `admin` | | `POST /v1/webhook_endpoints/{id}/rotate_secret` | Rotate a webhook endpoint's signing secret | `admin` | | `POST /v1/webhook_endpoints/{id}/test` | Send a webhook.test event to an endpoint | `admin` | | `GET /v1/webhook_endpoints/{id}/deliveries` | List an endpoint's delivery attempts | `account:read` | ## Register an endpoint `POST /v1/webhook_endpoints` takes the `url` and the `events` it receives: event types, a family such as `job.*`, or `*` for all. The answer is the only time you see its signing `secret` (`whsec_…`): store it with your other secrets. An account has at most 16 endpoints. | Field | Type | Description | | --- | --- | --- | | `object` | `string` | Always webhook_endpoint.
One of `webhook_endpoint` | | `id` | `string` | we_… | | `url` | `string` | https URL deliveries are POSTed to. | | `events` | `string[]` | Event types it receives: types, prefix wildcards (job.\*) or \* for all. | | `description` | `string` or `null` | | | `status` | `string` | One of `enabled`, `disabled` | | `disabled_reason` | `string` or `null` | Why it is disabled: gone (it answered 410), failing (deliveries failed for disable_after_failing_s), user. Null while enabled.
One of `gone`, `failing`, `user` | | `secret?` | `string` or `null` | The signing secret (whsec_…): only in the create and rotate_secret answers. Store it. | | `previous_secret_expires_at` | `string` (date-time) or `null` | Until then the previous secret signs too; null when none. | | `created_at` | `string` (date-time) | | | `updated_at` | `string` (date-time) | | The URL must use `https`, on port 443 or a port above 1023, without credentials, and its host must resolve only to public addresses: private, loopback, link-local and cloud metadata addresses are refused when you register it and checked again before every delivery. Redirects are not followed: a `3xx` answer is a failed delivery, so register the final URL. A job can also notify one of these endpoints that its subscriptions don't cover: set the job's `webhook_url` to the endpoint's URL. ## What a delivery looks like Each delivery is a `POST` of the event as JSON, with these headers: | Header | Type | Description | | --- | --- | --- | | `webhook-id` | `string` | The event ID (evt_…), the same on every retry: deduplicate deliveries on it. | | `webhook-timestamp` | `int` | When this attempt was signed, in Unix seconds: reject deliveries outside your tolerance (replays). | | `webhook-signature` | `string` | Space-separated v1,SIGNATURE entries, each the base64 HMAC-SHA256 of webhook-id, webhook-timestamp and the raw body joined by dots, with one active secret (two during a rotation): accept the delivery when one matches. | The body is small, a summary and links, never a job's results: fetch those from the links when you need them. ## Verify every delivery Compute the HMAC-SHA256 of `webhook-id.webhook-timestamp.` with the secret's bytes (the base64 after `whsec_`), and accept the delivery only when one of the `v1,` entries of `webhook-signature` matches, compared in constant time, and the timestamp is within 5 minutes of your clock. Use the raw body as it arrived: parsing and re-serializing JSON changes the bytes. TypeScript: ```typescript import { createHmac, timingSafeEqual } from "node:crypto"; /** The verified event, or null: pass the RAW body, before any JSON parsing. */ export function verify(rawBody: string, headers: Record, secret = process.env.SOCIALTAP_WEBHOOK_SECRET!): unknown { const id = headers["webhook-id"], ts = headers["webhook-timestamp"], sigs = headers["webhook-signature"]; if (!id || !ts || !sigs || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return null; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const want = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest(); const ok = sigs.split(" ").some((s) => { const [version, b64] = s.split(",", 2); const got = Buffer.from(b64 ?? "", "base64"); return version === "v1" && got.length === want.length && timingSafeEqual(got, want); }); return ok ? JSON.parse(rawBody) : null; // deduplicate on its id } ``` Python: ```python import base64, hashlib, hmac, json, os, time def verify(raw_body: bytes, headers: dict, secret: str = os.environ["SOCIALTAP_WEBHOOK_SECRET"]): """The verified event, or None: pass the RAW body, before any JSON parsing.""" msg_id, ts, sigs = headers.get("webhook-id"), headers.get("webhook-timestamp"), headers.get("webhook-signature") if not msg_id or not ts or not sigs or abs(time.time() - int(ts)) > 300: return None key = base64.b64decode(secret.removeprefix("whsec_")) want = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()).decode() for part in sigs.split(" "): version, _, got = part.partition(",") if version == "v1" and hmac.compare_digest(got, want): return json.loads(raw_body) # deduplicate on its id return None ``` Go: ```go // Verify returns the raw event when one signature matches: pass the RAW body. func Verify(body []byte, h http.Header, secret string) ([]byte, bool) { id, ts, sigs := h.Get("webhook-id"), h.Get("webhook-timestamp"), h.Get("webhook-signature") t, err := strconv.ParseInt(ts, 10, 64) if id == "" || sigs == "" || err != nil || math.Abs(float64(time.Now().Unix()-t)) > 300 { return nil, false } key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_")) if err != nil { return nil, false } mac := hmac.New(sha256.New, key) mac.Write([]byte(id + "." + ts + ".")) mac.Write(body) want := base64.StdEncoding.EncodeToString(mac.Sum(nil)) for _, part := range strings.Split(sigs, " ") { if version, got, ok := strings.Cut(part, ","); ok && version == "v1" && hmac.Equal([]byte(got), []byte(want)) { return body, true // deduplicate on the event's id } } return nil, false } ``` The SDKs ship the same check (`verifyWebhook` in TypeScript, `verify_webhook` in Python, `VerifyWebhook` in Go), and any Standard Webhooks library verifies these deliveries. Answer with any `2xx` quickly (within 15 seconds) and do the work afterwards. Deliveries can repeat and arrive out of order: deduplicate on the event ID (`webhook-id`), the same on every retry. ## Retries A delivery that doesn't get a `2xx` within 15 seconds is retried on this schedule, each delay varied by a little so retries don't arrive in lockstep: | Attempt | After the previous one | Since the event | | --- | --- | --- | | 1 | immediately | 0 | | 2 | 5 seconds | about 5 seconds | | 3 | 5 minutes | about 305 seconds | | 4 | 30 minutes | about 2105 seconds | | 5 | 2 hours | about 155 minutes | | 6 | 5 hours | about 455 minutes | | 7 | 10 hours | about 1055 minutes | | 8 | 14 hours | about 32 hours | | 9 | 20 hours | about 52 hours | | 10 | 1 day | about 76 hours | On `429`, `502` and `504`, a longer `retry-after` from your server is respected. A `410 Gone` answer disables the endpoint at once; an endpoint whose deliveries keep failing for 5 days is disabled too. A disabled endpoint's `disabled_reason` says why; set its `status` back to `enabled` with `PATCH /v1/webhook_endpoints/{id}` once it works. ## Delivery logs and redelivery `GET /v1/webhook_endpoints/{id}/deliveries` lists every delivery to an endpoint with its last status code, duration, the start of your server's answer and when the next attempt is due: | Field | Type | Description | | --- | --- | --- | | `object` | `string` | Always webhook_delivery.
One of `webhook_delivery` | | `id` | `string` | whd_… | | `event_id` | `string` | | | `event_type` | `string` | | | `webhook_endpoint_id` | `string` | | | `status` | `string` | pending: an attempt is due (next_attempt_at). failed: every attempt failed, or the endpoint was disabled or deleted.
One of `pending`, `succeeded`, `failed` | | `attempts` | `integer` | Attempts made so far. | | `status_code` | `integer` or `null` | The last attempt's HTTP status; null when it got none (timeout, refused, blocked URL). | | `duration_ms` | `integer` or `null` | The last attempt's duration. | | `response_excerpt` | `string` or `null` | The start of the last response body. | | `error` | `string` or `null` | Why the last attempt failed when there was no response. | | `created_at` | `string` (date-time) | | | `delivered_at` | `string` (date-time) or `null` | | | `next_attempt_at` | `string` (date-time) or `null` | | `POST /v1/events/{id}/redeliver` sends an event again (with the same `webhook-id`), to one endpoint or to every endpoint subscribed to it. `POST /v1/webhook_endpoints/{id}/test` sends a `webhook.test` event to one endpoint, to check your handler end to end. ## Rotate a secret `POST /v1/webhook_endpoints/{id}/rotate_secret` issues a new secret, shown once. For `previous_secret_expires_in` seconds (default and maximum 1 day) every delivery carries two signatures, one per secret, so you can deploy the new secret without dropping a delivery. Verify against both during the switch. ## Keys and test mode Reading webhook endpoints and their deliveries needs the `account:read` scope; creating, changing, deleting, rotating, testing and redelivering need `admin`. A test key manages a separate set of test endpoints: they receive test events only (a test job's `job.*` events and test deliveries), signed and retried like live ones, and live endpoints never receive them. --- Source: https://staging.socialtap.dev/docs/events/ # Events Docs Every event type, its data, and how to list and replay events: what webhooks deliver, kept for a month. An event records something that happened in your account. Every event is kept for 30 days and listed by `GET /v1/events`, whether or not a webhook endpoint received it, and each one is delivered to the [webhook endpoints](https://staging.socialtap.dev/docs/webhooks/index.md) subscribed to its type. | Route | What it does | Key scope | | --- | --- | --- | | `GET /v1/events` | List events, newest first | `account:read` | | `GET /v1/events/{id}` | Get an event | `account:read` | | `POST /v1/events/{id}/redeliver` | Deliver an event again | `admin` | ## The event object The body of a webhook delivery is the event object, exactly as `GET /v1/events/{id}` returns it: | Field | Type | Description | | --- | --- | --- | | `object` | `string` | Always event.
One of `event` | | `id` | `string` | The event ID (evt_…): the webhook-id header of every delivery, the same on retries. Deduplicate on it. | | `type` | `string` | The event type (event types table); new types may be added. | | `timestamp` | `string` (date-time) | When it happened. | | `data` | `any` | The type's data object (small: a summary plus links). | ## Event types | Type | When | data | | --- | --- | --- | | `job.completed` | A job ended with every item ended (check request_counts: items fail individually). | `JobEventData` | | `job.canceled` | A canceled job ended: its running items finished. | `JobEventData` | | `job.expired` | A job reached its deadline; its unfinished items expired. | `JobEventData` | | `credits.low` | A balance change took the balance from above the account's low_balance_threshold to at or below it (once per crossing; the owner is emailed too). | `CreditsLowData` | | `credits.depleted` | The balance reached 0 (once, until credits are added). | `CreditsDepletedData` | | `api_key.spend_limit_reached` | An API key's request (or job) was refused for its spend limit, the first time in the limit's interval. | `SpendLimitReachedData` | | `webhook.test` | Sent by POST /v1/webhook_endpoints/{id}/test to that endpoint only. | `WebhookTestData` | New types may be added; ignore the ones you don't handle. Subscribe an endpoint to a family with a wildcard (`job.*`) or to everything with `*`, and filter `GET /v1/events` the same way with its `type` parameter. ## Job events `job.completed`, `job.canceled` and `job.expired` carry the job's summary and links, never its results: | Field | Type | Description | | --- | --- | --- | | `object` | `string` | One of `job` | | `id` | `string` | | | `status` | `string` | One of `queued`, `running`, `canceling`, `completed`, `canceled`, `expired` | | `request_counts` | `object` | | | `request_counts.total` | `integer` | | | `request_counts.queued` | `integer` | Not started. | | `request_counts.running` | `integer` | Started (fetching, waiting for a retry or walking pages). | | `request_counts.succeeded` | `integer` | | | `request_counts.failed` | `integer` | | | `request_counts.canceled` | `integer` | | | `request_counts.expired` | `integer` | | | `credits` | `object` | | | `credits.max` | `integer` | max_credits: the most the job may spend. | | `credits.reserved` | `integer` | Credits still held for items not ended (0 once the job ends). | | `credits.charged` | `integer` | Credits charged so far (items' prices; failed items are free). | | `metadata` | `object` | | | `created_at` | `string` (date-time) | | | `finished_at` | `string` (date-time) or `null` | | | `url` | `string` | GET this (with your API key) for the job. | | `results_url` | `string` | GET this (with your API key) for its results. | `url` is the job and `results_url` its results, both read with your API key. ## Account events The balance and spend events come from the account itself (live keys only: test requests never touch the balance), and `webhook.test` from a test delivery. Their data: ### `credits.low` | Field | Type | Description | | --- | --- | --- | | `balance` | `integer` | The balance after the change that crossed the threshold. | | `low_balance_threshold` | `integer` | The account's low_balance_threshold at the time. | ### `credits.depleted` | Field | Type | Description | | --- | --- | --- | | `balance` | `integer` | The balance: 0. | ### `api_key.spend_limit_reached` | Field | Type | Description | | --- | --- | --- | | `api_key_id` | `string` | The key (key_…) whose spend limit was reached. | | `spend_limit` | `object` | Its spend limit. | | `spend_limit.amount` | `integer` | Most credits the key may spend per interval. | | `spend_limit.interval` | `string` | Calendar day or month, UTC.
One of `day`, `month` | | `used` | `integer` | Credits the key had used this interval (spent plus held by its requests in flight). | | `resets_at` | `string` (date-time) | Start of the next interval, RFC 3339 UTC: the key can spend again then. | ### `webhook.test` | Field | Type | Description | | --- | --- | --- | | `webhook_endpoint_id` | `string` | | --- Source: https://staging.socialtap.dev/docs/idempotency/ # Idempotency Docs Retry any POST safely with an Idempotency-Key: the same key and body never repeat the work, and a replay shows the current state. A request whose answer never arrives may or may not have run. For reads that is harmless: retry it, and set `max_age` to the seconds since the first attempt so the retry is served free from the cache. For requests that create something, such as a job, send an `idempotency-key` header and retry with the same key: the work is done once, whatever the number of attempts. ## How it works - Every `POST` accepts the header. Use a new random value for each new request (a UUID is ideal), up to 255 printable ASCII characters, and reuse it only to retry that same request. - Keys belong to your account and to one route, and are kept 1 day. - The first request with a key runs normally. Once it has succeeded, a retry with the same key and the same body doesn't run again: it answers with the first request's status and the resource's **current** state (a job created then is shown as it is now), with the header `idempotent-replayed: true`. Routes that show a secret once (a new webhook endpoint, a rotated secret) answer exactly what they answered the first time. - The body is compared after parsing, so key order and whitespace don't matter. | Situation | Answer | | --- | --- | | The same key with another body, or on another route | [`idempotency_key_reused`](https://staging.socialtap.dev/docs/errors/index.md#idempotency_key_reused) (422): send a new key | | The same key while its first request is still running | [`idempotency_key_in_progress`](https://staging.socialtap.dev/docs/errors/index.md#idempotency_key_in_progress) (409): retry after the `retry-after` seconds | | A key whose first request was refused or failed | Nothing is kept: retry with the same key once the request is fixed | The SDKs send a key with every `POST` and reuse it for their automatic retries. --- Source: https://staging.socialtap.dev/apis/facebook/search_ads/ # Facebook Ad Library Search API Facebook · Ad Beta Searches Meta's public Ad Library by keyword and returns the matching ads that ran on Facebook, Instagram, Messenger, Threads and the Audience Network: the advertiser's page, the days the ad first and last ran, whether it is still active, where it ran, and each creative's headline, text, link, call to action, images and videos. Political and social-issue ads also carry the disclosed payer and Meta's impressions, reach and spend ranges. Filter by country, active status and the day an ad first ran; results come in the Ad Library's own order, most impressions first. GET `https://staging.socialtap.dev/v1/facebook/search_ads` [Try it in the playground](https://staging.socialtap.dev/playground/?endpoint=facebook.search_ads) · [Quickstart](https://staging.socialtap.dev/docs/index.md) - From **1 credit** per page - Pages of up to **10** ads, most impressions first - Sources: Platform API ## Use cases - Monitor competitors' ad creatives and offers - Research ad copy in a market or niche - Track political and issue ad spending - Find the advertisers running ads on a topic ## Good to know - Payer, impressions, reach and spend are disclosed only for political and social-issue ads; they are null for other ads. - Dates are whole days (Meta reports the day an ad first and last ran, US Pacific time), so they are listed in approximate_fields. - since and until filter by the day an ad first ran (first_shown_at); a filtered page can hold fewer ads than the page size, and that is not the end of the list. - Ads that share one creative are grouped by Meta; each ad of a group is returned as its own item. - Languages, targeting and EU reach by country are not part of the search results; those fields are empty or null. ## Example request Send a `GET` request with your key in the `x-api-key` header. cURL: ```shell curl -G 'https://staging.socialtap.dev/v1/facebook/search_ads' \ --data-urlencode 'country=US' \ --data-urlencode 'query=coffee' \ --data-urlencode 'status=all' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` TypeScript: ```typescript const params = new URLSearchParams({ country: "US", query: "coffee", status: "all", }); const res = await fetch(`https://staging.socialtap.dev/v1/facebook/search_ads?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const { data, page, meta } = body; console.log(data, meta.credits_charged); ``` Python: ```python import os import requests res = requests.get( "https://staging.socialtap.dev/v1/facebook/search_ads", params={"country": "US", "query": "coffee", "status": "all"}, headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]}, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") data, page, meta = body["data"], body["page"], body["meta"] print(data, meta["credits_charged"]) ``` Go: ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" ) func main() { q := url.Values{} q.Set("country", "US") q.Set("query", "coffee") q.Set("status", "all") req, err := http.NewRequest("GET", "https://staging.socialtap.dev/v1/facebook/search_ads?"+q.Encode(), nil) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { Data json.RawMessage `json:"data"` Page json.RawMessage `json:"page"` Meta map[string]any `json:"meta"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if body.Error != nil { log.Fatalf("%s: %s", body.Error.Code, body.Error.Message) } fmt.Println(string(body.Data), body.Meta["credits_charged"]) } ``` ## Parameters | Name | Type | Description | Example | | --- | --- | --- | --- | | `query` required | `string`
max 200 chars | Keywords matched against the ad's text, in any order. Wrap the query in double quotes ("cold brew") to match an exact phrase. | `coffee` | | `country` optional | `string`
iso3166-1-alpha-2 | Region: an ISO 3166-1 alpha-2 country code such as US or BR (case-insensitive). Only ads delivered in this country. All countries when omitted. | `US` | | `status` optional | `string` | Only ads that are still running (active), only ads that stopped (inactive), or both (all, the default).
One of `all`, `active`, `inactive` | `active` | | `since` optional | `string`
date-time | Only items at or after this time: RFC 3339 (2026-10-01T12:00:00Z) or a date (2026-10-01, midnight UTC). On newest-first lists, paging stops when it is reached, so later pages are not fetched or charged. Filters by first_shown_at, the day the ad first ran. | `2026-10-01` | | `until` optional | `string`
date-time | Only items before this time (exclusive): RFC 3339 or a date (midnight UTC). Filters by first_shown_at, the day the ad first ran. | `2026-10-08` | | `limit` optional | `integer`
min 1
max 10 | Items per page, 1 to the endpoint's page size (the default and maximum; larger values are reduced). Reduces payload, not price: each page costs the same. | `10` | | `cursor` optional | `string` | page.next_cursor from the previous response. Send the same other parameters; omit to start from the first page. | — | Parameters not listed here are rejected (with a suggestion for a likely typo), so a misspelled one never silently changes nothing: see [Requests](https://staging.socialtap.dev/docs/requests/index.md). `limit` and `cursor` page through the list: see Pagination. Every endpoint also accepts [`max_age`](https://staging.socialtap.dev/docs/credits/index.md#param-max_age), [`fields`](https://staging.socialtap.dev/docs/credits/index.md#param-fields). ## Example response A `200` response is `{data, page, meta}`: `data` is an array of up to 10 [ad](https://staging.socialtap.dev/resources/ad/index.md) objects, `page` says whether there are more (pagination), `meta` describes the request ([meta fields](https://staging.socialtap.dev/docs/index.md#response)). 200 OK: ```json { "data": [ { "advertiser": { "avatar_url": "https://scontent-atl3-2.xx.fbcdn.net/v/t39.35426-6/739782028_1533198008291919_1079542453788497101_n.jpg?stp=dst-jpg_s60x60_tt6&_nc_cat=104&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-2.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&…", "display_name": "American Health Support Community", "id": "966175593248497", "is_verified": null, "url": "https://www.facebook.com/61588018263996", "username": null }, "approximate_fields": [ "first_shown_at" ], "audience_size": null, "country_codes": [], "creatives": [ { "body": "My aunt called me a creep for putting a camera in Nana’s living room.\n\nNana called me practical.\n\nThen the camera caught something worse than a fall.\n\nI need to be clear about one thing before anyone decides what kind of grandson I am.\n\nNana asked for the camera.\n\nNot me.\n\nNot my…", "cta_text": "Learn more", "link_description": null, "link_url": "https://shop.pipitea.com/hbt/kd/sp-nm", "media": [ { "alt_text": null, "duration_ms": null, "expires_at": null, "height": null, "thumbnail_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/742001725_2100792434176297_9060645049521596104_n.jpg?stp=dst-jpg_s600x600_tt6&_nc_cat=100&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBE…", "type": "image", "url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/742501415_1706621370582971_4085631001896338504_n.jpg?_nc_cat=103&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&oe=SCRUBBED", "width": null } ], "title": "Natural Relief in Weeks" } ], "first_shown_at": "2026-07-18T07:00:00Z", "id": "2619351891814847", "impressions": null, "is_active": true, "is_political": false, "languages": [], "last_shown_at": null, "paid_by": null, "platform": "facebook", "publisher_platforms": [ "facebook", "instagram", "audience_network" ], "reach": null, "spend": null, "type": "image", "url": "https://www.facebook.com/ads/library/?id=2619351891814847" }, { "advertiser": { "avatar_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/712623693_1599994994434095_1320701223312676781_n.jpg?stp=dst-jpg_s60x60_tt6&_nc_cat=110&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&…", "display_name": "American Health Support Community", "id": "966175593248497", "is_verified": null, "url": "https://www.facebook.com/61588018263996", "username": null }, "approximate_fields": [ "first_shown_at" ], "audience_size": null, "country_codes": [], "creatives": [ { "body": "I almost looked past the woman who saved my husband's life. She was seventy-nine, sitting alone at the booth next to ours at the Cracker Barrel off Highway 31. A glass of milk in front of her. She kept stirring it. She wasn't eating.\n\nIf I'd known, the night I leaned over and ask…", "cta_text": "Learn more", "link_description": "\"I've tried everything to support my kidney function naturally. Fish oil, CoQ10, cutting protein, stressing about every meal. Nothing moved the needle enough to satisfy my doctor. Hibiscus tea is the first thing that actually showed up in my numbers. It's been 3 months and my eGF…", "link_url": "https://shop.pipitea.com/hbt/kd/sp", "media": [ { "alt_text": null, "duration_ms": null, "expires_at": null, "height": null, "thumbnail_url": "https://scontent-atl3-2.xx.fbcdn.net/v/t39.35426-6/711729685_1717035472767079_186136568392095276_n.jpg?stp=dst-jpg_s600x600_tt6&_nc_cat=104&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-2.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED…", "type": "image", "url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/713729791_868879862958547_7409366389733397444_n.jpg?_nc_cat=107&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&oe=SCRUBBED", "width": null } ], "title": "Natural Relief in Weeks" } ], "first_shown_at": "2026-06-01T07:00:00Z", "id": "1923645134954720", "impressions": null, "is_active": true, "is_political": false, "languages": [], "last_shown_at": null, "paid_by": null, "platform": "facebook", "publisher_platforms": [ "facebook", "instagram", "audience_network" ], "reach": null, "spend": null, "type": "image", "url": "https://www.facebook.com/ads/library/?id=1923645134954720" }, { "advertiser": { "avatar_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/712025330_1303417705276292_736671271727941526_n.jpg?stp=dst-jpg_s60x60_tt6&_nc_cat=103&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&o…", "display_name": "American Health Support Community", "id": "966175593248497", "is_verified": null, "url": "https://www.facebook.com/61588018263996", "username": null }, "approximate_fields": [ "first_shown_at" ], "audience_size": null, "country_codes": [], "creatives": [ { "body": "I almost looked past the woman who saved my husband's life. She was seventy-nine, sitting alone at the booth next to ours at the Cracker Barrel off Highway 31. A glass of milk in front of her. She kept stirring it. She wasn't eating.\n\nIf I'd known, the night I leaned over and ask…", "cta_text": "Learn more", "link_description": "\"I've tried everything to support my kidney function naturally. Fish oil, CoQ10, cutting protein, stressing about every meal. Nothing moved the needle enough to satisfy my doctor. Hibiscus tea is the first thing that actually showed up in my numbers. It's been 3 months and my eGF…", "link_url": "https://shop.pipitea.com/hbt/kd/sp", "media": [ { "alt_text": null, "duration_ms": null, "expires_at": null, "height": null, "thumbnail_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/711737708_4485707625086258_2353150069319301753_n.jpg?stp=dst-jpg_s600x600_tt6&_nc_cat=109&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBE…", "type": "image", "url": "https://scontent-atl3-2.xx.fbcdn.net/v/t39.35426-6/710755472_911440771966686_1736050619245168045_n.jpg?_nc_cat=111&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-2.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&oe=SCRUBBED", "width": null } ], "title": "Natural Relief in Weeks" } ], "first_shown_at": "2026-06-01T07:00:00Z", "id": "2140497416496021", "impressions": null, "is_active": true, "is_political": false, "languages": [], "last_shown_at": null, "paid_by": null, "platform": "facebook", "publisher_platforms": [ "facebook", "instagram", "audience_network" ], "reach": null, "spend": null, "type": "image", "url": "https://www.facebook.com/ads/library/?id=2140497416496021" } ], "page": { "has_more": true, "next_cursor": "c1.EXAMPLE_CURSOR", "next_cursor_expires_at": "2026-10-26T12:00:00Z", "end_reason": null }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "facebook", "source": "api", "is_cached": false, "credits_charged": 1, "latency_ms": 0, "fetched_at": "2026-10-25T12:00:00Z", "input": null, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display; real responses are complete. ## Response fields Each item of `data` follows the shared [Ad schema](https://staging.socialtap.dev/resources/ad/index.md), identical on every platform. | Field | Type | Description | | --- | --- | --- | | `data[].platform` | `string` | Library owner's platform ID (facebook for Meta's library, tiktok, google, linkedin). | | `data[].id` | `string` | Library ad ID (string). | | `data[].url` | `string` (uri) | Public library page of the ad. | | `data[].advertiser` | `object` | The advertising page, business or account. | | `data[].advertiser.id` | `string` or `null` | Platform-native account ID; null when only the handle is known. | | `data[].advertiser.username` | `string` or `null` | Handle without the @; null when the account has none. | | `data[].advertiser.display_name` | `string` or `null` | Display name shown on the profile. | | `data[].advertiser.url` | `string` (uri) or `null` | Canonical profile URL. | | `data[].advertiser.avatar_url` | `string` (uri) or `null` | Profile picture URL. Platform CDN URLs can expire within hours. | | `data[].advertiser.is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `data[].paid_by` | `string` or `null` | Disclosed payer ("Paid for by"). | | `data[].is_active` | `boolean` or `null` | True while the ad is running. | | `data[].first_shown_at` | `string` (date-time) or `null` | First delivery, RFC 3339 UTC (often date-only: see approximate_fields). | | `data[].last_shown_at` | `string` (date-time) or `null` | Last delivery, RFC 3339 UTC; null while active. | | `data[].type` | `string` | Creative shape. New values may be added; treat unknown values as other.
One of `image`, `video`, `carousel`, `text`, `dynamic`, `other` | | `data[].publisher_platforms` | `string[]` | Where it ran, lowercase IDs (facebook, instagram, messenger, audience_network, youtube, …); \[\] if not disclosed. | | `data[].creatives` | `object[]` | The ad's creatives: one per card of a carousel and per variation of a dynamic or catalog ad, else one for the ad itself; \[\] if none are shown. | | `data[].creatives[].title` | `string` or `null` | Headline. | | `data[].creatives[].body` | `string` or `null` | Primary text. | | `data[].creatives[].link_url` | `string` (uri) or `null` | Destination URL. | | `data[].creatives[].link_description` | `string` or `null` | Link description text. | | `data[].creatives[].cta_text` | `string` or `null` | Call-to-action label (e.g. Shop now). | | `data[].creatives[].media` | `object[]` | Images and videos of this creative; \[\] if none. | | `data[].creatives[].media[].type` | `string` | Kind of file. New values may be added; treat unknown values as other.
One of `image`, `video`, `audio`, `gif` | | `data[].creatives[].media[].url` | `string` (uri) or `null` | Direct file URL; null when the platform withholds it, or when the link would only work from SocialTap's network or with a login. Often signed and expiring: see expires_at. | | `data[].creatives[].media[].expires_at` | `string` (date-time) or `null` | When url stops working, RFC 3339 UTC, read from the platform's signed URL; null when unknown or when it does not expire. | | `data[].creatives[].media[].thumbnail_url` | `string` (uri) or `null` | Poster or preview image URL. | | `data[].creatives[].media[].width` | `integer` or `null` | Width in pixels. | | `data[].creatives[].media[].height` | `integer` or `null` | Height in pixels. | | `data[].creatives[].media[].duration_ms` | `integer` or `null` | Duration in milliseconds (video and audio only). | | `data[].creatives[].media[].alt_text` | `string` or `null` | Accessibility text provided by the author or platform. | | `data[].languages` | `string[]` | BCP-47 languages of the ad; \[\] if not disclosed. | | `data[].country_codes` | `string[]` | ISO 3166-1 alpha-2 countries where it was shown; \[\] if not disclosed. | | `data[].is_political` | `boolean` or `null` | Categorized as a political or social-issue ad; false when the library files it under another category, null when it shows none. | | `data[].impressions` | `object` or `null` | Impressions or times shown range; null when not disclosed. | | `data[].impressions.lower_bound` | `integer` | Inclusive lower bound. | | `data[].impressions.upper_bound` | `integer` or `null` | Inclusive upper bound; null when open-ended (e.g. ">1M"). Equal to lower_bound when exact. | | `data[].reach` | `object` or `null` | Unique users reached range; null when not disclosed. | | `data[].reach.lower_bound` | `integer` | Inclusive lower bound. | | `data[].reach.upper_bound` | `integer` or `null` | Inclusive upper bound; null when open-ended (e.g. ">1M"). Equal to lower_bound when exact. | | `data[].audience_size` | `object` or `null` | Estimated target audience size range; null when not disclosed. | | `data[].audience_size.lower_bound` | `integer` | Inclusive lower bound. | | `data[].audience_size.upper_bound` | `integer` or `null` | Inclusive upper bound; null when open-ended (e.g. ">1M"). Equal to lower_bound when exact. | | `data[].spend` | `object` or `null` | Spend range; null when not disclosed. | | `data[].spend.lower_bound` | `integer` | Inclusive lower bound, in whole units of currency. | | `data[].spend.upper_bound` | `integer` or `null` | Inclusive upper bound in whole units of currency; null when open-ended. | | `data[].spend.currency` | `string` | ISO 4217 currency code, uppercase (USD). | | `data[].approximate_fields` | `string[]` | JSON paths of values that are rounded, date-only or parsed from display text; \[\] when all exact. | ## Pagination Items come in pages, **most impressions first**. To get the next page, send **the same request plus `cursor`** set to `page.next_cursor` from the previous response. Keep every other parameter the same (limit, max_age and fields may change); anything else that differs is rejected. When `page.has_more` is `false` you have every item. - **Order**: Most impressions first - **Page size**: Up to 10 ads (the default; `limit` lowers it) - **Cursor lifetime**: 1 hour; don't store cursors - **Price**: Per page, 1 credit ### The `page` object | Field | Type | Description | | --- | --- | --- | | `page.has_more` | `boolean` | True when another page exists; then next_cursor is set. An empty page with has_more true is not the end. | | `page.next_cursor` | `string` or `null` | Pass as cursor, with the same other parameters, to get the next page; null at the end. | | `page.next_cursor_expires_at` | `string` (date-time) or `null` | When next_cursor stops working, RFC 3339 UTC; null at the end. Don't store cursors. | | `page.end_reason` | `string` or `null` | Why there are no more pages; null while has_more is true. end: the list ended, or reached since/until. platform_limit: the platform stops serving further pages although the list goes on. max_pages: SocialTap's cap on pages in one walk. New values may be added. A short or empty page is not the end: only has_more false is.
One of `end`, `platform_limit`, `max_pages` | ### Next page cURL: next page: ```shell curl -G 'https://staging.socialtap.dev/v1/facebook/search_ads' \ --data-urlencode 'country=US' \ --data-urlencode 'query=coffee' \ --data-urlencode 'status=all' \ --data-urlencode 'cursor=c1.EXAMPLE_CURSOR' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` - The price is per page: each successful page costs the same, whatever `limit` is and however many items it has. Empty pages are charged too (an empty page with `has_more: true` is not the end). - A `limit` below 10 doesn't save credits: the next request serves the rest of the same platform page, which is fetched again and charged as a page each time, and items can shift between those requests if the platform changes the list. Omitting `limit` is the cheapest way to walk the list. - Errors are free, including a rejected cursor. - Pages served from the cache are free. - An expired or altered cursor, or one sent with different parameters, fails with [`invalid_cursor`](https://staging.socialtap.dev/docs/errors/index.md#invalid_cursor) (HTTP 400): Repeat the original request without cursor to start the list again. [Pagination guide](https://staging.socialtap.dev/docs/pagination/index.md) ## Pricing You pay per successful page: every page costs the same whatever its number of items, empty pages included. Errors are free. The price depends on which source produced the result, reported as `meta.source`. | Source | Price per successful page | Per 1,000 pages | | --- | --- | --- | | Platform API `api` | 1 credit | $0.50–$0.80 | | meta.source | Source | What it means | | --- | --- | --- | | `api` | Platform API | The platform's public, logged-out API. | Your balance must be at least **1 credit** to start a request; with less, it is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) (HTTP 402) and costs nothing. Only that minimum is set aside when the request starts. `meta.credits_charged` and the `x-credits-charged` header show what you paid. [How credits work](https://staging.socialtap.dev/docs/credits/index.md) · [Packs](https://staging.socialtap.dev/pricing/index.md) ## Errors Errors cost nothing. Codes marked “Yes, later” under Retry can succeed on a later attempt; the others return the same answer every time. | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `invalid_input` | 400 | No | The request parameters are missing or malformed, or the URL is not a supported link for this endpoint. | Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names. | | `invalid_cursor` | 400 | No | The cursor is malformed, expired, belongs to another endpoint, or the other parameters differ from the request that issued it. | Repeat the original request without cursor to start the list again. | | `unauthorized` | 401 | No | The x-api-key header is missing, unknown or revoked (or, on the dashboard's routes, the session cookie is missing or expired). | Send a valid API key in the x-api-key header; without one, get a key at details.signup_url (AI agents: POST details.agent_signup_url). | | `forbidden` | 403 | No | The API key is valid but may not make this request: its account is suspended, its scopes do not include the route, it is waiting for its owner's email confirmation, it is past its expiry, or the request came from an IP its allowlist does not include. | Do what details.reason says: if suspended, contact support; otherwise use a key whose scopes cover the route (details.required_scope), have the owner confirm the emailed link, create a new key, or call from an allowed IP. | | `insufficient_credits` | 402 | No | The account does not have enough credits for this request. | Buy a credit pack, then repeat the request; do not retry automatically. | | `spend_limit_reached` | 402 | No | The API key reached its spend limit for the current period; the account may still have credits. | Wait until details.resets_at or raise the key's spend limit (PATCH /v1/api_keys/{id}); do not retry automatically. | | `rate_limited` | 429 | Yes, later | The account exceeded its runaway-protection ceiling: plans do not limit request rate, but a high per-account safety ceiling stops broken client loops. Support raises it on request. | Wait for the number of seconds in the retry-after header, then retry; ask support to raise the ceiling if your sustained traffic needs more. | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `upstream_blocked` | 503 | Yes, later | The platform refused every attempt. | Retry later with backoff; failed requests are free. | | `upstream_error` | 502 | Yes, later | The platform failed or returned something unexpected. | Retry with backoff; failed requests are free. | | `overloaded` | 503 | Yes, later | SocialTap was momentarily at capacity and did not start the request: nothing was fetched or charged. | Retry after the number of seconds in the retry-after header; failed requests are free. | | `internal` | 500 | Yes, later | Unexpected SocialTap error. | Retry with backoff; contact support with the request_id if it persists. | not_found are item-level: the request was fine, the item can't be returned. ## Test mode A key starting with `st_test_` gets recorded responses, free and the same every time ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). These inputs return their recording; any other input returns the example above. | Input | Answer | | --- | --- | | `groceries delivered`
with status=all | 200 OK | | `coffee`
with country=US status=all Default | 200 OK | | `"cold brew"`
with country=US status=active | 200 OK | | `coffee`
with country=US since=2026-09-01T00:00:00Z status=all until=2026-10-01T00:00:00Z | 200 OK | | `coffee`
with country=GB status=inactive | 200 OK | | `zzqxjvbnmqwplkk`
with country=US status=all | 200 OK | | `コーヒー`
with country=JP status=all | 200 OK | | `voting information center`
with country=US status=all | 200 OK | | `running shoes`
with since=2026-10-01T00:00:00Z status=all | 200 OK | | `espresso`
with country=XX status=all | 400 [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | Magic inputs: send one of these as `query` to get that error. | Error | Value | | --- | --- | | [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | `socialtap_test_invalid_input` | | [`invalid_cursor`](https://staging.socialtap.dev/docs/errors/index.md#invalid_cursor) | `socialtap_test_invalid_cursor` | | [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) | `socialtap_test_insufficient_credits` | | [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) | `socialtap_test_spend_limit_reached` | | [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) | `socialtap_test_rate_limited` | | [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | `socialtap_test_not_found` | | [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | `socialtap_test_upstream_blocked` | | [`upstream_error`](https://staging.socialtap.dev/docs/errors/index.md#upstream_error) | `socialtap_test_upstream_error` | | [`overloaded`](https://staging.socialtap.dev/docs/errors/index.md#overloaded) | `socialtap_test_overloaded` | | [`internal`](https://staging.socialtap.dev/docs/errors/index.md#internal) | `socialtap_test_internal` | ## FAQ **How do I search for an exact phrase?** Wrap the query in double quotes, for example "cold brew"; without quotes every word must appear, in any order. **Which ads does it find?** Every ad in Meta's public Ad Library whose text matches the query: active and inactive ads on all of Meta's apps, from any advertiser. **How much does the Facebook Ad Library Search API cost?** 1 credit for platform api results (meta.source = api) per page of up to 10 items, charged for every successful page, empty pages included. SocialTap credits cost $0.50–$0.80 per 1,000 depending on the pack. **How do I get the next page?** Repeat the same request with cursor set to page.next_cursor from the previous response, keeping the other parameters. When page.has_more is false there are no more items. Cursors expire after 1 hour. **Am I charged for failed requests?** No. Requests that return an error cost 0 credits. Responses served from the cache are free too. ## Related ### More Facebook endpoints - [Facebook Video Transcript API](https://staging.socialtap.dev/apis/facebook/transcript/index.md): Facebook video transcript (From 1 credit · `GET /v1/facebook/transcript`) --- Source: https://staging.socialtap.dev/apis/facebook/transcript/ # Facebook Video Transcript API Facebook · Transcript Beta Returns the timed transcript of a public Facebook video or reel: the uploaded or auto-generated caption track when Facebook has one, otherwise speech-to-text on the video's audio. Accepts reel, watch, page video, video.php, fb.watch and share links, or a numeric video ID. GET `https://staging.socialtap.dev/v1/facebook/transcript` [Try it in the playground](https://staging.socialtap.dev/playground/?endpoint=facebook.transcript) · [Quickstart](https://staging.socialtap.dev/docs/index.md) - From **1 credit** per request, up to **122 credits** - Sources: Captions → Speech-to-text - p50 **1,517 ms** - **65.3%** success (26 requests measured) ## Use cases - Analyze public page and creator videos - Make Facebook video content searchable - Repurpose reels and videos into text - Feed video content into LLM pipelines ## Good to know - Videos that Facebook only serves to logged-in users return private_or_unavailable. ## Example request Send a `GET` request with your key in the `x-api-key` header. cURL: ```shell curl -G 'https://staging.socialtap.dev/v1/facebook/transcript' \ --data-urlencode 'url=https://www.facebook.com/reel/1803909017420449/' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` TypeScript: ```typescript const params = new URLSearchParams({ url: "https://www.facebook.com/reel/1803909017420449/", }); const res = await fetch(`https://staging.socialtap.dev/v1/facebook/transcript?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const { data, meta } = body; console.log(data, meta.credits_charged); ``` Python: ```python import os import requests res = requests.get( "https://staging.socialtap.dev/v1/facebook/transcript", params={"url": "https://www.facebook.com/reel/1803909017420449/"}, headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]}, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") data, meta = body["data"], body["meta"] print(data, meta["credits_charged"]) ``` Go: ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" ) func main() { q := url.Values{} q.Set("url", "https://www.facebook.com/reel/1803909017420449/") req, err := http.NewRequest("GET", "https://staging.socialtap.dev/v1/facebook/transcript?"+q.Encode(), nil) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { Data json.RawMessage `json:"data"` Meta map[string]any `json:"meta"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if body.Error != nil { log.Fatalf("%s: %s", body.Error.Code, body.Error.Message) } fmt.Println(string(body.Data), body.Meta["credits_charged"]) } ``` ## Parameters | Name | Type | Description | Example | | --- | --- | --- | --- | | `url` required | `string`
max 2,048 chars | Facebook video URL (reel, watch, page video, video.php, m./mbasic./web. hosts, fb.watch and share/v\|r links) or a numeric video ID. | `https://www.facebook.com/reel/1803909017420449` | `url` takes Facebook video links. A link of another kind or platform fails free with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input), naming the endpoint that takes it. Parameters not listed here are rejected (with a suggestion for a likely typo), so a misspelled one never silently changes nothing: see [Requests](https://staging.socialtap.dev/docs/requests/index.md). Every endpoint also accepts [`max_age`](https://staging.socialtap.dev/docs/credits/index.md#param-max_age), [`fields`](https://staging.socialtap.dev/docs/credits/index.md#param-fields). ## Example response A `200` response is `{data, meta}`: `data` is a [transcript](https://staging.socialtap.dev/resources/transcript/index.md) object, `meta` describes the request ([meta fields](https://staging.socialtap.dev/docs/index.md#response)). 200 OK: ```json { "data": { "available_languages": [ "en-US" ], "duration_ms": 59861, "id": "1803909017420449", "is_auto_generated": false, "language": "en-US", "platform": "facebook", "segments": [ { "end_ms": 4000, "start_ms": 33, "text": "But the standard that we should continuously aspire to exceed," }, { "end_ms": 7166, "start_ms": 4000, "text": "which is why we are bringing that intensity, that discipline, that" }, { "end_ms": 11400, "start_ms": 7166, "text": "engineering ownership back to NASA, because great nations do not remain great" } ], "source": "captions", "text": "But the standard that we should continuously aspire to exceed, which is why we are bringing that intensity, that discipline, that engineering ownership back to NASA, because great nations do not remain great by being caretakers of history, but by continuously making history. We h…", "url": "https://www.facebook.com/reel/1803909017420449/" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "facebook", "source": "captions", "is_cached": false, "credits_charged": 1, "latency_ms": 1517, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "video", "id": "1803909017420449", "url": "https://www.facebook.com/reel/1803909017420449/" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display; real responses are complete. ## Response fields `data` follows the shared [Transcript schema](https://staging.socialtap.dev/resources/transcript/index.md), identical on every platform. | Field | Type | Description | | --- | --- | --- | | `data.platform` | `string` | Platform the media is hosted on. | | `data.id` | `string` | Platform-native ID of the post (equals post.id). | | `data.url` | `string` (uri) | Canonical URL of the post. | | `data.language` | `string` or `null` | BCP-47 language of the returned transcript; null when unknown. | | `data.source` | `string` | captions = platform caption track; asr = speech-to-text on the audio.
One of `captions`, `asr` | | `data.is_auto_generated` | `boolean` | True when the track was machine-generated (platform auto-captions or ASR). | | `data.available_languages` | `string[]` | BCP-47 caption languages the platform offers for this post; \[\] if none or not listed. | | `data.duration_ms` | `integer` or `null` | Media duration in milliseconds; null when unknown. | | `data.segments` | `object[]` | Timed cues in order; never empty on success. | | `data.segments[].start_ms` | `integer` | Start time in milliseconds from the beginning of the media. | | `data.segments[].end_ms` | `integer` | End time in milliseconds from the beginning of the media. | | `data.segments[].text` | `string` | Cue text, whitespace-normalized. | | `data.text` | `string` | Full transcript: segment texts joined with single spaces. | ## Pricing You pay only for successful requests. The price depends on which source produced the result, reported as `meta.source`; sources are tried cheapest first, in the order Captions → Speech-to-text. | Source | Price per successful request | Per 1,000 requests | | --- | --- | --- | | Captions `captions` | 1 credit | $0.50–$0.80 | | Speech-to-text `asr` | 2 credits + 2 per started minute of media
up to 122 credits (60 started minutes of media) | $1.00–$1.60 + $1.00–$1.60 per started minute of media | | meta.source | Source | What it means | | --- | --- | --- | | `captions` | Captions | The platform's own caption track (uploaded or auto-generated). | | `asr` | Speech-to-text | Transcribed by SocialTap from the media's audio when the platform has no captions. | Example: a result from Speech-to-text `asr` with 3 started minutes of media costs 2 + 2 × 3 = **8 credits**. Your balance must be at least **1 credit** to start a request; with less, it is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) (HTTP 402) and costs nothing. Only that minimum is set aside when the request starts. One request costs at most **122 credits**, but that amount is never locked up in advance: right before costlier work, SocialTap sets aside more of your balance, the exact price when the number of started minutes of media is known beforehand, otherwise as much as your balance allows (and the work is limited to what that pays for). It then charges the actual price and releases the rest. If the result would cost more than your balance covers, the request is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) as well, and costs nothing. `meta.credits_charged` and the `x-credits-charged` header show what you paid. [How credits work](https://staging.socialtap.dev/docs/credits/index.md) · [Packs](https://staging.socialtap.dev/pricing/index.md) ## Errors Errors cost nothing. Codes marked “Yes, later” under Retry can succeed on a later attempt; the others return the same answer every time. | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `invalid_input` | 400 | No | The request parameters are missing or malformed, or the URL is not a supported link for this endpoint. | Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names. | | `unauthorized` | 401 | No | The x-api-key header is missing, unknown or revoked (or, on the dashboard's routes, the session cookie is missing or expired). | Send a valid API key in the x-api-key header; without one, get a key at details.signup_url (AI agents: POST details.agent_signup_url). | | `forbidden` | 403 | No | The API key is valid but may not make this request: its account is suspended, its scopes do not include the route, it is waiting for its owner's email confirmation, it is past its expiry, or the request came from an IP its allowlist does not include. | Do what details.reason says: if suspended, contact support; otherwise use a key whose scopes cover the route (details.required_scope), have the owner confirm the emailed link, create a new key, or call from an allowed IP. | | `insufficient_credits` | 402 | No | The account does not have enough credits for this request. | Buy a credit pack, then repeat the request; do not retry automatically. | | `spend_limit_reached` | 402 | No | The API key reached its spend limit for the current period; the account may still have credits. | Wait until details.resets_at or raise the key's spend limit (PATCH /v1/api_keys/{id}); do not retry automatically. | | `rate_limited` | 429 | Yes, later | The account exceeded its runaway-protection ceiling: plans do not limit request rate, but a high per-account safety ceiling stops broken client loops. Support raises it on request. | Wait for the number of seconds in the retry-after header, then retry; ask support to raise the ceiling if your sustained traffic needs more. | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | | `no_transcript` | 404 | No | The media exists but has no transcript: no caption track and no speech to transcribe. | Do not retry; try another language with lang if the platform offers one. | | `upstream_blocked` | 503 | Yes, later | The platform refused every attempt. | Retry later with backoff; failed requests are free. | | `upstream_error` | 502 | Yes, later | The platform failed or returned something unexpected. | Retry with backoff; failed requests are free. | | `overloaded` | 503 | Yes, later | SocialTap was momentarily at capacity and did not start the request: nothing was fetched or charged. | Retry after the number of seconds in the retry-after header; failed requests are free. | | `internal` | 500 | Yes, later | Unexpected SocialTap error. | Retry with backoff; contact support with the request_id if it persists. | not_found, private_or_unavailable and no_transcript are item-level: the request was fine, the item can't be returned. ## Test mode A key starting with `st_test_` gets recorded responses, free and the same every time ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). These inputs return their recording; any other input returns the example above. | Input | Answer | | --- | --- | | `https://www.facebook.com/reel/1718871615853822/` | 200 OK | | `https://www.facebook.com/reel/3034943296845097/` | 200 OK | | `https://www.facebook.com/reel/738566979178125/` | 200 OK | | `https://www.facebook.com/reel/1755978666533018/` | 200 OK | | `https://fb.watch/G1pb1cwZsM/` | 200 OK | | `https://www.facebook.com/reel/1570030614893129/` | 200 OK | | `https://www.facebook.com/reel/1113509447153884/` | 200 OK | | `https://fb.watch/G-oJWeu2nt/` | 200 OK | | `https://www.facebook.com/reel/1803909017420449/` Default | 200 OK | | `https://www.facebook.com/reel/1408202287429345/` | 200 OK | Magic inputs: send one of these as `url` to get that error. | Error | Value | | --- | --- | | [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | `https://socialtap.dev/test/invalid_input` | | [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) | `https://socialtap.dev/test/insufficient_credits` | | [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) | `https://socialtap.dev/test/spend_limit_reached` | | [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) | `https://socialtap.dev/test/rate_limited` | | [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | `https://socialtap.dev/test/not_found` | | [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | `https://socialtap.dev/test/private_or_unavailable` | | [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | `https://socialtap.dev/test/no_transcript` | | [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | `https://socialtap.dev/test/upstream_blocked` | | [`upstream_error`](https://staging.socialtap.dev/docs/errors/index.md#upstream_error) | `https://socialtap.dev/test/upstream_error` | | [`overloaded`](https://staging.socialtap.dev/docs/errors/index.md#overloaded) | `https://socialtap.dev/test/overloaded` | | [`internal`](https://staging.socialtap.dev/docs/errors/index.md#internal) | `https://socialtap.dev/test/internal` | ## Measured performance - **p50 latency**: 1,517 ms - **p95 latency**: 2,437 ms - **Success rate**: 65.3% - **Requests measured**: 26 **Staging measurements, not production.** Measured on our staging environment, over the 7 days before 2026-10-25. [How it is measured](https://staging.socialtap.dev/status/index.md#method). ## FAQ **How much does the Facebook Video Transcript API cost?** 1 credit for captions results (meta.source = captions) and 2 credits + 2 per started minute of media, up to 122 credits (60 started minutes of media) for speech-to-text results (meta.source = asr), charged only for successful requests. SocialTap credits cost $0.50–$0.80 per 1,000 depending on the pack. **Am I charged for failed requests?** No. Requests that return an error cost 0 credits. Responses served from the cache are free too. ## Related ### More endpoints returning transcripts - [Instagram Reels Transcript API](https://staging.socialtap.dev/apis/instagram/transcript/index.md): Instagram reel / video transcript (From 1 credit · `GET /v1/instagram/transcript`) - [TikTok Transcript API](https://staging.socialtap.dev/apis/tiktok/transcript/index.md): TikTok video transcript (From 1 credit · `GET /v1/tiktok/transcript`) - [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md): YouTube video transcript (From 1 credit · `GET /v1/youtube/transcript`) ### More Facebook endpoints - [Facebook Ad Library Search API](https://staging.socialtap.dev/apis/facebook/search_ads/index.md): Search Meta's Ad Library by keyword (From 1 credit per page · `GET /v1/facebook/search_ads`) --- Source: https://staging.socialtap.dev/apis/instagram/post/ # Instagram Post API Instagram · Post Beta Returns a public Instagram post, reel or carousel: the caption with its hashtags and mentions, the author and any collaborators, the publish time, every carousel item's image or video URL with its size, duration and alt text, and the like, comment and (for single videos) view counts. Accepts post, reel, tv, instagr.am and share links or a raw shortcode. GET `https://staging.socialtap.dev/v1/instagram/post` [Try it in the playground](https://staging.socialtap.dev/playground/?endpoint=instagram.post) · [Quickstart](https://staging.socialtap.dev/docs/index.md) - From **1 credit** per request - Sources: Public page ## Use cases - Track engagement on influencer and brand posts - Monitor competitors' reels and campaigns - Collect captions, hashtags and tags for social listening - Archive post media and metadata - Verify sponsored-post deliverables ## Good to know - Media URLs are Instagram CDN links that expire: media\[\].expires_at says when; download what you need before then. - view_count is set for single videos and reels only (Instagram shows no view count for photos and carousels). like_count and view_count are null when the owner hides them. - Instagram does not expose share or save counts, pinned state, sponsorship labels or the caption's language to logged-out visitors: those fields are null. - Private, age-restricted and deleted posts return private_or_unavailable or not_found and are not charged. ## Example request Send a `GET` request with your key in the `x-api-key` header. cURL: ```shell curl -G 'https://staging.socialtap.dev/v1/instagram/post' \ --data-urlencode 'url=https://www.instagram.com/p/De1S3KpCXf0/' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` TypeScript: ```typescript const params = new URLSearchParams({ url: "https://www.instagram.com/p/De1S3KpCXf0/", }); const res = await fetch(`https://staging.socialtap.dev/v1/instagram/post?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const { data, meta } = body; console.log(data, meta.credits_charged); ``` Python: ```python import os import requests res = requests.get( "https://staging.socialtap.dev/v1/instagram/post", params={"url": "https://www.instagram.com/p/De1S3KpCXf0/"}, headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]}, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") data, meta = body["data"], body["meta"] print(data, meta["credits_charged"]) ``` Go: ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" ) func main() { q := url.Values{} q.Set("url", "https://www.instagram.com/p/De1S3KpCXf0/") req, err := http.NewRequest("GET", "https://staging.socialtap.dev/v1/instagram/post?"+q.Encode(), nil) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { Data json.RawMessage `json:"data"` Meta map[string]any `json:"meta"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if body.Error != nil { log.Fatalf("%s: %s", body.Error.Code, body.Error.Message) } fmt.Println(string(body.Data), body.Meta["credits_charged"]) } ``` ## Parameters | Name | Type | Description | Example | | --- | --- | --- | --- | | `url` required | `string`
max 2,048 chars | Instagram post, reel or carousel URL (instagram.com/p/…, /reel/…, /reels/…, /tv/…, /\/p/…, m.instagram.com, instagr.am, instagram.com/share/… links) or a raw shortcode. Tracking params and img_index are ignored: the whole post is returned. | `https://www.instagram.com/reel/De1S3KpCXf0/` | `url` takes Instagram post and video links. A link of another kind or platform fails free with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input), naming the endpoint that takes it. Parameters not listed here are rejected (with a suggestion for a likely typo), so a misspelled one never silently changes nothing: see [Requests](https://staging.socialtap.dev/docs/requests/index.md). Every endpoint also accepts [`max_age`](https://staging.socialtap.dev/docs/credits/index.md#param-max_age), [`fields`](https://staging.socialtap.dev/docs/credits/index.md#param-fields). ## Example response A `200` response is `{data, meta}`: `data` is a [post](https://staging.socialtap.dev/resources/post/index.md) object, `meta` describes the request ([meta fields](https://staging.socialtap.dev/docs/index.md#response)). 200 OK: ```json { "data": { "approximate_fields": [], "author": { "avatar_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.82787-19/683576066_18653628823019133_9051036240972105113_n.jpg?stp=dst-jpg_s150x150_tt6&_nc_cat=1&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=24&_nc_ht=scontent-atl3-2.cdninstagram.com&_nc_gid=S…", "display_name": "National Geographic", "id": "787132", "is_verified": true, "url": "https://www.instagram.com/natgeo/", "username": "natgeo" }, "coauthors": [ { "avatar_url": "https://scontent-atl3-1.cdninstagram.com/v/t51.2885-19/293493364_162817252949028_8275460966213229030_n.jpg?stp=dst-jpg_s150x150_tt6&_nc_cat=108&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=24&_nc_ht=scontent-atl3-1.cdninstagram.com&_nc_ss=7b6…", "display_name": "National Geographic Society", "id": "4757225830", "is_verified": true, "url": "https://www.instagram.com/natgeosociety/", "username": "natgeosociety" }, { "avatar_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.2885-19/11419091_505296402966021_797003013_a.jpg?_nc_cat=101&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=24&_nc_ht=scontent-atl3-2.cdninstagram.com&_nc_ss=7b689&oh=SCRUBBED&oe=SCRUBBED", "display_name": "Joel Sartore- Photo Ark", "id": "225429667", "is_verified": true, "url": "https://www.instagram.com/joelsartore/", "username": "joelsartore" }, { "avatar_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.82787-19/681945023_18592998970017301_7811445524976503069_n.jpg?stp=dst-jpg_s150x150_tt6&_nc_cat=1&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=24&_nc_ht=scontent-atl3-2.cdninstagram.com&_nc_gid=S…", "display_name": "National Geographic Animals", "id": "1029649300", "is_verified": true, "url": "https://www.instagram.com/natgeoanimals/", "username": "natgeoanimals" } ], "duration_ms": 23744, "hashtags": [ "PhotoArk" ], "id": "De1S3KpCXf0", "is_nsfw": null, "is_pinned": null, "is_sponsored": null, "language": null, "media": [ { "alt_text": "Video by National Geographic on October 23, 2026. May be an image of snail and text.", "duration_ms": 23744, "expires_at": null, "height": 1920, "thumbnail_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.82787-15/862543567_18711062458019133_8845814561108513720_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=105&ig_cache_key=NDAxNDE5NzU5ODcwNDk5MDE5NjE4NzExMDYyNDUyMDE5MTMz.3-ccb7-5&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=…", "type": "video", "url": "https://scontent-atl3-1.cdninstagram.com/o1/v/t2/f2/m86/AQO0Zc3E_eoYHXxPOVvPDNzjiHnCGoREl8vxeookIy4831a0VLAN6T9CITX0dyH2cQS6Dy0fs5SGxYC42fWpG2JeAvlnP9yrsgmHUk8.mp4?_nc_cat=106&_nc_sid=SCRUBBED&_nc_ht=scontent-atl3-1.cdninstagram.com&_nc_ohc=SCRUBBED&efg=SCRUBBED&ccb=SCRUBBED&vs=S…", "width": 1080 } ], "mentions": [ "joelsartore", "natgeoanimals", "natgeosociety" ], "metrics": { "comment_count": 238, "like_count": 68253, "quote_count": null, "repost_count": null, "save_count": null, "share_count": null, "view_count": 759188 }, "platform": "instagram", "platform_type": "reel", "published_at": "2026-10-23T10:01:35Z", "text": "Warning—looking at nudibranchs may cause excessive awe. @joelsartore loves these little creatures, and so do we. 💛🧡🩷\n\nWe’re celebrating 20 years of the National Geographic #PhotoArk, an audacious project documenting the world's incredible biodiversity and highlighting the importa…", "thumbnail_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.82787-15/862543567_18711062458019133_8845814561108513720_n.jpg?stp=dst-jpg_e35_s640x640_tt6&_nc_cat=105&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=23&_nc_ht=scontent-atl3-2.cdninstagram.com&_nc…", "title": null, "type": "video", "url": "https://www.instagram.com/reel/De1S3KpCXf0/" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "instagram", "source": "page", "is_cached": false, "credits_charged": 1, "latency_ms": 0, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "post", "id": "De1S3KpCXf0", "url": "https://www.instagram.com/reel/De1S3KpCXf0/" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display; real responses are complete. ## Response fields `data` follows the shared [Post schema](https://staging.socialtap.dev/resources/post/index.md), identical on every platform. | Field | Type | Description | | --- | --- | --- | | `data.platform` | `string` | Platform ID (tiktok, youtube, …). | | `data.id` | `string` | Platform-native post ID (string; Instagram shortcode, Reddit without t3_). | | `data.url` | `string` (uri) | Canonical post URL. | | `data.type` | `string` | Normalized content shape. New values may be added; treat unknown values as other.
One of `video`, `image`, `carousel`, `text`, `link`, `audio` | | `data.platform_type` | `string` or `null` | The platform's own product label, lowercase (reel, short, story, live, tweet, …). | | `data.title` | `string` or `null` | Headline (YouTube title, Reddit title); null on caption-only platforms. | | `data.text` | `string` or `null` | Caption, description or body as plain text. | | `data.language` | `string` or `null` | BCP-47 language of text, when the platform states it. | | `data.published_at` | `string` (date-time) or `null` | When the post went public, RFC 3339 UTC. | | `data.author` | `object` | The posting account. | | `data.author.id` | `string` or `null` | Platform-native account ID; null when only the handle is known. | | `data.author.username` | `string` or `null` | Handle without the @; null when the account has none. | | `data.author.display_name` | `string` or `null` | Display name shown on the profile. | | `data.author.url` | `string` (uri) or `null` | Canonical profile URL. | | `data.author.avatar_url` | `string` (uri) or `null` | Profile picture URL. Platform CDN URLs can expire within hours. | | `data.author.is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `data.coauthors` | `object[]` | Other accounts credited as co-authors of the post (Instagram and Facebook collabs, YouTube collaborations), in the platform's order, author excluded; \[\] if none. | | `data.coauthors[].id` | `string` or `null` | Platform-native account ID; null when only the handle is known. | | `data.coauthors[].username` | `string` or `null` | Handle without the @; null when the account has none. | | `data.coauthors[].display_name` | `string` or `null` | Display name shown on the profile. | | `data.coauthors[].url` | `string` (uri) or `null` | Canonical profile URL. | | `data.coauthors[].avatar_url` | `string` (uri) or `null` | Profile picture URL. Platform CDN URLs can expire within hours. | | `data.coauthors[].is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `data.duration_ms` | `integer` or `null` | Length of the primary video or audio in milliseconds; null for other posts. | | `data.thumbnail_url` | `string` (uri) or `null` | Cover or poster image URL. | | `data.media` | `object[]` | Attachments in display order (carousel items); \[\] for text posts. | | `data.media[].type` | `string` | Kind of file. New values may be added; treat unknown values as other.
One of `image`, `video`, `audio`, `gif` | | `data.media[].url` | `string` (uri) or `null` | Direct file URL; null when the platform withholds it, or when the link would only work from SocialTap's network or with a login. Often signed and expiring: see expires_at. | | `data.media[].expires_at` | `string` (date-time) or `null` | When url stops working, RFC 3339 UTC, read from the platform's signed URL; null when unknown or when it does not expire. | | `data.media[].thumbnail_url` | `string` (uri) or `null` | Poster or preview image URL. | | `data.media[].width` | `integer` or `null` | Width in pixels. | | `data.media[].height` | `integer` or `null` | Height in pixels. | | `data.media[].duration_ms` | `integer` or `null` | Duration in milliseconds (video and audio only). | | `data.media[].alt_text` | `string` or `null` | Accessibility text provided by the author or platform. | | `data.hashtags` | `string[]` | Hashtags without #, case kept; \[\] if none. | | `data.mentions` | `string[]` | Mentioned or tagged usernames without @; \[\] if none. | | `data.is_pinned` | `boolean` or `null` | Pinned to the author's profile. | | `data.is_sponsored` | `boolean` or `null` | Labeled as a paid partnership or branded content. | | `data.is_nsfw` | `boolean` or `null` | Marked adult or sensitive by the platform. | | `data.metrics` | `object` | Counts at fetch time; null = hidden or not exposed, 0 = zero. | | `data.metrics.view_count` | `integer` or `null` | Views or plays. | | `data.metrics.like_count` | `integer` or `null` | Likes, hearts, total reactions or upvotes; never negative. Null when hidden by the owner. | | `data.metrics.comment_count` | `integer` or `null` | Comments and replies. | | `data.metrics.share_count` | `integer` or `null` | Shares or sends (not reposts). | | `data.metrics.repost_count` | `integer` or `null` | Reposts, retweets or reshares. | | `data.metrics.quote_count` | `integer` or `null` | Quote posts. | | `data.metrics.save_count` | `integer` or `null` | Saves, bookmarks or favorites. | | `data.approximate_fields` | `string[]` | JSON paths of values rounded by the platform or parsed from display text; \[\] when all exact. | ## Pricing You pay only for successful requests. The price depends on which source produced the result, reported as `meta.source`. | Source | Price per successful request | Per 1,000 requests | | --- | --- | --- | | Public page `page` | 1 credit | $0.50–$0.80 | | meta.source | Source | What it means | | --- | --- | --- | | `page` | Public page | Data embedded in the platform's public web page. | Your balance must be at least **1 credit** to start a request; with less, it is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) (HTTP 402) and costs nothing. Only that minimum is set aside when the request starts. `meta.credits_charged` and the `x-credits-charged` header show what you paid. [How credits work](https://staging.socialtap.dev/docs/credits/index.md) · [Packs](https://staging.socialtap.dev/pricing/index.md) ## Errors Errors cost nothing. Codes marked “Yes, later” under Retry can succeed on a later attempt; the others return the same answer every time. | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `invalid_input` | 400 | No | The request parameters are missing or malformed, or the URL is not a supported link for this endpoint. | Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names. | | `unauthorized` | 401 | No | The x-api-key header is missing, unknown or revoked (or, on the dashboard's routes, the session cookie is missing or expired). | Send a valid API key in the x-api-key header; without one, get a key at details.signup_url (AI agents: POST details.agent_signup_url). | | `forbidden` | 403 | No | The API key is valid but may not make this request: its account is suspended, its scopes do not include the route, it is waiting for its owner's email confirmation, it is past its expiry, or the request came from an IP its allowlist does not include. | Do what details.reason says: if suspended, contact support; otherwise use a key whose scopes cover the route (details.required_scope), have the owner confirm the emailed link, create a new key, or call from an allowed IP. | | `insufficient_credits` | 402 | No | The account does not have enough credits for this request. | Buy a credit pack, then repeat the request; do not retry automatically. | | `spend_limit_reached` | 402 | No | The API key reached its spend limit for the current period; the account may still have credits. | Wait until details.resets_at or raise the key's spend limit (PATCH /v1/api_keys/{id}); do not retry automatically. | | `rate_limited` | 429 | Yes, later | The account exceeded its runaway-protection ceiling: plans do not limit request rate, but a high per-account safety ceiling stops broken client loops. Support raises it on request. | Wait for the number of seconds in the retry-after header, then retry; ask support to raise the ceiling if your sustained traffic needs more. | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | | `upstream_blocked` | 503 | Yes, later | The platform refused every attempt. | Retry later with backoff; failed requests are free. | | `upstream_error` | 502 | Yes, later | The platform failed or returned something unexpected. | Retry with backoff; failed requests are free. | | `overloaded` | 503 | Yes, later | SocialTap was momentarily at capacity and did not start the request: nothing was fetched or charged. | Retry after the number of seconds in the retry-after header; failed requests are free. | | `internal` | 500 | Yes, later | Unexpected SocialTap error. | Retry with backoff; contact support with the request_id if it persists. | not_found and private_or_unavailable are item-level: the request was fine, the item can't be returned. ## Test mode A key starting with `st_test_` gets recorded responses, free and the same every time ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). These inputs return their recording; any other input returns the example above. | Input | Answer | | --- | --- | | `https://www.instagram.com/p/De492EVDJd3/` | 200 OK | | `https://www.instagram.com/p/DexcKhDDno3/` | 200 OK | | `https://www.instagram.com/p/DeuOR-CjKE9/` | 200 OK | | `https://www.instagram.com/p/De1S3KpCXf1/` | 404 [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | | `https://www.instagram.com/p/De1S3KpCXf0/` Default | 200 OK | | `https://www.instagram.com/p/ChdZ_3UALgB/` | 200 OK | | `https://www.instagram.com/share/reel/_oDaXMUlS/` | 200 OK | | `https://www.instagram.com/p/BBBBBBBBBBB/` | 403 [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | | `https://www.instagram.com/p/AAAAAAAAAAA/` | 403 [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | | `https://www.instagram.com/p/DDDDDDDDDDD/` | 200 OK | | `https://www.instagram.com/p/CCCCCCCCCCC/` | 503 [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | | `https://www.instagram.com/p/EEEEEEEEEEE/` | 200 OK | Magic inputs: send one of these as `url` to get that error. | Error | Value | | --- | --- | | [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | `https://socialtap.dev/test/invalid_input` | | [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) | `https://socialtap.dev/test/insufficient_credits` | | [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) | `https://socialtap.dev/test/spend_limit_reached` | | [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) | `https://socialtap.dev/test/rate_limited` | | [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | `https://socialtap.dev/test/not_found` | | [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | `https://socialtap.dev/test/private_or_unavailable` | | [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | `https://socialtap.dev/test/upstream_blocked` | | [`upstream_error`](https://staging.socialtap.dev/docs/errors/index.md#upstream_error) | `https://socialtap.dev/test/upstream_error` | | [`overloaded`](https://staging.socialtap.dev/docs/errors/index.md#overloaded) | `https://socialtap.dev/test/overloaded` | | [`internal`](https://staging.socialtap.dev/docs/errors/index.md#internal) | `https://socialtap.dev/test/internal` | ## FAQ **Does it return every image of a carousel?** Yes: media\[\] has one entry per carousel item, in display order, each with its own URL, size and, for videos, duration. **Who is in mentions?** Accounts @mentioned in the caption, then accounts tagged in the media, without duplicates. Collaborators of a collab post are in coauthors. **How much does the Instagram Post API cost?** 1 credit for public page results (meta.source = page), charged only for successful requests. SocialTap credits cost $0.50–$0.80 per 1,000 depending on the pack. **Am I charged for failed requests?** No. Requests that return an error cost 0 credits. Responses served from the cache are free too. ## Related ### More Instagram endpoints - [Instagram Reels Transcript API](https://staging.socialtap.dev/apis/instagram/transcript/index.md): Instagram reel / video transcript (From 1 credit · `GET /v1/instagram/transcript`) --- Source: https://staging.socialtap.dev/apis/instagram/transcript/ # Instagram Reels Transcript API Instagram · Transcript Beta Returns the timed transcript of a public Instagram reel or video post. Instagram rarely exposes caption tracks to logged-out clients, so most transcripts are speech-to-text on the post's audio. Accepts reel, post, tv, instagr.am and share links or a raw shortcode; for carousels, img_index selects the item. GET `https://staging.socialtap.dev/v1/instagram/transcript` [Try it in the playground](https://staging.socialtap.dev/playground/?endpoint=instagram.transcript) · [Quickstart](https://staging.socialtap.dev/docs/index.md) - From **1 credit** per request, up to **122 credits** - Sources: Captions → Speech-to-text - p50 **4,125 ms** - **98.1%** success (53 requests measured) ## Use cases - Turn reels into searchable text - Monitor influencer and competitor content - Repurpose reels into captions, blogs and posts - Feed reels into LLM analysis ## Good to know - Most results come from speech-to-text (meta.source = asr) and are priced per minute. - Image posts return invalid_input; videos without speech return no_transcript. ## Example request Send a `GET` request with your key in the `x-api-key` header. cURL: ```shell curl -G 'https://staging.socialtap.dev/v1/instagram/transcript' \ --data-urlencode 'url=https://www.instagram.com/p/De1S3KpCXf0/' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` TypeScript: ```typescript const params = new URLSearchParams({ url: "https://www.instagram.com/p/De1S3KpCXf0/", }); const res = await fetch(`https://staging.socialtap.dev/v1/instagram/transcript?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const { data, meta } = body; console.log(data, meta.credits_charged); ``` Python: ```python import os import requests res = requests.get( "https://staging.socialtap.dev/v1/instagram/transcript", params={"url": "https://www.instagram.com/p/De1S3KpCXf0/"}, headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]}, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") data, meta = body["data"], body["meta"] print(data, meta["credits_charged"]) ``` Go: ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" ) func main() { q := url.Values{} q.Set("url", "https://www.instagram.com/p/De1S3KpCXf0/") req, err := http.NewRequest("GET", "https://staging.socialtap.dev/v1/instagram/transcript?"+q.Encode(), nil) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { Data json.RawMessage `json:"data"` Meta map[string]any `json:"meta"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if body.Error != nil { log.Fatalf("%s: %s", body.Error.Code, body.Error.Message) } fmt.Println(string(body.Data), body.Meta["credits_charged"]) } ``` ## Parameters | Name | Type | Description | Example | | --- | --- | --- | --- | | `url` required | `string`
max 2,048 chars | Instagram reel/post URL (instagram.com/reel/…, /reels/…, /p/…, /tv/…, instagr.am, instagram.com/share/… links) or a raw shortcode. For carousel posts, ?img_index=N selects the item; default is the first video. | `https://www.instagram.com/reel/De1S3KpCXf0/` | `url` takes Instagram video and post links. A link of another kind or platform fails free with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input), naming the endpoint that takes it. Parameters not listed here are rejected (with a suggestion for a likely typo), so a misspelled one never silently changes nothing: see [Requests](https://staging.socialtap.dev/docs/requests/index.md). Every endpoint also accepts [`max_age`](https://staging.socialtap.dev/docs/credits/index.md#param-max_age), [`fields`](https://staging.socialtap.dev/docs/credits/index.md#param-fields). ## Example response A `200` response is `{data, meta}`: `data` is a [transcript](https://staging.socialtap.dev/resources/transcript/index.md) object, `meta` describes the request ([meta fields](https://staging.socialtap.dev/docs/index.md#response)). 200 OK: ```json { "data": { "available_languages": [], "duration_ms": 23745, "id": "De1S3KpCXf0", "is_auto_generated": true, "language": "en", "platform": "instagram", "segments": [ { "end_ms": 3580, "start_ms": 0, "text": "You are looking at nudibranchs. You ever seen anything that crazy looking?" }, { "end_ms": 9220, "start_ms": 3800, "text": "Every color in the rainbow. These are marine snails basically that have lost their shells" }, { "end_ms": 13480, "start_ms": 9220, "text": "over time and developed these elaborate colors. Usually brightly colored animals are toxic." } ], "source": "asr", "text": "You are looking at nudibranchs. You ever seen anything that crazy looking? Every color in the rainbow. These are marine snails basically that have lost their shells over time and developed these elaborate colors. Usually brightly colored animals are toxic. Some of these are, some…", "url": "https://www.instagram.com/reel/De1S3KpCXf0/" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "instagram", "source": "asr", "is_cached": false, "credits_charged": 4, "latency_ms": 4125, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "video", "id": "De1S3KpCXf0", "url": "https://www.instagram.com/reel/De1S3KpCXf0/" }, "credits_remaining": 9996, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display; real responses are complete. ## Response fields `data` follows the shared [Transcript schema](https://staging.socialtap.dev/resources/transcript/index.md), identical on every platform. | Field | Type | Description | | --- | --- | --- | | `data.platform` | `string` | Platform the media is hosted on. | | `data.id` | `string` | Platform-native ID of the post (equals post.id). | | `data.url` | `string` (uri) | Canonical URL of the post. | | `data.language` | `string` or `null` | BCP-47 language of the returned transcript; null when unknown. | | `data.source` | `string` | captions = platform caption track; asr = speech-to-text on the audio.
One of `captions`, `asr` | | `data.is_auto_generated` | `boolean` | True when the track was machine-generated (platform auto-captions or ASR). | | `data.available_languages` | `string[]` | BCP-47 caption languages the platform offers for this post; \[\] if none or not listed. | | `data.duration_ms` | `integer` or `null` | Media duration in milliseconds; null when unknown. | | `data.segments` | `object[]` | Timed cues in order; never empty on success. | | `data.segments[].start_ms` | `integer` | Start time in milliseconds from the beginning of the media. | | `data.segments[].end_ms` | `integer` | End time in milliseconds from the beginning of the media. | | `data.segments[].text` | `string` | Cue text, whitespace-normalized. | | `data.text` | `string` | Full transcript: segment texts joined with single spaces. | ## Pricing You pay only for successful requests. The price depends on which source produced the result, reported as `meta.source`; sources are tried cheapest first, in the order Captions → Speech-to-text. | Source | Price per successful request | Per 1,000 requests | | --- | --- | --- | | Captions `captions` | 1 credit | $0.50–$0.80 | | Speech-to-text `asr` | 2 credits + 2 per started minute of media
up to 122 credits (60 started minutes of media) | $1.00–$1.60 + $1.00–$1.60 per started minute of media | | meta.source | Source | What it means | | --- | --- | --- | | `captions` | Captions | The platform's own caption track (uploaded or auto-generated). | | `asr` | Speech-to-text | Transcribed by SocialTap from the media's audio when the platform has no captions. | Example: a result from Speech-to-text `asr` with 3 started minutes of media costs 2 + 2 × 3 = **8 credits**. Your balance must be at least **1 credit** to start a request; with less, it is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) (HTTP 402) and costs nothing. Only that minimum is set aside when the request starts. One request costs at most **122 credits**, but that amount is never locked up in advance: right before costlier work, SocialTap sets aside more of your balance, the exact price when the number of started minutes of media is known beforehand, otherwise as much as your balance allows (and the work is limited to what that pays for). It then charges the actual price and releases the rest. If the result would cost more than your balance covers, the request is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) as well, and costs nothing. `meta.credits_charged` and the `x-credits-charged` header show what you paid. [How credits work](https://staging.socialtap.dev/docs/credits/index.md) · [Packs](https://staging.socialtap.dev/pricing/index.md) ## Errors Errors cost nothing. Codes marked “Yes, later” under Retry can succeed on a later attempt; the others return the same answer every time. | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `invalid_input` | 400 | No | The request parameters are missing or malformed, or the URL is not a supported link for this endpoint. | Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names. | | `unauthorized` | 401 | No | The x-api-key header is missing, unknown or revoked (or, on the dashboard's routes, the session cookie is missing or expired). | Send a valid API key in the x-api-key header; without one, get a key at details.signup_url (AI agents: POST details.agent_signup_url). | | `forbidden` | 403 | No | The API key is valid but may not make this request: its account is suspended, its scopes do not include the route, it is waiting for its owner's email confirmation, it is past its expiry, or the request came from an IP its allowlist does not include. | Do what details.reason says: if suspended, contact support; otherwise use a key whose scopes cover the route (details.required_scope), have the owner confirm the emailed link, create a new key, or call from an allowed IP. | | `insufficient_credits` | 402 | No | The account does not have enough credits for this request. | Buy a credit pack, then repeat the request; do not retry automatically. | | `spend_limit_reached` | 402 | No | The API key reached its spend limit for the current period; the account may still have credits. | Wait until details.resets_at or raise the key's spend limit (PATCH /v1/api_keys/{id}); do not retry automatically. | | `rate_limited` | 429 | Yes, later | The account exceeded its runaway-protection ceiling: plans do not limit request rate, but a high per-account safety ceiling stops broken client loops. Support raises it on request. | Wait for the number of seconds in the retry-after header, then retry; ask support to raise the ceiling if your sustained traffic needs more. | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | | `no_transcript` | 404 | No | The media exists but has no transcript: no caption track and no speech to transcribe. | Do not retry; try another language with lang if the platform offers one. | | `upstream_blocked` | 503 | Yes, later | The platform refused every attempt. | Retry later with backoff; failed requests are free. | | `upstream_error` | 502 | Yes, later | The platform failed or returned something unexpected. | Retry with backoff; failed requests are free. | | `overloaded` | 503 | Yes, later | SocialTap was momentarily at capacity and did not start the request: nothing was fetched or charged. | Retry after the number of seconds in the retry-after header; failed requests are free. | | `internal` | 500 | Yes, later | Unexpected SocialTap error. | Retry with backoff; contact support with the request_id if it persists. | not_found, private_or_unavailable and no_transcript are item-level: the request was fine, the item can't be returned. ## Test mode A key starting with `st_test_` gets recorded responses, free and the same every time ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). These inputs return their recording; any other input returns the example above. | Input | Answer | | --- | --- | | `https://www.instagram.com/p/De11ONFn3Xk/` | 404 [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | | `https://www.instagram.com/p/De1S3KpCXf0/` Default | 200 OK | | `https://www.instagram.com/p/De4J7SciJp7/` | 200 OK | | `https://www.instagram.com/p/De492EVDJd3/` | 400 [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | | `https://www.instagram.com/p/DexcKhDDno3/?img_index=2` | 404 [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | | `https://www.instagram.com/p/De1S3KpCXf1/` | 404 [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | | `https://www.instagram.com/p/DeuOR-CjKE9/` | 400 [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | | `https://www.instagram.com/p/ChdZ_3UALgB/` | 404 [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | | `https://www.instagram.com/share/reel/_oDaXMUlS/` | 404 [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | | `https://www.instagram.com/p/De6q2K4lYUd/` | 404 [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | | `https://www.instagram.com/p/AAAAAAAAAAA/` | 200 OK | | `https://www.instagram.com/p/AAAAAAAAAAA/?img_index=1` | 400 [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | Magic inputs: send one of these as `url` to get that error. | Error | Value | | --- | --- | | [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | `https://socialtap.dev/test/invalid_input` | | [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) | `https://socialtap.dev/test/insufficient_credits` | | [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) | `https://socialtap.dev/test/spend_limit_reached` | | [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) | `https://socialtap.dev/test/rate_limited` | | [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | `https://socialtap.dev/test/not_found` | | [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | `https://socialtap.dev/test/private_or_unavailable` | | [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | `https://socialtap.dev/test/no_transcript` | | [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | `https://socialtap.dev/test/upstream_blocked` | | [`upstream_error`](https://staging.socialtap.dev/docs/errors/index.md#upstream_error) | `https://socialtap.dev/test/upstream_error` | | [`overloaded`](https://staging.socialtap.dev/docs/errors/index.md#overloaded) | `https://socialtap.dev/test/overloaded` | | [`internal`](https://staging.socialtap.dev/docs/errors/index.md#internal) | `https://socialtap.dev/test/internal` | ## Measured performance - **p50 latency**: 4,125 ms - **p95 latency**: 9,203 ms - **Success rate**: 98.1% - **Requests measured**: 53 **Staging measurements, not production.** Measured on our staging environment, over the 7 days before 2026-10-25. [How it is measured](https://staging.socialtap.dev/status/index.md#method). ## FAQ **How much does the Instagram Reels Transcript API cost?** 1 credit for captions results (meta.source = captions) and 2 credits + 2 per started minute of media, up to 122 credits (60 started minutes of media) for speech-to-text results (meta.source = asr), charged only for successful requests. SocialTap credits cost $0.50–$0.80 per 1,000 depending on the pack. **Am I charged for failed requests?** No. Requests that return an error cost 0 credits. Responses served from the cache are free too. ## Related ### More endpoints returning transcripts - [Facebook Video Transcript API](https://staging.socialtap.dev/apis/facebook/transcript/index.md): Facebook video transcript (From 1 credit · `GET /v1/facebook/transcript`) - [TikTok Transcript API](https://staging.socialtap.dev/apis/tiktok/transcript/index.md): TikTok video transcript (From 1 credit · `GET /v1/tiktok/transcript`) - [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md): YouTube video transcript (From 1 credit · `GET /v1/youtube/transcript`) ### More Instagram endpoints - [Instagram Post API](https://staging.socialtap.dev/apis/instagram/post/index.md): Instagram post, reel or carousel with media and metrics (From 1 credit · `GET /v1/instagram/post`) --- Source: https://staging.socialtap.dev/apis/tiktok/profile/ # TikTok Profile API TikTok · Profile Beta Returns a public TikTok account: user ID, username, display name, bio, bio link, avatar, verification badge, whether the account is private, its business category, join date and exact follower, following, video and total like counts. Accepts a username with or without the @, a tiktok.com profile URL, or a numeric user ID. GET `https://staging.socialtap.dev/v1/tiktok/profile` [Try it in the playground](https://staging.socialtap.dev/playground/?endpoint=tiktok.profile) · [Quickstart](https://staging.socialtap.dev/docs/index.md) - From **1 credit** per request - Sources: Public page ## Use cases - Track creator growth over time - Vet influencers before a campaign - Enrich CRM or lead lists with TikTok accounts - Resolve a post author's ID to a full profile ## Good to know - Private accounts return their profile with is_private true; their videos are not public. - type is organization for accounts TikTok marks as organizations and unknown otherwise: TikTok does not label people. - category is set only for business accounts. location, country_code, banner_url and metrics.total_view_count are always null: public TikTok profiles do not show them. - When the profile page is unavailable the answer comes from TikTok's embed profile: follower and like counts are then rounded (listed in approximate_fields), and the join date, bio link, category, type and video count are unknown. - Share links (vm.tiktok.com) are not accepted: pass the username or the profile URL. ## Example request Send a `GET` request with your key in the `x-api-key` header. cURL: ```shell curl -G 'https://staging.socialtap.dev/v1/tiktok/profile' \ --data-urlencode 'handle=nasa' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` TypeScript: ```typescript const params = new URLSearchParams({ handle: "nasa", }); const res = await fetch(`https://staging.socialtap.dev/v1/tiktok/profile?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const { data, meta } = body; console.log(data, meta.credits_charged); ``` Python: ```python import os import requests res = requests.get( "https://staging.socialtap.dev/v1/tiktok/profile", params={"handle": "nasa"}, headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]}, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") data, meta = body["data"], body["meta"] print(data, meta["credits_charged"]) ``` Go: ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" ) func main() { q := url.Values{} q.Set("handle", "nasa") req, err := http.NewRequest("GET", "https://staging.socialtap.dev/v1/tiktok/profile?"+q.Encode(), nil) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { Data json.RawMessage `json:"data"` Meta map[string]any `json:"meta"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if body.Error != nil { log.Fatalf("%s: %s", body.Error.Code, body.Error.Message) } fmt.Println(string(body.Data), body.Meta["credits_charged"]) } ``` ## Parameters | Name | Type | Description | Example | | --- | --- | --- | --- | | `handle` required | `string`
max 2,048 chars | TikTok username with or without the @ (nasa, @nasa), a profile URL (tiktok.com/@nasa, m.tiktok.com/@nasa, with or without query parameters), or a numeric user ID (tiktok.com/share/user/\ or the bare ID). Usernames are case-insensitive. | `nasa` | `handle` takes TikTok profile links. A link of another kind or platform fails free with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input), naming the endpoint that takes it. Parameters not listed here are rejected (with a suggestion for a likely typo), so a misspelled one never silently changes nothing: see [Requests](https://staging.socialtap.dev/docs/requests/index.md). Every endpoint also accepts [`max_age`](https://staging.socialtap.dev/docs/credits/index.md#param-max_age), [`fields`](https://staging.socialtap.dev/docs/credits/index.md#param-fields). ## Example response A `200` response is `{data, meta}`: `data` is a [profile](https://staging.socialtap.dev/resources/profile/index.md) object, `meta` describes the request ([meta fields](https://staging.socialtap.dev/docs/index.md#response)). 200 OK: ```json { "data": { "approximate_fields": [], "avatar_url": "https://p16-common-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/e5143212a59c09a008bf50f487d54d1f~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=9640&refresh_token=9341391e&x-expires=SCRUBBED&x-signature=SCRUBBED&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5", "banner_url": null, "bio": "Making the seemingly impossible, possible.✨", "category": null, "country_code": null, "created_at": "2026-07-20T15:55:49Z", "display_name": "NASA", "external_links": [ { "title": null, "url": "https://science.nasa.gov/feature/artemis-ii-preliminary-lunar-science-report" } ], "id": "7664638705177150477", "is_private": false, "is_verified": true, "location": null, "metrics": { "follower_count": 2140425, "following_count": 23, "post_count": 60, "total_like_count": 11385816, "total_view_count": null }, "platform": "tiktok", "type": "organization", "url": "https://www.tiktok.com/@nasa", "username": "nasa" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "tiktok", "source": "page", "is_cached": false, "credits_charged": 1, "latency_ms": 0, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "profile", "id": "7664638705177150477", "url": "https://www.tiktok.com/@nasa" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` ## Response fields `data` follows the shared [Profile schema](https://staging.socialtap.dev/resources/profile/index.md), identical on every platform. | Field | Type | Description | | --- | --- | --- | | `data.platform` | `string` | Platform ID (tiktok, youtube, …). | | `data.id` | `string` | Platform-native account ID (string; YouTube UC…, Reddit without t2_). | | `data.username` | `string` or `null` | Handle without the @; null when the account has none. | | `data.display_name` | `string` or `null` | Display name, channel title or page name; null when empty or not exposed. | | `data.url` | `string` (uri) | Canonical profile URL. | | `data.type` | `string` | Coarse account kind, as the platform labels it; unknown when it does not (TikTok labels organizations only). New values may be added; treat unknown values as unknown.
One of `person`, `organization`, `community`, `unknown` | | `data.bio` | `string` or `null` | Profile bio, channel description or page about text; null when empty or not exposed. | | `data.avatar_url` | `string` (uri) or `null` | Profile picture URL (largest available). Platform CDN URLs can expire within hours. | | `data.banner_url` | `string` (uri) or `null` | Header or cover image URL. | | `data.is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `data.is_private` | `boolean` or `null` | True when the account's posts are not public. | | `data.category` | `string` or `null` | Platform category label (e.g. Musician, Media & Entertainment), as shown; not normalized. Often set only for business or creator accounts. | | `data.external_links` | `object[]` | Links in the bio or channel links; \[\] if none or not exposed. | | `data.external_links[].url` | `string` (uri) | Link target, absolute (https:// is added where the platform stores the link without a scheme). | | `data.external_links[].title` | `string` or `null` | Link label shown by the platform. | | `data.location` | `string` or `null` | Self-declared location text. | | `data.country_code` | `string` or `null` | ISO 3166-1 alpha-2 country the platform states for the account. | | `data.created_at` | `string` (date-time) or `null` | Account creation or join date, RFC 3339 UTC (some platforms give only the day: see approximate_fields). | | `data.metrics` | `object` | Counts at fetch time; null = hidden or not exposed, 0 = zero. | | `data.metrics.follower_count` | `integer` or `null` | Followers or subscribers. | | `data.metrics.following_count` | `integer` or `null` | Accounts this profile follows. | | `data.metrics.post_count` | `integer` or `null` | Posts or videos as the platform counts them on the profile (for a private account, posts the caller cannot see). | | `data.metrics.total_like_count` | `integer` or `null` | Likes received across all of the profile's posts (not likes given). | | `data.metrics.total_view_count` | `integer` or `null` | Lifetime views across all of the profile's posts. | | `data.approximate_fields` | `string[]` | JSON paths of values rounded by the platform or parsed from display text (e.g. metrics.follower_count); \[\] when all exact. | ## Pricing You pay only for successful requests. The price depends on which source produced the result, reported as `meta.source`. | Source | Price per successful request | Per 1,000 requests | | --- | --- | --- | | Public page `page` | 1 credit | $0.50–$0.80 | | meta.source | Source | What it means | | --- | --- | --- | | `page` | Public page | Data embedded in the platform's public web page. | Your balance must be at least **1 credit** to start a request; with less, it is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) (HTTP 402) and costs nothing. Only that minimum is set aside when the request starts. `meta.credits_charged` and the `x-credits-charged` header show what you paid. [How credits work](https://staging.socialtap.dev/docs/credits/index.md) · [Packs](https://staging.socialtap.dev/pricing/index.md) ## Errors Errors cost nothing. Codes marked “Yes, later” under Retry can succeed on a later attempt; the others return the same answer every time. | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `invalid_input` | 400 | No | The request parameters are missing or malformed, or the URL is not a supported link for this endpoint. | Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names. | | `unauthorized` | 401 | No | The x-api-key header is missing, unknown or revoked (or, on the dashboard's routes, the session cookie is missing or expired). | Send a valid API key in the x-api-key header; without one, get a key at details.signup_url (AI agents: POST details.agent_signup_url). | | `forbidden` | 403 | No | The API key is valid but may not make this request: its account is suspended, its scopes do not include the route, it is waiting for its owner's email confirmation, it is past its expiry, or the request came from an IP its allowlist does not include. | Do what details.reason says: if suspended, contact support; otherwise use a key whose scopes cover the route (details.required_scope), have the owner confirm the emailed link, create a new key, or call from an allowed IP. | | `insufficient_credits` | 402 | No | The account does not have enough credits for this request. | Buy a credit pack, then repeat the request; do not retry automatically. | | `spend_limit_reached` | 402 | No | The API key reached its spend limit for the current period; the account may still have credits. | Wait until details.resets_at or raise the key's spend limit (PATCH /v1/api_keys/{id}); do not retry automatically. | | `rate_limited` | 429 | Yes, later | The account exceeded its runaway-protection ceiling: plans do not limit request rate, but a high per-account safety ceiling stops broken client loops. Support raises it on request. | Wait for the number of seconds in the retry-after header, then retry; ask support to raise the ceiling if your sustained traffic needs more. | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | | `upstream_blocked` | 503 | Yes, later | The platform refused every attempt. | Retry later with backoff; failed requests are free. | | `upstream_error` | 502 | Yes, later | The platform failed or returned something unexpected. | Retry with backoff; failed requests are free. | | `overloaded` | 503 | Yes, later | SocialTap was momentarily at capacity and did not start the request: nothing was fetched or charged. | Retry after the number of seconds in the retry-after header; failed requests are free. | | `internal` | 500 | Yes, later | Unexpected SocialTap error. | Retry with backoff; contact support with the request_id if it persists. | not_found and private_or_unavailable are item-level: the request was fine, the item can't be returned. ## Test mode A key starting with `st_test_` gets recorded responses, free and the same every time ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). These inputs return their recording; any other input returns the example above. | Input | Answer | | --- | --- | | `qq7x2nouser0z` | 404 [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | | `scout2015` | 200 OK | | `duolingo` | 200 OK | | `@khaby.lame` | 200 OK | | `bts_official_bighit` | 200 OK | | `zzq9x81nouser` | 404 [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | | `https://www.tiktok.com/@nasa` Default | 200 OK | | `7000000000000000001` | 404 [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | | `https://www.tiktok.com/share/user/107955` | 200 OK | Magic inputs: send one of these as `handle` to get that error. | Error | Value | | --- | --- | | [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | `socialtap_test_invalid_input` | | [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) | `socialtap_test_insufficient_credits` | | [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) | `socialtap_test_spend_limit_reached` | | [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) | `socialtap_test_rate_limited` | | [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | `socialtap_test_not_found` | | [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | `socialtap_test_private_or_unavailable` | | [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | `socialtap_test_upstream_blocked` | | [`upstream_error`](https://staging.socialtap.dev/docs/errors/index.md#upstream_error) | `socialtap_test_upstream_error` | | [`overloaded`](https://staging.socialtap.dev/docs/errors/index.md#overloaded) | `socialtap_test_overloaded` | | [`internal`](https://staging.socialtap.dev/docs/errors/index.md#internal) | `socialtap_test_internal` | ## FAQ **How much does the TikTok Profile API cost?** 1 credit for public page results (meta.source = page), charged only for successful requests. SocialTap credits cost $0.50–$0.80 per 1,000 depending on the pack. **Am I charged for failed requests?** No. Requests that return an error cost 0 credits. Responses served from the cache are free too. ## Related ### More TikTok endpoints - [TikTok Transcript API](https://staging.socialtap.dev/apis/tiktok/transcript/index.md): TikTok video transcript (From 1 credit · `GET /v1/tiktok/transcript`) --- Source: https://staging.socialtap.dev/apis/tiktok/transcript/ # TikTok Transcript API TikTok · Transcript Beta Returns the timed transcript of a public TikTok video: creator-edited captions, TikTok's auto-captions, or TikTok's own translation when you ask for another language with lang. Videos without any caption track are transcribed from their audio with speech-to-text. Accepts tiktok.com video, mobile, embed and vm./vt./t/ share links, or a numeric video ID. GET `https://staging.socialtap.dev/v1/tiktok/transcript` [Try it in the playground](https://staging.socialtap.dev/playground/?endpoint=tiktok.transcript) · [Quickstart](https://staging.socialtap.dev/docs/index.md) - From **1 credit** per request, up to **122 credits** - Sources: Captions → Speech-to-text - p50 **668 ms** - **99.1%** success (112 requests measured) ## Use cases - Analyze what creators say at scale - Track brand and product mentions in videos - Feed short-form video into LLM pipelines - Caption and translate TikTok content ## Good to know - Photo posts have no audio track and return no_transcript. - Videos with licensed music and no speech may transcribe the song lyrics. ## Example request Send a `GET` request with your key in the `x-api-key` header. cURL: ```shell curl -G 'https://staging.socialtap.dev/v1/tiktok/transcript' \ --data-urlencode 'url=7673108027645414678' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` TypeScript: ```typescript const params = new URLSearchParams({ url: "7673108027645414678", }); const res = await fetch(`https://staging.socialtap.dev/v1/tiktok/transcript?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const { data, meta } = body; console.log(data, meta.credits_charged); ``` Python: ```python import os import requests res = requests.get( "https://staging.socialtap.dev/v1/tiktok/transcript", params={"url": "7673108027645414678"}, headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]}, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") data, meta = body["data"], body["meta"] print(data, meta["credits_charged"]) ``` Go: ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" ) func main() { q := url.Values{} q.Set("url", "7673108027645414678") req, err := http.NewRequest("GET", "https://staging.socialtap.dev/v1/tiktok/transcript?"+q.Encode(), nil) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { Data json.RawMessage `json:"data"` Meta map[string]any `json:"meta"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if body.Error != nil { log.Fatalf("%s: %s", body.Error.Code, body.Error.Message) } fmt.Println(string(body.Data), body.Meta["credits_charged"]) } ``` ## Parameters | Name | Type | Description | Example | | --- | --- | --- | --- | | `url` required | `string`
max 2,048 chars | TikTok video URL (tiktok.com/@user/video/\, m.tiktok.com/v/\.html, vm.tiktok.com/\, vt.tiktok.com/\, tiktok.com/t/\, embed URLs) or a numeric video ID. | `https://www.tiktok.com/@scout2015/video/6718335390845095173` | | `lang` optional | `string`
bcp47 | Preferred language: a BCP-47 tag such as en or pt-BR (case-insensitive; _ is read as -). Selects the caption track (TikTok also offers its own translations); falls back to the video's original language, then any track. TikTok's three-letter codes (eng-US) work too. | `en` | `url` takes TikTok video and post links. A link of another kind or platform fails free with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input), naming the endpoint that takes it. Parameters not listed here are rejected (with a suggestion for a likely typo), so a misspelled one never silently changes nothing: see [Requests](https://staging.socialtap.dev/docs/requests/index.md). Every endpoint also accepts [`max_age`](https://staging.socialtap.dev/docs/credits/index.md#param-max_age), [`fields`](https://staging.socialtap.dev/docs/credits/index.md#param-fields). ## Example response A `200` response is `{data, meta}`: `data` is a [transcript](https://staging.socialtap.dev/resources/transcript/index.md) object, `meta` describes the request ([meta fields](https://staging.socialtap.dev/docs/index.md#response)). 200 OK: ```json { "data": { "available_languages": [ "en" ], "duration_ms": 495000, "id": "7673108027645414678", "is_auto_generated": false, "language": "en", "platform": "tiktok", "segments": [ { "end_ms": 4820, "start_ms": 100, "text": "Let me show you how to make a Japanese cream stew!" }, { "end_ms": 8380, "start_ms": 4821, "text": "I think there is a way to definitely make it from scratch," }, { "end_ms": 13120, "start_ms": 8381, "text": "but most Japanese people buy the stock for the cream stew." } ], "source": "captions", "text": "Let me show you how to make a Japanese cream stew! I think there is a way to definitely make it from scratch, but most Japanese people buy the stock for the cream stew. If you go to a Japanese supermarket, you can find this next to or near where the curry stock cubes are. This is…", "url": "https://www.tiktok.com/@niinoclock/video/7673108027645414678" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "tiktok", "source": "captions", "is_cached": false, "credits_charged": 1, "latency_ms": 668, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "video", "id": "7673108027645414678", "url": "https://www.tiktok.com/@niinoclock/video/7673108027645414678" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display; real responses are complete. ## Response fields `data` follows the shared [Transcript schema](https://staging.socialtap.dev/resources/transcript/index.md), identical on every platform. | Field | Type | Description | | --- | --- | --- | | `data.platform` | `string` | Platform the media is hosted on. | | `data.id` | `string` | Platform-native ID of the post (equals post.id). | | `data.url` | `string` (uri) | Canonical URL of the post. | | `data.language` | `string` or `null` | BCP-47 language of the returned transcript; null when unknown. | | `data.source` | `string` | captions = platform caption track; asr = speech-to-text on the audio.
One of `captions`, `asr` | | `data.is_auto_generated` | `boolean` | True when the track was machine-generated (platform auto-captions or ASR). | | `data.available_languages` | `string[]` | BCP-47 caption languages the platform offers for this post; \[\] if none or not listed. | | `data.duration_ms` | `integer` or `null` | Media duration in milliseconds; null when unknown. | | `data.segments` | `object[]` | Timed cues in order; never empty on success. | | `data.segments[].start_ms` | `integer` | Start time in milliseconds from the beginning of the media. | | `data.segments[].end_ms` | `integer` | End time in milliseconds from the beginning of the media. | | `data.segments[].text` | `string` | Cue text, whitespace-normalized. | | `data.text` | `string` | Full transcript: segment texts joined with single spaces. | ## Pricing You pay only for successful requests. The price depends on which source produced the result, reported as `meta.source`; sources are tried cheapest first, in the order Captions → Speech-to-text. | Source | Price per successful request | Per 1,000 requests | | --- | --- | --- | | Captions `captions` | 1 credit | $0.50–$0.80 | | Speech-to-text `asr` | 2 credits + 2 per started minute of media
up to 122 credits (60 started minutes of media) | $1.00–$1.60 + $1.00–$1.60 per started minute of media | | meta.source | Source | What it means | | --- | --- | --- | | `captions` | Captions | The platform's own caption track (uploaded or auto-generated). | | `asr` | Speech-to-text | Transcribed by SocialTap from the media's audio when the platform has no captions. | Example: a result from Speech-to-text `asr` with 3 started minutes of media costs 2 + 2 × 3 = **8 credits**. Your balance must be at least **1 credit** to start a request; with less, it is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) (HTTP 402) and costs nothing. Only that minimum is set aside when the request starts. One request costs at most **122 credits**, but that amount is never locked up in advance: right before costlier work, SocialTap sets aside more of your balance, the exact price when the number of started minutes of media is known beforehand, otherwise as much as your balance allows (and the work is limited to what that pays for). It then charges the actual price and releases the rest. If the result would cost more than your balance covers, the request is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) as well, and costs nothing. `meta.credits_charged` and the `x-credits-charged` header show what you paid. [How credits work](https://staging.socialtap.dev/docs/credits/index.md) · [Packs](https://staging.socialtap.dev/pricing/index.md) ## Errors Errors cost nothing. Codes marked “Yes, later” under Retry can succeed on a later attempt; the others return the same answer every time. | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `invalid_input` | 400 | No | The request parameters are missing or malformed, or the URL is not a supported link for this endpoint. | Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names. | | `unauthorized` | 401 | No | The x-api-key header is missing, unknown or revoked (or, on the dashboard's routes, the session cookie is missing or expired). | Send a valid API key in the x-api-key header; without one, get a key at details.signup_url (AI agents: POST details.agent_signup_url). | | `forbidden` | 403 | No | The API key is valid but may not make this request: its account is suspended, its scopes do not include the route, it is waiting for its owner's email confirmation, it is past its expiry, or the request came from an IP its allowlist does not include. | Do what details.reason says: if suspended, contact support; otherwise use a key whose scopes cover the route (details.required_scope), have the owner confirm the emailed link, create a new key, or call from an allowed IP. | | `insufficient_credits` | 402 | No | The account does not have enough credits for this request. | Buy a credit pack, then repeat the request; do not retry automatically. | | `spend_limit_reached` | 402 | No | The API key reached its spend limit for the current period; the account may still have credits. | Wait until details.resets_at or raise the key's spend limit (PATCH /v1/api_keys/{id}); do not retry automatically. | | `rate_limited` | 429 | Yes, later | The account exceeded its runaway-protection ceiling: plans do not limit request rate, but a high per-account safety ceiling stops broken client loops. Support raises it on request. | Wait for the number of seconds in the retry-after header, then retry; ask support to raise the ceiling if your sustained traffic needs more. | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | | `no_transcript` | 404 | No | The media exists but has no transcript: no caption track and no speech to transcribe. | Do not retry; try another language with lang if the platform offers one. | | `upstream_blocked` | 503 | Yes, later | The platform refused every attempt. | Retry later with backoff; failed requests are free. | | `upstream_error` | 502 | Yes, later | The platform failed or returned something unexpected. | Retry with backoff; failed requests are free. | | `overloaded` | 503 | Yes, later | SocialTap was momentarily at capacity and did not start the request: nothing was fetched or charged. | Retry after the number of seconds in the retry-after header; failed requests are free. | | `internal` | 500 | Yes, later | Unexpected SocialTap error. | Retry with backoff; contact support with the request_id if it persists. | not_found, private_or_unavailable and no_transcript are item-level: the request was fine, the item can't be returned. ## Test mode A key starting with `st_test_` gets recorded responses, free and the same every time ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). These inputs return their recording; any other input returns the example above. | Input | Answer | | --- | --- | | `https://www.tiktok.com/@scout2015/video/6718335390845095173` | 200 OK | | `https://www.tiktok.com/@elan.media_/video/7663447693913509152`
with lang=eng-US | 200 OK | | `7689027164263435538`
with lang=de | 200 OK | | `https://www.tiktok.com/@niinoclock/video/7673108027645414678` Default | 200 OK | | `https://www.tiktok.com/embed/v2/7691047892483656974` | 200 OK | | `https://www.tiktok.com/@elan.media_/video/7663447693913509152?is_from_webapp=1&sender_device=pc` | 200 OK | | `7699163285702135073` | 200 OK | | `https://m.tiktok.com/v/7689027164263435538.html`
with lang=ja | 200 OK | Magic inputs: send one of these as `url` to get that error. | Error | Value | | --- | --- | | [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | `https://socialtap.dev/test/invalid_input` | | [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) | `https://socialtap.dev/test/insufficient_credits` | | [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) | `https://socialtap.dev/test/spend_limit_reached` | | [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) | `https://socialtap.dev/test/rate_limited` | | [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | `https://socialtap.dev/test/not_found` | | [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | `https://socialtap.dev/test/private_or_unavailable` | | [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | `https://socialtap.dev/test/no_transcript` | | [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | `https://socialtap.dev/test/upstream_blocked` | | [`upstream_error`](https://staging.socialtap.dev/docs/errors/index.md#upstream_error) | `https://socialtap.dev/test/upstream_error` | | [`overloaded`](https://staging.socialtap.dev/docs/errors/index.md#overloaded) | `https://socialtap.dev/test/overloaded` | | [`internal`](https://staging.socialtap.dev/docs/errors/index.md#internal) | `https://socialtap.dev/test/internal` | ## Measured performance - **p50 latency**: 668 ms - **p95 latency**: 2,705 ms - **Success rate**: 99.1% - **Requests measured**: 112 **Staging measurements, not production.** Measured on our staging environment, over the 7 days before 2026-10-25. [How it is measured](https://staging.socialtap.dev/status/index.md#method). ## FAQ **How much does the TikTok Transcript API cost?** 1 credit for captions results (meta.source = captions) and 2 credits + 2 per started minute of media, up to 122 credits (60 started minutes of media) for speech-to-text results (meta.source = asr), charged only for successful requests. SocialTap credits cost $0.50–$0.80 per 1,000 depending on the pack. **Am I charged for failed requests?** No. Requests that return an error cost 0 credits. Responses served from the cache are free too. ## Related ### More endpoints returning transcripts - [Facebook Video Transcript API](https://staging.socialtap.dev/apis/facebook/transcript/index.md): Facebook video transcript (From 1 credit · `GET /v1/facebook/transcript`) - [Instagram Reels Transcript API](https://staging.socialtap.dev/apis/instagram/transcript/index.md): Instagram reel / video transcript (From 1 credit · `GET /v1/instagram/transcript`) - [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md): YouTube video transcript (From 1 credit · `GET /v1/youtube/transcript`) ### More TikTok endpoints - [TikTok Profile API](https://staging.socialtap.dev/apis/tiktok/profile/index.md): Public TikTok profile and follower counts (From 1 credit · `GET /v1/tiktok/profile`) --- Source: https://staging.socialtap.dev/apis/youtube/comments/ # YouTube Comments API YouTube · Comment Beta Returns the public top-level comments of a YouTube video, short or live replay, a page at a time: text, author handle and channel, posting time, likes, reply count, and whether the comment is pinned, hearted, edited or written by the creator. Sort newest first (the default, which also supports since and until) or by YouTube's Top ranking, and follow page.next_cursor for more. Accepts every YouTube URL form or a bare 11-character video ID. GET `https://staging.socialtap.dev/v1/youtube/comments` [Try it in the playground](https://staging.socialtap.dev/playground/?endpoint=youtube.comments) · [Quickstart](https://staging.socialtap.dev/docs/index.md) - From **1 credit** per page - Pages of up to **20** comments, newest first - Sources: Platform API ## Use cases - Track audience sentiment on your videos and competitors' - Monitor new comments since a date - Find questions and feedback to answer - Build datasets of public comments for research ## Good to know - YouTube shows comment times as relative labels ("3 weeks ago"): published_at is the latest time the label allows and is listed in approximate_fields, so since and until are approximate to the label's unit. - Like and reply counts above a thousand are rounded by YouTube ("4.8K") and listed in approximate_fields. - Comments show the author's @handle; author.display_name is null. - The newest order includes comments YouTube's Top ranking holds back as potential spam. - Videos with comments turned off and private videos return private_or_unavailable; deleted videos return not_found. - Replies are not included; metrics.reply_count says how many a comment has. ## Example request Send a `GET` request with your key in the `x-api-key` header. cURL: ```shell curl -G 'https://staging.socialtap.dev/v1/youtube/comments' \ --data-urlencode 'url=jNQXAC9IVRw' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` TypeScript: ```typescript const params = new URLSearchParams({ url: "jNQXAC9IVRw", }); const res = await fetch(`https://staging.socialtap.dev/v1/youtube/comments?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const { data, page, meta } = body; console.log(data, meta.credits_charged); ``` Python: ```python import os import requests res = requests.get( "https://staging.socialtap.dev/v1/youtube/comments", params={"url": "jNQXAC9IVRw"}, headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]}, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") data, page, meta = body["data"], body["page"], body["meta"] print(data, meta["credits_charged"]) ``` Go: ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" ) func main() { q := url.Values{} q.Set("url", "jNQXAC9IVRw") req, err := http.NewRequest("GET", "https://staging.socialtap.dev/v1/youtube/comments?"+q.Encode(), nil) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { Data json.RawMessage `json:"data"` Page json.RawMessage `json:"page"` Meta map[string]any `json:"meta"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if body.Error != nil { log.Fatalf("%s: %s", body.Error.Code, body.Error.Message) } fmt.Println(string(body.Data), body.Meta["credits_charged"]) } ``` ## Parameters | Name | Type | Description | Example | | --- | --- | --- | --- | | `url` required | `string`
max 2,048 chars | YouTube video URL (watch, youtu.be, shorts, live, embed, m./music. hosts) or an 11-character video ID. | `https://www.youtube.com/watch?v=jNQXAC9IVRw` | | `sort` optional | `string` | Order of the results. newest (default): newest first. popular: YouTube's Top comments ranking. Either way the video's pinned comment comes first.
One of `newest`, `popular` | — | | `since` optional | `string`
date-time | Only items at or after this time: RFC 3339 (2026-10-01T12:00:00Z) or a date (2026-10-01, midnight UTC). On newest-first lists, paging stops when it is reached, so later pages are not fetched or charged. Compared with published_at, which YouTube only shows as a relative label ("2 days ago"): the walk stops at the first comment whose label is older, so comments up to one label unit older can be included. | `2026-10-01` | | `until` optional | `string`
date-time | Only items before this time (exclusive): RFC 3339 or a date (midnight UTC). | `2026-10-08` | | `limit` optional | `integer`
min 1
max 20 | Items per page, 1 to the endpoint's page size (the default and maximum; larger values are reduced). Reduces payload, not price: each page costs the same. | `20` | | `cursor` optional | `string` | page.next_cursor from the previous response. Send the same other parameters; omit to start from the first page. | — | `url` takes YouTube video links. A link of another kind or platform fails free with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input), naming the endpoint that takes it. Parameters not listed here are rejected (with a suggestion for a likely typo), so a misspelled one never silently changes nothing: see [Requests](https://staging.socialtap.dev/docs/requests/index.md). `limit` and `cursor` page through the list: see Pagination. Every endpoint also accepts [`max_age`](https://staging.socialtap.dev/docs/credits/index.md#param-max_age), [`fields`](https://staging.socialtap.dev/docs/credits/index.md#param-fields). ## Example response A `200` response is `{data, page, meta}`: `data` is an array of up to 20 [comment](https://staging.socialtap.dev/resources/comment/index.md) objects, `page` says whether there are more (pagination), `meta` describes the request ([meta fields](https://staging.socialtap.dev/docs/index.md#response)). 200 OK: ```json { "data": [ { "approximate_fields": [ "published_at", "metrics.like_count", "metrics.reply_count" ], "author": { "avatar_url": "https://yt3.ggpht.com/i8RqA9tYqbbnOi4mlnYEY7IR3kXAoDqQbM_5Sfzw7OjTubiGsOaTdlrIZmj-I0q0Vup68eqs=s88-c-k-c0x00ffffff-no-rj", "display_name": null, "id": "UCC5NfQ6Mf0dq_eEwv4P_hWA", "is_verified": true, "url": "https://www.youtube.com/channel/UCC5NfQ6Mf0dq_eEwv4P_hWA", "username": "SanDiegoZoo" }, "edited_at": null, "id": "UgzuC3zzpRZkjc5Qzsd4AaABAg", "is_by_post_author": false, "is_edited": false, "is_liked_by_author": true, "is_pinned": true, "metrics": { "like_count": 4800000, "reply_count": 1000 }, "parent_id": null, "platform": "youtube", "post_id": "jNQXAC9IVRw", "published_at": "2020-10-26T18:59:19Z", "text": "We're so honored that the first ever YouTube video was filmed here!", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw&lc=UgzuC3zzpRZkjc5Qzsd4AaABAg" }, { "approximate_fields": [ "published_at" ], "author": { "avatar_url": "https://yt3.ggpht.com/U9zdJRwdpo6WwsenxMf6AyMyRMBTWoKsyAWCnJXdbPbNKh-YNEWP3AQRgFto91lckyGCBYENnaw=s88-c-k-c0x00ffffff-no-rj", "display_name": null, "id": "UCjoZE_5A9tO470CK3G6U7dg", "is_verified": false, "url": "https://www.youtube.com/channel/UCjoZE_5A9tO470CK3G6U7dg", "username": "CTwGary" }, "edited_at": null, "id": "UgxHbErLhWMUy5GlFnp4AaABAg", "is_by_post_author": false, "is_edited": false, "is_liked_by_author": false, "is_pinned": false, "metrics": { "like_count": 0, "reply_count": 0 }, "parent_id": null, "platform": "youtube", "post_id": "jNQXAC9IVRw", "published_at": "2026-10-26T18:58:19Z", "text": "I can’t believe it, but it’s 21 years old now", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw&lc=UgxHbErLhWMUy5GlFnp4AaABAg" }, { "approximate_fields": [ "published_at" ], "author": { "avatar_url": "https://yt3.ggpht.com/Ar50xvd1O4GCXVttK6DgX0Y0cuhBl2b_HUFQjh-gajKbmS-qjuNAHOT5fw_RX_DS5gzfphkcKAI=s88-c-k-c0x00ffffff-no-rj", "display_name": null, "id": "UCFgjcRO4y8XFQL7E8_TcLlg", "is_verified": false, "url": "https://www.youtube.com/channel/UCFgjcRO4y8XFQL7E8_TcLlg", "username": "Nihal-poke" }, "edited_at": null, "id": "UgzLOxwwIAUe0jUI4RR4AaABAg", "is_by_post_author": false, "is_edited": false, "is_liked_by_author": false, "is_pinned": false, "metrics": { "like_count": 0, "reply_count": 0 }, "parent_id": null, "platform": "youtube", "post_id": "jNQXAC9IVRw", "published_at": "2026-10-26T18:58:19Z", "text": "I watch this video many times 😭", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw&lc=UgzLOxwwIAUe0jUI4RR4AaABAg" } ], "page": { "has_more": true, "next_cursor": "c1.EXAMPLE_CURSOR", "next_cursor_expires_at": "2026-10-26T12:00:00Z", "end_reason": null }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "youtube", "source": "api", "is_cached": false, "credits_charged": 1, "latency_ms": 0, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "video", "id": "jNQXAC9IVRw", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display; real responses are complete. ## Response fields Each item of `data` follows the shared [Comment schema](https://staging.socialtap.dev/resources/comment/index.md), identical on every platform. | Field | Type | Description | | --- | --- | --- | | `data[].platform` | `string` | Platform ID (tiktok, youtube, …). | | `data[].id` | `string` | Platform-native comment ID (string; Reddit without t1_). | | `data[].post_id` | `string` | ID of the post commented on (equals post.id). | | `data[].parent_id` | `string` or `null` | ID of the comment this replies to; null for top-level comments. | | `data[].url` | `string` (uri) or `null` | Permalink, when the platform has one. | | `data[].text` | `string` | Comment body as plain text. | | `data[].author` | `object` | The commenting account. | | `data[].author.id` | `string` or `null` | Platform-native account ID; null when only the handle is known. | | `data[].author.username` | `string` or `null` | Handle without the @; null when the account has none. | | `data[].author.display_name` | `string` or `null` | Display name shown on the profile. | | `data[].author.url` | `string` (uri) or `null` | Canonical profile URL. | | `data[].author.avatar_url` | `string` (uri) or `null` | Profile picture URL. Platform CDN URLs can expire within hours. | | `data[].author.is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `data[].published_at` | `string` (date-time) or `null` | When the comment was posted, RFC 3339 UTC. Where the platform shows only a relative time ("3 weeks ago"), the latest time that label allows (the fetch time minus the stated amount), listed in approximate_fields. | | `data[].edited_at` | `string` (date-time) or `null` | When the comment was last edited, RFC 3339 UTC; null when never edited or when the platform marks edits without a time (see is_edited). | | `data[].is_edited` | `boolean` or `null` | The comment was edited after posting (the platform's edited mark); null when the platform does not say. | | `data[].is_pinned` | `boolean` or `null` | Pinned by the post's author. | | `data[].is_liked_by_author` | `boolean` or `null` | Liked or hearted by the post's author. | | `data[].is_by_post_author` | `boolean` or `null` | Written by the post's author (the creator's own comment, marked as such by the platform). | | `data[].metrics` | `object` | Counts at fetch time; null = hidden or not exposed, 0 = zero. | | `data[].metrics.like_count` | `integer` or `null` | Likes or upvotes; never negative. | | `data[].metrics.reply_count` | `integer` or `null` | Direct replies. | | `data[].approximate_fields` | `string[]` | JSON paths of values rounded by the platform or parsed from display text; \[\] when all exact. | ## Pagination Items come in pages, **newest first**. To get the next page, send **the same request plus `cursor`** set to `page.next_cursor` from the previous response. Keep every other parameter the same (limit, max_age and fields may change); anything else that differs is rejected. When `page.has_more` is `false` you have every item. - **Order**: Newest first - **Page size**: Up to 20 comments (the default; `limit` lowers it) - **Cursor lifetime**: 1 hour; don't store cursors - **Price**: Per page, 1 credit ### The `page` object | Field | Type | Description | | --- | --- | --- | | `page.has_more` | `boolean` | True when another page exists; then next_cursor is set. An empty page with has_more true is not the end. | | `page.next_cursor` | `string` or `null` | Pass as cursor, with the same other parameters, to get the next page; null at the end. | | `page.next_cursor_expires_at` | `string` (date-time) or `null` | When next_cursor stops working, RFC 3339 UTC; null at the end. Don't store cursors. | | `page.end_reason` | `string` or `null` | Why there are no more pages; null while has_more is true. end: the list ended, or reached since/until. platform_limit: the platform stops serving further pages although the list goes on. max_pages: SocialTap's cap on pages in one walk. New values may be added. A short or empty page is not the end: only has_more false is.
One of `end`, `platform_limit`, `max_pages` | ### Next page cURL: next page: ```shell curl -G 'https://staging.socialtap.dev/v1/youtube/comments' \ --data-urlencode 'url=jNQXAC9IVRw' \ --data-urlencode 'cursor=c1.EXAMPLE_CURSOR' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` - The price is per page: each successful page costs the same, whatever `limit` is and however many items it has. Empty pages are charged too (an empty page with `has_more: true` is not the end). - A `limit` below 20 doesn't save credits: the next request serves the rest of the same platform page, which is fetched again and charged as a page each time, and items can shift between those requests if the platform changes the list. Omitting `limit` is the cheapest way to walk the list. - Errors are free, including a rejected cursor. - Pages served from the cache are free. - An expired or altered cursor, or one sent with different parameters, fails with [`invalid_cursor`](https://staging.socialtap.dev/docs/errors/index.md#invalid_cursor) (HTTP 400): Repeat the original request without cursor to start the list again. [Pagination guide](https://staging.socialtap.dev/docs/pagination/index.md) ## Pricing You pay per successful page: every page costs the same whatever its number of items, empty pages included. Errors are free. The price depends on which source produced the result, reported as `meta.source`. | Source | Price per successful page | Per 1,000 pages | | --- | --- | --- | | Platform API `api` | 1 credit | $0.50–$0.80 | | meta.source | Source | What it means | | --- | --- | --- | | `api` | Platform API | The platform's public, logged-out API. | Your balance must be at least **1 credit** to start a request; with less, it is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) (HTTP 402) and costs nothing. Only that minimum is set aside when the request starts. `meta.credits_charged` and the `x-credits-charged` header show what you paid. [How credits work](https://staging.socialtap.dev/docs/credits/index.md) · [Packs](https://staging.socialtap.dev/pricing/index.md) ## Errors Errors cost nothing. Codes marked “Yes, later” under Retry can succeed on a later attempt; the others return the same answer every time. | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `invalid_input` | 400 | No | The request parameters are missing or malformed, or the URL is not a supported link for this endpoint. | Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names. | | `invalid_cursor` | 400 | No | The cursor is malformed, expired, belongs to another endpoint, or the other parameters differ from the request that issued it. | Repeat the original request without cursor to start the list again. | | `unauthorized` | 401 | No | The x-api-key header is missing, unknown or revoked (or, on the dashboard's routes, the session cookie is missing or expired). | Send a valid API key in the x-api-key header; without one, get a key at details.signup_url (AI agents: POST details.agent_signup_url). | | `forbidden` | 403 | No | The API key is valid but may not make this request: its account is suspended, its scopes do not include the route, it is waiting for its owner's email confirmation, it is past its expiry, or the request came from an IP its allowlist does not include. | Do what details.reason says: if suspended, contact support; otherwise use a key whose scopes cover the route (details.required_scope), have the owner confirm the emailed link, create a new key, or call from an allowed IP. | | `insufficient_credits` | 402 | No | The account does not have enough credits for this request. | Buy a credit pack, then repeat the request; do not retry automatically. | | `spend_limit_reached` | 402 | No | The API key reached its spend limit for the current period; the account may still have credits. | Wait until details.resets_at or raise the key's spend limit (PATCH /v1/api_keys/{id}); do not retry automatically. | | `rate_limited` | 429 | Yes, later | The account exceeded its runaway-protection ceiling: plans do not limit request rate, but a high per-account safety ceiling stops broken client loops. Support raises it on request. | Wait for the number of seconds in the retry-after header, then retry; ask support to raise the ceiling if your sustained traffic needs more. | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | | `upstream_blocked` | 503 | Yes, later | The platform refused every attempt. | Retry later with backoff; failed requests are free. | | `upstream_error` | 502 | Yes, later | The platform failed or returned something unexpected. | Retry with backoff; failed requests are free. | | `overloaded` | 503 | Yes, later | SocialTap was momentarily at capacity and did not start the request: nothing was fetched or charged. | Retry after the number of seconds in the retry-after header; failed requests are free. | | `internal` | 500 | Yes, later | Unexpected SocialTap error. | Retry with backoff; contact support with the request_id if it persists. | not_found and private_or_unavailable are item-level: the request was fine, the item can't be returned. ## Test mode A key starting with `st_test_` gets recorded responses, free and the same every time ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). These inputs return their recording; any other input returns the example above. | Input | Answer | | --- | --- | | `HtVdAasjOgU`
with sort=popular | 200 OK | | `XqZsoesa55w` | 403 [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | | `qUTSzYEtUG0` | 200 OK | | `jNQXAC9IVRw` Default | 200 OK | | `dQw4w9WgXcQ`
with since=2026-10-01T00:00:00Z | 200 OK | | `kJQP7kiw5Fk`
with sort=popular | 200 OK | | `Q39EVAstoRM` | 404 [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | | `yZIXLfi8CZQ` | 403 [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | | `3cWm9B_0_kI` | 200 OK | | `jNQXAC9IVRw`
with sort=popular | 200 OK | | `dQw4w9WgXcQ`
with sort=popular | 200 OK | | `E_HgDSKu_50` | 200 OK | Magic inputs: send one of these as `url` to get that error. | Error | Value | | --- | --- | | [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | `https://socialtap.dev/test/invalid_input` | | [`invalid_cursor`](https://staging.socialtap.dev/docs/errors/index.md#invalid_cursor) | `https://socialtap.dev/test/invalid_cursor` | | [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) | `https://socialtap.dev/test/insufficient_credits` | | [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) | `https://socialtap.dev/test/spend_limit_reached` | | [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) | `https://socialtap.dev/test/rate_limited` | | [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | `https://socialtap.dev/test/not_found` | | [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | `https://socialtap.dev/test/private_or_unavailable` | | [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | `https://socialtap.dev/test/upstream_blocked` | | [`upstream_error`](https://staging.socialtap.dev/docs/errors/index.md#upstream_error) | `https://socialtap.dev/test/upstream_error` | | [`overloaded`](https://staging.socialtap.dev/docs/errors/index.md#overloaded) | `https://socialtap.dev/test/overloaded` | | [`internal`](https://staging.socialtap.dev/docs/errors/index.md#internal) | `https://socialtap.dev/test/internal` | ## FAQ **How much does the YouTube Comments API cost?** 1 credit for platform api results (meta.source = api) per page of up to 20 items, charged for every successful page, empty pages included. SocialTap credits cost $0.50–$0.80 per 1,000 depending on the pack. **How do I get the next page?** Repeat the same request with cursor set to page.next_cursor from the previous response, keeping the other parameters. When page.has_more is false there are no more items. Cursors expire after 1 hour. **Am I charged for failed requests?** No. Requests that return an error cost 0 credits. Responses served from the cache are free too. ## Related ### More YouTube endpoints - [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md): YouTube video transcript (From 1 credit · `GET /v1/youtube/transcript`) --- Source: https://staging.socialtap.dev/apis/youtube/transcript/ # YouTube Transcript API YouTube · Transcript Beta Returns the timed transcript of a public YouTube video, short or live replay: the creator's manual captions when they exist, otherwise YouTube's auto-generated track, in the language you ask for. Videos without any caption track are transcribed from their audio with speech-to-text. Accepts every YouTube URL form (watch, youtu.be, shorts, live, embed, music.) or a bare 11-character video ID. GET `https://staging.socialtap.dev/v1/youtube/transcript` [Try it in the playground](https://staging.socialtap.dev/playground/?endpoint=youtube.transcript) · [Quickstart](https://staging.socialtap.dev/docs/index.md) - From **1 credit** per request, up to **122 credits** - Sources: Captions → Speech-to-text - p50 **511 ms** - **96.5%** success (29 requests measured) ## Use cases - Summarize or search video content with LLMs - Build subtitle and translation tools - Index a channel's back catalog for RAG - Repurpose videos into articles and posts ## Good to know - Manual captions are preferred over auto-generated ones; is_auto_generated tells you which you got. - Private, members-only, rental and age-restricted videos return private_or_unavailable. ## Example request Send a `GET` request with your key in the `x-api-key` header. cURL: ```shell curl -G 'https://staging.socialtap.dev/v1/youtube/transcript' \ --data-urlencode 'url=dQw4w9WgXcQ' \ -H "x-api-key: $SOCIALTAP_API_KEY" ``` TypeScript: ```typescript const params = new URLSearchParams({ url: "dQw4w9WgXcQ", }); const res = await fetch(`https://staging.socialtap.dev/v1/youtube/transcript?${params}`, { headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const { data, meta } = body; console.log(data, meta.credits_charged); ``` Python: ```python import os import requests res = requests.get( "https://staging.socialtap.dev/v1/youtube/transcript", params={"url": "dQw4w9WgXcQ"}, headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]}, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") data, meta = body["data"], body["meta"] print(data, meta["credits_charged"]) ``` Go: ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" ) func main() { q := url.Values{} q.Set("url", "dQw4w9WgXcQ") req, err := http.NewRequest("GET", "https://staging.socialtap.dev/v1/youtube/transcript?"+q.Encode(), nil) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { Data json.RawMessage `json:"data"` Meta map[string]any `json:"meta"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if body.Error != nil { log.Fatalf("%s: %s", body.Error.Code, body.Error.Message) } fmt.Println(string(body.Data), body.Meta["credits_charged"]) } ``` ## Parameters | Name | Type | Description | Example | | --- | --- | --- | --- | | `url` required | `string`
max 2,048 chars | YouTube video URL (watch, youtu.be, shorts, live, embed, m./music. hosts) or an 11-character video ID. | `https://www.youtube.com/watch?v=dQw4w9WgXcQ` | | `lang` optional | `string`
bcp47 | Preferred language: a BCP-47 tag such as en or pt-BR (case-insensitive; _ is read as -). Selects the caption track; a manual track is preferred over an auto-generated one. Default: the video's original-language manual track, else its auto-generated track. | `en` | `url` takes YouTube video links. A link of another kind or platform fails free with [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input), naming the endpoint that takes it. Parameters not listed here are rejected (with a suggestion for a likely typo), so a misspelled one never silently changes nothing: see [Requests](https://staging.socialtap.dev/docs/requests/index.md). Every endpoint also accepts [`max_age`](https://staging.socialtap.dev/docs/credits/index.md#param-max_age), [`fields`](https://staging.socialtap.dev/docs/credits/index.md#param-fields). ## Example response A `200` response is `{data, meta}`: `data` is a [transcript](https://staging.socialtap.dev/resources/transcript/index.md) object, `meta` describes the request ([meta fields](https://staging.socialtap.dev/docs/index.md#response)). 200 OK: ```json { "data": { "available_languages": [ "en", "de-DE", "ja" ], "duration_ms": 213000, "id": "dQw4w9WgXcQ", "is_auto_generated": false, "language": "en", "platform": "youtube", "segments": [ { "end_ms": 3040, "start_ms": 1360, "text": "[♪♪♪]" }, { "end_ms": 21880, "start_ms": 18640, "text": "♪ We're no strangers to love ♪" }, { "end_ms": 26960, "start_ms": 22640, "text": "♪ You know the rules and so do I ♪" } ], "source": "captions", "text": "[♪♪♪] ♪ We're no strangers to love ♪ ♪ You know the rules and so do I ♪ ♪ A full commitment's what I'm thinking of ♪ ♪ You wouldn't get this from any other guy ♪ ♪ I just wanna tell you how I'm feeling ♪ ♪ Gotta make you understand ♪ ♪ Never gonna give you up ♪ ♪ Never gonna let …", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "youtube", "source": "captions", "is_cached": false, "credits_charged": 1, "latency_ms": 511, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "video", "id": "dQw4w9WgXcQ", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display; real responses are complete. ## Response fields `data` follows the shared [Transcript schema](https://staging.socialtap.dev/resources/transcript/index.md), identical on every platform. | Field | Type | Description | | --- | --- | --- | | `data.platform` | `string` | Platform the media is hosted on. | | `data.id` | `string` | Platform-native ID of the post (equals post.id). | | `data.url` | `string` (uri) | Canonical URL of the post. | | `data.language` | `string` or `null` | BCP-47 language of the returned transcript; null when unknown. | | `data.source` | `string` | captions = platform caption track; asr = speech-to-text on the audio.
One of `captions`, `asr` | | `data.is_auto_generated` | `boolean` | True when the track was machine-generated (platform auto-captions or ASR). | | `data.available_languages` | `string[]` | BCP-47 caption languages the platform offers for this post; \[\] if none or not listed. | | `data.duration_ms` | `integer` or `null` | Media duration in milliseconds; null when unknown. | | `data.segments` | `object[]` | Timed cues in order; never empty on success. | | `data.segments[].start_ms` | `integer` | Start time in milliseconds from the beginning of the media. | | `data.segments[].end_ms` | `integer` | End time in milliseconds from the beginning of the media. | | `data.segments[].text` | `string` | Cue text, whitespace-normalized. | | `data.text` | `string` | Full transcript: segment texts joined with single spaces. | ## Pricing You pay only for successful requests. The price depends on which source produced the result, reported as `meta.source`; sources are tried cheapest first, in the order Captions → Speech-to-text. | Source | Price per successful request | Per 1,000 requests | | --- | --- | --- | | Captions `captions` | 1 credit | $0.50–$0.80 | | Speech-to-text `asr` | 2 credits + 2 per started minute of media
up to 122 credits (60 started minutes of media) | $1.00–$1.60 + $1.00–$1.60 per started minute of media | | meta.source | Source | What it means | | --- | --- | --- | | `captions` | Captions | The platform's own caption track (uploaded or auto-generated). | | `asr` | Speech-to-text | Transcribed by SocialTap from the media's audio when the platform has no captions. | Example: a result from Speech-to-text `asr` with 3 started minutes of media costs 2 + 2 × 3 = **8 credits**. Your balance must be at least **1 credit** to start a request; with less, it is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) (HTTP 402) and costs nothing. Only that minimum is set aside when the request starts. One request costs at most **122 credits**, but that amount is never locked up in advance: right before costlier work, SocialTap sets aside more of your balance, the exact price when the number of started minutes of media is known beforehand, otherwise as much as your balance allows (and the work is limited to what that pays for). It then charges the actual price and releases the rest. If the result would cost more than your balance covers, the request is refused with [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) as well, and costs nothing. `meta.credits_charged` and the `x-credits-charged` header show what you paid. [How credits work](https://staging.socialtap.dev/docs/credits/index.md) · [Packs](https://staging.socialtap.dev/pricing/index.md) ## Errors Errors cost nothing. Codes marked “Yes, later” under Retry can succeed on a later attempt; the others return the same answer every time. | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `invalid_input` | 400 | No | The request parameters are missing or malformed, or the URL is not a supported link for this endpoint. | Fix the parameter named in param (the message says how); for a link of the wrong kind, call the endpoint the message names. | | `unauthorized` | 401 | No | The x-api-key header is missing, unknown or revoked (or, on the dashboard's routes, the session cookie is missing or expired). | Send a valid API key in the x-api-key header; without one, get a key at details.signup_url (AI agents: POST details.agent_signup_url). | | `forbidden` | 403 | No | The API key is valid but may not make this request: its account is suspended, its scopes do not include the route, it is waiting for its owner's email confirmation, it is past its expiry, or the request came from an IP its allowlist does not include. | Do what details.reason says: if suspended, contact support; otherwise use a key whose scopes cover the route (details.required_scope), have the owner confirm the emailed link, create a new key, or call from an allowed IP. | | `insufficient_credits` | 402 | No | The account does not have enough credits for this request. | Buy a credit pack, then repeat the request; do not retry automatically. | | `spend_limit_reached` | 402 | No | The API key reached its spend limit for the current period; the account may still have credits. | Wait until details.resets_at or raise the key's spend limit (PATCH /v1/api_keys/{id}); do not retry automatically. | | `rate_limited` | 429 | Yes, later | The account exceeded its runaway-protection ceiling: plans do not limit request rate, but a high per-account safety ceiling stops broken client loops. Support raises it on request. | Wait for the number of seconds in the retry-after header, then retry; ask support to raise the ceiling if your sustained traffic needs more. | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | | `no_transcript` | 404 | No | The media exists but has no transcript: no caption track and no speech to transcribe. | Do not retry; try another language with lang if the platform offers one. | | `upstream_blocked` | 503 | Yes, later | The platform refused every attempt. | Retry later with backoff; failed requests are free. | | `upstream_error` | 502 | Yes, later | The platform failed or returned something unexpected. | Retry with backoff; failed requests are free. | | `overloaded` | 503 | Yes, later | SocialTap was momentarily at capacity and did not start the request: nothing was fetched or charged. | Retry after the number of seconds in the retry-after header; failed requests are free. | | `internal` | 500 | Yes, later | Unexpected SocialTap error. | Retry with backoff; contact support with the request_id if it persists. | not_found, private_or_unavailable and no_transcript are item-level: the request was fine, the item can't be returned. ## Test mode A key starting with `st_test_` gets recorded responses, free and the same every time ([test mode](https://staging.socialtap.dev/docs/test-mode/index.md)). These inputs return their recording; any other input returns the example above. | Input | Answer | | --- | --- | | `HtVdAasjOgU` | 403 [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | | `https://www.youtube.com/live/qVv6vCqciTM` | 200 OK | | `jNQXAC9IVRw` | 200 OK | | `dQw4w9WgXcQ`
with lang=en | 200 OK | | `dQw4w9WgXcQ`
with lang=de | 200 OK | | `https://youtu.be/dQw4w9WgXcQ`
with lang=ja | 200 OK | | `jNQXAC9IVRw`
with lang=fr | 404 [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | | `jfKfPfyJRdk` | 404 [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | | `https://www.youtube.com/watch?v=kCc8FmEb1nY` | 200 OK | | `https://www.youtube.com/watch?v=dQw4w9WgXcQ` Default | 200 OK | | `https://www.youtube.com/watch?v=iG9CE55wbtY`
with lang=pt-BR | 200 OK | | `lqQg6PlCWgI` | 404 [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | | `https://www.youtube.com/shorts/tjjjtzRLHvA` | 404 [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | | `https://music.youtube.com/watch?v=kJQP7kiw5Fk` | 200 OK | | `Q39EVAstoRM` | 404 [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | | `yYr8q0y5Jfg` | 403 [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | | `yZIXLfi8CZQ` | 403 [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | | `6SJNVb0GnPI` | 404 [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | Magic inputs: send one of these as `url` to get that error. | Error | Value | | --- | --- | | [`invalid_input`](https://staging.socialtap.dev/docs/errors/index.md#invalid_input) | `https://socialtap.dev/test/invalid_input` | | [`insufficient_credits`](https://staging.socialtap.dev/docs/errors/index.md#insufficient_credits) | `https://socialtap.dev/test/insufficient_credits` | | [`spend_limit_reached`](https://staging.socialtap.dev/docs/errors/index.md#spend_limit_reached) | `https://socialtap.dev/test/spend_limit_reached` | | [`rate_limited`](https://staging.socialtap.dev/docs/errors/index.md#rate_limited) | `https://socialtap.dev/test/rate_limited` | | [`not_found`](https://staging.socialtap.dev/docs/errors/index.md#not_found) | `https://socialtap.dev/test/not_found` | | [`private_or_unavailable`](https://staging.socialtap.dev/docs/errors/index.md#private_or_unavailable) | `https://socialtap.dev/test/private_or_unavailable` | | [`no_transcript`](https://staging.socialtap.dev/docs/errors/index.md#no_transcript) | `https://socialtap.dev/test/no_transcript` | | [`upstream_blocked`](https://staging.socialtap.dev/docs/errors/index.md#upstream_blocked) | `https://socialtap.dev/test/upstream_blocked` | | [`upstream_error`](https://staging.socialtap.dev/docs/errors/index.md#upstream_error) | `https://socialtap.dev/test/upstream_error` | | [`overloaded`](https://staging.socialtap.dev/docs/errors/index.md#overloaded) | `https://socialtap.dev/test/overloaded` | | [`internal`](https://staging.socialtap.dev/docs/errors/index.md#internal) | `https://socialtap.dev/test/internal` | ## Measured performance - **p50 latency**: 511 ms - **p95 latency**: 989 ms - **Success rate**: 96.5% - **Requests measured**: 29 **Staging measurements, not production.** Measured on our staging environment, over the 7 days before 2026-10-25. [How it is measured](https://staging.socialtap.dev/status/index.md#method). ## FAQ **How much does the YouTube Transcript API cost?** 1 credit for captions results (meta.source = captions) and 2 credits + 2 per started minute of media, up to 122 credits (60 started minutes of media) for speech-to-text results (meta.source = asr), charged only for successful requests. SocialTap credits cost $0.50–$0.80 per 1,000 depending on the pack. **Am I charged for failed requests?** No. Requests that return an error cost 0 credits. Responses served from the cache are free too. ## Related ### More endpoints returning transcripts - [Facebook Video Transcript API](https://staging.socialtap.dev/apis/facebook/transcript/index.md): Facebook video transcript (From 1 credit · `GET /v1/facebook/transcript`) - [Instagram Reels Transcript API](https://staging.socialtap.dev/apis/instagram/transcript/index.md): Instagram reel / video transcript (From 1 credit · `GET /v1/instagram/transcript`) - [TikTok Transcript API](https://staging.socialtap.dev/apis/tiktok/transcript/index.md): TikTok video transcript (From 1 credit · `GET /v1/tiktok/transcript`) ### More YouTube endpoints - [YouTube Comments API](https://staging.socialtap.dev/apis/youtube/comments/index.md): Top-level comments of a YouTube video (From 1 credit per page · `GET /v1/youtube/comments`) --- Source: https://staging.socialtap.dev/resources/ad/ # Ad API A public ad library entry: advertiser, creatives, run dates, placements and, where disclosed, reach and spend ranges. ## Endpoints 1 endpoint returns ads on Facebook. - [Facebook Ad Library Search API](https://staging.socialtap.dev/apis/facebook/search_ads/index.md): Search Meta's Ad Library by keyword (From 1 credit per page · `GET /v1/facebook/search_ads`) ## Schema Every endpoint above returns an array of these objects as `data`, with a [`page`](https://staging.socialtap.dev/docs/pagination/index.md) object. The standard [`meta`](https://staging.socialtap.dev/docs/index.md#response) comes with every response. | Field | Type | Description | | --- | --- | --- | | `platform` | `string` | Library owner's platform ID (facebook for Meta's library, tiktok, google, linkedin). | | `id` | `string` | Library ad ID (string). | | `url` | `string` (uri) | Public library page of the ad. | | `advertiser` | `object` | The advertising page, business or account. | | `advertiser.id` | `string` or `null` | Platform-native account ID; null when only the handle is known. | | `advertiser.username` | `string` or `null` | Handle without the @; null when the account has none. | | `advertiser.display_name` | `string` or `null` | Display name shown on the profile. | | `advertiser.url` | `string` (uri) or `null` | Canonical profile URL. | | `advertiser.avatar_url` | `string` (uri) or `null` | Profile picture URL. Platform CDN URLs can expire within hours. | | `advertiser.is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `paid_by` | `string` or `null` | Disclosed payer ("Paid for by"). | | `is_active` | `boolean` or `null` | True while the ad is running. | | `first_shown_at` | `string` (date-time) or `null` | First delivery, RFC 3339 UTC (often date-only: see approximate_fields). | | `last_shown_at` | `string` (date-time) or `null` | Last delivery, RFC 3339 UTC; null while active. | | `type` | `string` | Creative shape. New values may be added; treat unknown values as other.
One of `image`, `video`, `carousel`, `text`, `dynamic`, `other` | | `publisher_platforms` | `string[]` | Where it ran, lowercase IDs (facebook, instagram, messenger, audience_network, youtube, …); \[\] if not disclosed. | | `creatives` | `object[]` | The ad's creatives: one per card of a carousel and per variation of a dynamic or catalog ad, else one for the ad itself; \[\] if none are shown. | | `creatives[].title` | `string` or `null` | Headline. | | `creatives[].body` | `string` or `null` | Primary text. | | `creatives[].link_url` | `string` (uri) or `null` | Destination URL. | | `creatives[].link_description` | `string` or `null` | Link description text. | | `creatives[].cta_text` | `string` or `null` | Call-to-action label (e.g. Shop now). | | `creatives[].media` | `object[]` | Images and videos of this creative; \[\] if none. | | `creatives[].media[].type` | `string` | Kind of file. New values may be added; treat unknown values as other.
One of `image`, `video`, `audio`, `gif` | | `creatives[].media[].url` | `string` (uri) or `null` | Direct file URL; null when the platform withholds it, or when the link would only work from SocialTap's network or with a login. Often signed and expiring: see expires_at. | | `creatives[].media[].expires_at` | `string` (date-time) or `null` | When url stops working, RFC 3339 UTC, read from the platform's signed URL; null when unknown or when it does not expire. | | `creatives[].media[].thumbnail_url` | `string` (uri) or `null` | Poster or preview image URL. | | `creatives[].media[].width` | `integer` or `null` | Width in pixels. | | `creatives[].media[].height` | `integer` or `null` | Height in pixels. | | `creatives[].media[].duration_ms` | `integer` or `null` | Duration in milliseconds (video and audio only). | | `creatives[].media[].alt_text` | `string` or `null` | Accessibility text provided by the author or platform. | | `languages` | `string[]` | BCP-47 languages of the ad; \[\] if not disclosed. | | `country_codes` | `string[]` | ISO 3166-1 alpha-2 countries where it was shown; \[\] if not disclosed. | | `is_political` | `boolean` or `null` | Categorized as a political or social-issue ad; false when the library files it under another category, null when it shows none. | | `impressions` | `object` or `null` | Impressions or times shown range; null when not disclosed. | | `impressions.lower_bound` | `integer` | Inclusive lower bound. | | `impressions.upper_bound` | `integer` or `null` | Inclusive upper bound; null when open-ended (e.g. ">1M"). Equal to lower_bound when exact. | | `reach` | `object` or `null` | Unique users reached range; null when not disclosed. | | `reach.lower_bound` | `integer` | Inclusive lower bound. | | `reach.upper_bound` | `integer` or `null` | Inclusive upper bound; null when open-ended (e.g. ">1M"). Equal to lower_bound when exact. | | `audience_size` | `object` or `null` | Estimated target audience size range; null when not disclosed. | | `audience_size.lower_bound` | `integer` | Inclusive lower bound. | | `audience_size.upper_bound` | `integer` or `null` | Inclusive upper bound; null when open-ended (e.g. ">1M"). Equal to lower_bound when exact. | | `spend` | `object` or `null` | Spend range; null when not disclosed. | | `spend.lower_bound` | `integer` | Inclusive lower bound, in whole units of currency. | | `spend.upper_bound` | `integer` or `null` | Inclusive upper bound in whole units of currency; null when open-ended. | | `spend.currency` | `string` | ISO 4217 currency code, uppercase (USD). | | `approximate_fields` | `string[]` | JSON paths of values that are rounded, date-only or parsed from display text; \[\] when all exact. | Example (Facebook): ```json { "data": [ { "advertiser": { "avatar_url": "https://scontent-atl3-2.xx.fbcdn.net/v/t39.35426-6/739782028_1533198008291919_1079542453788497101_n.jpg?stp=dst-jpg_s60x60_tt6&_nc_cat=104&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-2.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&…", "display_name": "American Health Support Community", "id": "966175593248497", "is_verified": null, "url": "https://www.facebook.com/61588018263996", "username": null }, "approximate_fields": [ "first_shown_at" ], "audience_size": null, "country_codes": [], "creatives": [ { "body": "My aunt called me a creep for putting a camera in Nana’s living room.\n\nNana called me practical.\n\nThen the camera caught something worse than a fall.\n\nI need to be clear about one thing before anyone decides what kind of grandson I am.\n\nNana asked for the camera.\n\nNot me.\n\nNot my…", "cta_text": "Learn more", "link_description": null, "link_url": "https://shop.pipitea.com/hbt/kd/sp-nm", "media": [ { "alt_text": null, "duration_ms": null, "expires_at": null, "height": null, "thumbnail_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/742001725_2100792434176297_9060645049521596104_n.jpg?stp=dst-jpg_s600x600_tt6&_nc_cat=100&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBE…", "type": "image", "url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/742501415_1706621370582971_4085631001896338504_n.jpg?_nc_cat=103&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&oe=SCRUBBED", "width": null } ], "title": "Natural Relief in Weeks" } ], "first_shown_at": "2026-07-18T07:00:00Z", "id": "2619351891814847", "impressions": null, "is_active": true, "is_political": false, "languages": [], "last_shown_at": null, "paid_by": null, "platform": "facebook", "publisher_platforms": [ "facebook", "instagram", "audience_network" ], "reach": null, "spend": null, "type": "image", "url": "https://www.facebook.com/ads/library/?id=2619351891814847" }, { "advertiser": { "avatar_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/712623693_1599994994434095_1320701223312676781_n.jpg?stp=dst-jpg_s60x60_tt6&_nc_cat=110&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&…", "display_name": "American Health Support Community", "id": "966175593248497", "is_verified": null, "url": "https://www.facebook.com/61588018263996", "username": null }, "approximate_fields": [ "first_shown_at" ], "audience_size": null, "country_codes": [], "creatives": [ { "body": "I almost looked past the woman who saved my husband's life. She was seventy-nine, sitting alone at the booth next to ours at the Cracker Barrel off Highway 31. A glass of milk in front of her. She kept stirring it. She wasn't eating.\n\nIf I'd known, the night I leaned over and ask…", "cta_text": "Learn more", "link_description": "\"I've tried everything to support my kidney function naturally. Fish oil, CoQ10, cutting protein, stressing about every meal. Nothing moved the needle enough to satisfy my doctor. Hibiscus tea is the first thing that actually showed up in my numbers. It's been 3 months and my eGF…", "link_url": "https://shop.pipitea.com/hbt/kd/sp", "media": [ { "alt_text": null, "duration_ms": null, "expires_at": null, "height": null, "thumbnail_url": "https://scontent-atl3-2.xx.fbcdn.net/v/t39.35426-6/711729685_1717035472767079_186136568392095276_n.jpg?stp=dst-jpg_s600x600_tt6&_nc_cat=104&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-2.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED…", "type": "image", "url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/713729791_868879862958547_7409366389733397444_n.jpg?_nc_cat=107&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&oe=SCRUBBED", "width": null } ], "title": "Natural Relief in Weeks" } ], "first_shown_at": "2026-06-01T07:00:00Z", "id": "1923645134954720", "impressions": null, "is_active": true, "is_political": false, "languages": [], "last_shown_at": null, "paid_by": null, "platform": "facebook", "publisher_platforms": [ "facebook", "instagram", "audience_network" ], "reach": null, "spend": null, "type": "image", "url": "https://www.facebook.com/ads/library/?id=1923645134954720" }, { "advertiser": { "avatar_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/712025330_1303417705276292_736671271727941526_n.jpg?stp=dst-jpg_s60x60_tt6&_nc_cat=103&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&o…", "display_name": "American Health Support Community", "id": "966175593248497", "is_verified": null, "url": "https://www.facebook.com/61588018263996", "username": null }, "approximate_fields": [ "first_shown_at" ], "audience_size": null, "country_codes": [], "creatives": [ { "body": "I almost looked past the woman who saved my husband's life. She was seventy-nine, sitting alone at the booth next to ours at the Cracker Barrel off Highway 31. A glass of milk in front of her. She kept stirring it. She wasn't eating.\n\nIf I'd known, the night I leaned over and ask…", "cta_text": "Learn more", "link_description": "\"I've tried everything to support my kidney function naturally. Fish oil, CoQ10, cutting protein, stressing about every meal. Nothing moved the needle enough to satisfy my doctor. Hibiscus tea is the first thing that actually showed up in my numbers. It's been 3 months and my eGF…", "link_url": "https://shop.pipitea.com/hbt/kd/sp", "media": [ { "alt_text": null, "duration_ms": null, "expires_at": null, "height": null, "thumbnail_url": "https://scontent-atl3-1.xx.fbcdn.net/v/t39.35426-6/711737708_4485707625086258_2353150069319301753_n.jpg?stp=dst-jpg_s600x600_tt6&_nc_cat=109&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-1.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBE…", "type": "image", "url": "https://scontent-atl3-2.xx.fbcdn.net/v/t39.35426-6/710755472_911440771966686_1736050619245168045_n.jpg?_nc_cat=111&ccb=SCRUBBED&_nc_sid=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=14&_nc_ht=scontent-atl3-2.xx&_nc_gid=SCRUBBED&_nc_ss=7b289&oh=SCRUBBED&oe=SCRUBBED", "width": null } ], "title": "Natural Relief in Weeks" } ], "first_shown_at": "2026-06-01T07:00:00Z", "id": "2140497416496021", "impressions": null, "is_active": true, "is_political": false, "languages": [], "last_shown_at": null, "paid_by": null, "platform": "facebook", "publisher_platforms": [ "facebook", "instagram", "audience_network" ], "reach": null, "spend": null, "type": "image", "url": "https://www.facebook.com/ads/library/?id=2140497416496021" } ], "page": { "has_more": true, "next_cursor": "c1.EXAMPLE_CURSOR", "next_cursor_expires_at": "2026-10-26T12:00:00Z", "end_reason": null }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "facebook", "source": "api", "is_cached": false, "credits_charged": 1, "latency_ms": 0, "fetched_at": "2026-10-25T12:00:00Z", "input": null, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display. ## Item errors Besides the [request-level errors](https://staging.socialtap.dev/docs/errors/index.md) every endpoint can return, ad endpoints return: | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | --- Source: https://staging.socialtap.dev/resources/comment/ # Comment API A public comment or reply on a post, with its author, thread position and like/reply counts. ## Endpoints 1 endpoint returns comments on YouTube. - [YouTube Comments API](https://staging.socialtap.dev/apis/youtube/comments/index.md): Top-level comments of a YouTube video (From 1 credit per page · `GET /v1/youtube/comments`) ## Schema Every endpoint above returns an array of these objects as `data`, with a [`page`](https://staging.socialtap.dev/docs/pagination/index.md) object. The standard [`meta`](https://staging.socialtap.dev/docs/index.md#response) comes with every response. | Field | Type | Description | | --- | --- | --- | | `platform` | `string` | Platform ID (tiktok, youtube, …). | | `id` | `string` | Platform-native comment ID (string; Reddit without t1_). | | `post_id` | `string` | ID of the post commented on (equals post.id). | | `parent_id` | `string` or `null` | ID of the comment this replies to; null for top-level comments. | | `url` | `string` (uri) or `null` | Permalink, when the platform has one. | | `text` | `string` | Comment body as plain text. | | `author` | `object` | The commenting account. | | `author.id` | `string` or `null` | Platform-native account ID; null when only the handle is known. | | `author.username` | `string` or `null` | Handle without the @; null when the account has none. | | `author.display_name` | `string` or `null` | Display name shown on the profile. | | `author.url` | `string` (uri) or `null` | Canonical profile URL. | | `author.avatar_url` | `string` (uri) or `null` | Profile picture URL. Platform CDN URLs can expire within hours. | | `author.is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `published_at` | `string` (date-time) or `null` | When the comment was posted, RFC 3339 UTC. Where the platform shows only a relative time ("3 weeks ago"), the latest time that label allows (the fetch time minus the stated amount), listed in approximate_fields. | | `edited_at` | `string` (date-time) or `null` | When the comment was last edited, RFC 3339 UTC; null when never edited or when the platform marks edits without a time (see is_edited). | | `is_edited` | `boolean` or `null` | The comment was edited after posting (the platform's edited mark); null when the platform does not say. | | `is_pinned` | `boolean` or `null` | Pinned by the post's author. | | `is_liked_by_author` | `boolean` or `null` | Liked or hearted by the post's author. | | `is_by_post_author` | `boolean` or `null` | Written by the post's author (the creator's own comment, marked as such by the platform). | | `metrics` | `object` | Counts at fetch time; null = hidden or not exposed, 0 = zero. | | `metrics.like_count` | `integer` or `null` | Likes or upvotes; never negative. | | `metrics.reply_count` | `integer` or `null` | Direct replies. | | `approximate_fields` | `string[]` | JSON paths of values rounded by the platform or parsed from display text; \[\] when all exact. | Example (YouTube): ```json { "data": [ { "approximate_fields": [ "published_at", "metrics.like_count", "metrics.reply_count" ], "author": { "avatar_url": "https://yt3.ggpht.com/i8RqA9tYqbbnOi4mlnYEY7IR3kXAoDqQbM_5Sfzw7OjTubiGsOaTdlrIZmj-I0q0Vup68eqs=s88-c-k-c0x00ffffff-no-rj", "display_name": null, "id": "UCC5NfQ6Mf0dq_eEwv4P_hWA", "is_verified": true, "url": "https://www.youtube.com/channel/UCC5NfQ6Mf0dq_eEwv4P_hWA", "username": "SanDiegoZoo" }, "edited_at": null, "id": "UgzuC3zzpRZkjc5Qzsd4AaABAg", "is_by_post_author": false, "is_edited": false, "is_liked_by_author": true, "is_pinned": true, "metrics": { "like_count": 4800000, "reply_count": 1000 }, "parent_id": null, "platform": "youtube", "post_id": "jNQXAC9IVRw", "published_at": "2020-10-26T18:59:19Z", "text": "We're so honored that the first ever YouTube video was filmed here!", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw&lc=UgzuC3zzpRZkjc5Qzsd4AaABAg" }, { "approximate_fields": [ "published_at" ], "author": { "avatar_url": "https://yt3.ggpht.com/U9zdJRwdpo6WwsenxMf6AyMyRMBTWoKsyAWCnJXdbPbNKh-YNEWP3AQRgFto91lckyGCBYENnaw=s88-c-k-c0x00ffffff-no-rj", "display_name": null, "id": "UCjoZE_5A9tO470CK3G6U7dg", "is_verified": false, "url": "https://www.youtube.com/channel/UCjoZE_5A9tO470CK3G6U7dg", "username": "CTwGary" }, "edited_at": null, "id": "UgxHbErLhWMUy5GlFnp4AaABAg", "is_by_post_author": false, "is_edited": false, "is_liked_by_author": false, "is_pinned": false, "metrics": { "like_count": 0, "reply_count": 0 }, "parent_id": null, "platform": "youtube", "post_id": "jNQXAC9IVRw", "published_at": "2026-10-26T18:58:19Z", "text": "I can’t believe it, but it’s 21 years old now", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw&lc=UgxHbErLhWMUy5GlFnp4AaABAg" }, { "approximate_fields": [ "published_at" ], "author": { "avatar_url": "https://yt3.ggpht.com/Ar50xvd1O4GCXVttK6DgX0Y0cuhBl2b_HUFQjh-gajKbmS-qjuNAHOT5fw_RX_DS5gzfphkcKAI=s88-c-k-c0x00ffffff-no-rj", "display_name": null, "id": "UCFgjcRO4y8XFQL7E8_TcLlg", "is_verified": false, "url": "https://www.youtube.com/channel/UCFgjcRO4y8XFQL7E8_TcLlg", "username": "Nihal-poke" }, "edited_at": null, "id": "UgzLOxwwIAUe0jUI4RR4AaABAg", "is_by_post_author": false, "is_edited": false, "is_liked_by_author": false, "is_pinned": false, "metrics": { "like_count": 0, "reply_count": 0 }, "parent_id": null, "platform": "youtube", "post_id": "jNQXAC9IVRw", "published_at": "2026-10-26T18:58:19Z", "text": "I watch this video many times 😭", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw&lc=UgzLOxwwIAUe0jUI4RR4AaABAg" } ], "page": { "has_more": true, "next_cursor": "c1.EXAMPLE_CURSOR", "next_cursor_expires_at": "2026-10-26T12:00:00Z", "end_reason": null }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "youtube", "source": "api", "is_cached": false, "credits_charged": 1, "latency_ms": 0, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "video", "id": "jNQXAC9IVRw", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display. ## Item errors Besides the [request-level errors](https://staging.socialtap.dev/docs/errors/index.md) every endpoint can return, comment endpoints return: | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | --- Source: https://staging.socialtap.dev/resources/post/ # Post API A public post (video, reel, short, photo, carousel, tweet or text) with its author, media and engagement metrics. ## Endpoints 1 endpoint returns posts on Instagram. - [Instagram Post API](https://staging.socialtap.dev/apis/instagram/post/index.md): Instagram post, reel or carousel with media and metrics (From 1 credit · `GET /v1/instagram/post`) ## Schema Every endpoint above returns this object as `data`. The standard [`meta`](https://staging.socialtap.dev/docs/index.md#response) comes with every response. | Field | Type | Description | | --- | --- | --- | | `platform` | `string` | Platform ID (tiktok, youtube, …). | | `id` | `string` | Platform-native post ID (string; Instagram shortcode, Reddit without t3_). | | `url` | `string` (uri) | Canonical post URL. | | `type` | `string` | Normalized content shape. New values may be added; treat unknown values as other.
One of `video`, `image`, `carousel`, `text`, `link`, `audio` | | `platform_type` | `string` or `null` | The platform's own product label, lowercase (reel, short, story, live, tweet, …). | | `title` | `string` or `null` | Headline (YouTube title, Reddit title); null on caption-only platforms. | | `text` | `string` or `null` | Caption, description or body as plain text. | | `language` | `string` or `null` | BCP-47 language of text, when the platform states it. | | `published_at` | `string` (date-time) or `null` | When the post went public, RFC 3339 UTC. | | `author` | `object` | The posting account. | | `author.id` | `string` or `null` | Platform-native account ID; null when only the handle is known. | | `author.username` | `string` or `null` | Handle without the @; null when the account has none. | | `author.display_name` | `string` or `null` | Display name shown on the profile. | | `author.url` | `string` (uri) or `null` | Canonical profile URL. | | `author.avatar_url` | `string` (uri) or `null` | Profile picture URL. Platform CDN URLs can expire within hours. | | `author.is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `coauthors` | `object[]` | Other accounts credited as co-authors of the post (Instagram and Facebook collabs, YouTube collaborations), in the platform's order, author excluded; \[\] if none. | | `coauthors[].id` | `string` or `null` | Platform-native account ID; null when only the handle is known. | | `coauthors[].username` | `string` or `null` | Handle without the @; null when the account has none. | | `coauthors[].display_name` | `string` or `null` | Display name shown on the profile. | | `coauthors[].url` | `string` (uri) or `null` | Canonical profile URL. | | `coauthors[].avatar_url` | `string` (uri) or `null` | Profile picture URL. Platform CDN URLs can expire within hours. | | `coauthors[].is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `duration_ms` | `integer` or `null` | Length of the primary video or audio in milliseconds; null for other posts. | | `thumbnail_url` | `string` (uri) or `null` | Cover or poster image URL. | | `media` | `object[]` | Attachments in display order (carousel items); \[\] for text posts. | | `media[].type` | `string` | Kind of file. New values may be added; treat unknown values as other.
One of `image`, `video`, `audio`, `gif` | | `media[].url` | `string` (uri) or `null` | Direct file URL; null when the platform withholds it, or when the link would only work from SocialTap's network or with a login. Often signed and expiring: see expires_at. | | `media[].expires_at` | `string` (date-time) or `null` | When url stops working, RFC 3339 UTC, read from the platform's signed URL; null when unknown or when it does not expire. | | `media[].thumbnail_url` | `string` (uri) or `null` | Poster or preview image URL. | | `media[].width` | `integer` or `null` | Width in pixels. | | `media[].height` | `integer` or `null` | Height in pixels. | | `media[].duration_ms` | `integer` or `null` | Duration in milliseconds (video and audio only). | | `media[].alt_text` | `string` or `null` | Accessibility text provided by the author or platform. | | `hashtags` | `string[]` | Hashtags without #, case kept; \[\] if none. | | `mentions` | `string[]` | Mentioned or tagged usernames without @; \[\] if none. | | `is_pinned` | `boolean` or `null` | Pinned to the author's profile. | | `is_sponsored` | `boolean` or `null` | Labeled as a paid partnership or branded content. | | `is_nsfw` | `boolean` or `null` | Marked adult or sensitive by the platform. | | `metrics` | `object` | Counts at fetch time; null = hidden or not exposed, 0 = zero. | | `metrics.view_count` | `integer` or `null` | Views or plays. | | `metrics.like_count` | `integer` or `null` | Likes, hearts, total reactions or upvotes; never negative. Null when hidden by the owner. | | `metrics.comment_count` | `integer` or `null` | Comments and replies. | | `metrics.share_count` | `integer` or `null` | Shares or sends (not reposts). | | `metrics.repost_count` | `integer` or `null` | Reposts, retweets or reshares. | | `metrics.quote_count` | `integer` or `null` | Quote posts. | | `metrics.save_count` | `integer` or `null` | Saves, bookmarks or favorites. | | `approximate_fields` | `string[]` | JSON paths of values rounded by the platform or parsed from display text; \[\] when all exact. | Example (Instagram): ```json { "data": { "approximate_fields": [], "author": { "avatar_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.82787-19/683576066_18653628823019133_9051036240972105113_n.jpg?stp=dst-jpg_s150x150_tt6&_nc_cat=1&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=24&_nc_ht=scontent-atl3-2.cdninstagram.com&_nc_gid=S…", "display_name": "National Geographic", "id": "787132", "is_verified": true, "url": "https://www.instagram.com/natgeo/", "username": "natgeo" }, "coauthors": [ { "avatar_url": "https://scontent-atl3-1.cdninstagram.com/v/t51.2885-19/293493364_162817252949028_8275460966213229030_n.jpg?stp=dst-jpg_s150x150_tt6&_nc_cat=108&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=24&_nc_ht=scontent-atl3-1.cdninstagram.com&_nc_ss=7b6…", "display_name": "National Geographic Society", "id": "4757225830", "is_verified": true, "url": "https://www.instagram.com/natgeosociety/", "username": "natgeosociety" }, { "avatar_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.2885-19/11419091_505296402966021_797003013_a.jpg?_nc_cat=101&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=24&_nc_ht=scontent-atl3-2.cdninstagram.com&_nc_ss=7b689&oh=SCRUBBED&oe=SCRUBBED", "display_name": "Joel Sartore- Photo Ark", "id": "225429667", "is_verified": true, "url": "https://www.instagram.com/joelsartore/", "username": "joelsartore" }, { "avatar_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.82787-19/681945023_18592998970017301_7811445524976503069_n.jpg?stp=dst-jpg_s150x150_tt6&_nc_cat=1&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=24&_nc_ht=scontent-atl3-2.cdninstagram.com&_nc_gid=S…", "display_name": "National Geographic Animals", "id": "1029649300", "is_verified": true, "url": "https://www.instagram.com/natgeoanimals/", "username": "natgeoanimals" } ], "duration_ms": 23744, "hashtags": [ "PhotoArk" ], "id": "De1S3KpCXf0", "is_nsfw": null, "is_pinned": null, "is_sponsored": null, "language": null, "media": [ { "alt_text": "Video by National Geographic on October 23, 2026. May be an image of snail and text.", "duration_ms": 23744, "expires_at": null, "height": 1920, "thumbnail_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.82787-15/862543567_18711062458019133_8845814561108513720_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=105&ig_cache_key=NDAxNDE5NzU5ODcwNDk5MDE5NjE4NzExMDYyNDUyMDE5MTMz.3-ccb7-5&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=…", "type": "video", "url": "https://scontent-atl3-1.cdninstagram.com/o1/v/t2/f2/m86/AQO0Zc3E_eoYHXxPOVvPDNzjiHnCGoREl8vxeookIy4831a0VLAN6T9CITX0dyH2cQS6Dy0fs5SGxYC42fWpG2JeAvlnP9yrsgmHUk8.mp4?_nc_cat=106&_nc_sid=SCRUBBED&_nc_ht=scontent-atl3-1.cdninstagram.com&_nc_ohc=SCRUBBED&efg=SCRUBBED&ccb=SCRUBBED&vs=S…", "width": 1080 } ], "mentions": [ "joelsartore", "natgeoanimals", "natgeosociety" ], "metrics": { "comment_count": 238, "like_count": 68253, "quote_count": null, "repost_count": null, "save_count": null, "share_count": null, "view_count": 759188 }, "platform": "instagram", "platform_type": "reel", "published_at": "2026-10-23T10:01:35Z", "text": "Warning—looking at nudibranchs may cause excessive awe. @joelsartore loves these little creatures, and so do we. 💛🧡🩷\n\nWe’re celebrating 20 years of the National Geographic #PhotoArk, an audacious project documenting the world's incredible biodiversity and highlighting the importa…", "thumbnail_url": "https://scontent-atl3-2.cdninstagram.com/v/t51.82787-15/862543567_18711062458019133_8845814561108513720_n.jpg?stp=dst-jpg_e35_s640x640_tt6&_nc_cat=105&ccb=SCRUBBED&_nc_sid=SCRUBBED&efg=SCRUBBED&_nc_ohc=SCRUBBED&_nc_oc=SCRUBBED&_nc_zt=23&_nc_ht=scontent-atl3-2.cdninstagram.com&_nc…", "title": null, "type": "video", "url": "https://www.instagram.com/reel/De1S3KpCXf0/" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "instagram", "source": "page", "is_cached": false, "credits_charged": 1, "latency_ms": 0, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "post", "id": "De1S3KpCXf0", "url": "https://www.instagram.com/reel/De1S3KpCXf0/" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display. ## Item errors Besides the [request-level errors](https://staging.socialtap.dev/docs/errors/index.md) every endpoint can return, post endpoints return: | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | --- Source: https://staging.socialtap.dev/resources/profile/ # Profile API A public account (creator, channel, page, company or community) with its bio, links and follower metrics. ## Endpoints 1 endpoint returns profiles on TikTok. - [TikTok Profile API](https://staging.socialtap.dev/apis/tiktok/profile/index.md): Public TikTok profile and follower counts (From 1 credit · `GET /v1/tiktok/profile`) ## Schema Every endpoint above returns this object as `data`. The standard [`meta`](https://staging.socialtap.dev/docs/index.md#response) comes with every response. | Field | Type | Description | | --- | --- | --- | | `platform` | `string` | Platform ID (tiktok, youtube, …). | | `id` | `string` | Platform-native account ID (string; YouTube UC…, Reddit without t2_). | | `username` | `string` or `null` | Handle without the @; null when the account has none. | | `display_name` | `string` or `null` | Display name, channel title or page name; null when empty or not exposed. | | `url` | `string` (uri) | Canonical profile URL. | | `type` | `string` | Coarse account kind, as the platform labels it; unknown when it does not (TikTok labels organizations only). New values may be added; treat unknown values as unknown.
One of `person`, `organization`, `community`, `unknown` | | `bio` | `string` or `null` | Profile bio, channel description or page about text; null when empty or not exposed. | | `avatar_url` | `string` (uri) or `null` | Profile picture URL (largest available). Platform CDN URLs can expire within hours. | | `banner_url` | `string` (uri) or `null` | Header or cover image URL. | | `is_verified` | `boolean` or `null` | Platform verification badge; null where the platform has no badge. | | `is_private` | `boolean` or `null` | True when the account's posts are not public. | | `category` | `string` or `null` | Platform category label (e.g. Musician, Media & Entertainment), as shown; not normalized. Often set only for business or creator accounts. | | `external_links` | `object[]` | Links in the bio or channel links; \[\] if none or not exposed. | | `external_links[].url` | `string` (uri) | Link target, absolute (https:// is added where the platform stores the link without a scheme). | | `external_links[].title` | `string` or `null` | Link label shown by the platform. | | `location` | `string` or `null` | Self-declared location text. | | `country_code` | `string` or `null` | ISO 3166-1 alpha-2 country the platform states for the account. | | `created_at` | `string` (date-time) or `null` | Account creation or join date, RFC 3339 UTC (some platforms give only the day: see approximate_fields). | | `metrics` | `object` | Counts at fetch time; null = hidden or not exposed, 0 = zero. | | `metrics.follower_count` | `integer` or `null` | Followers or subscribers. | | `metrics.following_count` | `integer` or `null` | Accounts this profile follows. | | `metrics.post_count` | `integer` or `null` | Posts or videos as the platform counts them on the profile (for a private account, posts the caller cannot see). | | `metrics.total_like_count` | `integer` or `null` | Likes received across all of the profile's posts (not likes given). | | `metrics.total_view_count` | `integer` or `null` | Lifetime views across all of the profile's posts. | | `approximate_fields` | `string[]` | JSON paths of values rounded by the platform or parsed from display text (e.g. metrics.follower_count); \[\] when all exact. | Example (TikTok): ```json { "data": { "approximate_fields": [], "avatar_url": "https://p16-common-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/e5143212a59c09a008bf50f487d54d1f~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=9640&refresh_token=9341391e&x-expires=SCRUBBED&x-signature=SCRUBBED&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5", "banner_url": null, "bio": "Making the seemingly impossible, possible.✨", "category": null, "country_code": null, "created_at": "2026-07-20T15:55:49Z", "display_name": "NASA", "external_links": [ { "title": null, "url": "https://science.nasa.gov/feature/artemis-ii-preliminary-lunar-science-report" } ], "id": "7664638705177150477", "is_private": false, "is_verified": true, "location": null, "metrics": { "follower_count": 2140425, "following_count": 23, "post_count": 60, "total_like_count": 11385816, "total_view_count": null }, "platform": "tiktok", "type": "organization", "url": "https://www.tiktok.com/@nasa", "username": "nasa" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "tiktok", "source": "page", "is_cached": false, "credits_charged": 1, "latency_ms": 0, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "profile", "id": "7664638705177150477", "url": "https://www.tiktok.com/@nasa" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` ## Item errors Besides the [request-level errors](https://staging.socialtap.dev/docs/errors/index.md) every endpoint can return, profile endpoints return: | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | --- Source: https://staging.socialtap.dev/resources/transcript/ # Transcript API Timed transcript of a public video: the platform's caption track, or speech-to-text on its audio when there is none. ## Endpoints 4 endpoints return transcripts on Facebook, Instagram, TikTok and YouTube. - [Facebook Video Transcript API](https://staging.socialtap.dev/apis/facebook/transcript/index.md): Facebook video transcript (From 1 credit · `GET /v1/facebook/transcript`) - [Instagram Reels Transcript API](https://staging.socialtap.dev/apis/instagram/transcript/index.md): Instagram reel / video transcript (From 1 credit · `GET /v1/instagram/transcript`) - [TikTok Transcript API](https://staging.socialtap.dev/apis/tiktok/transcript/index.md): TikTok video transcript (From 1 credit · `GET /v1/tiktok/transcript`) - [YouTube Transcript API](https://staging.socialtap.dev/apis/youtube/transcript/index.md): YouTube video transcript (From 1 credit · `GET /v1/youtube/transcript`) ## Schema Every endpoint above returns this object as `data`. The standard [`meta`](https://staging.socialtap.dev/docs/index.md#response) comes with every response. | Field | Type | Description | | --- | --- | --- | | `platform` | `string` | Platform the media is hosted on. | | `id` | `string` | Platform-native ID of the post (equals post.id). | | `url` | `string` (uri) | Canonical URL of the post. | | `language` | `string` or `null` | BCP-47 language of the returned transcript; null when unknown. | | `source` | `string` | captions = platform caption track; asr = speech-to-text on the audio.
One of `captions`, `asr` | | `is_auto_generated` | `boolean` | True when the track was machine-generated (platform auto-captions or ASR). | | `available_languages` | `string[]` | BCP-47 caption languages the platform offers for this post; \[\] if none or not listed. | | `duration_ms` | `integer` or `null` | Media duration in milliseconds; null when unknown. | | `segments` | `object[]` | Timed cues in order; never empty on success. | | `segments[].start_ms` | `integer` | Start time in milliseconds from the beginning of the media. | | `segments[].end_ms` | `integer` | End time in milliseconds from the beginning of the media. | | `segments[].text` | `string` | Cue text, whitespace-normalized. | | `text` | `string` | Full transcript: segment texts joined with single spaces. | Example (Facebook): ```json { "data": { "available_languages": [ "en-US" ], "duration_ms": 59861, "id": "1803909017420449", "is_auto_generated": false, "language": "en-US", "platform": "facebook", "segments": [ { "end_ms": 4000, "start_ms": 33, "text": "But the standard that we should continuously aspire to exceed," }, { "end_ms": 7166, "start_ms": 4000, "text": "which is why we are bringing that intensity, that discipline, that" }, { "end_ms": 11400, "start_ms": 7166, "text": "engineering ownership back to NASA, because great nations do not remain great" } ], "source": "captions", "text": "But the standard that we should continuously aspire to exceed, which is why we are bringing that intensity, that discipline, that engineering ownership back to NASA, because great nations do not remain great by being caretakers of history, but by continuously making history. We h…", "url": "https://www.facebook.com/reel/1803909017420449/" }, "meta": { "request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f", "platform": "facebook", "source": "captions", "is_cached": false, "credits_charged": 1, "latency_ms": 1517, "fetched_at": "2026-10-25T12:00:00Z", "input": { "kind": "video", "id": "1803909017420449", "url": "https://www.facebook.com/reel/1803909017420449/" }, "credits_remaining": 9999, "client_request_id": null, "warnings": [], "mode": "live" } } ``` Long arrays and strings are shortened for display. ## Item errors Besides the [request-level errors](https://staging.socialtap.dev/docs/errors/index.md) every endpoint can return, transcript endpoints return: | Code | HTTP | Retry | Meaning | What to do | | --- | --- | --- | --- | --- | | `not_found` | 404 | No | The item does not exist or was deleted. | Check the URL or ID; do not retry. | | `private_or_unavailable` | 403 | No | The item exists but is private, age- or region-gated, or requires a login to view. | Only public items can be fetched; do not retry. | | `no_transcript` | 404 | No | The media exists but has no transcript: no caption track and no speech to transcribe. | Do not retry; try another language with lang if the platform offers one. |