A case is the container for one KYC/KYB process. It holds the companies and individuals under
investigation, the checks that verify them, the controls and the risk attached to them, and the
review that closes it. Every other object in this API belongs to a case.
Cases are created from the console, or with Create a case. This page
describes the object. Start KYB process and
Start KYC process describe the integration flow.
Creating a case
POST /v1/cases accepts the entire entity graph in a single request: the main company, its
affiliated companies, the individuals, and the relations between them. Entities reference one
another through caller-assigned ref values, which the API resolves into identifiers.
curl -X POST https://api.dotfile.com/v1/cases \
-H "X-DOTFILE-API-KEY: $DOTFILE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp onboarding",
"external_id": "customer_8412",
"template_key": "kyb_standard",
"companies": [
{ "ref": "main-co", "type": "main", "name": "Acme Corp", "country": "FR", "registration_number": "552100554" },
{
"ref": "holding",
"type": "affiliated",
"name": "Acme Holding",
"country": "LU",
"relations": [
{ "source_company_ref": "main-co", "roles": ["shareholder"], "ownership_percentage": 80 }
]
}
],
"individuals": [
{
"ref": "ceo",
"first_name": "Jane",
"last_name": "Doe",
"email": "[email protected]",
"is_business_contact": true,
"is_beneficial_owner": true,
"relations": [
{ "source_company_ref": "main-co", "roles": ["legal_representative", "shareholder"], "ownership_percentage": 20, "position": "CEO" }
]
}
]
}'| Field | Constraint |
|---|---|
companies | At most 50. When present, exactly one entry must have type: main. The main company is the root of the graph and cannot carry a source_company_ref. |
individuals | At most 50. At most one is_signatory and one is_delegator per case. |
ref | Unique within the request. Optional: an entity without a ref is created, but no other entity in the payload can reference it. |
source_company_ref | The ref of the company the entity is attached to. Defaults to the main company. An affiliated company or an individual declared without relations is attached to the main company. |
template_key | Determines which checks are created. See Templates. |
The response is the complete case: id, controls, client_portal, and every company and
individual with its assigned identifier, checks and relations. No subsequent GET is
required.
Creating the case first and adding entities against its id remains supported and is not
deprecated — Create a company and
Create an individual both take a case_id. Nested creation writes
the whole graph in a single transaction.
Validation errors
An invalid payload returns 400 with code CASE_CREATION_VALIDATION_ERROR and an errors
array listing every failure at once, each entry carrying a code and the ref it concerns.
| Code | Condition |
|---|---|
duplicate_ref | Two entities in the payload share a ref |
invalid_ref | A source_company_ref matches no company in the payload |
no_main_company | companies is present and no entry has type: main |
multiple_main_companies | More than one company has type: main |
main_company_cannot_have_source_ref | The main company declares a relation with a source_company_ref |
not_unique_source | The resulting graph has more than one root |
source_not_main | The root of the graph is not the main company |
main_not_source | The main company is not the root of the graph |
graph_has_cycles | The relations form a cycle |
duplicate_relation | Two relations connect the same pair of entities |
max_companies_exceeded, max_individuals_exceeded | More than 50 companies or 50 individuals |
multiple_signatories, multiple_delegators | More than one individual carries the flag |
invalid_company, invalid_individual | Field-level schema failure. The entry carries a messages array |
Identifiers
Each case has a server-generated
UUID v4 id. external_id is
optional, supplied at creation or with Update a case, and must be unique
within the workspace. Both appear in the case header in the console.
external_id is accepted in place of id in the path of
Retrieve a case, Update a case and
Delete a case: the segment is resolved as an id first, then as an
external_id. POST /v1/cases/{id}/generate-report and
POST /v1/cases/{id}/document-templates/{key}/render accept the internal id only.
external_id is an identifier. Application data belongs in
custom properties or metadata.
Case lifecycle

| Status | Meaning |
|---|---|
draft | Pre-created case, typically completed through the client portal |
open | Under investigation: data collected, checks run and reviewed |
approved | Closed by an approved review |
rejected | Closed by a rejected review |
closed | No longer under investigation. Checks and entities are retained and remain available if the case is reopened |
POST /v1/cases and PATCH /v1/cases/{id} accept draft and open. approved, rejected
and closed are set only through a case review.
Automation does not run on a case in draft, rejected or closed. A company added to a draft
case receives no checks until the case reaches open, and
Run a template on a draft case returns 400
(AUTOMATION_PIPELINE_DRAFT_CASE).
Case review
Create a case review records a decision and updates the case status
accordingly.
| Field | Description |
|---|---|
status | approved, rejected or closed |
comment | Optional. May be required, depending on workspace settings |
reviewed_at | Optional. Defaults to the current time; an earlier value back-dates the decision |
next_review_at | approved reviews only. Derived from workspace settings and the case risk level unless supplied. null schedules no periodic review |
The review is returned as review on the case and is included in every case webhook event. See
the approval flow documentation.
Case flags
contact_has_actions and reviewer_has_actions are booleans indicating whether an action is
pending from the contact and from the reviewer respectively. flags is an ordered array
describing the stage the case has reached.
| Flag | Set when |
|---|---|
all_checks_approved | The case has at least one check and all of them are approved |
for_review | The reviewer has actions and the contact has none |
for_first_collect | The contact has actions and no initial collection has completed |
for_recollection | An initial collection has completed and the contact has actions again, typically rejected or expired checks |
first_collect_completed | An initial collection has completed |
flags is always ordered all_checks_approved, for_review, for_first_collect,
for_recollection, first_collect_completed, so its first element identifies the stage.

Custom Properties
Custom properties are workspace-defined fields on cases, companies and individuals, configured
in workspace settings.

custom_properties is accepted on the create and update endpoints of all three objects. Keys
are the property keys defined in the workspace; values must satisfy the property type. null
clears a value. Archived properties are rejected on write and omitted on read.
List custom properties returns the keys, types and
settings configured in a workspace.
{
"name": "Some example case",
"status": "open",
"custom_properties": {
"ubo_us_citizen": true,
"main_activity_country": ["FR"]
}
}| Type | JSON type | Constraint |
|---|---|---|
| Choices | array of strings | Option keys. One element unless Allow Multiple is set |
| Countries | array of strings | ISO 3166-1 alpha-2, e.g. FR. One element unless Allow Multiple is set |
| Date | string | ISO 8601 yyyy-MM-dd, e.g. 2023-01-31 |
| string | Valid email address, 250 characters maximum | |
| Numeric | number | Within the property's min and max. The numeric format is a display setting only |
| Phone number | string | E.164, e.g. +33654489875 |
| Text | string | 250 characters, or 10 000 when isMultiLine is set |
| URL | string | Valid URL, 2 000 characters maximum |
| Yes/No | boolean | true or false |
Choices and Countries values are always arrays, including when the property accepts a single
value. Numeric values are JSON numbers, not strings.
Metadata
metadata is a flat object of string keys to string values, set on
create and update.
{
"metadata": {
"custom_key": "true",
"my_data": "this is important",
"count": "42"
}
}On update, metadata replaces the existing object; keys are not merged. null removes all of
it. Metadata is carried on the case object wherever it appears, webhook payloads included.
Metadata is not validated, and it is not among the fields
List all cases filters on. Automation rules and risk code can read it, but
the field is deprecated there in favour of custom_properties.
Risk
A case has one current risk and a history of the assessments that preceded it. Automatic risk is
submitted with Create an automatic risk. Manual risk is set from the
console.

| Field | Description |
|---|---|
level | not_defined, low, medium, high, critical or prohibited |
mode | automatic or manual |
score | Integer between -2 147 483 648 and 2 147 483 647 |
components | Object of string keys to numbers: the breakdown of the score |
flags | Object of string keys to booleans: the breakdown of the level |
comment | Optional string |
{
"level": "medium",
"score": 10067,
"components": {
"customer_score": 1400,
"product_score": 3000,
"delivery_channel_score": 2000,
"transaction_score": 1667,
"geography_score": 2000
},
"flags": {
"prohibited_country_ubo": true,
"complex_company_alert": false
}
}Manual risk takes precedence. While a manual risk is current, a submitted automatic risk is
stored but does not become current; automatic risk is re-enabled from the console.
A submission identical to the current or the latest risk — same level, score, comment,
components and flags — returns the existing risk rather than creating a duplicate. This is not
an error.
In a workspace with an online risk engine, POST /v1/cases/{id}/risks returns 400. The engine
owns the risk in that configuration.
Case relations
Relations describe ownership and representation between the entities of a case. The console
renders them as a graph.

A case has one main company, the entity under investigation, and any number of affiliated
companies, which sit between the main company and its beneficial owners. Individuals are
attached to either.
The relation object

relations on a company or an individual is an array of:
| Field | Description |
|---|---|
to_company_id | The company the relation points to: the entity being held |
from_company_id, from_individual_id | The holder. Exactly one of the two is set |
ownership_percentage | 0 to 100, or null |
voting_rights_percentage | 0 to 100, or null |
roles | Array of shareholder and legal_representative. No other value is accepted |
position | String, 150 characters maximum |
Beneficial ownership is not a role. It is is_beneficial_owner, a boolean on the individual,
alongside is_controlling_person, is_signatory, is_delegator and is_business_contact. A
roles array containing any value other than the two above returns 400.
Nested case creation names the far end of a relation
source_company_ref. The relation
endpoints and therelationsarrays on companies and individuals name itto_company_id. The
two fields designate the same entity, the company being held, and both default to the main
company when omitted.
Creating relations
In the case payload — relations on a nested company or individual, referencing
source_company_ref. See the example above.
At entity creation — relations on Create a company or
Create an individual, referencing to_company_id. When
to_company_id or the whole relations array is omitted, the entity is attached to the main
company.
{
"case_id": "{{case_id}}",
"is_business_contact": true,
"is_beneficial_owner": true,
"first_name": "Tony",
"last_name": "Stark",
"email": "[email protected]",
"relations": [
{
"to_company_id": "{{affiliated_company_id}}",
"roles": ["shareholder"],
"position": "CEO",
"ownership_percentage": 33.33,
"voting_rights_percentage": 33.33
}
]
}On an existing graph — three dedicated endpoints:
| Endpoint | Behaviour |
|---|---|
| Create a relation | POST /v1/cases/{caseId}/relations. Requires from_individual_id or from_company_id; to_company_id defaults to the main company. The case must already have a main company, and a relation between a given pair must not already exist |
| Update a relation | Updates the percentages, roles, position and to_company_id. from_company_id and from_individual_id are immutable: moving that end requires creating the new relation and deleting the old one |
| Delete a relation | Rejected when it would leave an entity with no relation |
Graph constraints
Every route validates the resulting graph: one root, which must be the main company; no cycles;
no two relations between the same pair of entities; no entity left without a relation. A request
that violates any of these is rejected without partial application.
Ownership percentages

ownership_percentage and voting_rights_percentage appear at two levels and do not carry the
same meaning:
- on an individual — the total interest in the main company;
- on a relation — the direct interest in the company named by
to_company_id.
Totals are computed from the direct percentages declared along the chain.
Retrieving cases
Retrieve a case returns the case with its companies and individuals, each
carrying its own checks and relations. ?property_origin=true adds, per property, the source
the value came from.
List all cases filters on external_id, assignee_id, name, tags,
status, created_at, updated_at and last_activity_at, and sorts on name, created_at
and last_activity_at, defaulting to created_at. See
Filtering, sorting and pagination.
Related
- Templates — how checks are created on a case
- Checks — what each check type verifies and returns
- Webhooks — the events a case emits
- Start KYB process — the end-to-end integration flow