Skip to content

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. null when 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}` } },
    );
  }
}

Honoring Retry-After on 429

import time, httpx

def get_with_backoff(url, headers, max_retries=3):
    for attempt in range(max_retries):
        resp = httpx.get(url, headers=headers)
        if resp.status_code != 429:
            return resp
        retry_after = int(resp.headers.get("Retry-After", "5"))
        time.sleep(retry_after)
    return resp