The model
What an assessment is, what happens to it, and why the same request returns different evidence in different markets.
One idea organises this page. A market is described to the API as data, not as code.
Everything that varies between countries is published on
GET /v1/countries/{code}: which identifiers that market keys on and in what
format, which classification schemes a rule can be written against, and which scale a
rating arrives on. The request and response shapes do not vary. A client that reads the
country resource at runtime works in the next market without new code.
Lifecycle#
Six states. Three are terminal.
POST /v1/assessments → 202
│
├─ mode: screening ──► screening ──► complete
│
└─ mode: full ──────► screening ──┬─► complete (ended at the first stage)
│
├─► awaiting_bank_data
│ │
│ │ POST /{id}/bank-data
│ ▼
└─────► processing ──► complete
screening or processing ── cannot finish ──► failed
awaiting_bank_data ── window elapses ──► expired
screening straight to processing.| State | Meaning |
|---|---|
screening | Public registry sources are being gathered and the first-stage gates evaluated |
awaiting_bank_data | The first stage finished without a terminal outcome and the policy defines a condition that only bank data can evaluate. Never reached in screening mode |
processing | Paid sources and the full policy evaluation are running |
complete | Terminal. result is present |
failed | Terminal. Processing could not complete |
expired | Terminal. Required input did not arrive, so the assessment closed without a result |
reject is a policy outcome on a complete assessment: the system
worked and the answer was no. failed means no assessment was produced at all.
Screening and full assessments#
screening runs the first stage and stops there. full continues
into the rest of the policy. Evaluation stops at the first gate that fails, so a case
rejected on eligibility never reaches anything further.
Which conditions and ratios make up the first stage is declared on the policy version and
readable at GET /v1/policies/{version}. It is a property of the policy, not of
the market.
Immutability#
Inputs are append-only. Results are written once.
POST /v1/assessments/{id}/bank-data is the single permitted append, and it is
legal only in awaiting_bank_data. There is no endpoint that modifies a
completed assessment, and there is no re-run: re-evaluating a case creates a
new assessment carrying previous_assessment_id, and the earlier one
keeps its result. A verdict acted on in one month still reads the same in the next,
alongside the policy version that produced it.
previous_assessment_id records lineage and carries nothing forward. The new
assessment fetches its sources again and evaluates every gate again.
Idempotency#
Send Idempotency-Key on every POST. Keys are retained 24 hours.
Use one per logical case, not one per HTTP attempt.
| You send | You get |
|---|---|
| New key | The request runs |
| Same key, identical body | The original response, replayed. No work is done, nothing is billed again |
| Same key, different body | 409 with idempotency_error |
| No key | The request runs. A retry runs it again |
Policy versions#
A policy version is a tenant-scoped string such as tenant-policy-2026-07,
never a bare date. Every outcome names the version that produced it and every reason code
repeats it. Versions are never edited in place, so a superseded version still describes
exactly what it decided, and any outcome can be replayed against the rules in force when it
was produced.
Missing data#
A missing value is a state, not an error, and you will meet it on a large fraction of real
applicants. Every line item, ratio and indicator carries an availability saying
why it is absent.
| Value | Meaning |
|---|---|
available | Obtained |
not_filed | The entity is legally exempt from filing it. The document does not exist |
not_published | No public source for this entity type in this market |
not_consented | Needs a consent nobody holds yet |
not_entitled | Needs a licence or statutory basis that neither the caller nor Credit Atlas holds |
source_unavailable | The source exists and the call failed. The only one worth retrying |
not_supported | Not implemented for this market yet |
A ratio that cannot be computed returns band: null with the inputs it was
missing. It never bands favourably, and the schema enforces that rather than describing it.
Evidence sufficiency#
An input that could not be obtained never improves an outcome. A sufficiency gate runs before the decision rules and can only cap the result, never raise it. It is judged per ratio group, so thin evidence in one group is visible rather than averaged away.
When too little could be computed to reach any conclusion, the outcome is
insufficient_evidence. That is a statement about the inputs, not about the
applicant, and it is not a rejection.
policy_outcome.capped_by says which happened: policy means the
rules chose the outcome, evidence means thin evidence forced it. Those need
different handling in your queue.
In at least one market the typical company has only one of the three credit-risk ratios computable, because of what its size class is obliged to prepare. Build for a cap as the common case there, not the exception.
Policy checks#
A policy rests on two kinds of evidence. Ratios are one. The other is a set of discrete
conditions: a rating floor, a default count, a sanctions result, a trading-history
threshold, a revenue trend. Those arrive as checks[], each with its own
availability and its own result.
A condition that was never evaluated reports not_evaluated, never
pass. Sanctions screening that could not be performed is not a statement that
the applicant is clear, and the contract will not let it be reported as one. A mandatory
condition that could not be evaluated caps the outcome.
One condition can appear more than once. A condition run against each representative
produces one entry per person, distinguished by subject, so a clean company
with one problematic director is distinguishable from the reverse. Companies sent in
related_companies are screened the same way, one entry per company.
Beneficial ownership is evidence you supply rather than evidence we gather. Access to those
registers is restricted to entities with a statutory basis for customer due diligence, so
where you hold that basis the lookup belongs on your side and its outcome goes in
partner_data.beneficial_owners. An omitted block makes the condition
not_evaluated rather than passing it.
Existing credit is reported alongside the checks. result.credit_exposure
carries, per subject, the open facilities the bureau holds, the outstanding balance, the
monthly instalments and the credit limits in use, with the facilities itemised where the
bureau itemises them.
If a policy marks a condition mandatory and no source in that market can evaluate it,
every assessment there caps below the most favourable outcome.
GET /v1/countries/{code} says what each market can answer, and
policy_outcome.evidence_sufficiency.unavailable_mandatory_checks names the
conditions responsible on any individual assessment.
Rating scales#
Markets do not share one rating scale. One publishes ordered letter grades; another a number where higher is better; another a number where lower is better, because it is a default probability rather than a quality grade.
Every scale a market uses is published in rating_scales on the country
capability map, with its ordering and its direction, and every rating check names the scale
it was evaluated on. A policy sets a floor per market, in that market's own terms.
On an ordinal scale the ranking is the order of grades, best first. Comparing
the grades as strings ranks AAA below AA and B above
AAA. Resolve through the scale.
Credit Atlas does not convert between markets’ scales, and a floor set in one market says nothing about any other.
Canonical keys#
Every market files financial statements to its own scheme, in its own language, at its own
level of detail. Statement lines are normalised to a fixed vocabulary of canonical keys such
as cash, current_assets, revenue and
equity, and every ratio is defined on those keys. Adding a market is a mapping
table between that market's filing scheme and the canonical keys.
Identifiers#
Published as patterns on GET /v1/countries/{code} so you can validate before
sending rather than discovering the format from a 400.
| Market | Primary identifier | Note |
|---|---|---|
| Estonia | ^[0-9]{8}$ |
Eight digits, uniform |
| Latvia | ^[0-9]{6}$|^[0-9]{8,9}$|^[3-5][0-9]{10}$ |
Do not assume eleven digits. Most modern registrations have eleven, but tens of thousands of legacy ones have six, eight or nine, and a strict eleven-digit rule rejects real company records |
| Poland | ^[0-9]{10}$ |
Ten digits, checksum-validated. Companies also carry a separate court register number; sole traders do not |
Transaction categories#
Every assessment reports coverage.bank_labels, which takes
provider, classifier or not_supported.
Some account-information providers return a category per transaction. A raw open-banking feed returns a counterparty name, a free-text reference and nothing else. Any indicator defined over categories, such as lender payments, collections activity, round-value patterns or unrecognised credits, is unavailable in the second case unless a classifier has been built for that market's language and text.
Where categories exist, every transaction carries category_source, and the
schema requires it on any transaction that carries a category. A threshold calibrated on
provider labels must never be silently applied to classifier output.
Each indicator declares what it needs in requires:
transaction_amounts, transaction_labels,
counterparty_names, counterparty_jurisdiction,
acquiring_data. You can read why an indicator is unavailable in a market from
the indicator itself, before sending a request.
triggered: null
false means the pattern was looked for and is absent. null
means nobody looked, and it is never reported as false.