# Errors

Errors come back as JSON with a stable `code` for your program and a `message` written for a person. Branch on the code; show or log the message.

## Calling a Task

Calls use OpenAI's error shape, so every OpenAI SDK raises its usual exception class (`RateLimitError` for a 429, and so on):

```json
{"error": {"message": "support-triage is still training v1. It answers once v1 merges.", "type": "invalid_request_error", "param": "model", "code": "model_not_ready"}}
```

| Status | Code | Why | What to do |
|---|---|---|---|
| 400 | `invalid_json` | The body isn't a JSON object | Send JSON with `Content-Type: application/json` |
| 400 | `invalid_model` | `model` is missing or malformed | Set it to a Task name, or `name@vN` |
| 400 | `invalid_messages` | `messages` is empty, or a message has no valid role | Send at least one message with role `system`, `user`, `assistant` or `tool` |
| 400 | `invalid_parameter` | A parameter is out of range, such as `max_tokens` over the Task's ceiling | Read `param` and the message for the allowed range |
| 400 | `unsupported_parameter` | A parameter Facet doesn't take, such as `n` above 1 | Leave it unset |
| 401 | `invalid_api_key` | The key is missing, revoked, expired or not a Facet key | Send `Authorization: Bearer fct_...` with a live key |
| 403 | `workspace_suspended` | The workspace is suspended | An Owner writes to support@halfspacelabs.com |
| 404 | `model_not_found` | No Task by that name for this key, or no such version | Check the name; a key limited to one Task can't call others |
| 409 | `model_not_ready` | The Task's first version is still being built | Wait until v1 is live; its Overview shows when |
| 409 | `version_archived` | A pinned version was archived | Restore it from its Version page, or call the live version |
| 429 | `rate_limit_exceeded` | Too many calls or tokens this minute, or too many in flight | Wait for `Retry-After` seconds and retry; see [Limits](/docs/limits) |
| 429 | `spend_limit_reached` | The workspace reached its monthly spend limit | An Owner raises it in **Settings, Billing**, or wait for next month |
| 503 | `pool_unavailable` | The model is starting or briefly unavailable | Retry after `Retry-After` seconds |
| 503 | `adapter_unavailable` | This version couldn't load just now | Retry; if it persists, write to us with the `x-facet-request-id` |
| 5xx | `pool_error` | Something failed on our side | Retry once; then write to us with the request ID |

## The API and your agent

The `/v1` API and the MCP tools share one shape. A refusal that needs someone's approval names who:

```json
{"error": {"code": "strict_merge", "message": "merge protection is Strict: a Task owner approves each Ready Change", "approvers": [{"id": "usr_...", "name": "Dana", "email": "dana@example.com"}]}}
```

| Status | Code | Why | What to do |
|---|---|---|---|
| 400 | `invalid` | A field is missing or malformed | Read the message |
| 401 | `unauthenticated` | No session or key | Sign in, or send `Authorization: Bearer fct_...` |
| 403 | `csrf` | A browser write without its CSRF token | Use the web app, or send a key instead of a cookie |
| 403 | `not_allowed` | Your role or key doesn't cover this | Ask an Owner for a role that does |
| 403 | `not_task_owner` | Only the Task's owners may do this | Ask a Task owner |
| 403 | `not_workspace_owner` | Only workspace Owners may do this | Ask an Owner |
| 403 | `agent_never_approves` | Facet's own agent can't approve or merge its own work | A person approves |
| 403 | `not_ready` | Only a Ready change merges | Wait until it's Ready |
| 403 | `spend_limit_reached` | The month's spend is at the limit | An Owner raises it, or wait for next month |
| 403 | `quote_over_limit` | The quote doesn't fit in what's left this month | Raise the spend limit, or approve a smaller plan |
| 403 | `free_allowance_used` | The free allowance doesn't cover it | An Owner picks a plan |
| 403 | `draft_limit` | This month's drafts are used | Add a card for more, or wait for next month |
| 403 | `draft_busy` | As many drafts as allowed are being written | Wait for one to finish |
| 403 | `draft_budget` | This draft spent its budget | Start a new draft |
| 403 | `workspace_suspended` | The workspace is suspended; you can still sign in and read | An Owner writes to support@halfspacelabs.com |
| 404 | `not_found` | No such thing in this workspace | Check the ID and the workspace |
| 409 | `strict_merge` | **Approve each merge** is on for this Task | One of `approvers` approves the merge |
| 409 | `loosens_expectation` | Lowering a bar, removing or making an Expectation optional | One of `approvers` approves it |
| 409 | `conflict` | It changed while you were editing | Reload and try again |
| 409 | `needs_card` | This needs a card on file | An Owner adds one in **Settings, Billing** |
| 409 | `workspace_deleting` | The workspace is being deleted | An Owner can undo it within 7 days |
| 422 | (varies) | An import Facet can't take | The message says why |
| 429 | `rate_limited` | Too many calls this minute | Wait for `Retry-After` seconds; see [Limits](/docs/limits) |
| 500 | `internal` | Something failed on our side | Retry; then write to us |

## Retrying

- Retry `429` and `503` after the `Retry-After` header. The OpenAI SDKs do this on their own.
- Don't retry `400`, `401`, `403`, `404` or `409` unchanged: something has to change first.
- Retry a `500` once, then write to support@halfspacelabs.com with the request ID.

## Next steps

- [Limits](/docs/limits): the numbers behind `rate_limit_exceeded`
- [Pricing](/docs/pricing): the spend limit and what counts toward it
