Errors¶
Every non-2xx response from FirmaDB follows RFC 9457 Problem Details for HTTP APIs with Content-Type: application/problem+json. Beyond the RFC, every error includes two extensions designed for self-correcting agents:
correction— a one-line, actionable remediation hint.suggested_request— a ready-to-use corrected request body or query string.nullwhen not applicable.
For AI agents
Always read correction and suggested_request before giving up or asking the user. A 400 or 404 with suggested_request is usually one retry away from success — branch on code, not title.
Error envelope¶
Every error has the same shape:
{
"type": "https://errors.firmadb.com/company-not-found",
"title": "Company not found",
"status": 404,
"detail": "No company with registry_id '999999999' exists in FR. Format looks valid (9 digits, SIREN convention). FR was last loaded 2026-05-03.",
"instance": "/v1/companies/FR/999999999",
"code": "company_not_found",
"request_id": "req_01HZAB7MJX9V8GG6P3K5R2QC4P",
"retryable": false,
"retry_after_seconds": null,
"correction": "Try searching by name with /v1/companies/search?country=FR&q=...",
"suggested_request": null,
"documentation_url": "https://errors.firmadb.com/company-not-found"
}
| Field | Type | Description |
|---|---|---|
type |
URI | Stable URI that resolves to a real docs page. |
title |
string | Short human-readable title. |
status |
integer | HTTP status code. |
detail |
string | Occurrence-specific detail — what was tried, what went wrong, why. |
instance |
string | Request URI or URN. |
code |
string | Stable machine-readable code. Branch on this, never on title/detail. |
request_id |
string | req_-prefixed ULID, also returned in the X-Request-Id header. |
retryable |
boolean | Whether the same request might succeed on retry. |
retry_after_seconds |
integer / null | Seconds to wait before retrying, when retryable is true. |
correction |
string | Actionable one-line remediation hint. |
suggested_request |
object / null | Corrected request the client can send. |
documentation_url |
URL | Direct link to this error type's docs. |
Error catalog¶
400 — Client request errors¶
invalid_country¶
The country parameter is not in the supported set. The error includes a supported_countries array.
{
"type": "https://errors.firmadb.com/invalid-country-code",
"title": "Invalid country code",
"status": 400,
"code": "invalid_country",
"detail": "Country 'XX' is not supported. FirmaDB covers 19 European countries.",
"supported_countries": ["FR", "GB", "SE", "BE", "CZ", "NO", "SK", "PL", "IE", "BG", "LT", "FI", "EE", "MD", "RS", "CY", "DK", "LV", "HR"],
"correction": "Use one of the supported ISO 3166-1 alpha-2 codes.",
"suggested_request": null
}
invalid_registry_id¶
The registry_id failed the country-specific format check. The error includes the expected_format from the capability manifest.
{
"type": "https://errors.firmadb.com/invalid-registry-id-format",
"title": "Invalid registry ID format",
"status": 400,
"code": "invalid_registry_id",
"detail": "registry_id '123' is too short for FR. SIREN identifiers are exactly 9 digits.",
"expected_format": "^[0-9]{9}$",
"correction": "Pad with leading zeros if needed, or verify the source ID. SIRET (14 digits) is not the same as SIREN (9 digits).",
"suggested_request": null
}
query_too_short¶
Search query is under 3 characters.
{
"type": "https://errors.firmadb.com/query-too-short",
"title": "Query too short",
"status": 400,
"code": "query_too_short",
"detail": "Search query must be at least 3 characters. You sent 'AB' (2 characters).",
"minimum_length": 3,
"correction": "Provide at least 3 characters for fuzzy search.",
"suggested_request": null
}
invalid_parameter¶
Generic validation failure (wrong type, out of range). The error always includes parameter, value, and constraint.
{
"type": "https://errors.firmadb.com/invalid-parameter",
"title": "Invalid parameter",
"status": 400,
"code": "invalid_parameter",
"detail": "Parameter 'limit' must be an integer between 1 and 50. You sent '500'.",
"parameter": "limit",
"value": "500",
"constraint": "integer 1-50",
"correction": "Use limit between 1 and 50. For large result sets, paginate with cursor.",
"suggested_request": {"limit": 50}
}
country_required¶
A structured filter (nace, status, registered_date_from, registered_date_to, sort other than relevance) was used without country.
{
"type": "https://errors.firmadb.com/country-required-for-filter",
"title": "Country required for structured filter",
"status": 400,
"code": "country_required",
"detail": "Filter 'nace' requires the 'country' parameter. Cross-country structured filters are not supported.",
"supported_filters_without_country": ["q"],
"correction": "Add country=FR (or another ISO alpha-2 code) to your query.",
"suggested_request": {"q": "tech", "country": "FR", "nace": "62"}
}
invalid_cursor¶
The pagination cursor is malformed, expired, or was issued for a different query.
{
"type": "https://errors.firmadb.com/invalid-cursor",
"title": "Invalid cursor",
"status": 400,
"code": "invalid_cursor",
"detail": "Cursor was issued for a different query (q=Acme&country=FR) and cannot be reused with the current parameters.",
"correction": "Restart pagination from the first page using the same query parameters that produced the cursor.",
"suggested_request": null
}
batch_validation_failed¶
One or more items in a batch request failed validation. The whole envelope was rejected. Per-item errors are returned as embedded RFC 9457 sub-problems with an index field.
{
"type": "https://errors.firmadb.com/batch-validation-failed",
"title": "Batch validation failed",
"status": 400,
"code": "batch_validation_failed",
"detail": "Batch envelope failed validation. See errors[] for per-item details.",
"errors": [
{
"index": 2,
"code": "invalid_registry_id",
"detail": "registry_id missing for batch item at index 2",
"correction": "Provide both country and registry_id for each reference."
}
]
}
401 — Authentication errors¶
unauthenticated¶
Missing, malformed, or expired API key. The response also sets the WWW-Authenticate: Bearer realm="firmadb" header.
{
"type": "https://errors.firmadb.com/unauthenticated",
"title": "Unauthenticated",
"status": 401,
"code": "unauthenticated",
"detail": "Missing Authorization header. Pass your API key as 'Authorization: Bearer fdb_...'",
"correction": "Add the header: Authorization: Bearer <your_api_key>. See https://docs.firmadb.com/authentication/.",
"suggested_request": null
}
403 — Authorization errors¶
insufficient_scope¶
The restricted key is valid but lacks the scope required for this operation.
{
"type": "https://errors.firmadb.com/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"code": "insufficient_scope",
"detail": "This restricted key is missing the 'companies:enrich' scope required for batch lookup.",
"required_scopes": ["companies:enrich"],
"granted_scopes": ["companies:read", "companies:search"],
"missing_scopes": ["companies:enrich"],
"correction": "Use a key with companies:enrich, or create a new restricted key that includes it.",
"suggested_request": null
}
404 — Not found¶
company_not_found¶
The country and registry ID are well-formed, but no company matches. 404s are free — they don't consume billable results. The error includes country_freshness so you can tell whether it might be a data lag.
{
"type": "https://errors.firmadb.com/company-not-found",
"title": "Company not found",
"status": 404,
"code": "company_not_found",
"detail": "No company with registry_id '999999999' exists in FR. Format looks valid (9 digits, SIREN convention). FR was last loaded 2026-05-03.",
"country_freshness": {
"country": "FR",
"last_loaded_at": "2026-05-03T02:00:00Z",
"record_count": 16851670
},
"correction": "Try searching by name with /v1/companies/search?country=FR&q=<company name>.",
"suggested_request": null
}
409 — Conflict¶
idempotency_conflict¶
Same Idempotency-Key was sent with a different request body within the 24-hour retention window. The error includes differing_fields (field names only — values are not echoed back, for security).
{
"type": "https://errors.firmadb.com/idempotency-key-conflict",
"title": "Idempotency key conflict",
"status": 409,
"code": "idempotency_conflict",
"detail": "Idempotency-Key '550e8400-...' was previously used with a different request body.",
"differing_fields": ["references[1].registry_id"],
"correction": "Use a fresh Idempotency-Key for new requests, or send the original request body to retry.",
"suggested_request": null
}
429 — Rate limit / quota¶
rate_limit_exceeded¶
Per-minute rate limit reached. Includes the Retry-After header.
{
"type": "https://errors.firmadb.com/rate-limit-exceeded",
"title": "Rate limit exceeded",
"status": 429,
"code": "rate_limit_exceeded",
"retryable": true,
"retry_after_seconds": 23,
"limit": {
"resource": "companies-read",
"limit": 60,
"remaining": 0,
"reset_seconds": 23
},
"correction": "Slow your request rate. Honor the Retry-After header. Upgrade your tier for higher per-minute limits.",
"suggested_request": null
}
quota_exhausted¶
Monthly result quota is fully consumed. Free-tier requests cannot be retried until the next billing cycle.
{
"type": "https://errors.firmadb.com/quota-exhausted",
"title": "Monthly result quota exhausted",
"status": 429,
"code": "quota_exhausted",
"retryable": false,
"limit": {
"bucket": "monthly",
"scope": "account",
"limit": 1000,
"remaining": 0,
"reset_iso": "2026-06-01T00:00:00Z"
},
"price": {
"next_tier": "solo",
"monthly_fee_eur": 29,
"included_results": 10000,
"upgrade_url": "https://firmadb.com/upgrade"
},
"correction": "Upgrade to Solo (10,000 results/month) or wait until the quota resets on 2026-06-01.",
"suggested_request": null
}
503 — Service unavailable¶
search_unavailable¶
The search index (Meilisearch) is degraded or down. Direct lookups by registry ID still work.
{
"type": "https://errors.firmadb.com/search-unavailable",
"title": "Search temporarily unavailable",
"status": 503,
"code": "search_unavailable",
"retryable": true,
"retry_after_seconds": 30,
"degraded_components": ["meilisearch"],
"healthy_components": ["postgresql"],
"correction": "Use exact lookup by (country, registry_id) while search recovers. The /v1/health endpoint reports per-component status.",
"suggested_request": null
}
Practical patterns¶
Self-correcting on suggested_request¶
If the response includes a non-null suggested_request, retry with it before surfacing the error to the user:
import httpx
resp = httpx.get(
"https://api.firmadb.com/v1/companies/search",
params={"q": "tech", "nace": "62"}, # missing country
headers={"Authorization": f"Bearer {api_key}"},
)
if resp.status_code == 400:
problem = resp.json()
if problem.get("suggested_request"):
resp = httpx.get(
"https://api.firmadb.com/v1/companies/search",
params=problem["suggested_request"],
headers={"Authorization": f"Bearer {api_key}"},
)
let resp = await fetch(
"https://api.firmadb.com/v1/companies/search?q=tech&nace=62",
{ headers: { Authorization: `Bearer ${apiKey}` } },
);
if (resp.status === 400) {
const problem = await resp.json();
if (problem.suggested_request) {
const params = new URLSearchParams(problem.suggested_request);
resp = await fetch(
`https://api.firmadb.com/v1/companies/search?${params}`,
{ headers: { Authorization: `Bearer ${apiKey}` } },
);
}
}