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

# Errors

> The error format, every error code, and how to handle each

The API uses standard HTTP status codes. A `2xx` means the request worked. A `4xx` means something in the request needs fixing. A `5xx` means something went wrong on our side.

***

## Error format

Every error has the same shape.

```json theme={null}
{
  "error": {
    "type": "not_found",
    "code": "customer_not_found",
    "message": "There is no customer with that code. To create one in Xero, send `create_customer`.",
    "param": "customer_code",
    "request_id": "req_4f1a9c2e7b8d3a605e1c9f02"
  }
}
```

| Field | Description |
| - | - |
| `type` | The family of error. It decides the HTTP status. |
| `code` | A stable, machine-readable string. **Branch on this.** |
| `message` | A sentence for a person. It can be reworded at any time, so never match on it. |
| `param` | The request field the error is about. Present on most `422` and some `404` and `409` errors. Nested fields use dots, such as `create_customer.name`. |
| `request_id` | The same value as the `Request-Id` header. Quote it when you contact support. |

## Error types

| Type | Status | Meaning |
| - | - | - |
| `authentication_error` | `401` | The API key is missing, malformed, revoked or expired. |
| `permission_error` | `403` | The key is valid but isn't allowed to do this. |
| `invalid_request_error` | `422` | A field is missing or invalid. Also `400` for a malformed body and `413` for an oversized one. |
| `not_found` | `404` | No such object, or no such endpoint. |
| `conflict` | `409` | The request clashes with the current state, or with an earlier request that used the same `Idempotency-Key`. |
| `rate_limit_error` | `429` | Too many requests. |
| `api_error` | `500` | Something went wrong on our side. |

***

## Error codes

### Authentication and permission

| Status | Code | Meaning |
| - | - | - |
| `401` | `invalid_api_key` | The `Authorization` header is missing or malformed, or the key doesn't exist. |
| `401` | `api_key_revoked` | The key has been revoked. |
| `401` | `api_key_expired` | The key has expired. |
| `403` | `insufficient_scope` | The key lacks the scope this endpoint needs. |
| `403` | `sandbox_not_available` | A `saasybill_test_` key was used. Test keys can't be used yet. |
| `403` | `xero_contact_creation_disabled` | The request asked to create a Xero contact, but the organisation hasn't allowed it. See [Customers and Xero](/api-reference/guides/customers-and-xero). |
| `403` | `plan_limit_reached` | Creating this would exceed a limit on the organisation's Saasybill plan, such as its customer or subscription limit. |

### Invalid requests

| Status | Code | Meaning |
| - | - | - |
| `422` | `invalid_parameter` | A field is missing or invalid. `param` names it. |
| `400` | `invalid_json` | The body isn't valid JSON. |
| `413` | `payload_too_large` | The body is over 100,000 characters. |
| `422` | `idempotency_key_required` | The request needs an `Idempotency-Key` header. |
| `422` | `idempotency_key_invalid` | The key isn't 1 to 255 printable characters. |

### Unit changes

Unit change errors are `422`, and `param` names the field. The code is one of these.

| Code | Meaning |
| - | - |
| `units_invalid` | `units` isn't a whole number of zero or more. |
| `units_below_minimum` | `units` is under the minimum set on the plan or the subscription. |
| `units_unchanged` | The subscription already has this many units. |
| `plan_has_no_units` | The plan is a flat fee plan, which doesn't bill by units. |
| `effective_date_required` | `prorate` is `true` but `effective_date` is missing. |
| `effective_date_out_of_period` | `effective_date` is outside the current billing period. |
| `proration_required` | This change lowers the amount payable and raises a credit note, so `prorate` can't be `false`. |
| `proration_behaviour_not_applicable` | `proration_behaviour` was sent without `prorate: true`. |
| `setup_mode_on` | The organisation is in Setup Mode, where subscriptions can't be updated. |
| `subscription_not_changeable` | The subscription has ended or completed. |

A change made inside the invoice window can also be refused when `document` is missing, or names a choice that isn't available. The error message lists the choices. See [Change units](/api-reference/guides/change-units#inside-the-invoice-window).

### Not found

| Status | Code | Meaning |
| - | - | - |
| `404` | `customer_not_found` | No customer has that id or code. |
| `404` | `plan_not_found` | No plan has that id or code. |
| `404` | `charge_not_found` | No charge has that id. |
| `404` | `subscription_not_found` | No subscription has that id. |
| `404` | `invoice_not_found` | No invoice has that id. |
| `404` | `unknown_endpoint` | The path isn't part of the API. |

### Conflicts

| Status | Code | Meaning |
| - | - | - |
| `409` | `idempotency_conflict` | The `Idempotency-Key` was already used with a different request. |
| `409` | `request_in_progress` | A request with this `Idempotency-Key` is still running. Try again shortly. |
| `409` | `invalid_state` | The subscription can't be ended or restored right now. The message says why. |
| `409` | `xero_accounts_not_mapped` | The organisation hasn't mapped its Xero accounts. An admin does this on the Integrations tab. |
| `409` | `xero_not_connected` | The organisation isn't connected to Xero. |
| `409` | `xero_contact_name_taken` | Xero already has a contact with that name. Use that contact's code, or a different name. |
| `409` | `xero_contact_archived` | The Xero contact with that code is archived. Restore it in Xero, or use another code. |
| `409` | `customer_not_importable` | A Xero contact has that code but hasn't been imported as a customer. Import it on the Customers page in Saasybill. |
| `409` | `customer_not_created` | The contact exists in Xero but couldn't be added as a customer. |

### Server errors

| Status | Code | Meaning |
| - | - | - |
| `429` | `rate_limited` | Over the rate limit. Wait for `Retry-After` seconds. |
| `500` | `internal_error` | Something went wrong on our side. |
| `502` | `xero_error` | Xero refused or failed a request the API made on your behalf. The message says why. |

<Info>
  New codes can be added to any family. Handle the ones you care about, and treat the rest by their status.
</Info>

***

## Handling errors

| Status | What to do |
| - | - |
| `400`, `422` | Fix the request. Read `param` to find the field. Don't retry unchanged. |
| `401` | Check the key. Don't retry. |
| `403` | The key or the organisation isn't set up for this. Don't retry. |
| `404` | The object doesn't exist. Check the id or code. |
| `409` | Read `code`. For `request_in_progress`, wait and retry. For the rest, resolve the state and try again. |
| `429` | Wait for the `Retry-After` header, then retry. |
| `500`, `502` | Retry with backoff, using the **same** `Idempotency-Key`. If it keeps failing, contact support with the `request_id`. |

<Tip>
  A write that times out may or may not have happened. Retry it with the same `Idempotency-Key` and Saasybill either completes it or returns the first answer. It never does the work twice. See [Idempotency](/api-reference/idempotency).
</Tip>
