> ## 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.

# Billing, errors, and limits

> Credit pricing, charges, refunds, retry behavior, and rate-limit handling.

The Scraper API uses prepaid credits. There is no subscription: you buy a balance and
spend credits when a lookup completes.

## Credit costs

| Request                                    | Cost                         |
| ------------------------------------------ | ---------------------------- |
| Normalized `profile`                       | 1 credit                     |
| Normalized `post`                          | 1 credit                     |
| Normalized `posts`                         | 1 credit per page            |
| Any TikTok or Instagram platform operation | 1 credit per request or page |
| `POST /scrape/tiktok/audience`             | **25 credits**               |

Optional enrichment flags, including `include_carousels`, `include_replies`,
`include_transcript`, and `include_comments`, do not add a separate credit charge.

### Buying credits

The standard purchase ladder is:

| Credits in one purchase | Standard price           |
| ----------------------- | ------------------------ |
| 1,000–100,000           | \$1.50 per 1,000 credits |
| 100,001–500,000         | \$1.00 per 1,000 credits |
| More than 500,000       | \$0.80 per 1,000 credits |

The minimum purchase is 1,000 credits. The entire purchase uses the rate for the tier it
falls into; rates are not blended across tiers. Your dashboard quote is authoritative
because account-specific pricing can apply.

## When a request is charged

| Outcome                                                                                                              | Charged?                         |
| -------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| Successful lookup                                                                                                    | Yes, at the endpoint's full rate |
| Each successful page in a listing                                                                                    | Yes, separately                  |
| Platform-confirmed not found (`404`)                                                                                 | Yes, at the endpoint's full rate |
| Invalid input, invalid key, empty balance, oversized body, or account rate limit (`400`, `401`, `402`, `413`, `429`) | No                               |
| SideShift/data-source failure or timeout (`500`, `502`, `504`)                                                       | No; any reservation is refunded  |
| System-capacity rejection (`SCRAPER_SYSTEM_BUSY`)                                                                    | No; any reservation is refunded  |

<Warning>
  A confirmed `404` is a completed lookup and is billed. Do not retry a not-found response
  on a loop. A not-found TikTok audience request costs the full 25 credits.
</Warning>

On success, read `meta.credits_charged` and `meta.credits_remaining`. Error bodies do not
have a `meta` object; read `X-Scraper-Credits-Charged` to see whether an error was billed.

## Error response

Application errors use JSON with an error code, message, and request identifier:

```json theme={"system"}
{
  "error": "INVALID_INPUT",
  "message": "Invalid request body",
  "request_id": "req_8f3c9a2b1d4e6f70"
}
```

| Code                                  | Meaning                                                             | Action                                                               |
| ------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `PROFILE_NOT_FOUND`, `POST_NOT_FOUND` | The platform confirmed the resource is unavailable. Usually billed. | Stop scheduling that identifier; repeated lookups repeat the charge. |
| `UPSTREAM_ERROR`, `UPSTREAM_TIMEOUT`  | A transient data-source failure. Refunded.                          | Retry with exponential backoff.                                      |
| `SCRAPER_RATE_LIMITED`                | Your account or endpoint limit is exhausted. Free.                  | Wait for `Retry-After`.                                              |
| `SCRAPER_SYSTEM_BUSY`                 | Shared scraper capacity is temporarily full. Refunded.              | Wait for `Retry-After` and retry.                                    |
| `INSUFFICIENT_SCRAPER_CREDITS`        | Your balance is empty. Free.                                        | Buy credits in the dashboard.                                        |
| `INVALID_INPUT`                       | Request validation failed. Free.                                    | Correct the named field and resend.                                  |
| `ENDPOINT_NOT_FOUND`                  | The requested platform operation does not exist. Free.              | Use a documented operation path.                                     |

<Note>
  A request that exceeds the server's approximately 120-second ceiling can be terminated
  by the gateway with a plain-text `504` rather than JSON. Its credit reservation is
  reclaimed automatically, which can take several minutes.
</Note>

## Retry policy

Retry only refunded transient failures: `429`, `500`, `502`, and `504`. Honor
`Retry-After` whenever it is present. Do not automatically retry deterministic `4xx`
responses, especially billed `404` responses.

```js theme={"system"}
async function scrape(url, body, attempts = 5) {
  for (let attempt = 1; ; attempt += 1) {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "x-api-key": process.env.SIDESHIFT_SCRAPER_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (response.ok) return (await response.json()).data;

    const retriable = [429, 500, 502, 504].includes(response.status);
    if (!retriable || attempt === attempts) {
      throw new Error(await response.text());
    }

    const retryAfterSeconds = Number(response.headers.get("Retry-After"));
    const baseDelayMs = retryAfterSeconds > 0
      ? retryAfterSeconds * 1000
      : Math.min(30_000 * 2 ** (attempt - 1), 300_000);
    const jitterMs = Math.random() * 1000;
    await new Promise((resolve) => setTimeout(resolve, baseDelayMs + jitterMs));
  }
}
```

## Rate limits

New accounts default to **120 requests per minute** across all scraper endpoints. An
account can also have a per-endpoint limit. Both use continuously refilling token buckets,
so the full minute's capacity can be used in a burst and then refills over time.

Successful responses and `429` errors include:

| Header                          | Meaning                                     |
| ------------------------------- | ------------------------------------------- |
| `X-Scraper-RateLimit-Limit`     | Capacity of the limit closest to exhaustion |
| `X-Scraper-RateLimit-Remaining` | Remaining requests in that bucket           |
| `X-Scraper-RateLimit-Reset`     | Approximate reset time                      |
| `Retry-After`                   | Seconds to wait after a `429`               |

Prefer `Retry-After` on a `429`. A slow request can make the reset timestamp stale before
you receive the response.

`SCRAPER_SYSTEM_BUSY` is separate from your account limit. It is free, does not consume
your account bucket, and should be handled by the same retry policy.

For sustained production volume, contact SideShift support to discuss higher limits.
