---
title: Errors
description: "Error response format and error codes used across the Syntext API."
---

# Errors

The Syntext API uses conventional HTTP status codes and returns a consistent error shape on every failure.

## Error Response Format

```json
{
  "error": {
    "code": "not_found",
    "message": "Project does not exist",
    "details": { }
  }
}
```

<ParamField body="error.code" type="string">
  A stable, machine-readable error code. Match on this, not the message.
</ParamField>

<ParamField body="error.message" type="string">
  A human-readable description of what went wrong.
</ParamField>

<ParamField body="error.details" type="object">
  Optional. Additional context — e.g. field-level validation errors.
</ParamField>

## HTTP Status Codes

| Status | Meaning |
|--------|---------|
| `200` | Success |
| `201` | Resource created |
| `202` | Accepted for async processing (analytics beacons) |
| `400` | Bad request — malformed body or invalid parameters |
| `401` | Unauthorized — missing or invalid credentials |
| `403` | Forbidden — valid credentials, but the plan limit was reached or access is denied |
| `404` | Not found — the resource doesn't exist or belongs to another project |
| `409` | Conflict — e.g. concurrent edit conflict in the editor |
| `422` | Validation failed |
| `429` | Rate limited — see [Rate Limits](/api-reference/rate-limits) |
| `500` | Internal error — safe to retry with backoff |
| `503` | Service unavailable — a dependency (e.g. the AI provider) is not configured or reachable |

## Error Codes

| Code | Status | Description |
|------|--------|-------------|
| `bad_request` | 400 | The request body or parameters are invalid |
| `validation` | 400/422 | A field failed schema validation — check `details` |
| `unauthorized` | 401 | Missing, expired, or invalid API key / token |
| `forbidden` | 403 | You don't have access to this resource |
| `usage_limit_exceeded` | 403 | A monthly plan limit was reached (builds, AI queries, …). The error includes `current` and `limit` fields. |
| `not_found` | 404 | Resource doesn't exist |
| `conflict` | 409 | The resource changed since you last read it |
| `rate_limited` | 429 | Too many requests. The error includes `retryAfterSeconds`. |
| `internal_error` | 500 | Unexpected server error |
| `service_unavailable` | 503 | A required service is not available |

<Note>
Error codes are case-insensitive — treat `NOT_FOUND` and `not_found` as equivalent when matching.
</Note>

## Usage Limit Errors

When an organization exceeds a plan limit, the API returns `403` with the current usage:

```json
{
  "error": {
    "code": "USAGE_LIMIT_EXCEEDED",
    "message": "Monthly build limit reached (100/100). Upgrade your plan for more builds.",
    "current": 100,
    "limit": 100
  }
}
```

## Handling Errors in the SDK

The [TypeScript SDK](/sdks/typescript) throws typed errors you can catch by class:

```typescript
import { Syntext, NotFoundError, RateLimitError } from '@syntext/sdk'

try {
  await client.builds.trigger('prj_abc123')
} catch (err) {
  if (err instanceof RateLimitError) {
    // wait err.retryAfterSeconds and retry
  } else if (err instanceof NotFoundError) {
    // project doesn't exist
  } else {
    throw err
  }
}
```

## Retry Guidance

- **`429` / `500`**: retry with exponential backoff. The SDKs do this automatically.
- **`4xx` (other)**: do not retry — fix the request.
- Build triggers are idempotent per commit: re-triggering the same commit creates a new build but never corrupts state.
