# Errors

> **Network reference:** For the full cross-tool error code reference, see [tools.fast/api/docs/errors](https://tools.fast/api/docs/errors).

## Error envelope

All API errors return a JSON object with at least two fields:

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

Some errors include additional context fields. URL fetch errors include an `isRetryable` boolean indicating whether the request can be retried (e.g. transient network failures vs permanent validation errors).

## `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 or application/json required."
}
```

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

### No files provided
```json
{
  "error": "request.no_files",
  "detail": "Exactly one file must be provided."
}
```

### Invalid extension
```json
{
  "error": "request.invalid_extension",
  "detail": "File extension '.mp3' is not accepted by this compressor."
}
```
<!-- code-tabs:end -->

### URL fetch errors (`400`)

Returned when URL-based input (`inputUrl`) fails validation or the remote server returns an error.

<!-- code-tabs:start default=invalid-url -->
### Invalid URL
```json
{
  "error": "url_fetch.invalid_url",
  "detail": "inputUrl is required in JSON body.",
  "isRetryable": false
}
```

### Invalid scheme
```json
{
  "error": "url_fetch.invalid_scheme",
  "detail": "Only HTTPS URLs are allowed.",
  "isRetryable": false
}
```

### Private IP blocked
```json
{
  "error": "url_fetch.private_ip",
  "detail": "URL resolved to blocked IP address (private/reserved).",
  "isRetryable": false
}
```

### Mutually exclusive
```json
{
  "error": "url_fetch.mutually_exclusive",
  "detail": "Cannot provide both a file upload and inputUrl. Use one or the other.",
  "isRetryable": false
}
```

### Output URL reserved
```json
{
  "error": "url_fetch.output_url_reserved",
  "detail": "outputUrl is reserved for future use and not yet supported.",
  "isRetryable": false
}
```

### Remote server error
```json
{
  "error": "url_fetch.http_error",
  "detail": "Remote server returned 503 Service Unavailable.",
  "isRetryable": true
}
```
<!-- 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)."
}
```

This error intentionally does not distinguish between an invalid key and an IP not in the allowlist.

## `402` Payment Required

The caller's account does not have enough credits.

```json
{
  "error": "entitlements.insufficient_credits",
  "detail": "Insufficient credits. Required: 5, Available: 2"
}
```

## `403` Forbidden

The resource exists but the caller does not own it, or the action is not allowed for the caller's account.

<!-- code-tabs:start default=forbidden -->
### Job forbidden
```json
{
  "error": "jobs.forbidden",
  "detail": "You do not have access to this job."
}
```

### URL input not allowed for guests
```json
{
  "error": "url_fetch.guest_not_allowed",
  "detail": "URL-based input is not available for guest users. Please sign in.",
  "isRetryable": false
}
```
<!-- code-tabs:end -->

## `404` Not Found

The requested job does not exist.

```json
{
  "error": "jobs.not_found",
  "detail": null
}
```

## `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, or canceled jobs."
}
```

### Not deletable
```json
{
  "error": "jobs.not_deletable",
  "detail": "Cannot delete a job that is still queued or running. Cancel it first."
}
```

<!-- code-tabs:end -->

## `410` Gone

The job completed but its output artifacts have expired and been cleaned up. Output artifacts are retained for **1 hour** after job completion, then automatically deleted. To clean up immediately after downloading, call `DELETE /compress/job/{id}`.

```json
{
  "error": "jobs.expired",
  "detail": "Job artifacts were deleted on 2025-01-15 12:00:00Z after the retention period expired."
}
```

## `413` Payload Too Large

The uploaded file exceeds the per-compressor 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."
}
```

### URL fetch rate limited
```json
{
  "error": "url_fetch.rate_limited",
  "detail": "Too many concurrent URL fetches. Please wait and retry.",
  "isRetryable": true
}
```
<!-- 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.

## `5xx` Server Error

Server-side errors are rare but possible. Retry with exponential backoff (1s, 2s, 4s, 8s, max 30s). If errors persist, contact [support@tools.fast](mailto:support@tools.fast).

## Error codes reference

| Code | Status | Description |
|------|--------|-------------|
| `request.invalid_content_type` | 400 | Must use `multipart/form-data` or `application/json` |
| `request.no_files` | 400 | No file attached |
| `request.empty_file` | 400 | Uploaded file has zero bytes |
| `request.invalid_extension` | 400 | File extension not accepted by compressor |
| `request.multiple_files` | 400 | Only one file per request |
| `request.file_too_large` | 413 | File exceeds size limit |
| `api_key.invalid_or_ip_not_allowed` | 401 | Bad API key or IP not in allowlist |
| `entitlements.insufficient_credits` | 402 | Not enough credits |
| `jobs.forbidden` | 403 | Job owned by another user |
| `jobs.not_found` | 404 | Job ID does not exist |
| `jobs.not_ready` | 409 | Download requested before job finishes |
| `jobs.not_cancellable` | 409 | Job already completed/failed |
| `jobs.not_deletable` | 409 | Cannot delete queued/running jobs — cancel first |
| `jobs.expired` | 410 | Output artifacts cleaned up |
| `rate_limited` | 429 | IP rate limit exceeded |
| `queue.limit_exceeded` | 429 | Too many queued jobs |
| `compress.invalid_webhook` | 400 | Webhook URL or secret failed validation |
| `compress.unsupported_format` | 400 | File format not recognized by the universal compressor |
| `compress.{format}.validation.unsupported_format` | 400 | File format not accepted by the format-specific compressor |
| `estimate.unsupported_format` | 400 | No compressor found for the requested format |
| `schema.unsupported_format` | 400 | No compressor found for the requested format |
| `url_fetch.invalid_url` | 400 | URL is missing, empty, or malformed |
| `url_fetch.invalid_scheme` | 400 | Only HTTPS allowed |
| `url_fetch.private_ip` | 400 | URL resolved to private/reserved IP (SSRF blocked) |
| `url_fetch.dns_failure` | 400 | DNS resolution failed for the URL host |
| `url_fetch.timeout` | 400 | Remote server did not respond in time |
| `url_fetch.too_large` | 400 | Remote file exceeds size limit |
| `url_fetch.invalid_content` | 400 | Remote server returned an empty response |
| `url_fetch.http_error` | 400 | Remote server returned an error status |
| `url_fetch.redirect_loop` | 400 | Redirect chain exceeded maximum hops |
| `url_fetch.redirect_denied` | 400 | Redirect target resolved to private/reserved IP |
| `url_fetch.domain_circuit_open` | 400 | Domain temporarily blocked due to repeated failures |
| `url_fetch.url_expired` | 400 | Presigned URL has expired (S3, GCS, Azure SAS) |
| `url_fetch.no_filename` | 400 | Could not determine filename from URL or response headers |
| `url_fetch.mutually_exclusive` | 400 | Cannot provide both file upload and inputUrl |
| `url_fetch.output_url_reserved` | 400 | outputUrl is not yet supported |
| `url_fetch.guest_not_allowed` | 403 | URL input requires a signed-in account |
| `url_fetch.rate_limited` | 429 | Too many concurrent URL fetches |
| `compare.not_ready` | 409 | Job not yet succeeded for comparison |
| `compare.expired` | 410 | Output artifacts cleaned up |
| `compare.timeout` | 504 | Preview generation timed out |
| `server.internal_error` | 500 | Unexpected server error — retry with exponential backoff |
