# Errors

The Frontline Public API returns errors as JSON with a consistent envelope, an HTTP status code, and a machine-readable error code.

## Error envelope


```json
{
    "ok": false,
    "error": {
        "code": "bad_request",
        "message": "Human-readable description of what went wrong.",
        "details": {
            /* optional, present on validation errors */
        }
    }
}
```

- `ok` is always `false` on error responses.
- `error.code` is a stable, machine-readable string. Branch on this instead of parsing `message`.
- `error.message` is meant for humans and may change without notice.
- `error.details` is **optional** and appears mainly on validation failures (e.g. zod issues from the request body or query).


## Status codes and error codes

| HTTP | `error.code` | When it happens |
|  --- | --- | --- |
| 400 | `bad_request` | Validation failed (body, params, query). Inspect `error.details` for the offending fields. |
| 400 | `cli_outdated` | Caller is `@getfrontline/cli` below the minimum supported version. See "CLI outdated" below. |
| 401 | `unauthorized` | Missing, malformed, revoked, or wrong-scope API key. See [Authentication](/docs/authentication). |
| 402 | `internal_error` | Subscription/billing gate (e.g. plan does not allow the operation). |
| 403 | `forbidden` | The key is valid but does not have access to the target resource. |
| 404 | `not_found` | The resource does not exist or has been soft-deleted. |
| 409 | `conflict` | Uniqueness or relational constraint violated (e.g. duplicate name, foreign key). |
| 429 | `internal_error` | Rate limit exceeded. See [Rate limits](/docs/rate-limits). |
| 500 | `internal_error` | Unexpected server error. Safe to retry once; if it persists, report it. |


## Validation errors (`400 bad_request`)

When zod rejects the request body or query string, `error.details` carries the structured issues:


```json
{
    "ok": false,
    "error": {
        "code": "bad_request",
        "message": "Invalid request",
        "details": {
            "issues": [
                {
                    "path": ["name"],
                    "message": "String must contain at least 1 character(s)",
                    "code": "too_small"
                }
            ]
        }
    }
}
```

Use `details.issues[].path` to highlight the bad field in your UI.

The `code` on each issue is one of:

- A standard zod code (`invalid_type`, `too_small`, `too_big`, `invalid_enum_value`, `invalid_string`, …) for shape errors caught by request validation.
- One of the Public-API codes below for cross-resource validation:


| `code` | When it appears |
|  --- | --- |
| `not_found` | A referenced id (`aiModelId`, `customToolIds[]`, `intentIds[]`, `assistantId`, `tableId`, `triggeredByAgentIds[]`, etc.) does not exist or is not accessible from the caller's account. |
| `wrong_kind` | The reference exists but does not match the surrounding constraint (e.g. a `recordTypeId` that does not belong to the given `tableId`). |
| `wrong_owner` | The reference exists in the platform but belongs to a different account. |


### Example: node references

When you create a flow or workflow node with references to resources that don't exist, the API rejects the request **before** persisting anything and returns every offending reference in a single response:


```json
{
    "ok": false,
    "error": {
        "code": "bad_request",
        "message": "One or more node references are invalid",
        "details": {
            "issues": [
                {
                    "path": ["data", "aiModelId"],
                    "code": "not_found",
                    "value": 99999999,
                    "message": "AI model 99999999 does not exist."
                },
                {
                    "path": ["data", "customToolIds", 0],
                    "code": "not_found",
                    "value": 42,
                    "message": "Tool 42 does not exist or is not accessible from this account."
                }
            ]
        }
    }
}
```

Branch on `issue.code === "not_found"` to highlight the broken reference; the human-readable message tells the end user which id was rejected.

## CLI outdated (`400 cli_outdated`)

Returned exclusively to the `@getfrontline/cli` binaries when the client version is older than the minimum the server supports. The payload includes extra context:


```json
{
    "ok": false,
    "error": {
        "code": "cli_outdated",
        "message": "CLI version is too old. Please update @getfrontline/cli.",
        "details": { "minSupportedVersion": "1.0.9" },
        "suggestion": "Update with: npm i -g @getfrontline/cli (or your package manager equivalent)."
    }
}
```

The CLI handles this automatically and prints `suggestion` to the user.

## Recommended client handling


```js
async function callApi(path, init) {
    const res = await fetch(`https://prod-api.getfrontline.ai/public/v1${path}`, init);
    const body = await res.json().catch(() => ({}));

    if (!res.ok) {
        const code = body?.error?.code ?? "unknown";
        const message = body?.error?.message ?? res.statusText;
        throw Object.assign(new Error(message), {
            code,
            status: res.status,
            details: body?.error?.details,
        });
    }

    return body;
}
```

Branch on `err.code` (`unauthorized`, `not_found`, `conflict`, …) in your business logic; only show `err.message` to end users.