# Error Codes

All Tools.FAST APIs use a consistent error response format. The same error codes and structure apply across Convert.FAST, Compress.FAST, and every tool in the network.

## Error envelope

Every error response returns a JSON object with at least two fields:

```json
{
  "error": "machine.readable_code",
  "detail": "Human-readable explanation."
}
```

Some errors include additional context fields (e.g., `sourceFormat`, `supportedTargets`, `megabytes`).

## HTTP status codes

### `400` Bad Request

Invalid request payload, parameters, or content type.

<!-- code-tabs:start default=invalid-content-type -->
### Invalid content type
```json
{
  "error": "request.invalid_content_type",
  "detail": "multipart/form-data required."
}
```

### No files provided
```json
{
  "error": "request.no_files",
  "detail": "No files were provided in the request."
}
```

### Empty file
```json
{
  "error": "request.empty_file",
  "detail": "Uploaded file is empty."
}
```

### Invalid extension
```json
{
  "error": "request.invalid_extension",
  "detail": "File extension not accepted by converter."
}
```

### Invalid parameter
```json
{
  "error": "request.invalid_parameter",
  "detail": "Invalid value for parameter 'quality'."
}
```
<!-- code-tabs:end -->

### `401` Unauthorized

The API key is missing, invalid, or not allowed from the caller's IP.

```json
{
  "error": "api_key.invalid_or_ip_not_allowed",
  "detail": "X-Fast-Api-Key was provided but is invalid for this request (or IP not allowlisted)."
}
```

### `402` Payment Required

The caller's account does not have enough credits or has exceeded a usage limit.

<!-- code-tabs:start default=insufficient-credits -->
### Insufficient credits
```json
{
  "error": "entitlements.insufficient_credits",
  "detail": "Insufficient credits. Required: 5, Available: 2"
}
```

### Daily limit exceeded (free/guest)
```json
{
  "error": "usage.daily_limit_exceeded",
  "detail": "Daily usage limit exceeded. Used: 100/100"
}
```
<!-- code-tabs:end -->

### `403` Forbidden

The resource exists but the caller does not own it.

```json
{
  "error": "batch.forbidden",
  "detail": "You do not have access to this batch."
}
```

### `404` Not Found

The requested job or batch does not exist.

<!-- code-tabs:start default=job-not-found -->
### Job not found
```json
{
  "error": "jobs.not_found",
  "detail": null
}
```

### Batch not found
```json
{
  "error": "batch.not_found",
  "detail": null
}
```
<!-- code-tabs:end -->

### `409` Conflict

The request conflicts with the current state of the resource.

<!-- code-tabs:start default=not-ready -->
### Job not ready
```json
{
  "error": "jobs.not_ready",
  "detail": "Job is still processing or waiting to start."
}
```

### Not cancellable
```json
{
  "error": "jobs.not_cancellable",
  "detail": "Cannot cancel completed/failed jobs."
}
```
<!-- code-tabs:end -->

### `410` Gone

The job completed but its output artifacts have expired and been cleaned up.

```json
{
  "error": "jobs.expired",
  "detail": "Job output has expired and is no longer available for download."
}
```

### `413` Payload Too Large

The uploaded file exceeds the per-converter or global size limit.

```json
{
  "error": "request.file_too_large",
  "detail": "File exceeds the maximum allowed size.",
  "megabytes": 50
}
```

### `429` Too Many Requests

The caller has been throttled. Check the `Retry-After` header.

<!-- code-tabs:start default=rate-limited -->
### Rate limited
```json
{
  "error": "rate_limited",
  "detail": "Too many requests. Retry after 12 seconds."
}
```

### Queue limit exceeded
```json
{
  "error": "queue.limit_exceeded",
  "detail": "Too many queued jobs. Wait for existing jobs to complete."
}
```

### Pool saturated
```json
{
  "error": "queue.pool_saturated",
  "detail": "Worker pool is at capacity. Try again shortly."
}
```
<!-- code-tabs:end -->

**Headers on 429 responses:**

| Header | Description |
|--------|-------------|
| `Retry-After` | Seconds until you can retry |

Best practice: implement exponential backoff in your client, or honour the `Retry-After` value directly.

## Complete error codes reference

### Request errors

| Code | Status | Description |
|------|--------|-------------|
| `request.invalid_content_type` | 400 | Must use `multipart/form-data` |
| `request.no_files` | 400 | No file attached to the request |
| `request.empty_file` | 400 | Uploaded file has zero bytes |
| `request.invalid_extension` | 400 | File extension not accepted by the converter |
| `request.invalid_parameter` | 400 | A request parameter has an invalid value |
| `request.multiple_files` | 400 | Only one file per request is allowed |
| `request.image_too_large` | 413 | Image dimensions exceed the maximum |
| `request.file_too_large` | 413 | File exceeds the size limit for this converter/tier |

### Authentication errors

| Code | Status | Description |
|------|--------|-------------|
| `api_key.invalid_or_ip_not_allowed` | 401 | API key is missing, invalid, or IP not in allowlist |

### Entitlements errors

| Code | Status | Description |
|------|--------|-------------|
| `entitlements.insufficient_credits` | 402 | Not enough credits to process this job |
| `entitlements.queue_limit_exceeded` | 402 | Maximum queued jobs reached for this tier |
| `entitlements.invalid_user` | 402 | User account is not in a valid state |
| `entitlements.error` | 500 | Internal entitlements service error |

### Batch errors

| Code | Status | Description |
|------|--------|-------------|
| `batch.not_found` | 404 | Batch ID does not exist |
| `batch.forbidden` | 403 | Batch belongs to another user |
| `batch.limit_exceeded` | 429 | Too many active batches |
| `batch.zip_limit_exceeded` | 400 | ZIP download exceeds size limit |
| `batch.output_limit_exceeded` | 400 | Batch output exceeds limit |
| `batch.empty` | 400 | Batch contains no jobs |
| `batch.expired` | 410 | Batch artifacts have been cleaned up |

### Job errors

| Code | Status | Description |
|------|--------|-------------|
| `jobs.not_found` | 404 | Job ID does not exist |
| `jobs.not_ready` | 409 | Job is still processing; download not available yet |
| `jobs.expired` | 410 | Job output has been cleaned up |
| `jobs.forbidden` | 403 | Job belongs to another user |
| `jobs.timeout` | 500 | Job exceeded the processing time limit |
| `jobs.not_deletable` | 409 | Job cannot be deleted in its current state |

### Queue errors

| Code | Status | Description |
|------|--------|-------------|
| `queue.limit_exceeded` | 429 | Too many queued jobs; wait for existing jobs to finish |
| `queue.pool_saturated` | 429 | Worker pool is at capacity |
| `rate_limited` | 429 | IP-level rate limit exceeded |

### Usage errors

| Code | Status | Description |
|------|--------|-------------|
| `usage.daily_limit_exceeded` | 402 | Free/guest daily credit cap reached |

### Tool-specific errors

Individual tools may define additional error codes under their own namespace. See each tool's API documentation for converter-specific errors:

- [Convert.FAST errors](https://convert.fast/api/docs/errors) -- `convert.*` codes for format-specific failures
- [Compress.FAST errors](https://compress.fast/api/docs/errors) -- `compress.*` codes for compression failures

## Handling errors in code

<!-- code-tabs:start default=curl -->
### cURL
```bash
RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "https://api.tools.fast/convert" \
  -H "X-Fast-Api-Key: $API_KEY" \
  -F "file=@photo.heic" \
  -F "targetFormat=jpg")

HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

if [ "$HTTP_CODE" -ge 400 ]; then
  ERROR=$(echo "$BODY" | jq -r '.error')
  DETAIL=$(echo "$BODY" | jq -r '.detail')
  echo "Error $HTTP_CODE: [$ERROR] $DETAIL"

  if [ "$HTTP_CODE" = "429" ]; then
    echo "Rate limited. Retry after delay."
  fi

  exit 1
fi

JOB_ID=$(echo "$BODY" | jq -r '.id')
echo "Job submitted: $JOB_ID"
```

### PowerShell
```powershell
try {
  $job = Invoke-RestMethod -Method Post "https://api.tools.fast/convert" `
    -Headers @{ "X-Fast-Api-Key" = $env:API_KEY } `
    -Form @{ file = Get-Item "photo.heic"; targetFormat = "jpg" }

  Write-Host "Job submitted: $($job.id)"
}
catch {
  $statusCode = $_.Exception.Response.StatusCode.value__
  $body = $_.ErrorDetails.Message | ConvertFrom-Json

  Write-Host "Error ${statusCode}: [$($body.error)] $($body.detail)"

  if ($statusCode -eq 429) {
    Write-Host "Rate limited. Retry after delay."
  }

  exit 1
}
```
<!-- code-tabs:end -->
