Search by Name, Then Look Up by ID¶
Most real-world workflows start with a name, not a registry number. The two-step search → lookup pattern is the cheapest, most accurate way to resolve a name to a full company record.
1. SEARCH → GET /v1/companies/search?q=...&country=FR (free)
2. LOOKUP → GET /v1/companies/FR/{registry_id} (1 result)
Search is free and fuzzy. Lookup is exact and consumes one billable result. Together they let you spend nothing on the disambiguation step and pay only for the records you actually keep.
Why two steps?
Search is optimised for recall — it returns ranked candidates so you (or your model) can pick the right one. Lookup is optimised for completeness — it returns the canonical record with every field the registry publishes. Trying to get both in one call would either bill for every search hit or strip detail from the result.
Step 1 — Search¶
Pass a name fragment in q and always include the country you expect. Country-scoped search is fast and costs one rate-limit unit; cross-country search scans all 19 partitions and costs 10×.
const url = new URL("https://api.firmadb.com/v1/companies/search");
url.searchParams.set("q", "Société Générale");
url.searchParams.set("country", "FR");
url.searchParams.set("limit", "5");
const resp = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.FIRMADB_API_KEY}` },
});
const { results } = await resp.json();
What you get back¶
Search returns a ranked list of candidates. Each candidate carries a registry_id, a score between 0 and 1, and the matched_fields that produced the hit.
{
"results": [
{
"country": "FR",
"registry_id": "914002639",
"name": "L'OREAL",
"status": "active",
"score": 0.97,
"matched_fields": ["name"],
"address": { "city": "PARIS", "postal_code": "75009" }
},
{
"country": "FR",
"registry_id": "552120222",
"name": "L'OREAL PRODUITS DE LUXE FRANCE",
"status": "active",
"score": 0.84,
"matched_fields": ["name"]
}
],
"total_count": 47,
"total_count_relation": "approximate",
"has_more": true,
"next_cursor": "eyJrIjoiMC44NCIsImlkIjoiNTUyMTIwMjIyIn0"
}
Approximate counts
total_count_relation: "approximate" means the value can be off by up to ±20%. Never use an approximate count for billing math — call firmadb_estimate_cost instead.
Picking the right candidate¶
For distinctive names, the top result is almost always correct. For common names ("ABC Trading"), inspect address, status, and score before committing to a lookup:
score >= 0.9and a single result → safe to look up directly.- Multiple high-scoring results → disambiguate by city, postcode, or NACE.
- All scores below 0.5 → try a query variant (drop the legal suffix, add a city).
Query tips¶
- Strip legal suffixes. Search "Acme Holdings", not "Acme Holdings Limited". Suffixes like
SA,Ltd,GmbH,BV,OYadd noise. - Use ASCII fallbacks. "Société Générale" matches "Société Générale". Search is diacritic-insensitive.
- Minimum 3 characters. Shorter queries return
400 query_too_short. - Filter aggressively. Add
nace=orstatus=activewhen you know them — fewer candidates means less to disambiguate.
Step 2 — Lookup¶
Once you have a registry_id, switch to the exact lookup endpoint. This is the canonical record: every field the registry publishes, plus FirmaDB metadata (source_url, data_freshness, request_id).
A successful response (abbreviated):
{
"country": "FR",
"registry_id": "552120222",
"name": "SOCIETE GENERALE",
"status": "active",
"status_active": true,
"address": {
"street": "29 BOULEVARD HAUSSMANN",
"postal_code": "75009",
"city": "PARIS",
"country": "FR"
},
"nace_code": "64.19Z",
"website": null,
"source_url": "https://annuaire-entreprises.data.gouv.fr/etablissement/55212022200013",
"data_freshness": {
"last_confirmed_at": "2026-05-04T03:14:22Z",
"freshness_class": "fresh"
},
"request_id": "req_01HZAB7MJX9V8GG6P3K5R2QC4P"
}
Always preserve source_url and data_freshness
These two fields are your provenance chain. End-users — especially compliance reviewers — need a way to verify the data against the original government registry. Strip them at your peril.
End-to-end example¶
import httpx, os
API = "https://api.firmadb.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FIRMADB_API_KEY']}"}
def resolve(name: str, country: str) -> dict | None:
search = httpx.get(
f"{API}/companies/search",
params={"q": name, "country": country, "limit": 5},
headers=HEADERS,
).json()
if not search["results"]:
return None
best = search["results"][0]
if best["score"] < 0.5:
return None # too noisy — ask the user to clarify
return httpx.get(
f"{API}/companies/{best['country']}/{best['registry_id']}",
headers=HEADERS,
).json()
print(resolve("Societe Generale", "FR")["name"])
# SOCIETE GENERALE
Handling not-found¶
If the registry ID looks valid but no record exists, you'll get a 404 company_not_found. The error includes a correction and often a suggested_request — agents should retry with the suggestion before giving up.
{
"type": "https://errors.firmadb.com/company-not-found",
"title": "Company not found",
"status": 404,
"code": "company_not_found",
"detail": "No company with registry_id '12345' exists in FR.",
"correction": "Try searching by name with /v1/companies/search?country=FR&q=...",
"suggested_request": null
}
See errors for the full catalog and recovery patterns.
What's next¶
- Multiple records in one call → Enrich a CRM List
- Just need active/dissolved? → KYB Status Verification
- Building an agent? → MCP Server Setup