Errors
All Management API errors follow one consistent JSON envelope: message, error_code, and detail. Knowing how to interpret each field helps you surface actionable feedback to your users and react appropriately in automation.
Response Envelope
Error object
{
"message": "Permission denied",
"error_code": "permission_denied",
"detail": null
}
- Name
message- Type
- string
- Description
Human-readable description. Use for logs or user-facing copy.
- Name
error_code- Type
- string
- Description
Stable machine-readable code. Rely on this field for branching.
- Name
detail- Type
- object | array | null
- Description
Optional extra context. Validation errors populate this field with field-level issues; most other errors return
null.
The machine-readable key is always error_code (never error). Branch your automation on error_code, not on message.
HTTP Status Codes
| Status | Typical Meaning | Example error_code |
|---|---|---|
| 400 Bad Request | Malformed JSON or unsupported payload | json_parse_error |
| 401 Unauthorized | Missing / invalid auth, expired token, or invalid API key | authentication_failed |
| 402 Payment Required | Spend cap reached (paid) or plan allowance exhausted (Free); billable writes are blocked | spend_cap_reached, plan_exhausted |
| 403 Forbidden | Authenticated but lacks permissions (role mismatch), or a structural plan limit was exceeded | permission_denied, plan_limit_exceeded |
| 404 Not Found | Environment, collection, resource, etc. does not exist or you cannot access it | environment_not_found, resource_not_found, locale_not_found |
| 405 Method Not Allowed | Wrong HTTP verb for the endpoint | method_not_allowed |
| 409 Conflict | Two writers collided on the same resource; nothing was lost — retry the same write | revision_number_conflict |
| 412 Precondition Failed | An If-Match precondition no longer holds: the resource moved on — re-read before retrying | revision_precondition_failed |
| 422 Unprocessable Content | Request failed validation or endpoint-specific business rules | validation_error, trailing_slash_required, protected_environment_cannot_be_deleted |
| 429 Too Many Requests | Per-plan requests-per-minute rate limit exceeded; carries a Retry-After header and is not billed | rate_limited |
| 500 Internal Server Error | Unexpected backend error | internal_server_error |
Endpoints often expose specific codes such as protected_environment_cannot_be_deleted, api_prefix_exists, or collection_already_connected_to_api. The HTTP status always matches the error type even when the code changes.
Validation Errors (422)
Field-level validation issues set error_code to validation_error and populate detail with an array of issues:
Validation error
{
"message": "Invalid data was provided",
"error_code": "validation_error",
"detail": [
{
"type": "string_too_short",
"loc": ["name"],
"msg": "String should have at least 1 character",
"input": ""
}
]
}
- Name
type- Type
- string
- Description
Error classifier (e.g.,
string_too_short,value_error).
- Name
loc- Type
- array
- Description
Location of the invalid field. Nested fields appear as multiple entries, such as
["fields", 0, "id"].
- Name
msg- Type
- string
- Description
Readable description that can be shown to end users.
- Name
input- Type
- any
- Description
Value that failed validation.
- Name
ctx- Type
- object | null
- Description
Optional metadata (for example,
{"min_length": 1}).
Content validation errors
Validating or publishing a revision checks the content against the collection's schema. These errors use a different item shape and are returned under detail.errors:
Content validation error
{
"message": "Content validation failed",
"error_code": "validation_failed",
"detail": {
"errors": [
{
"json_path": "$.title",
"message": "'title' is a required property",
"validator": "required",
"validator_value": ["title"]
}
]
}
}
The errors list is capped at 100 items. When more issues exist, the response also carries errors_truncated: true and errors_total with the full count. The response never includes the collection schema itself — fetch it via schema introspection or the versions API when you need it.
Endpoint-Specific Errors
Business rule violations use dedicated error codes so you can react explicitly:
Endpoint-specific error
{
"message": "Protected environment cannot be deleted",
"error_code": "protected_environment_cannot_be_deleted",
"detail": null
}
Examples include environment_toggle_error or too_many_versions. These codes indicate business constraints specific to the endpoint and are returned with detail: null. Structural plan limits are not endpoint-specific: they always use plan_limit_exceeded (see below).
Conditional Writes (409 / 412 / 422)
Two clients reading the same resource, each computing an update and each publishing it, is not a rare race — it is the ordinary shape of two agents working one collection. Without a precondition the second write silently supersedes the first, and neither client can tell.
Send the revision you expect to still be current as an If-Match header on Create Revision or Upsert Resource:
Conditional write
curl -X POST "https://api.foxnose.net/v1/:env/collections/:collection/resources/:resource/revisions/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: 8f2b1c9d" \
-d '{"data": {"title": "Updated"}}'
The value is the key of a revision — the one you read before computing the update. Quotes are accepted and stripped, so "8f2b1c9d" and 8f2b1c9d are the same precondition. Weak validators (W/"...") and * are not supported: W/ means "semantically equivalent, possibly not byte-identical", which is not a claim this API can evaluate, and * would assert only that some revision exists, which is not a lost-update guard.
The condition is evaluated under a row lock, in the same transaction that inserts the revision, so it cannot be satisfied and then invalidated before the write lands.
Which code means what
Three outcomes are easy to confuse, and they call for different handling:
| Status | error_code | What happened | What to do |
|---|---|---|---|
| 409 | revision_number_conflict | Two writers collided on the next revision number. Nobody's intent was lost. | Retry the same write. |
| 412 | revision_precondition_failed | The resource moved on: its current revision is no longer the one you named. | Re-read, recompute, then write. Retrying the same body would overwrite whatever moved it. |
| 422 | invalid_revision_precondition | The header itself is malformed — a weak validator, *, several values, or something that is not a revision key. | Fix the header. |
A 412 response names the revision that is current now. That saves a lookup to discover which revision won, but not the re-read itself: the response does not carry the record's content, so a client still has to fetch it before recomputing.
412 Precondition Failed
{
"message": "The resource's current revision is not the one this write expected. Re-read the record and recompute the update before retrying.",
"error_code": "revision_precondition_failed",
"detail": {
"current_revision": "a71e4f30"
}
}
detail.current_revision is null when the resource does not exist. Sending If-Match while upserting a resource that has never been created is therefore a refusal, not a create: the precondition asserts a state the resource does not have.
The same precondition is available at the edge on the Flux PUT update route as If-Match, and to MCP clients as the expected_revision argument of update_record. Within the Management API it applies to Create Revision and Upsert Resource; no other write accepts it. Read responses carry the value to send back in _sys.revision.
Structural Limits (403)
Exceeding a structural plan quota (projects, environments, Flux APIs, locales, API keys, custom roles) returns 403 with plan_limit_exceeded. The detail object identifies the entity and its limit so you can prompt an upgrade:
Plan Limit Exceeded
{
"message": "Plan limit exceeded",
"error_code": "plan_limit_exceeded",
"detail": {
"entity": "flux_apis",
"limit": 5,
"current": 5,
"upgrade_url": "https://foxnose.net/billing"
}
}
See Limits for per-plan quotas.
Billing Gate (402)
The Management API authors content, so it consumes the Writes metering axis. When billing is gated, write requests return 402:
plan_exhausted— a Free plan has used up its allowance for a metering axis (here,writes). Only the exhausted axis is blocked.spend_cap_reached— a paid plan has hit its spend cap. Billable writes are blocked until the cap is raised or the cycle resets.
Plan Exhausted (Free)
{
"error_code": "plan_exhausted",
"axis": "writes",
"window_resets_at": "2026-08-01T00:00:00Z",
"upgrade_url": "https://foxnose.net/billing"
}
Spend Cap Reached (paid)
{
"error_code": "spend_cap_reached",
"cap_usd": 38.0,
"cycle_resets_at": "2026-08-01T00:00:00Z",
"raise_cap_url": "https://foxnose.net/billing"
}
Failed requests are never billed: 4xx, 5xx, 429, and OPTIONS responses do not consume allowance.
Trailing Slash Requirement
All Management API URLs must end with a trailing slash (/). For GET requests, a missing slash results in an automatic 301 redirect. For all other HTTP methods (POST, PUT, PATCH, DELETE), the API returns a 422 error:
Trailing slash error
{
"message": "URL must end with a trailing slash.",
"error_code": "trailing_slash_required",
"detail": null
}
Always ensure your request URLs include a trailing slash to avoid this error.
Handling Patterns
import axios from 'axios';
async function createFolder(payload) {
try {
const { data } = await axios.post('https://api.foxnose.net/v1/7c9h4pwu/collections/', payload);
return data;
} catch (error) {
if (!error.response) throw error;
const { status, data } = error.response;
if (status === 422 && data.error_code === 'validation_error') {
return data.detail.map((item) => ({
field: item.loc.join('.'),
message: item.msg,
}));
}
if (data.error_code === 'permission_denied') {
throw new Error('You lack access to this environment.');
}
throw new Error(data.message);
}
}