# Errors (https://terac.com/docs/developers/guides/errors)

The two error body shapes the Terac API returns, how to read the error code from either one, and every error code with its HTTP status.

A failed request returns an HTTP status of 400 or above and a JSON body. The body has one of two shapes, depending on where the request was refused:

* **API key errors** come from the key check that runs before any operation. They return `401`, `403` or `429`, with the fields nested under `error`.
* **Operation errors** come from the operation itself: validation, missing resources, state conflicts, server errors, and unknown paths. They return the fields at the top level.

## How to read an error [#how-to-read-an-error]

1. Branch on the HTTP status first. It is reliable whichever shape the body has.
2. Read `code` at the top level of the body.
3. If there is no top-level `code`, read `error.code`. Only the API key check returns this shape.
4. If `code` is `BAD_REQUEST` and `issues` is present, read `issues` to find which fields were rejected.

```ts
const body = await response.json();
const code = body.code ?? body.error?.code;
const message = body.message ?? body.error?.message;
```

The status alone does not always tell you the shape. A `401` or a `429` always comes from the key check and is nested, but a `403` can be nested (from the key check) or flat (from an operation). The fallback above handles both.

## API key errors [#api-key-errors]

The key check runs before the request reaches any operation. It returns a body with `error.code` and `error.message`, and no other fields:

```json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "API key required. Include Authorization: Bearer <key> header."
  }
}
```

| HTTP status | `error.code`   | `error.message`                                                      | Cause                                                                                                       |
| ----------- | -------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| 401         | `UNAUTHORIZED` | `API key required. Include Authorization: Bearer <key> header.`      | The `Authorization` header is missing or does not start with `Bearer `.                                     |
| 401         | `UNAUTHORIZED` | `Invalid or expired API key`                                         | The key is unknown, disabled, expired, or revoked, or its owner no longer exists.                           |
| 401         | `UNAUTHORIZED` | `API key is not scoped to an organization. Please create a new key.` | The key is not linked to an organization. Create a new key from your organization settings.                 |
| 403         | `FORBIDDEN`    | `This account is not permitted to use the API.`                      | The account that owns the key is banned or deleted.                                                         |
| 429         | `RATE_LIMITED` | `Rate limit exceeded. Retry after 42 seconds.`                       | The key is rate limited. The number of seconds changes with each response. See [Rate limits](https://terac.com/docs/developers/guides/errors#rate-limits). |

### Rate limits [#rate-limits]

A rate limited key returns `429` with `error.code` `RATE_LIMITED` and a `Retry-After` header that gives the number of seconds to wait. Wait that long, then retry. The key is still valid, so do not rotate or revoke it.

The limit itself is described in [Authentication](https://terac.com/docs/developers/guides/authentication#rate-limits).

## Operation errors [#operation-errors]

Every error raised after the key check returns the fields at the top level:

```json
{
  "message": "Input validation failed",
  "code": "BAD_REQUEST",
  "issues": [
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "undefined",
      "path": ["name"],
      "message": "Required"
    }
  ],
  "data": {
    "code": "BAD_REQUEST",
    "httpStatus": 400,
    "path": "createProject"
  }
}
```

| Field     | Type   | Present                 | Description                                                                                                                                                                                                               |
| --------- | ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`    | string | Always                  | An error code from the [table below](https://terac.com/docs/developers/guides/errors#error-codes).                                                                                                                                                                       |
| `message` | string | Always                  | What went wrong. For input validation errors it is always `Input validation failed`, and the detail is in `issues`.                                                                                                       |
| `issues`  | array  | Input validation errors | One entry per rejected value. Absent on every other error.                                                                                                                                                                |
| `data`    | object | Always                  | Repeats `code` and the HTTP status (`httpStatus`), and names the operation in `path` when the request matched one. It can carry other fields. It is not part of the OpenAPI schema, so read the top-level fields instead. |

### Validation issues [#validation-issues]

When the request does not match the operation's schema (a missing field, a wrong type, a value outside the allowed range or enum), the response is `400` with `code` `BAD_REQUEST`, `message` `Input validation failed`, and an `issues` array. Each issue has:

| Field     | Type                         | Description                                                                                                                                                                                                      |
| --------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path`    | array of strings and numbers | Where the value is. Object keys are strings and array positions are numbers, so `["tasks", 0, "task_type"]` is the `task_type` of the first task. Query and path parameters appear by name, such as `["limit"]`. |
| `message` | string                       | Why the value was rejected, such as `Required` or `Expected number, received nan`.                                                                                                                               |
| `code`    | string                       | The kind of problem, such as `invalid_type`, `too_small`, `too_big`, `invalid_enum_value`, or `custom`.                                                                                                          |

Some issue codes carry extra fields that describe the problem, such as `expected` and `received` on `invalid_type`, or `minimum` on `too_small`. A number field that is missing or not numeric reports `Expected number, received nan`, not `Required`.

Some rules are checked after the schema, such as an unknown filter slug. Those are also `400 BAD_REQUEST`, but they have no `issues`: the `message` names the problem and, where it can, the valid values.

## Error codes [#error-codes]

| HTTP status | `code`                   | Shape  | Returned when                                                                                                                                                                                                                  |
| ----------- | ------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 400         | `BAD_REQUEST`            | Flat   | The request is invalid: it fails the schema (see `issues`), or breaks a rule the schema cannot express, such as an unknown filter slug or an unsupported filter operator. The `message` says which.                            |
| 400         | `PARSE_ERROR`            | Flat   | The body is not valid JSON. An empty body sent with `Content-Type: application/json` is also rejected this way; send `{}` instead.                                                                                             |
| 401         | `UNAUTHORIZED`           | Nested | The API key is missing, invalid, expired, or revoked. See [API key errors](https://terac.com/docs/developers/guides/errors#api-key-errors).                                                                                                                                   |
| 403         | `FORBIDDEN`              | Nested | The account that owns the API key is banned or deleted.                                                                                                                                                                        |
| 403         | `FORBIDDEN`              | Flat   | The key's owner lacks the organization permission the operation needs. The `message` names the missing scope.                                                                                                                  |
| 403         | `FORBIDDEN`              | Flat   | The operation works only in a sandbox organization, and the API key belongs to one that is not. Creating a test submission is the only such operation.                                                                         |
| 404         | `NOT_FOUND`              | Flat   | The resource does not exist or belongs to another organization. Also returned for a path or HTTP method the API does not have, with the message `Not found`.                                                                   |
| 409         | `CONFLICT`               | Flat   | The action is not allowed in the resource's current state, such as launching an opportunity that is not a draft or approving a submission that is not awaiting review. Also returned when a webhook URL is already subscribed. |
| 412         | `PRECONDITION_FAILED`    | Flat   | Something you can fix must be in place first: the organization balance cannot cover a launch, the opportunity is not ready to launch, or a webhook endpoint failed the confirmation request. The `message` says what to fix.   |
| 415         | `UNSUPPORTED_MEDIA_TYPE` | Flat   | A `POST` or `PATCH` request without `Content-Type: application/json`. Send that header and a JSON body on every `POST` and `PATCH`, including actions such as launch that take no fields (send `{}`).                          |
| 422         | `UNPROCESSABLE_CONTENT`  | Flat   | A feasibility request cannot price the opportunity (not priced yet, already used, or closed), or Terac cannot create the test submission asked for on that opportunity. The `message` says which and what to do.               |
| 429         | `RATE_LIMITED`           | Nested | The API key is rate limited. Wait the number of seconds in the `Retry-After` header, then retry. See [Rate limits](https://terac.com/docs/developers/guides/errors#rate-limits).                                                                                              |
| 500         | `INTERNAL_SERVER_ERROR`  | Flat   | An unexpected error on Terac's side. The `message` is usually `Unknown error`. Retry with backoff. Before retrying a create, list the resources first: the failed attempt may have created one.                                |