Firmaradar Python SDK
Official Python SDK for Firmaradar — the enrichment platform for Norwegian company intelligence. Firmaradar fuses data from multiple authoritative sources (Brønnøysundregistrene, Skatteetaten, foreign PEP/sanctions registers, public-grants registries) and adds proprietary enrichment on top — so a single call returns a decision-ready view for KYC, AML, credit, ownership and risk workflows, not a raw registry record.
The SDK is a thin, fully typed client over the Firmaradar REST API
(httpx + Pydantic v2), with sync and async variants and optional
LangChain / LlamaIndex tool wrappers for AI-agent stacks.
Installation
pip install firmaradar # core SDK
pip install 'firmaradar[langchain]' # + LangChain tool wrappers
pip install 'firmaradar[llamaindex]' # + LlamaIndex tool spec
pip install 'firmaradar[all]' # everything
From source (this repo):
pip install ./sdk/python # or: pip install -e './sdk/python[all]'
Requires Python 3.10+.
Authentication
The SDK authenticates with a Firmaradar API key, sent as the X-API-Key
header. Pass it explicitly or set the FIRMARADAR_API_KEY environment
variable:
from firmaradar import Firmaradar
fr = Firmaradar(api_key="fr_...") # or: export FIRMARADAR_API_KEY=fr_...
API keys are managed on your Firmaradar account at firmaradar.no.
Quickstart
from firmaradar import Firmaradar
fr = Firmaradar() # reads FIRMARADAR_API_KEY
# Find a company, then pull its decision-ready profile
page = fr.companies.search("Equinor")
orgnr = page.items[0].orgnr
company = fr.companies.get(orgnr, fields=["group", "owners", "grants"])
print(company.navn, "-", company.summary)
# Ownership tree towards ultimate beneficial owners
tree = fr.companies.ownership(orgnr, direction="up", depth=5)
for owner in tree.owners:
print(owner.navn, owner.eierandel_prosent)
# Transparent risk score with component breakdown
score = fr.risk.score(orgnr)
print(score.score, score.level, score.components)
Async
Every operation has an async twin. Either construct AsyncFirmaradar
directly, or derive an async session from an existing client:
import asyncio
from firmaradar import Firmaradar
fr = Firmaradar()
async def screen(orgnrs: list[str]):
async with fr.async_session() as session:
return await asyncio.gather(*(session.companies.get(o) for o in orgnrs))
companies = asyncio.run(screen(["923609016", "914594685"]))
Async AML reports (submit → poll)
AML screening requires a signed DPA with Firmaradar. Calling
start_report confirms the screening and records the purpose in the
audit trail (60-month retention per Hvitvaskingsloven §35):
job = fr.aml.start_report("923609016", purpose="kyc_onboarding")
status = fr.aml.get_report(job.rapport_id)
while status.status in ("pending", "running"):
time.sleep(5)
status = fr.aml.get_report(job.rapport_id)
if status.status == "done":
print(status.score, status.level, status.pdf_url)
Operations
| Namespace | Method | What it returns |
|---|---|---|
companies |
search(q, nace, kommune, limit, cursor) |
Paginated company search |
companies |
get(orgnr, fields, owners, ...) |
Full enriched company profile |
companies |
roles(orgnr, ...) |
BRREG roles (board, CEO, signature, auditor) |
companies |
ownership(orgnr, direction, depth, ...) |
Ownership tree (down / up-UBO / both) |
companies |
financials(orgnr, years, regnskapstype) |
Financial history per accounting year |
companies |
announcements(orgnr) |
BRREG announcements (normalized categories) |
risk |
score(orgnr) |
Risk score 0-100 + level + components |
risk |
check_fiv(orgnr) |
Foretak-i-vanskeligheter (NUES a-e) assessment |
aml |
start_report(orgnr, purpose) |
Start async AML report (DPA required) |
aml |
get_report(report_id) |
Poll AML report status/result |
monitoring |
add(orgnr, ip_alerts, categories) |
Add company to monitoring |
monitoring |
remove(orgnr) |
Remove company from monitoring (idempotent) |
monitoring |
list() |
List monitored companies |
All responses are typed Pydantic models (firmaradar.models). Models
tolerate additive API changes — unknown fields are kept, never fatal.
Error handling
Non-2xx responses raise typed exceptions mapped from the API's error
contract (HTTP status + stable error_code):
from firmaradar import (
AuthenticationError, # 401 — bad/expired API key
PermissionDeniedError, # 403 — not authorized / compliance gate / DPA missing
NotFoundError, # 404 — unknown orgnr / report id
ConflictError, # 409 — e.g. company already monitored
ValidationError, # 400/422 — malformed parameters
QuotaExceededError, # 402/429 — quota or rate limit (see .retry_after_s)
ServiceUnavailableError, # 5xx — transient; retry later
APIConnectionError, # network unreachable (APITimeoutError for timeouts)
)
try:
fr.monitoring.add("923609016")
except ConflictError:
pass # already monitored — fine
except QuotaExceededError as exc:
retry_in = exc.retry_after_s or 60
except PermissionDeniedError as exc:
print(exc.status_code, exc.error_code, exc) # e.g. 403 EXTENSION_NOT_ACTIVE
Malformed organisation numbers are rejected client-side (ValueError)
before they cost an API call; "923 609 016"-style formatting is
normalized automatically.
LangChain integration
from firmaradar.langchain import get_tools # requires [langchain]
from langchain_anthropic import ChatAnthropic
from langchain.agents import create_tool_calling_agent
tools = get_tools(api_key="fr_...") # list[StructuredTool], sync + async
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-5"), tools, prompt)
LlamaIndex integration
from firmaradar.llamaindex import get_tool_spec # requires [llamaindex]
from llama_index.core.agent import ReActAgent
agent = ReActAgent.from_tools(get_tool_spec(api_key="fr_...").to_tool_list())
Tool names, descriptions and argument schemas are shared between both integrations and mirror the Firmaradar MCP server — the same operations behave identically whether an agent reaches them over MCP, LangChain or LlamaIndex.
Configuration
| Setting | Argument | Environment variable | Default |
|---|---|---|---|
| API key | api_key= |
FIRMARADAR_API_KEY |
— (required) |
| Base URL | base_url= |
FIRMARADAR_BASE_URL |
https://firmaradar.no |
| Timeout (s) | timeout= |
FIRMARADAR_TIMEOUT_S |
30 |
Development
python3 -m pytest sdk/python/tests/ -q # no network, no DB — httpx MockTransport
ruff check sdk/python
License
Apache License 2.0 — see the LICENSE file. © Firmaradar AS.
Release files for firmaradar 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| firmaradar-0.1.0.tar.gz | 33.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| firmaradar-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 61.9 kB
Release files / firmaradar-0.1.0.tar.gz
| Download URL | firmaradar-0.1.0.tar.gz |
|---|---|
| Size | 33.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
44a4d5cd23c39ecf7b7ac8ffb49148b9d3d09d3c18594d2e59e10262453f7d21
|
|
BLAKE2b-256 checksum How to use checksums |
aae10470bebab01e1a63452bf2928287157013e958d1ebf67aecd3dc31687526
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.2
|
Release files / firmaradar-0.1.0-py3-none-any.whl
| Download URL | firmaradar-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9eddedaafa05694119aa795c01282448e3ed43f12894404b07cdcc0ea1ab5d86
|
|
BLAKE2b-256 checksum How to use checksums |
b7ef7d5eb3a58fab2947f7130795a005244a93e171444b9bfbc5a13dface5186
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.2
|