Every endpoint under /v1 requires a key, with two exceptions: the discovery root at
GET /v1 and the contract at GET /v1/openapi. Both are open to anyone — a document
describing the API, behind a credential the reader does not have yet, has no reader.
Getting a key
Keys are minted in the Firmus console. The token is shown once, at the moment it is created; only its digest is stored, so nothing — including support — can recover it afterwards. A list of keys shows the prefix and the last four characters.
A token is its prefix, an underscore, and thirty-two alphanumeric characters:
fk_live_XxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX
| Prefix | What it is |
|---|---|
fk_live | The default. Secret; send it from a server. |
fk_test | The same thing, labelled. It reaches the same data and is enforced identically — there is no sandbox and no separate data set. It exists so a key used outside production can be recognised in a list. |
fk_pub | Publishable. Meant to ship inside a page, and the only prefix that may be sent in a query string. Its first 100 calls a day from any one address are free on every plan; see rate limits. |
Sending a key
The header form works everywhere and is the one to reach for:
read -rs FIRMUS_KEY && export FIRMUS_KEY
curl -H "Authorization: Bearer $FIRMUS_KEY" \
https://api.firmus.sh/v1/fr/legal-entities/552032534The scheme name is case-insensitive, as HTTP requires. If a request carries both a header and a query parameter, the header is what authenticates it.
The query-parameter form
A publishable key may travel as ?key=:
read -rs FIRMUS_PUBLIC_KEY && export FIRMUS_PUBLIC_KEY
curl "https://api.firmus.sh/v1/fr/suggest?q=danone&key=$FIRMUS_PUBLIC_KEY"Any other prefix sent that way is refused from the prefix alone, before anything looks the
key up. The form reaches /v1/{country}/suggest and no other endpoint, whatever the
key: a query string ends up in access logs, Referer headers and browser history, and the
other three endpoints read the canonical layer.
Use it for an autocomplete field in somebody else's page: a cross-origin GET that sets
no header needs no CORS preflight, so there is no round trip before every keystroke. Add
any header and you lose that.
A publishable key that also carries read reaches the other three — over
Authorization: Bearer, at the cost of one preflight. That is the trade the documentation's
"Try it" panels make, and it is why they can answer without an account. The console mints
publishable keys with suggest alone today, so this is not a combination you can create
there yet.
Scopes
A key carries one or both. They are chosen when it is created and shown in the console.
| Scope | Reaches |
|---|---|
read | /v1/{country}/legal-entities/… and /v1/{country}/establishments/… |
suggest | /v1/{country}/suggest |
A key with no scope cannot be created. Any prefix may hold any scope — what a publishable
key may do is bounded by where it can be sent, not by what it can carry — though the
console mints a publishable key with suggest alone: a key shipped inside a page is
readable by anyone who loads it, so its scopes are public too.
When a request is refused
Four rejections, in the order they are checked:
| Status | code | What happened |
|---|---|---|
| 401 | api_key_missing | No credential at all. |
| 401 | api_key_query_forbidden | A key arrived as ?key= where that form is not accepted: the wrong prefix, or the right one on an endpoint other than /suggest. Move it to the header. |
| 401 | api_key_invalid | The credential does not name a live key. |
| 403 | insufficient_scope | The key is real and live; its scopes do not cover this endpoint. |
Every 401 carries WWW-Authenticate, so a client library can learn the scheme without
reading the contract:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="Firmus API"
Content-Type: application/json
{
"error": {
"status": 401,
"code": "api_key_missing",
"message": "This endpoint requires an API key. Send it as `Authorization: Bearer <key>`.",
"documentation_url": "https://docs.firmus.sh/reference/errors#api_key_missing"
}
}A key that never existed, one that is malformed and one that was revoked are answered identically.
The two you are most likely to meet are these. A key in the query string where that form is
not accepted — a secret key anywhere, or a publishable one outside /suggest:
{
"error": {
"status": 401,
"code": "api_key_query_forbidden",
"message": "The ?key= query parameter carries a public (fk_pub) key on /v1/{country_code}/suggest and nothing else. Send this key as `Authorization: Bearer <key>` instead — and if it is not a public key, treat it as compromised.",
"documentation_url": "https://docs.firmus.sh/reference/errors#api_key_query_forbidden"
}
}One code covers both, because the remedy is the same either way: move the key to the
header. Branch on code, not on the message.
And a key reaching an endpoint its scopes do not cover:
{
"error": {
"status": 403,
"code": "insufficient_scope",
"message": "This API key does not carry the `read` scope.",
"documentation_url": "https://docs.firmus.sh/reference/errors#insufficient_scope"
}
}The 403 names the scope that was needed.
Rotating and revoking
Revoke a key in the console. Revoking takes effect within about a second. A revoked key is kept, and stays visible in the list.
To rotate without downtime: mint the new key, deploy it, confirm traffic has moved, then
revoke the old one. The console's last_used_at is what confirms it — it is approximate,
so give it a minute before reading anything into a quiet key.
Before the key: GET /v1
The discovery root answers what this API is, where the contract and this documentation live, how to authenticate, what the free tier allows, and every endpoint with its summary and parameters — with no credential at all.
curl https://api.firmus.sh/v1Every field of it is derived from the contract below, so the two cannot disagree.
The contract: GET /v1/openapi
The OpenAPI document is served by the API itself and needs no key.
curl https://api.firmus.sh/v1/openapi # YAML, the default
curl https://api.firmus.sh/v1/openapi.json # the same document as JSONIt describes the responses the API is asserted to serve, and is what the generated clients
will be built from. Responses carry a public cache directive and an ETag; a conditional
request gets a 304 when the document has not moved.