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:
codeis the stable, machine-readable identifier: a namespaced string likevalidation/failedorauth/invalidToken. This is the field to branch on. It does not change, and it is more specific than the status.statusmirrors the HTTP status code. Several codes can share one status, so never branch on the status alone.typeis a URL to this problem's page in the Problems reference. Follow it for causes, fixes, and examples.titleis a short human label for the problem type. It is for people, not for parsing.detaildescribes this one occurrence and can vary from request to request. Log it, but do not match on it.instanceties the error to a single request:urn:floracodex:request:<id>. That<id>is the same value as theFC-Request-Idresponse header.fieldViolationsappears only onvalidation/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. CarriesfieldViolations.auth/*(mostly 401 and 403). Missing, invalid, revoked, or insufficient credentials. Two cases are transient rather than your fault:auth/tooManyRequests(429) andauth/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/*andquota/*(429). Too many requests, or the monthly quota is spent. Both set aRetry-Afterheader. 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 inRetry-After, then retry. See the Rate limits guide for backoff. - 500 and 503 whose
detailsays the call is safe to retry (for exampleauth/identityProviderUnavailableorcodex/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 ofauth/*): 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.