Source: https://developers.tokportal.com/rate-limits/
Markdown: https://developers.tokportal.com/rate-limits.md

# Rate Limits & Pagination

## Rate Limits

The TokPortal API allows **120 requests per minute per API key**. The limit uses a **token bucket** algorithm that refills continuously, so you do not need to wait for a fixed window to reset.

Bundle capacity is a separate workspace-wide safeguard. Bundle creation and publication use rolling capacity windows shared by the workspace owner and every Team member. This capacity is not exposed as a live counter. A request is affected only when it would exceed the available capacity; it then returns HTTP `429` with `capacity_cooldown` and should be retried later.

Performance Max and API bulk creation are evaluated for the complete batch before credits are debited. If the batch would exceed capacity, the entire request is rejected rather than partially created. Do not split a rejected batch into parallel requests to work around the cooldown.

Authenticated responses include rate limit headers:

```http
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
X-RateLimit-Reset: 1779724800
```

`X-RateLimit-Reset` is a Unix timestamp estimating when the token bucket will be full again.

### Rate Limit Response

When you exceed the limit, the API returns a `429 Too Many Requests` response:

```http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1779724800
Retry-After: 1
```

```json
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please slow down.",
    "details": {
      "retry_after_seconds": 2
    }
  }
}
```

### Best Practices

- **Read `Retry-After` first** on 429 responses. It is the canonical wait time before retrying.
- **Check `retry_after_seconds`** in the error response and wait before retrying.
- **Track `X-RateLimit-Remaining`** to smooth bursts before you hit the limit.
- **Use exponential backoff** — if you receive consecutive 429s, increase the wait time between retries.
- **Spread requests evenly** rather than sending them in bursts. The token bucket refills at approximately 2 tokens per second, so a steady rate of 2 req/s is sustainable.
- **Use separate API keys** for independent services to isolate rate limits.
- **Treat `capacity_cooldown` separately from API-key throttling.** It is shared by the entire client workspace, so rotating keys or sending requests through a Team member does not create additional capacity.

## Pagination

All list endpoints support pagination via query parameters.

### Query Parameters

| Parameter  | Default | Max   | Description                              |
| ---------- | ------- | ----- | ---------------------------------------- |
| `page`     | `1`     | —     | The page number to retrieve (1-indexed). |
| `per_page` | `25`    | `100` | Number of items per page.                |

### Example Request

```bash
curl -X GET "https://app.tokportal.com/api/ext/bundles?page=2&per_page=50" \
  -H "X-API-Key: sk_xxx"
```

### Response Format

Paginated responses include a `pagination` object alongside `data`:

```json
{
  "data": [
    { "id": "bnd_abc123", "status": "published" },
    { "id": "bnd_def456", "status": "configuring" }
  ],
  "pagination": {
    "page": 2,
    "per_page": 50,
    "total": 137,
    "total_pages": 3
  }
}
```

### Iterating Through All Pages

```typescript
async function fetchAllBundles(apiKey: string) {
  const results = [];
  let page = 1;

  while (true) {
    const res = await fetch(
      `https://app.tokportal.com/api/ext/bundles?page=${page}&per_page=100`,
      { headers: { "X-API-Key": apiKey } }
    );
    const { data, pagination } = await res.json();

    results.push(...data);

    if (page >= pagination.total_pages) break;
    page++;
  }

  return results;
}
```

## Idempotency

For mutating requests (`POST`, `PUT`, `PATCH`, and `DELETE`) that document idempotency support, you can include an `Idempotency-Key` header to safely retry requests without creating duplicate resources or double-charging credits.

Do not send this header to an endpoint that can return a credential, verification code, webhook signing secret, signed upload URL, or private analytics report artifact. TokPortal rejects it before the operation runs with `400 IDEMPOTENCY_KEY_NOT_ALLOWED_FOR_SENSITIVE_RESPONSE` and `details.retry_without_idempotency_key: true`. This prevents secret-bearing responses from being stored in the replay ledger. The affected operations are credential reveal, verification-code retrieval, webhook endpoint creation, presigned video or image upload creation, and analytics report creation/export. Follow the recovery instructions on the endpoint page instead.

### Usage

```bash
curl -X POST https://app.tokportal.com/api/ext/bundles \
  -H "X-API-Key: sk_xxx" \
  -H "Idempotency-Key: my-unique-request-id-12345" \
  -H "Content-Type: application/json" \
  -d '{ "platform": "tiktok", "country": "US", "videos": 10 }'
```

### How It Works

- The first request with a given `Idempotency-Key` is processed normally. After its response is durably stored as `completed`, exact retries return that response for 24 hours without executing the operation again.
- Subsequent requests with the **same key, method, path, and request body** return the completed original response with `Idempotent-Replayed: true`.
- Replayed responses include the `Idempotent-Replayed: true` header.
- Reusing the same key with a different request body returns `IDEMPOTENCY_KEY_REUSED`.
- Retrying while the first request is still running returns `IDEMPOTENCY_KEY_IN_PROGRESS`. Retry the exact request after a short delay.
- If `IDEMPOTENCY_KEY_IN_PROGRESS` persists beyond the normal request duration, stop automated retries and do not switch to a new key. The mutation may have committed before TokPortal stored its response. The `processing` claim deliberately does not expire automatically because executing again could duplicate a resource or debit.
- Persistent `processing` claims require TokPortal support/operator reconciliation against request, resource, and financial logs. Provide `X-TokPortal-Request-ID`, method, path, idempotency key, timestamp, and your non-secret correlation reference. Never send API keys, credentials, or verification codes.
- Idempotency keys are scoped to your API key. Only `completed` responses expire after **24 hours from completion**; unresolved `processing` claims remain fail-closed until reconciled.
- Use a unique value per logical operation (e.g., a UUID or a meaningful identifier from your system).

### When to Use Idempotency Keys

Idempotency keys are recommended for any operation that:

- **Creates a resource** (bundles, video slots, edit slots)
- **Debits credits**
- **Publishes, deletes, or updates state**
- **Could be retried** due to network timeouts or uncertain responses

This prevents accidental double-charges or duplicate resource creation.

## Idempotency Errors

| Code                                                 | HTTP Status | Meaning                                                                                                                                              |
| ---------------------------------------------------- | ----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IDEMPOTENCY_KEY_TOO_LONG`                           |         400 | The key is longer than 255 characters.                                                                                                               |
| `IDEMPOTENCY_KEY_NOT_ALLOWED_FOR_SENSITIVE_RESPONSE` |         400 | The endpoint can return sensitive material and rejected the key before execution. Remove the header and retry once.                                  |
| `IDEMPOTENCY_KEY_REUSED`                             |         409 | The key already exists for a different request body.                                                                                                 |
| `IDEMPOTENCY_KEY_IN_PROGRESS`                        |         409 | The claim is still `processing`. Retry the exact request briefly; if it persists, stop and request reconciliation. Never retry with a different key. |
| `IDEMPOTENCY_UNAVAILABLE`                            |         503 | The ledger could not be claimed, so no mutation ran. Retry the exact request with the same key.                                                      |
