> ## Documentation Index
> Fetch the complete documentation index at: https://docs.horizonpay.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Invoice AI API Errors: Status Codes & Problem+JSON

> HTTP status codes, the application/problem+json error envelope, error type slugs, and best practices for handling errors in your integration.

# Errors

The Invoice AI API signals errors through standard HTTP status codes combined with a structured error body. Every error response uses the `application/problem+json` content type, defined by [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).

## Error Response Format

An error response body always contains these four fields:

<ResponseField name="type" type="string" required>
  A URI that uniquely identifies the error type. Stable across API versions —
  safe to branch your error-handling code on. Points to documentation for that
  specific error when fetched.
</ResponseField>

<ResponseField name="title" type="string" required>
  A short, human-readable summary of the error type. Does **not** change
  between occurrences of the same error. Use `type` for programmatic checks;
  use `title` for display.
</ResponseField>

<ResponseField name="status" type="integer" required>
  The HTTP status code for this occurrence. Matches the response's HTTP status
  line exactly.
</ResponseField>

<ResponseField name="detail" type="string" required>
  A human-readable explanation specific to this occurrence of the error. May
  include the ID of the affected resource or a description of what failed
  validation. Suitable for displaying to an operator; not guaranteed to be
  stable across releases.
</ResponseField>

### Example

```json theme={null}
{
  "type": "https://invoice.horizonpay.co/errors/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Invoice in_xxx was not found"
}
```

<Note>
  Error responses are not wrapped in a `data` envelope. Only successful
  responses use `{ "data": ... }`. Check the HTTP status code first; if it is
  4xx or 5xx, parse the body as `application/problem+json`.
</Note>

## HTTP Status Codes

### 400 — Bad Request

The request body failed schema validation. The `detail` field describes which field was invalid. Fix the request body before retrying.

```json theme={null}
{
  "type": "https://invoice.horizonpay.co/errors/validation",
  "title": "Bad Request",
  "status": 400,
  "detail": "\"amount\" must be a positive integer"
}
```

### 401 — Unauthorized

The `Authorization` header is missing, malformed, or contains a key that does not exist or has been revoked. See the [Authentication](/api-reference/authentication) reference.

```json theme={null}
{
  "type": "https://invoice.horizonpay.co/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "No valid API key was provided."
}
```

### 403 — Forbidden

The API key is valid but does not have the scope required by this endpoint. Add the missing scope to the key or create a new key. See [Scopes](/api-reference/authentication#scopes).

```json theme={null}
{
  "type": "https://invoice.horizonpay.co/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "This API key does not have the invoices:write scope."
}
```

<Note>
  A request for a resource that belongs to a different workspace returns `404
      Not Found`, not `403 Forbidden`. Returning `403` would confirm the resource
  exists, enabling enumeration of other workspaces' IDs.
</Note>

### 404 — Not Found

The requested resource does not exist, has been deleted, or belongs to another workspace.

```json theme={null}
{
  "type": "https://invoice.horizonpay.co/errors/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Invoice in_xxx was not found"
}
```

### 409 — Conflict

A uniqueness or concurrency constraint was violated. The most common cause is retrying a request with the same `Idempotency-Key` but a different request body. Resolve the conflict before retrying: either use a new `Idempotency-Key` for a genuinely new request, or resend the original body to safely replay the original request.

```json theme={null}
{
  "type": "https://invoice.horizonpay.co/errors/conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "An idempotency key was reused with a different request body."
}
```

### 422 — Unprocessable Entity

The request was syntactically valid and well-formed, but the operation cannot be performed because the resource is in the wrong state. Common examples:

* Attempting to finalize an invoice that is already `open` or `paid`.
* Attempting to void an invoice that has already been voided.
* Attempting to add a line item to an invoice that is no longer a `draft`.

```json theme={null}
{
  "type": "https://invoice.horizonpay.co/errors/invalid-state",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "This invoice has already been finalized and cannot be edited."
}
```

<Tip>
  Before calling `finalize` or `pay`, fetch the invoice and check its `status`
  field. Only `draft` invoices can be finalized; only `open` invoices can be
  paid. This avoids most 422 errors without a round-trip to discover them.
</Tip>

### 429 — Too Many Requests

Your API key has exceeded the rate limit. Back off and retry after the interval indicated in the `Retry-After` response header (in seconds).

```json theme={null}
{
  "type": "https://invoice.horizonpay.co/errors/rate-limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Retry after 10 seconds."
}
```

### 500 — Internal Server Error

An unexpected error occurred on the Invoice AI servers. These are rare. If you receive persistent `500` errors, check the [status page](https://status.horizonpay.co) and contact support.

```json theme={null}
{
  "type": "https://invoice.horizonpay.co/errors/internal",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "An unexpected error occurred. Please try again later."
}
```

## Error Type Reference

The `type` URI carries a machine-stable slug. Use these slugs in your error-handling logic — never branch on the `title` string, which may be rephrased over time.

| `type` slug     | HTTP status | Meaning                                           |
| --------------- | ----------- | ------------------------------------------------- |
| `validation`    | 400         | Request body failed schema validation             |
| `unauthorized`  | 401         | Missing or invalid API key                        |
| `forbidden`     | 403         | Key lacks the required scope                      |
| `not-found`     | 404         | Resource does not exist or is not accessible      |
| `conflict`      | 409         | Idempotency key reuse or uniqueness violation     |
| `invalid-state` | 422         | Resource is in the wrong state for this operation |
| `rate-limited`  | 429         | Rate limit exceeded                               |
| `internal`      | 500         | Unexpected server-side error                      |

## Handling Errors

### Check the content type

Before parsing an error body, verify the response `Content-Type` is `application/problem+json`. Proxies and CDN edge nodes occasionally return their own HTML error pages for network-level errors.

```javascript theme={null}
async function apiRequest(url, options) {
  const res = await fetch(url, options);

  if (!res.ok) {
    const contentType = res.headers.get('content-type') ?? '';
    if (contentType.includes('application/problem+json')) {
      const problem = await res.json();
      throw new ApiError(problem);
    }
    // Unexpected non-JSON error (e.g. a proxy's HTML error page)
    throw new Error(`HTTP ${res.status}: ${res.statusText}`);
  }

  return res.json();
}
```

### Retry strategy

Not all errors are worth retrying. Use the following guidance:

| Status | Retry? | Notes                                          |
| ------ | ------ | ---------------------------------------------- |
| 400    | ❌ No   | Fix the request body first                     |
| 401    | ❌ No   | Check and rotate your API key                  |
| 403    | ❌ No   | Add the required scope to the key              |
| 404    | ❌ No   | The resource does not exist                    |
| 409    | ❌ No   | Resolve the conflict; do not blindly retry     |
| 422    | ❌ No   | Fetch the resource and check its current state |
| 429    | ✅ Yes  | Respect the `Retry-After` header               |
| 500    | ✅ Yes  | Use exponential backoff with jitter            |

```javascript theme={null}
async function withRetry(fn, maxAttempts = 4) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const retryable = err.status === 429 || err.status >= 500;
      if (!retryable || attempt === maxAttempts) throw err;

      const retryAfter = err.retryAfter ?? Math.pow(2, attempt) * 500;
      await new Promise(resolve => setTimeout(resolve, retryAfter));
    }
  }
}
```

<Warning>
  Never implement a blind retry loop for `409 Conflict`. A conflict caused by
  an idempotency key mismatch will not resolve itself — retrying only produces
  more `409` responses.
</Warning>
