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,403or429, with the fields nested undererror. - 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
- Branch on the HTTP status first. It is reliable whichever shape the body has.
- Read
codeat the top level of the body. - If there is no top-level
code, readerror.code. Only the API key check returns this shape. - If
codeisBAD_REQUESTandissuesis present, readissuesto find which fields were rejected.
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
The key check runs before the request reaches any operation. It returns a body with error.code and error.message, and no other fields:
{
"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. |
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.
Operation errors
Every error raised after the key check returns the fields at the top level:
{
"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. |
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
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
| 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. |
| 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. |
| 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. |