Automated KYC Onboarding: Screen Polish Companies in 3 API Calls
compliance · kyc · aml · automation · 5 min read
Every bank, fintech, and compliance team onboarding Polish companies faces the same grind: open multiple government portals, type in a NIP or KRS number, wait for each page to load, copy-paste results into a spreadsheet, repeat. For one company it’s annoying. For a hundred it’s someone’s entire Tuesday.
Under EU’s 6th Anti-Money Laundering Directive (6AMLD) and Poland’s AML Act, obligated institutions must verify beneficial ownership, screen for sanctions and insolvency, and check whether the entity is a regulated financial institution. Each check pulls from a different public registry. None of them have a public API.
Here’s how to automate all three checks in a single Python pipeline using pay-per-result actors from the getregdata Apify suite — no subscriptions, no browser automation to maintain, just structured JSON from official public registries.
The 3-check pipeline
NIP (tax ID)
|
+--[1] CRBR --> Who owns this company? (UBO check)
|
+--[2] KRZ --> Are they bankrupt or in enforcement?
|
+--[3] KNF --> Are they a regulated financial institution?
|
v
DECISION: onboard, escalate, or decline
Prerequisites
You need an Apify account with an API token. The free tier includes $5 in monthly credits, which covers roughly 600 UBO checks, 800 debtor lookups, and thousands of KNF searches.
pip install apify-client
If you’re building this into an AI agent workflow, install the packaged skill instead:
git clone https://github.com/Nolpak14/getregdata.git
# The skills/regdata-kyc-aml/ directory contains the full skill for Hermes/Claude agents
Step 1: Who owns it? (CRBR)
Poland’s Central Register of Beneficial Owners (CRBR) is the mandatory UBO register. Every Polish company must file their natural-person beneficial owners — anyone with more than 25% ownership or control. Search by NIP or KRS number and get back names, citizenship, ownership percentage, and control nature.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
def check_ubo(nip: str) -> dict:
"""Return UBO data for a Polish company by NIP."""
run = client.actor("regdata/crbr-beneficial-owners-scraper").call(
run_input={
"queries": [{"nip": nip}],
}
)
items = client.dataset(run["defaultDatasetId"]).list_items().items
if not items:
return {"status": "not_found", "owners": []}
item = items[0]
# Company fields are flat on the row - there is no nested "company" object.
return {
"status": "ok",
"company": {
"name": item["name"],
"nip": item["nip"],
"krs": item["krs"],
"legalForm": item["legalForm"],
},
"owners": item.get("beneficialOwners", []),
}
What you get: structured UBO data with names, citizenship, country of residence, and the nature of control. Each owner carries firstName / lastName separately (there is no combined full-name field), and the control detail sits in a nested entitlements list — one entry per basis of control, each with a natureOfControl description and its registry code. Ownership size, where the company declared one, is on the entitlement as amount plus unit; it is frequently null, because a beneficial owner can be declared on the basis of senior management rather than a shareholding.
Use the row’s found boolean to distinguish “the register answered and has no record” from a failed lookup.
Step 2: Are they insolvent? (KRZ)
The National Debtor Registry (KRZ) covers bankruptcy proceedings, restructuring cases, and enforcement actions for Polish entities. It’s updated daily by the courts and covers nine search modes — companies, individuals, sole traders, case signatures, proceedings, shareholders, assets, advisors.
For KYC onboarding, the company search by NIP is the fast path to flagging risk:
def check_insolvency(nip: str) -> dict:
"""Check KRZ for active insolvency/enforcement proceedings."""
run = client.actor("regdata/krz-debtor-scraper").call(
run_input={
"searchMode": "entity",
"identifier": nip,
"maxResults": 50,
}
)
items = client.dataset(run["defaultDatasetId"]).list_items().items
if not items:
return {"status": "clean", "proceedings": []}
# Each row IS one proceeding. There is no nested "proceedings" list on a row,
# so a match means len(items) proceedings, not one item containing a list.
return {
"status": "flagged",
"debtor_name": items[0]["entityName"],
"proceedings": [
{
"proceedingId": row["proceedingId"],
"caseSignature": row["caseSignature"],
"proceedingType": row["proceedingType"],
"court": row["court"],
}
for row in items
],
}
What you get, per proceeding: the entity it belongs to (entityName, plus nip / krs / regon where the register holds them) and the proceeding’s own identifier.
One thing to plan for: in entity search mode the register returns the match and its proceedingId, but caseSignature, proceedingType, proceedingStatus and court come back null. Entity mode answers “is this company in the register, and under which proceedings” — it does not classify them. To get the case signature and the proceeding type, take each proceedingId and run the actor again in proceedingDetails mode. For a pass/fail onboarding gate the entity search alone is enough; for a credit file that has to name the proceeding, budget the second call.
A clean KRZ check means the company has no active insolvency or enforcement proceedings — but only if the run reports that the register actually answered. An empty result set is an all-clear only when the registry responded; if it was down, an empty dataset means “we do not know”, not “nothing found”. The actor now fails loudly (non-zero exit) when the registry is unavailable or returns something other than data, precisely so an outage can never be read as “no proceedings found”. Check the run status before you treat a KRZ result as clean.
And even a genuine all-clear doesn’t guarantee financial health. For that you’d pair it with financial statement analysis, which is a separate workflow.
Step 3: Are they a regulated institution? (KNF)
Poland’s Financial Supervision Authority (KNF) maintains registries of licensed financial institutions: payment institutions, e-money issuers, credit intermediaries, lending companies, and pawnbroking operators. Over 75,000 entities across three registries.
If a company you’re onboarding shows up here, it changes your risk profile: you’re now dealing with a regulated financial entity, which may trigger enhanced due diligence requirements under AML rules.
def check_knf(nip: str) -> dict:
"""Check KNF registries for financial institution status."""
run = client.actor("regdata/knf-registry-scraper").call(
run_input={
"nip": nip,
}
)
items = client.dataset(run["defaultDatasetId"]).list_items().items
if not items:
return {"status": "not_regulated", "entries": []}
return {
"status": "regulated",
"entries": items,
}
What you get per row: entityTypeLabel (the readable class, for example “Domestic payment institution”) alongside the raw entityType code, the registry the row came from, plus registryNumber, status and registrationDate. A company that appears here is a regulated financial entity — your AML obligations shift accordingly.
One NIP can return more than one row, because the same entity is often listed in several KNF registries at once. Treat any row as regulated rather than assuming a single match.
The full pipeline
Now wire all three checks together with a decision function:
from dataclasses import dataclass
from typing import List
@dataclass
class KYCResult:
nip: str
ubo_status: str
owners: List[dict]
insolvency_status: str
insolvency_proceedings: List[dict]
knf_status: str
knf_entries: List[dict]
decision: str # "clear", "review", "decline"
flags: List[str]
def run_kyc_pipeline(nip: str) -> KYCResult:
"""Run the full 3-check KYC pipeline for a Polish NIP."""
flags = []
# Step 1: UBO
ubo = check_ubo(nip)
if ubo["status"] == "not_found":
flags.append("UBO: no CRBR record found")
# Step 2: Insolvency
insolvency = check_insolvency(nip)
if insolvency["status"] == "flagged":
flags.append(f"KRZ: {len(insolvency['proceedings'])} active proceedings")
# Step 3: KNF
knf = check_knf(nip)
if knf["status"] == "regulated":
flags.append(f"KNF: regulated entity ({knf['entries'][0].get('entityTypeLabel', 'unknown')})")
# Decision logic
if insolvency["status"] == "flagged":
decision = "decline"
elif knf["status"] == "regulated" or len(flags) > 0:
decision = "review"
else:
decision = "clear"
return KYCResult(
nip=nip,
ubo_status=ubo["status"],
owners=ubo.get("owners", []),
insolvency_status=insolvency["status"],
insolvency_proceedings=insolvency.get("proceedings", []),
knf_status=knf["status"],
knf_entries=knf.get("entries", []),
decision=decision,
flags=flags,
)
# Example: batch screen your onboarding queue
nips_to_screen = ["6770065406", "7792308495", "1080004850"]
for nip in nips_to_screen:
result = run_kyc_pipeline(nip)
print(f"{nip}: {result.decision.upper()}")
if result.flags:
for flag in result.flags:
print(f" - {flag}")
print()
Output for a clean company — CRBR has its declaration, and neither the debtor register nor the KNF registries return anything:
6770065406: CLEAR
A regulated entity comes back for review rather than declined — being licensed is not a negative, it just changes your obligations:
7792308495: REVIEW
- KNF: regulated entity (Domestic payment institution)
And a company in the debtor register:
1080004850: DECLINE
- KRZ: 1 active proceedings
Note what the last one does not say. This entity has a CRBR declaration on file, so there is no UBO flag — the decline is driven entirely by the debtor register. A company can be fully compliant on ownership disclosure and still be insolvent; the three checks are independent and you want to see which one actually fired.
Using it as an AI agent skill
If you’re running AI agents for compliance workflows, the getregdata repo has this pipeline pre-packaged as an installable skill. The regdata-kyc-aml skill wraps all three actors with proper error handling, rate limiting, and output formatting — so an agent can run a KYC check with a single prompt:
"Run a full KYC check on NIP 6770065406 and return a pass/fail decision with supporting evidence"
The skill at github.com/Nolpak14/getregdata/skills/regdata-kyc-aml/ handles the rest.
What this pipeline gives you
- Speed: 3 API calls replaces ~15 minutes of manual portal-hopping. Batch 100 companies and you save roughly an entire workday.
- Consistency: The same checks run the same way every time. No skipping the KNF check because someone got tired.
- Audit trail: Structured JSON output from every step. Drop it into your case management system or compliance database.
- Pay-per-result: You pay only for what you query. No monthly subscription for occasional use. CRBR is $0.008 per result, KRZ is $0.006 per result plus a $0.025 search-session fee, and KNF is $0.004 per result. The free tier covers roughly 600 UBO checks per month.
What it doesn’t cover
This pipeline screens Polish companies only. It doesn’t check sanctions lists, PEP databases, or adverse media — those are separate data sources you’d integrate alongside these registry checks. The KRZ check tells you about active proceedings but doesn’t surface historical ones that have concluded. And the UBO data is what the company declared to CRBR — it’s not independently verified.
For cross-border due diligence, the same suite has actors covering Spain (BORME corporate acts, Registro Mercantil company profiles), Austria (Ediktsdatei insolvency, WKO business directory), and France (Societe.com company data with director networks and financials).
The getregdata European Business Data Suite covers official company registries across Europe, the US and beyond. All data comes from official public registries. Pay-per-result, no subscription.
Turn this into working data. Browse the registries and use cases, or start free on Apify.
Originally published on Dev.to.