Skip to content
firmus.shdocs

Every page's text, headings and title. Code samples are not indexed.

Errors

The one error shape, every code a client can meet, and how to tell a refusal from an outage.

Every failure — from any endpoint, at any status — has the same body:

{
  "error": {
    "status": 404,
    "code": "not_found",
    "message": "The requested resource does not exist.",
    "documentation_url": "https://docs.firmus.sh/reference/errors#not_found"
  }
}

Branch on code, and only on code. The status repeats the HTTP status so a logged body is self-contained; the message is written for a human reading a log and may be reworded at any time.

documentation_url is written for that same human: it points at the section of this page that explains the code. Follow it, do not branch on it — it is present only for the codes below, and absent from any other refusal this API makes.

The codes

StatuscodeWhen
400invalid_queryq on /suggest has nothing searchable in it.
400invalid_requestA body, a filter or a cursor cannot be read.
401api_key_missingNo credential.
401api_key_query_forbiddenA key arrived in the query string where that form is not accepted.
401api_key_invalidThe credential does not name a live key.
402quota_exceededAn allowance in the plan is spent, or a request does not fit in what is left.
403insufficient_scopeThe key is live but lacks the scope this endpoint needs.
403public_key_forbiddenA publishable key called a batch, job, watch or segment endpoint.
404not_foundNo record, job, watchlist or segment — or no data for that country yet.
409job_not_finishedA file job's result was asked for while the job is still running.
411length_requiredA file job was sent without a Content-Length.
429rate_limitedAn allowance measured over a window is used up.
429concurrency_limitedAnother batch is still being answered.
503segment_index_unavailableThe segment index is not built on this server yet.

A new refusal reason arrives as a new code in the same object. Handle an unrecognised code by its HTTP status and that stays additive.

invalid_query

400. The q parameter on /suggest is missing, blank, or has no letter or digit in it. The only rejection on that endpoint — its other parameters are clamped into range rather than refused, so a per_page of 500 is answered with the maximum and not with an error.

invalid_request

400. The body sent to /resolve or /enrich is not a batch: not JSON, no rows array, an empty one, or more than 500 rows — or, on /enrich, a fields or event_kinds value outside its list. message says which. A single row that cannot be read does not refuse the batch — it comes back invalid in its place, with its own message.

On /jobs, a submission that cannot become a job: no file or more than one source, an unknown operation, a mapping naming a column the file does not have, no column to resolve from, an enrich job with no SIREN column, an unknown field, a CSV that cannot be read, or more rows or bytes than a job on the plan holds. message says which, and what was detected.

On a timeline (/legal-entities/{registry_number}/events), a filter that cannot be read: an unknown kind or severity, a date not written YYYY-MM-DD, an include_bulk other than true, false, 1 or 0, or a cursor this API did not issue. message says which.

On /watchlists, a list that cannot be written: a field this API does not know, an unknown kind, severity or digest frequency, a boolean other than true, false, 1 or 0, a digest with no recipient, an organization already holding 100 lists, no number to add or remove, more than 1 000 (100 000 from a job), both numbers and job_id, or a job_id that is not a finished job of your organization. On the watched-events feed, /events, a cursor this API did not issue. message says which.

On /segments, a segment that cannot be run: an unknown filter or body field, a value a filter does not accept, a facet_limit outside 1 to 100, sole traders without the acknowledgement or without save; on an export, a field enrich does not accept or a limit that is not a whole number of 1 or more. message names the filter and what it accepts. A misspelt filter is refused rather than ignored.

api_key_missing

401. No credential arrived. Send the key as Authorization: Bearer <key>. The response also carries WWW-Authenticate: Bearer, so a client that understands the header can act on it without reading the body.

api_key_query_forbidden

401. A key arrived in the query string where that form is not accepted — either the wrong prefix, or a publishable key on an endpoint other than /suggest. A query string ends up in access logs, Referer headers and browser history, so only a publishable key travels that way, and only on the one endpoint a browser calls directly. Send any other key as a Bearer header.

api_key_invalid

401. The credential does not name a live key. A revoked key is answered here too, and identically to one that never existed — the two are not distinguished, so that a caller holding a stolen key learns nothing from the difference.

quota_exceeded

402. The organization has used one of the allowances in its plan. Three are monthly:

  • lookups, on Free alone: 1 000 a month. Pro and Scale include them without limit.
  • agent rows, on every plan and on /resolve, /enrich and /jobs only: the rows of all three, counted together, 1 000 a month on Free, 25 000 on Pro and 250 000 on Scale. What your unfinished file jobs were admitted for and have not processed yet counts as used. A batch is admitted while one row is left beyond that; a file job is admitted only if every row, once per step, fits in it, and message gives both figures.
  • exported rows, on every plan and on POST /v1/{country_code}/segments/{id}/export only: 1 000 a month on Free, 25 000 on Pro and 250 000 on Scale. An export is admitted only if every company the segment may list, or its limit when smaller, fits in what is left, counting what unfinished exports hold, and message gives both figures. Send a limit no larger than what is left to export the first rows, in SIREN order. See export a segment.

Agent rows and exported rows are the two ways a paid key meets 402 on a monthly allowance. 402 rather than 429 because this is not a rate: the allowance is spent, and it returns at the start of the next calendar month, UTC. Retry-After counts the seconds to that instant; waiting less will not help. The code is the same for all three. On /resolve, /enrich and /jobs, read the member at r=0 in the RateLimit header to tell which one refused: quota or rows. Exported rows have no member there; a 402 from an export that is not the lookups is them.

Three are not monthly, and carry no Retry-After: they are never reset, and are freed by deleting what holds them or by changing plan. So a 402 without Retry-After is one of these:

  • watched companies, on POST /v1/{country_code}/watchlists and POST /v1/{country_code}/watchlists/{id}/watches only, 5 on Free, 1 000 on Pro and 10 000 on Scale, counted as distinct numbers across every watchlist. A request whose new numbers do not fit in what is left adds none of them, and message gives the figures. See watch companies.
  • saved segments, on POST /v1/{country_code}/segments with save: true only, 3 on Free, 100 on Pro and 1 000 on Scale. Nothing is saved; the same request without save is answered. Delete a saved segment to free its place.
  • watched segments, on PUT /v1/{country_code}/watchlists/{watchlist_id}/segments/{segment_id} only, 1 on Free, 10 on Pro and 100 on Scale, counted as distinct segments followed across every watchlist. Nothing is followed. Stop following a segment to free its place. See follow a segment.

insufficient_scope

403. The key is live but does not carry the scope this endpoint needs. Scopes are chosen when the key is created and shown in the console: read reaches the canonical endpoints, /resolve, /enrich, /jobs, the watchlists, the watched-events feed and the segments, suggest reaches /suggest. Any prefix may hold any scope.

public_key_forbidden

403. A publishable key (fk_pub) holding the read scope called /resolve, /enrich, /jobs, /watchlists, the watched-events feed /events or /segments; one without it gets insufficient_scope first. A batch or a file of your customers' records is sent from a server, with a secret key — a key that ships in a browser cannot send one, change your watchlists or segments, read their feed, or export companies.

not_found

404. No record carries that identifier, no live job, watchlist or saved segment of your organization has that id — one that expired or belongs to another organization included — or the country has no data published yet. The second is a miss and not an error: a request for a country Firmus has not loaded is answered the same way as a request for a company that does not exist.

job_not_finished

409. GET /v1/{country_code}/jobs/{id}/result was called while the job is queued or running: a result exists once the job has finished. Every job finishes, so retrying is safe; reading the job, GET /v1/{country_code}/jobs/{id}, also says how far it has got. A job that finishes failed serves the rows it answered before it stopped, and its error says how many.

length_required

411. POST /v1/{country_code}/jobs received a body without a Content-Length header, as a chunked upload sends it. A job's body is checked against the plan's bytes before it is read, and a chunked body has no length to check. Send the file with its length: curl -F and curl -d do, and so does any client that uploads from a file or a buffer rather than a stream.

rate_limited

429. An allowance measured over a window is used up. Retry-After gives the seconds to wait, and RateLimit / RateLimit-Policy name every policy in force, what each allows and when each resets. The window is fixed rather than sliding, so a burst across a boundary can briefly admit twice the limit.

concurrency_limited

429. One batch runs at a time per organization, /resolve and /enrich together, and each server answers one at a time. Send the batch again after Retry-After seconds. The RateLimit headers on this response describe the rate, the rows and, on Free, the quota, which it was admitted under: this refusal is not an allowance over a window, and they say nothing about it. A batch runs for at most 10 seconds; for a file that needs more than one batch, submit a file job instead, which takes the whole file at once and is never refused for a running batch.

segment_index_unavailable

503, on running, re-running or exporting a segment. This server cannot count segments yet, as on a new deployment before its data for segments is ready. Nothing was counted and no job was created; retry later, after a few minutes rather than a few seconds. It carries no Retry-After. Every other endpoint answers meanwhile.

Telling a refusal from an outage

Most of what looks like an outage is a refusal with a name on it, and the response says which:

What you seeWhat it is
401 with an api_key_* codeThe credential, not the service. See authentication.
403 insufficient_scopeThe key is fine; it lacks the scope this endpoint needs.
404 not_foundNo such record, or a country with no data yet — not an error.
429 rate_limitedAn allowance is spent. Retry-After says how long to wait.
402 quota_exceededAn allowance is spent: lookups on Free, agent or exported rows, or a stock.
429 concurrency_limitedAnother batch is still running. Retry after Retry-After.
5xx, or no response at allOurs.

Wire the six in the middle into a dashboard separately from the last row: an integration that counts them as failures will look broken while working as designed.

Every response carries an X-Request-Id, readable from a browser. Keep it in your logs — for anything in the last row, it is the fastest way to find the request you are describing.

Next