Flora CodexFlora Codex

Errors

Every error from the Flora Codex API comes back in one shape: RFC 9457 problem+json. Learn to read one envelope and you can handle all of them. The rule that matters most: branch your code on the code field, not on the HTTP status and not on the human-readable text.

The frozen v1 surface is the exception. It keeps its original Trefle-compatible error format and does not emit problem+json. This guide covers v2 and v3.

The envelope

Error responses use Content-Type: application/problem+json. Here is a validation failure on a search request:

{
  "type": "https://docs.floracodex.com/problems/validation-failed",
  "title": "Request validation failed",
  "status": 400,
  "code": "validation/failed",
  "detail": "pH min must not be greater than pH max",
  "instance": "urn:floracodex:request:01J9Z3F6Q2K8M4T7V0XB5N1WCE",
  "fieldViolations": [
    { "field": "filters.pH.min", "message": "pH min must not be greater than pH max" }
  ]
}

What each field is for:

  • code is the stable, machine-readable identifier: a namespaced string like validation/failed or auth/invalidToken. This is the field to branch on. It does not change, and it is more specific than the status.
  • status mirrors the HTTP status code. Several codes can share one status, so never branch on the status alone.
  • type is a URL to this problem's page in the Problems reference. Follow it for causes, fixes, and examples.
  • title is a short human label for the problem type. It is for people, not for parsing.
  • detail describes this one occurrence and can vary from request to request. Log it, but do not match on it.
  • instance ties the error to a single request: urn:floracodex:request:<id>. That <id> is the same value as the FC-Request-Id response header.
  • fieldViolations appears only on validation/failed. It is covered in Validation errors below.

Branch on code

code is the contract. status, title, and detail are not: a single status is shared across many codes, and the human text can be reworded at any time. Switch on code:

const res = await fetch(url, { headers });
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  switch (problem.code) {
    case 'validation/failed':
      showFieldErrors(problem.fieldViolations);
      break;
    case 'auth/invalidToken':
    case 'auth/tokenRevoked':
      reauthenticate();
      break;
    case 'rateLimit/exceeded':
    case 'quota/exhausted':
      backOff(res.headers.get('Retry-After'));
      break;
    default:
      reportProblem(problem); // include problem.instance
  }
}

Let an unrecognized code fall through to a generic handler. New codes can appear over time, so treat a code you do not know like any other failure of its status class.

Problem families

Codes are namespaced by the part of the platform that raised them. The families you will meet most often:

  • validation/* (400). The request did not pass validation. Carries fieldViolations.
  • auth/* (mostly 401 and 403). Missing, invalid, revoked, or insufficient credentials. Two cases are transient rather than your fault: auth/tooManyRequests (429) and auth/identityProviderUnavailable (503).
  • appKeys/* (403). The API key is missing a required restriction, invalid or inactive, or blocked by one of its allowlists (IP, referrer, User-Agent).
  • subscription/* (403). The project has no active subscription, or no project could be resolved for metering.
  • rateLimit/* and quota/* (429). Too many requests, or the monthly quota is spent. Both set a Retry-After header. See the Rate limits guide.
  • codex/* (404, 500). The botanical resource was not found, or the search backend could not complete the request.
  • http/*. Generic, status-derived fallbacks, used when no more specific code applies.

The full catalog, with a page for every code, lives in the Problems reference.

Validation errors

A validation/failed response lists each field it rejected, so you can point a user straight at the problem:

"fieldViolations": [
  { "field": "filters.pH.min", "message": "pH min must not be greater than pH max" },
  { "field": "page", "message": "page must not be less than 0" }
]

field is a dot-path, so a nested filter reads as filters.pH.min, and message is human-readable. The API also rejects unknown parameters: send a query or body field the endpoint does not define, and you get a validation/failed that names it. This is deliberate. A misspelled parameter fails loudly instead of being ignored and returning the wrong data.

Tracing a failure

Every response carries an FC-Request-Id header, and every error repeats that value in instance as urn:floracodex:request:<id>. Log it. When you report a problem to support, quote the id, and we can find the exact request.

Retrying

Whether a retry helps depends on the family:

  • 429 (rateLimit/*, quota/*, auth/tooManyRequests): wait for the number of seconds in Retry-After, then retry. See the Rate limits guide for backoff.
  • 500 and 503 whose detail says the call is safe to retry (for example auth/identityProviderUnavailable or codex/searchFailed): retry with exponential backoff, and add a little jitter so a fleet of clients does not retry in lockstep.
  • Deterministic 4xx (validation/*, subscription/*, appKeys/*, and the rest of auth/*): the same request will fail the same way. Fix the request or the credentials first.

When you are unsure, the problem's page in the Problems reference says whether it is retryable.

Last updated 18 June 2026