---
title: Errors & Status Codes
description: Error envelope and the status codes Retidal returns.
---

Error responses carry a JSON body with an `error` message. Some endpoints add fields such as `retryAfter` (seconds), `affectedIndices`, `message`, or a per-item `errors[]` array.

```json
{ "error": "API Key 无效" }
```

## Prerequisites

- Familiarity with which API you called — [Ingestion & Decisioning](/docs/quickstart) or [Management](/docs/authentication) — since the same code can mean different things on each.

## Status codes

| Code | Meaning |
| --- | --- |
| `200` | Success (synchronous). `POST /api/v1/t` returns `200` only with `?sync=1` (or header `X-Trackly-Sync: 1`). |
| `202` | Accepted — validated and dispatched; persistence completes asynchronously. The default for `POST /api/v1/t` and `POST /api/v1/identify`. Do **not** treat `202` as proof of persistence — only a `sync=1` `200` on `POST /api/v1/t` with `processed: true` guarantees it. |
| `302` | Short link resolved (`GET /s/{code}`) — always a 302 redirect, never 301, so the redirect is never cached and attribution is never lost. |
| `400` | Malformed JSON, missing required fields, batch-size overflow (`POST /api/v1/t`), or (identify/profile) a `traits` payload over 8 KiB / nested past 2 levels (`POST /api/v1/identify`, `PUT /api/v1/profile`). |
| `401` | Missing or invalid API key (`POST /api/v1/t`) or session (`GET /api/auth/me`). |
| `403` | Authenticated but missing the required capability scope (`POST /api/v1/decide`) or project role (`GET /api/admin/users`). |
| `404` | Resource not found (also returned for cross-tenant access, by design — resource existence is never leaked across projects) — e.g. `GET /api/v1/coupons/{code}`, `GET /api/projects/{projectId}`. |
| `409` | Conflict — a coupon that is not in a redeemable state (`already_redeemed`, `expired`, `revoked`, `pool_not_ready`) on `POST /api/v1/coupons/redeem`. |
| `410` | Short link is paused or expired (`GET /s/{code}`). |
| `422` | Validation rejected the request. For ingestion (`POST /api/v1/t`) in `enforce` mode with every event in the batch rejected, the body includes `errors[]` and `enforceRejected`. |
| `429` | Rate limited. Respect `retryAfter` (ingestion) or `retryAfterSeconds` (Management API). `POST /api/v1/decide` and `POST /api/v1/coupons/redeem` are limited to 30 req/min per (project, IP); `PUT /api/v1/profile` to 60 req/min per (project, IP). |
| `503` | A dependency (queue / selector lookup) was transiently unavailable (`POST /api/v1/t`). Retry after `retryAfter`. |

## Ingestion response contract

`POST /api/v1/t` is the one endpoint where the status code alone does not tell you what happened — read the body.

| Mode | Trigger | Status | Body |
| --- | --- | --- | --- |
| Async (default, `POST /api/v1/t`) | no `sync` param | `202` | `{ accepted, failed, queued, processed: false, invalid? }` — events were validated and handed to background processing; persistence is **not proven**. |
| Sync (`POST /api/v1/t`) | `?sync=1` or header `X-Trackly-Sync: 1` | `200` | `{ accepted, failed, queued, processed: true, errors?, enforceRejected? }` — `processed: true` is the only proof non-metering events reached D1 and Analytics Engine. Use this mode for integration checks and health checks only; keep production traffic on the async default. |
| Enforce, all rejected (`POST /api/v1/t`) | validation mode `enforce`, every event fails catalog validation, sync path | `422` | `{ accepted: 0, failed, queued: 0, processed: true, enforceRejected, errors: [{ index, eventName, errors: [{ field, rule, expected, actual, message }] }] }` |
| Queue/selector unavailable (`POST /api/v1/t`) | metering event present and the queue binding is missing or the send failed, or the metering-selector lookup fails | `503` | `{ error: "QUEUE_UNAVAILABLE", retryAfter: 60 }` or `{ error: "SELECTOR_LOOKUP_FAILED", retryAfter: 30 }` |

`POST /internal/backfill/events` is always synchronous (`processed: true`) regardless of query params, and requires every metering (conversion) event to carry a client-supplied `eventId`; a missing one fails the whole request with `400 { error: "MISSING_EVENT_ID", affectedIndices: [...] }`.

## Ingestion validation modes

`POST /api/v1/t` honors a per-key or per-project validation mode (resolution order: key-level `validationMode` > project `settings.ingestValidation.mode` > `warn`):

- `off` — accept everything, no catalog validation.
- `warn` (default) — accept, but surface warnings via `?sync=1` `errors[]` and Analytics Engine.
- `enforce` — reject events that fail catalog validation on `POST /api/v1/t`. Only property-schema violations (not envelope warnings like a missing `visitorId`) cause rejection. If **every** event in the batch is rejected, the response is `422`.

## What this unlocks

Knowing each code's exact meaning unlocks correctly handling retries and validation failures in your integration instead of treating every non-`2xx` the same — see [Send data](/docs/sending-data/events) for the event contract these codes are validated against.

## Verify it worked

Trigger a known error and confirm the body matches this page's contract:

```bash
curl -s -X POST https://api.retidal.com/api/v1/t \
  -H "X-API-Key: invalid" \
  -H "Content-Type: application/json" \
  -d '{}'
# expect: 401 { "error": "..." } — an invalid key is rejected before validation runs
```

## If it doesn't work

The same status code means different things depending on which call produced it — on the
Management API, `POST /api/auth/login` sign-in returns `429` when rate limited, and
`POST /api/auth/register` registration returns `503` when a dependency is unavailable,
neither of which is an ingestion or decisioning fault. So pick the row by what you were
doing, not by the number:

- **Sending events** to `POST /api/v1/t` and getting a rejection (`400`, `401`, `422`,
  `503`) → [Events not arriving](/docs/troubleshooting/events-not-arriving).
- **Calling `POST /api/v1/decide` or `POST /api/v1/coupons/redeem`** and getting no
  decision, a `409` on a coupon, or a `429` from the 30 req/min limit →
  [Decisioning not firing](/docs/troubleshooting/decisioning-not-firing).
- **Following a short link** (`GET /s/{code}`) that returns `410` or lands somewhere
  unexpected → [Attribution looks wrong](/docs/troubleshooting/attribution-looks-wrong).
- **Any Management API call or console action** failing — e.g. `GET /api/tenant/profile`
  returning `401`/`403` on a session or scoped key, `404` on a resource in another
  project, and the auth and registration limits above → the
  [health checklist](/docs/troubleshooting/health-checklist).
