> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sideshift.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Facebook — page info

> Accepts a vanity username or a numeric `profile_id`. 1 credit.



## OpenAPI

````yaml /openapi/scraper.yaml post /scrape/facebook/profile
openapi: 3.0.4
info:
  title: SideShift Scraper API
  version: 1.3.0
  description: >
    One scraper API with canonical /scrape/{platform}/{resource} paths.
    Normalized

    profile, posts, post, and TikTok audience resources remain stable; focused
    TikTok

    and Instagram operations preserve platform fields when their schemas are
    genuinely

    different. Completed lookups cost one credit except TikTok audience, which
    costs 25.


    ## Quickstart


    Every endpoint is a POST with a JSON body and your key in the `x-api-key`
    header.


    ```bash

    curl -X POST https://app.sideshift.app/api/v1/scrape/tiktok/posts \
      -H "x-api-key: scrape_live_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "username": "mrbeast" }'
    ```


    ```jsonc

    {
      "data": {
        "posts": [
          {
            "id": "7670566355153833246",
            "title": "The last one was hard 😴",
            "views": 25081146,
            "likes": 2543164,
            "comments": 85606,
            "shares": 47126,
            "bookmarks": 126013,
            "uploadedAt": 1785942939,          // unix seconds
            "postPage": "https://www.tiktok.com/@mrbeast/video/7670566355153833246",
            "creator": "mrbeast",
            "videoUrl": "https://…",           // direct media URL, short-lived
            "thumbnail": "https://…",
            "platform": "tiktok",
            "hashtags": []
          }
          // … the rest of the page
        ],
        "profile_pictures": { "mrbeast": "https://…" },
        "next_cursor": "1768430796798"       // pass back as "cursor" for page 2
      },
      "request_id": "req_k4XjoVoiYdzZ1ibg",
      "upstream_calls": 1,
      "meta": { "credits_charged": 1, "credits_remaining": 104987 }
    }

    ```


    ## 1. Authentication


    Create a key in the [Scraper dashboard](https://app.sideshift.app/scraper)
    under **Keys**.

    The full key is shown once. Send it in the `x-api-key` header on every
    request, and revoke

    it from the dashboard whenever you need to.


    Scraper keys start with `scrape_live_`. Integration API keys (`sk_live_*`,
    `sk_test_*`) are

    rejected with `401` on scraper routes, and scraper keys are rejected on
    integration routes.


    > **Call from your backend only.** Never embed a scraper key in client-side
    code.


    ## 2. Credits and billing


    One credit is one lookup. You buy a credit balance up front; there is no
    subscription. You

    are charged when we complete the work, and refunded when we can't.


    | Outcome | Charged |

    |---|---|

    | Successful scrape (each page of a listing counts as one) | 1 credit |

    | Confirmed not-found: the platform answered, the account or post isn't
    there (`404`) | Full rate for the resource: 1 credit, or 25 for audience |

    | TikTok audience demographics (`/scrape/tiktok/audience`) | 25 credits |

    | Rejected before we start work: bad input, bad key, empty balance,
    oversized body, over your rate limit (`400` `401` `402` `413` `429`) | Free
    |

    | Our problem or the platform's: errors and timeouts (`500` `502` `504`) |
    Free, auto-refunded |


    A full catalog costs `ceil(total_posts / page_size)` credits, one per page.
    A 300-video

    TikTok account is about 10.


    `meta.credits_charged` and `meta.credits_remaining` come back on every
    success. Error bodies

    have no `meta` block, so read the `X-Scraper-Credits-Charged` header to see
    what a billed

    error cost you. Refunds are automatic; you never have to claim one. Top up
    in the

    [Scraper dashboard](https://app.sideshift.app/scraper).


    ## 3. Resources


    | Resource | Path | Returns |

    |---|---|---|

    | **Profile** | `POST /scrape/{platform}/profile` | Display name, bio,
    follower/following counts, avatar, post count. |

    | **Posts** | `POST /scrape/{platform}/posts` | One page of the creator's
    recent posts, plus a `next_cursor` where the platform supports one. |

    | **Single Post** | `POST /scrape/{platform}/post` | One post, by URL. |

    | **Audience** | `POST /scrape/tiktok/audience` | TikTok audience
    distribution by country. 25 credits. |


    Platforms: `tiktok`, `instagram`, `youtube`, `facebook`, `snapchat`,
    `twitter` (X), `linkedin`.


    Normalized profile, posts, post, and audience endpoints read their
    documented fields and

    ignore the rest. Focused TikTok and Instagram operations reject unknown or
    misspelled

    fields with the JSON error envelope before billing. Bodies over 32 KB return

    `413 PAYLOAD_TOO_LARGE`. Unknown TikTok or Instagram operations return a
    JSON

    `ENDPOINT_NOT_FOUND` response; paths outside the documented platform
    namespaces may return

    the framework's standard HTTP `404`.


    ## 4. Pagination, page size, and speed


    Same request, same response shape everywhere. What differs is how much each
    platform gives

    you per call:


    | Platform | Posts per page | More pages? | `videoUrl` in listings | Speed |

    |---|---|---|---|---|

    | TikTok | 30 | Yes, cursor | Yes | 2–4 s |

    | Instagram | 12 | Yes, cursor | Yes | 8–15 s |

    | YouTube | 30 | Yes, cursor | No; fetch the single post | 15–45 s |

    | Facebook | Up to 30 | Yes, cursor | Yes | 10–30 s |

    | Snapchat | Up to 30 | No; single page | Yes | 4–10 s |

    | X | 20 | Yes, cursor | Videos only | 3–8 s |

    | LinkedIn | 1–11 | No; single page | Videos only | 4–10 s |


    Listings return video-style content: reels on Instagram and Facebook, Shorts
    by default on

    YouTube (`contentType: video` switches to longform), Spotlights on Snapchat.


    Set your client timeout to 60 seconds, and 90 for YouTube and Facebook. Run
    scrapes from a

    background job, not inside a user request.


    ### Walking a full catalog


    Pass each response's `next_cursor` back as `cursor`. Stop on the cursor, not
    the page size:

    the first page can overshoot, and short pages turn up mid-walk.


    ```js

    const posts = [];

    let cursor;

    do {
      const res = await fetch("https://app.sideshift.app/api/v1/scrape/tiktok/posts", {
        method: "POST",
        headers: { "x-api-key": KEY, "Content-Type": "application/json" },
        body: JSON.stringify(cursor ? { username: "mrbeast", cursor } : { username: "mrbeast" }),
      });
      const { data } = await res.json();
      posts.push(...data.posts);
      cursor = data.next_cursor;
    } while (cursor);

    ```


    Cursors are opaque: pass one back verbatim, and never construct or derive
    one. A stale or

    unrecognised cursor is not an error, so you get a billed page that is not
    where you meant to

    be, and a loop that trusts it may never terminate.


    Snapchat and LinkedIn return a single page and never issue a cursor, so
    their listings are

    only what the platform exposes, not the account's full history. A creator
    with no retrievable

    posts returns `200` with an empty `posts` array, not a `404`, and is billed.


    New posts arrive at the front of page one. After the first backfill, poll
    page one on your

    schedule and keep what you haven't seen; re-fetch known posts only when you
    want updated

    metrics.


    ## 5. Reading the response


    Metrics are cumulative totals as of the moment you called. `uploadedAt` is
    unix seconds.

    `id` is the platform's own post id and is stable, which makes it the right
    key for storing

    and de-duplicating posts. On TikTok, Instagram, and Facebook, `videoUrl` and
    `thumbnail` are

    signed URLs that expire within hours: download the media when you receive
    it, and never

    store the URL. On TikTok photo posts, `videoUrl` points at the post's audio
    track rather

    than a video and nothing in the response flags it, so check the response
    `Content-Type`

    before treating the bytes as video.


    Two fields need care if you work across platforms:


    - `creator` is a lowercase handle on TikTok, Instagram, and LinkedIn,
    canonical case on X,
      and the account's **display name** on YouTube and Facebook (`Coca-Cola`, not `cocacola`).
      Join accounts on the identifier you requested, never on `creator`.
    - `postPage` on a single-post lookup echoes the URL you sent, so the same
    post can produce
      several different `postPage` values. Deduplicate on `id` plus `platform`, never on
      `postPage`.

    Single-post responses carry two keys that listings omit entirely,
    `transcript` and

    `topComments`, so read them defensively. Both are filled only when you ask
    for them with

    `include_transcript` or `include_comments`, and only on **TikTok, YouTube,
    and Facebook**;

    Instagram, Snapchat, X, and LinkedIn accept the flags and return `null`.
    Neither flag costs

    extra credits.


    Profile responses return `username`, `display_name`, and `follower_count` on
    all seven

    platforms. Everything else depends on what the source exposes, and
    unavailable fields are

    usually omitted rather than returned as `null`, so test for a usable value
    rather than for

    key presence.


    Field-level types, per-platform gaps, and the metrics that are structurally
    always `0` are

    in the `UnifiedPost` and `ScrapeProfileResponse` schemas below.


    ## 6. Errors and refunds


    Every error is JSON with an `error` code, a human-readable `message`, and a
    `request_id` to

    quote at support.


    | Code | What happened | What to do |

    |---|---|---|

    | `PROFILE_NOT_FOUND`<br>`POST_NOT_FOUND` | The platform confirmed it
    doesn't exist. Billed, because it was a real lookup. | Remove the identifier
    from your schedule. Retrying it costs a credit every time. |

    | `UPSTREAM_ERROR`<br>`UPSTREAM_TIMEOUT` | The platform failed or timed out.
    Refunded. | Retry with backoff: 30 s, then a few minutes. YouTube and
    Facebook throw these the most. |

    | `SCRAPER_RATE_LIMITED` | One of *your* limits is exhausted. The
    `X-Scraper-RateLimit-*` headers report the one that denied you. | Wait for
    `Retry-After`. If you hit this steadily, ask us to raise your limit. |

    | `SCRAPER_SYSTEM_BUSY` | *Our* capacity, nothing about your account.
    Refunded, and it does not consume your rate limit. | Wait for `Retry-After`
    and retry. |

    | `INSUFFICIENT_SCRAPER_CREDITS` | Balance is empty. Nothing charged. | Top
    up in the dashboard. `meta.credits_remaining` is on every success for
    alerting. |

    | `INVALID_INPUT` | The body failed validation. The `message` names the
    field and why. | Fix and resend. |


    The retry rule in one line: **retry refunded errors, never retry billed ones
    on a loop, and

    always honour `Retry-After`.**


    Not every `5xx` body is JSON. A request that outruns the roughly 120-second
    server ceiling

    is terminated by the gateway and returns a bare `504` with a `text/plain`
    body; its

    reservation is reclaimed within about ten minutes rather than at the moment
    it fails.


    ### A retry wrapper you can copy


    The wrapper below is the retry rule as code: it honours `Retry-After` on a
    `429`, backs

    off with jitter on refunded `5xx` errors, and gives up immediately on
    everything else.

    Route every call through it and both kinds of transient failure stop
    reaching your code.


    ```js

    async function scrape(url, body, attempts = 5) {
      for (let attempt = 1; ; attempt++) {
        const res = await fetch(url, {
          method: "POST",
          headers: { "x-api-key": KEY, "Content-Type": "application/json" },
          body: JSON.stringify(body),
        });
        if (res.ok) return (await res.json()).data;

        // 4xx other than 429 is deterministic (bad input, bad key, billed not-found):
        // retrying repeats the same answer, and on a 404 it repeats the same charge.
        const retriable = res.status === 429 || res.status >= 500;
        if (!retriable || attempt === attempts) throw new Error(await res.text());

        // A 429 says exactly when to come back. Refunded 5xx errors don't, so back off:
        // 30 s, 60 s, 2 min, 4 min. Jitter keeps parallel workers from retrying in step.
        const retryAfter = Number(res.headers.get("Retry-After"));
        const waitMs = retryAfter > 0
          ? retryAfter * 1000
          : Math.min(30_000 * 2 ** (attempt - 1), 300_000);
        await new Promise((resolve) => setTimeout(resolve, waitMs + Math.random() * 1000));
      }
    }


    const profile = await scrape(
      "https://app.sideshift.app/api/v1/scrape/tiktok/profile",
      { username: "mrbeast" },
    );

    ```


    It slots straight into the catalog walk in section 4: swap the raw `fetch`
    for

    `scrape(...)` and the loop rides out rate limits and platform hiccups
    unattended.


    ## 7. Rate limits


    New accounts start at 120 requests per minute across all endpoints. You may
    additionally have a

    per-endpoint limit. The account total is shared, so a request can be denied
    by the total

    while its own endpoint still has allowance. Both are token buckets: you can
    spend a minute's

    allowance in one burst, and it refills continuously rather than at a window
    edge.


    `X-Scraper-RateLimit-Limit`, `-Remaining`, and `-Reset` come back on every
    `200` and every

    `429`, reporting whichever limit is closest to being reached. Prefer
    `Retry-After` on a

    `429`: `-Reset` is computed when your request is admitted, so on a slow
    platform it can

    already be in the past by the time you read it.


    In almost all cases your limit is exactly what you get: stay under your rpm
    and your

    requests are admitted. The exception is the moment the scraping system as a
    whole is

    saturated across all customers, when a request can be turned away with

    `SCRAPER_SYSTEM_BUSY` even though your own buckets still have tokens. That
    answer is

    free, refunded, and carries a `Retry-After`; the wrapper in section 6
    absorbs it without

    any extra code. Treat it as something your client retries automatically, not
    an outage.


    For production volume, contact support to raise your limits.
servers:
  - url: https://app.sideshift.app/api/v1
    description: Production
security:
  - apiKeyAuth: []
tags:
  - name: Profile
    description: >-
      Profile-level info for a creator (display name, bio, follower/following
      counts, avatar, post count).
  - name: Posts
    description: One page of a creator's recent posts.
  - name: Single Post
    description: One post, looked up by its URL.
  - name: Audience
    description: Audience location data for a creator.
paths:
  /scrape/facebook/profile:
    post:
      tags:
        - Profile
      summary: Facebook — page info
      description: Accepts a vanity username or a numeric `profile_id`. 1 credit.
      operationId: scrapeFacebookProfile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileRequest'
            example:
              username: nike
      responses:
        '200':
          description: Profile info.
          headers:
            X-Scraper-Credits-Charged:
              $ref: '#/components/headers/CreditsCharged'
            X-Scraper-Credits-Remaining:
              $ref: '#/components/headers/CreditsRemaining'
            X-Scraper-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-Scraper-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-Scraper-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScrapeProfileResponse'
              examples:
                sample:
                  $ref: '#/components/examples/FacebookProfileExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '404':
          $ref: '#/components/responses/ProfileNotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '504':
          $ref: '#/components/responses/UpstreamTimeout'
components:
  schemas:
    ProfileRequest:
      type: object
      required:
        - username
      properties:
        username:
          type: string
          description: >-
            Creator handle (with or without a leading `@`). YouTube accepts a
            `@handle` or `UC…` channel id; Facebook accepts a vanity username or
            a numeric `profile_id`; LinkedIn needs `company/<slug>` for
            organisations and a bare slug for people. A full profile URL is not
            accepted on any platform.
          example: mrbeast
    ScrapeProfileResponse:
      type: object
      required:
        - data
        - request_id
        - upstream_calls
      properties:
        data:
          type: object
          description: >-
            Profile-level info. username, display_name and follower_count come
            back on all seven platforms; the rest depend on the source.
            Unavailable fields are omitted from the object rather than returned
            as null, except where noted below — and some sources return an empty
            string or 0 instead, so test for a usable value rather than for key
            presence.
          properties:
            username:
              type: string
              description: >-
                NOT a canonical identifier and not a reliable echo. On TikTok,
                Instagram and Snapchat it is the platform's own handle, always
                lower-case, so it can differ from what you sent. On YouTube,
                Facebook, X and LinkedIn it is your input echoed back with one
                leading @ removed and no case correction, so a UC… channel id, a
                Facebook numeric id and a company/<slug> all round-trip
                unchanged. Do not use it as an identity key.
            display_name:
              type: string
              description: >-
                Human-readable account name. Can be an empty string for Facebook
                numeric-id lookups.
            bio:
              type: string
              nullable: true
              description: >-
                Profile bio / description / channel "about". May be null,
                absent, or empty depending on the platform and account. Treat
                all three as no bio.
            follower_count:
              type: integer
              description: >-
                On YouTube this is the subscriber count, parsed from YouTube's
                rounded display text (3 significant figures), so it does not
                move between calls and is unsuitable for growth deltas.
            following_count:
              type: integer
              description: >-
                Never returned on YouTube or Snapchat (key absent). Always 0 on
                Facebook and LinkedIn, where the source does not expose it.
            profile_picture:
              type: string
              description: >-
                Signed and short-lived on TikTok/Instagram/Facebook; stable on X
                and Snapchat. Never returned on YouTube — read
                data.profile_pictures from /scrape/youtube/posts instead.
            post_count:
              type: integer
              description: >-
                Real only on TikTok and X. Never returned on YouTube or Snapchat
                (key absent), and always 0 on Instagram, Facebook and LinkedIn
                regardless of how much the account has published.
        request_id:
          type: string
        upstream_calls:
          type: integer
        meta:
          $ref: '#/components/schemas/CreditsMeta'
    CreditsMeta:
      type: object
      description: >-
        Per-request credit accounting (also surfaced in the X-Scraper-Credits-*
        response headers).
      properties:
        credits_charged:
          type: integer
        credits_remaining:
          type: integer
    ScrapeError:
      type: object
      required:
        - error
        - message
        - request_id
      properties:
        error:
          type: string
          enum:
            - INVALID_JSON
            - INVALID_INPUT
            - PAYLOAD_TOO_LARGE
            - UNAUTHORIZED
            - INSUFFICIENT_SCRAPER_CREDITS
            - PROFILE_NOT_FOUND
            - POST_NOT_FOUND
            - SCRAPER_RATE_LIMITED
            - SCRAPER_SYSTEM_BUSY
            - UPSTREAM_ERROR
            - UPSTREAM_TIMEOUT
            - SCRAPER_USAGE_FINALIZE_FAILED
            - INTERNAL_ERROR
        message:
          type: string
          description: Human-readable, SideShift-owned message.
        platform:
          type: string
          enum:
            - tiktok
            - instagram
            - youtube
            - facebook
            - snapchat
            - twitter
            - linkedin
        identifier:
          type: string
          description: The username or URL that was requested, when known.
        field:
          type: string
          description: Offending field on a validation error.
        reason:
          type: string
          description: Validation reason.
        request_id:
          type: string
  headers:
    CreditsCharged:
      schema:
        type: integer
      description: Credits debited for this request.
    CreditsRemaining:
      schema:
        type: integer
      description: Account credit balance after this request.
    RateLimitLimit:
      schema:
        type: integer
      description: >-
        Requests allowed per minute by the limit closest to being reached — your
        account total, or the per-endpoint limit for this endpoint if you have
        one.
    RateLimitRemaining:
      schema:
        type: integer
      description: >-
        Requests you can still make right now against that same limit. It is a
        token bucket, so this refills continuously at the per-minute rate rather
        than jumping back to the full limit at a window edge.
    RateLimitReset:
      schema:
        type: integer
      description: >-
        Unix seconds. Computed when the request is admitted, not when the
        response is written, so on a success it reads about a minute out MINUS
        the call's latency — on a slow platform it can already be in the past by
        the time you read it. On a 429, when the denied limit will have refilled
        enough to serve the request; prefer the `Retry-After` header, which says
        the same thing in seconds from now.
  examples:
    FacebookProfileExample:
      summary: Facebook profile — Nike
      value:
        data:
          username: nike
          display_name: Nike Reels
          bio: Just Do It.
          follower_count: 39623333
          following_count: 0
          profile_picture: https://scontent.xx.fbcdn.net/…/profile.jpg
          post_count: 0
        request_id: req_30415263748596a7
        upstream_calls: 1
        meta:
          credits_charged: 1
          credits_remaining: 9986
  responses:
    BadRequest:
      description: Invalid JSON or input. No credits are charged.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScrapeError'
          example:
            error: INVALID_INPUT
            message: 'Invalid username: must match @?[a-zA-Z0-9._-]{1,100}'
            field: username
            reason: must match @?[a-zA-Z0-9._-]{1,100}
            request_id: req_8f3c9a2b1d4e6f70
    Unauthorized:
      description: >-
        Missing or invalid scraper key (or an Integration key was used). No
        credits are charged.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScrapeError'
          example:
            error: UNAUTHORIZED
            message: >-
              Scraper routes require an independent scraper key from the Scraper
              API dashboard.
            request_id: req_8f3c9a2b1d4e6f70
    InsufficientCredits:
      description: >-
        Not enough scraper credits to run (or finalize) the request. No net
        credits are charged.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScrapeError'
          example:
            error: INSUFFICIENT_SCRAPER_CREDITS
            message: Insufficient scraper credits
            platform: tiktok
            request_id: req_8f3c9a2b1d4e6f70
    ProfileNotFound:
      description: >-
        The profile is missing, private, restricted, or geo-blocked. Returned by
        the `profile`, `posts` and `audience` resources; the `post` resource
        returns `POST_NOT_FOUND` instead. Billed at the resource's full rate
        whenever the data source confirmed the lookup, which is the normal case
        — that is 1 credit for `profile`/`posts` and 25 for `audience`. When
        credits are refunded the `message` says so; a billed response does not
        mention credits at all, so read the `X-Scraper-Credits-Charged` header
        for the amount — there is no `meta` block on an error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScrapeError'
          example:
            error: PROFILE_NOT_FOUND
            message: Profile not found or unavailable.
            platform: instagram
            identifier: someuser
            request_id: req_8f3c9a2b1d4e6f70
    PayloadTooLarge:
      description: Request body exceeds 32 KB. Rejected before any credit reservation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScrapeError'
          example:
            error: PAYLOAD_TOO_LARGE
            message: Request body must be 32KB or smaller
            request_id: req_8f3c9a2b1d4e6f70
    RateLimited:
      description: >
        Two distinct conditions share this status, and the `error` code tells
        them apart.


        `SCRAPER_RATE_LIMITED` — one of YOUR limits is exhausted: either your
        account

        total or, if you have one, this endpoint's own per-minute limit. The

        X-Scraper-RateLimit-* headers report whichever one denied you. Spreading
        load

        across endpoints only helps if it was the per-endpoint limit that denied
        you —

        the account total is shared, so a request can be denied by it while this

        endpoint's own bucket still has tokens.


        `SCRAPER_SYSTEM_BUSY` — the scraping system is at capacity across all
        customers.

        Nothing about your account is wrong; retry after the `Retry-After`
        interval.

        Expect an occasional one even when you are comfortably under your own
        limits,

        so make this retry automatic (the wrapper in section 6 of the
        introduction

        already handles it) rather than something that alerts you.


        Neither charges credits: no lookup is performed and the reservation is
        refunded

        in full. `SCRAPER_SYSTEM_BUSY` additionally does not consume your rate
        limit,

        whereas a `402` insufficient-balance block does.
      headers:
        Retry-After:
          description: Seconds to wait before retrying. Sent with both 429 codes.
          required: false
          schema:
            type: integer
            minimum: 1
            example: 3
        X-Scraper-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-Scraper-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-Scraper-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScrapeError'
          example:
            error: SCRAPER_RATE_LIMITED
            message: Scraper API rate limit exceeded
            platform: tiktok
            request_id: req_8f3c9a2b1d4e6f70
    ServerError:
      description: >
        An unexpected server error, or the usage could not be finalized after a
        successful

        scrape. When finalization fails, the reservation is refunded in full

        (`SCRAPER_USAGE_FINALIZE_FAILED`); a bare `INTERNAL_ERROR` reflects an
        unexpected

        fault and leaves no net credit change once the stranded reservation is
        reclaimed,

        which happens within about ten minutes rather than immediately.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScrapeError'
          example:
            error: SCRAPER_USAGE_FINALIZE_FAILED
            message: Request could not be finalized. Your credits were refunded.
            platform: tiktok
            request_id: req_8f3c9a2b1d4e6f70
    UpstreamError:
      description: The data source failed. The reservation is refunded in full.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScrapeError'
          example:
            error: UPSTREAM_ERROR
            message: Data source failed. Your credits were refunded.
            platform: youtube
            request_id: req_8f3c9a2b1d4e6f70
    UpstreamTimeout:
      description: >-
        The data source timed out. The reservation is refunded in full. A
        request that instead outruns the ~120 s platform function ceiling is
        terminated by the gateway before the API can respond, and returns a bare
        `504` with a `text/plain` body, no error code, and none of the
        `X-Scraper-*` headers; its reservation is reclaimed asynchronously
        within about ten minutes. Never assume a `504` body parses as JSON.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScrapeError'
          example:
            error: UPSTREAM_TIMEOUT
            message: Data source timed out. Your credits were refunded.
            platform: facebook
            request_id: req_8f3c9a2b1d4e6f70
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Independent Scraper API key (`scrape_live_*`). Generate one in the
        Scraper dashboard.

````