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.


HTTP Status Codes

StatusTypical MeaningExample error_code
400 Bad RequestMalformed JSON or unsupported payloadjson_parse_error
401 UnauthorizedMissing / invalid auth, expired token, or invalid API keyauthentication_failed
402 Payment RequiredSpend cap reached (paid) or plan allowance exhausted (Free); billable writes are blockedspend_cap_reached, plan_exhausted
403 ForbiddenAuthenticated but lacks permissions (role mismatch), or a structural plan limit was exceededpermission_denied, plan_limit_exceeded
404 Not FoundEnvironment, collection, resource, etc. does not exist or you cannot access itenvironment_not_found, resource_not_found, locale_not_found
405 Method Not AllowedWrong HTTP verb for the endpointmethod_not_allowed
409 ConflictTwo writers collided on the same resource; nothing was lost — retry the same writerevision_number_conflict
412 Precondition FailedAn If-Match precondition no longer holds: the resource moved on — re-read before retryingrevision_precondition_failed
422 Unprocessable ContentRequest failed validation or endpoint-specific business rulesvalidation_error, trailing_slash_required, protected_environment_cannot_be_deleted
429 Too Many RequestsPer-plan requests-per-minute rate limit exceeded; carries a Retry-After header and is not billedrate_limited
500 Internal Server ErrorUnexpected backend errorinternal_server_error

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:

Statuserror_codeWhat happenedWhat to do
409revision_number_conflictTwo writers collided on the next revision number. Nobody's intent was lost.Retry the same write.
412revision_precondition_failedThe 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.
422invalid_revision_preconditionThe 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.


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);
  }
}

  • Limits — structural plan quotas and the 403 plan_limit_exceeded contract.
  • Billing — metering, spend cap, and the 402 responses (spend_cap_reached, plan_exhausted).

Was this page helpful?