# Errors and limits

> HTTP status codes, error response format, rate limits, and limit-reached responses.

## Error response format

All errors use the same envelope shape:

```json
{
  "data": null,
  "error": "Human-readable error message",
  "meta": { /* optional error detail */ }
}
```

## Status codes

Every error response has a `meta.code`. When an endpoint has no more specific code, the code comes from the status: `400` `invalid_request`, `401` `unauthorized`, `402` `limit_reached`, `403` `forbidden`, `404` `not_found`, `405` `method_not_allowed`, `409` `conflict`, `410` `gone`, `422` `unprocessable`, `429` `rate_limit_exceeded`, `500` `internal_error`. The codes in the table below take priority.

| Status | Meaning |
|--------|---------|
| `400` | Bad request: invalid query parameters or request body (`meta.code`: `invalid_query`, `invalid_body`, `invalid_params`) |
| `401` | Unauthorized: missing, invalid, revoked or expired API key (`meta.code`: `invalid_api_key`), or a key that stopped working because of who it acts as or who created it (`key_member_removed`, `key_creator_removed`, `key_creator_not_admin`, `key_creator_banned`; see [Who a key acts as](https://www.trackagoat.com/docs/api-reference/authentication.md#who-a-key-acts-as)) |
| `402` | Limit reached: org has hit a count-based plan limit (`meta.code`: `limit_reached`), or a date range exceeds tier history retention (`retention_exceeded`) |
| `403` | Forbidden: key lacks the required scope (`meta.code`: `insufficient_scope`), the member the key acts as isn't an org admin and the action needs one (`insufficient_role`), a write to another member's personalization document (`not_your_document`), or the org/user is banned |
| `404` | Not found: resource doesn't exist or isn't in your org. A path that matches no endpoint at all (any method) returns `404` JSON with `meta.code: route_not_found` and a pointer to `/api/v2/openapi.json`. |
| `409` | Conflict: a duplicate or a state conflict. Adding a handle that's already tracked in the project returns `meta.code: already_tracked` (see below). An `Idempotency-Key` reused on a different endpoint returns `idempotency_conflict`; one whose first request is still running returns `idempotency_in_progress` with a `Retry-After` header. A notes replace whose `expected_readme_version` is out of date returns `notes_conflict` (see [Notes and documents](https://www.trackagoat.com/docs/api-reference/notes.md#errors)). |
| `410` | Gone: removed API version. Every `/api/v1/*` path returns 410 (`meta.code`: `api_version_removed`). Use `/api/v2` instead. |
| `422` | Unprocessable: an `Idempotency-Key` was reused with a different request body (`meta.code`: `idempotency_key_reused`). Use a new key for a new request. |
| `429` | Rate limited: daily API quota or per-key sliding-window exceeded (`meta.code`: `rate_limit_exceeded`) |
| `500` | Server error (`meta.code`: `internal_error`) |

## 402: Limit reached

Returned when a write operation would exceed a count-based plan limit. The `meta` field contains details:

```json
{
  "data": null,
  "error": "Limit reached: max_accounts_per_org",
  "meta": {
    "code": "limit_reached",
    "limitKey": "max_accounts_per_org",
    "current": 5,
    "max": 5
  }
}
```

See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md) for all limit keys.

## 409: Already tracked

Returned by `POST /api/v2/creators`, `POST /api/v2/projects/{id}/creators` and `POST /api/v2/creators/{id}/accounts` when one of the handles is already tracked in the project. Nothing is created. `error` names the handle and the creator that tracks it, and `meta` carries the ids so you can use the existing creator instead:

```json
{
  "data": null,
  "error": "@examplecreator (TikTok) is already tracked in this project as creator \"Sam Rivera\" (3f1c…).",
  "meta": {
    "code": "already_tracked",
    "platform": "tiktok",
    "handle": "examplecreator",
    "account_id": "9a7e…",
    "creator_id": "3f1c…"
  }
}
```

If the same platform and handle appear twice in one request, you get `400` with `meta.code: duplicate_account` instead.

## Idempotency-Key errors

Write endpoints that accept an `Idempotency-Key` header (see [Idempotency](https://www.trackagoat.com/docs/api-reference/overview.md#idempotency)) can return:

| `meta.code` | Status | Meaning | What to do |
|---|---|---|---|
| `invalid_idempotency_key` | 400 | The key is longer than 255 characters | Use a UUID |
| `idempotency_in_progress` | 409 | A request with this key is still running. `Retry-After` and `meta.retry_after_seconds` say how long to wait | Retry with the same key and body after the delay |
| `idempotency_conflict` | 409 | The key was already used on a different endpoint | Use a new key |
| `idempotency_key_reused` | 422 | The key was already used with a different request body | Use a new key for the new request |

A `5xx` or `429` response is never replayed: retrying with the same key runs the request again.

## 404: Unknown endpoint

A request to a path that no endpoint matches returns JSON, never the website's HTML page, so a typo'd or unsupported call can't look like a success:

```json
{
  "data": null,
  "error": "No API endpoint matches DELETE /api/v2/goals/3f1c….",
  "meta": {
    "code": "route_not_found",
    "method": "DELETE",
    "path": "/api/v2/goals/3f1c…",
    "hint": "Check the path and method against the OpenAPI spec at /api/v2/openapi.json (docs: /docs/api-reference/overview).",
    "openapi": "/api/v2/openapi.json"
  }
}
```

A known path called with a method it doesn't support returns `405`.

## 403: Insufficient scope

Returned when a write request (`POST`/`PATCH`/`DELETE`) is made with a key that lacks the `write` scope, or an admin endpoint (org management, PIN reads, hard deletes) is called without the `admin` scope. A key's `admin` scope only counts while its owner is an org admin.

```json
{
  "data": null,
  "error": "This action requires the 'write' scope. Granted: [read]",
  "meta": {
    "code": "insufficient_scope",
    "required": "write",
    "granted": ["read"]
  }
}
```

See [Authentication](https://www.trackagoat.com/docs/api-reference/authentication.md#permission-scopes) for the scope model.

## 403: Insufficient role

A key acts as one member and can never do more than they can in the app. Endpoints that only org admins can use in the app (payments and payout setup, removing creators, manual scrapes, share pages, comment settings, organization and project documents) return this when the member isn't an org admin. The OpenAPI spec marks each of them "Requires an org admin".

```json
{
  "data": null,
  "error": "You need to be an organization admin to do this. Ask an org admin to update your role in Organization settings.",
  "meta": { "code": "insufficient_role", "required": "org_admin" }
}
```

Use a key that acts as an org admin, or ask an org admin to do it.

## 429: Rate limited

Two separate systems can produce 429s, and **both** use `meta.code: "rate_limit_exceeded"`.

**Per-key sliding window**: more than 60 requests per minute from a single API key. The response carries a `Retry-After` header (seconds). Retry after a brief delay.

```json
{
  "data": null,
  "error": "Rate limit exceeded",
  "meta": {
    "code": "rate_limit_exceeded",
    "bucket": "v1Api",
    "retryAfter": 7
  }
}
```

**Daily quota**: your org's daily `api_requests` allowance was exhausted. Resets at midnight in your org's time zone. Like the per-key limiter, this response carries a `Retry-After` header (seconds until the reset), so a client can back off uniformly on any 429 without special-casing which limiter fired.

```json
{
  "data": null,
  "error": "Daily API request limit exceeded",
  "meta": {
    "code": "rate_limit_exceeded",
    "retryAfter": 33421
  }
}
```

The public [`GET /api/v2/changelog`](https://www.trackagoat.com/docs/api-reference/changelog.md) has neither: it is limited to 60 requests a minute per IP address (bucket `publicChangelog`) and never counts toward the daily quota.

## Handling errors

> **Tip: Always check meta**
>
> The `meta` field on error responses contains structured detail: use `meta.code` for programmatic error handling rather than parsing the `error` string.

```javascript
const res = await fetch('https://www.trackagoat.com/api/v2/creators', {
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();

if (!res.ok) {
  if (res.status === 429) {
    const retryAfter = body.meta?.retryAfter ?? 60;
    // wait retryAfter seconds and retry
  } else {
    throw new Error(body.error ?? `HTTP ${res.status}`);
  }
}

const creators = body.data;
```

- [Authentication](https://www.trackagoat.com/docs/api-reference/authentication.md): API key setup and Bearer token usage.
- [Limits](https://www.trackagoat.com/docs/guides/limits.md): Plan tiers, limit keys, and 402 responses.

---
Source: https://www.trackagoat.com/docs/api-reference/errors
