The Frontline Public API returns errors as JSON with a consistent envelope, an HTTP status code, and a machine-readable error code.
{
"ok": false,
"error": {
"code": "bad_request",
"message": "Human-readable description of what went wrong.",
"details": {
/* optional, present on validation errors */
}
}
}okis alwaysfalseon error responses.error.codeis a stable, machine-readable string. Branch on this instead of parsingmessage.error.messageis meant for humans and may change without notice.error.detailsis optional and appears mainly on validation failures (e.g. zod issues from the request body or query).
| 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. |
| 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. |
| 500 | internal_error | Unexpected server error. Safe to retry once; if it persists, report it. |
When zod rejects the request body or query string, error.details carries the structured issues:
{
"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. |
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:
{
"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.
Returned exclusively to the @getfrontline/cli binaries when the client version is older than the minimum the server supports. The payload includes extra context:
{
"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.
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.