The Case Object

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" }
        ]
      }
    ]
  }'
FieldConstraint
companiesAt 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.
individualsAt most 50. At most one is_signatory and one is_delegator per case.
refUnique within the request. Optional: an entity without a ref is created, but no other entity in the payload can reference it.
source_company_refThe 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_keyDetermines 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.

CodeCondition
duplicate_refTwo entities in the payload share a ref
invalid_refA source_company_ref matches no company in the payload
no_main_companycompanies is present and no entry has type: main
multiple_main_companiesMore than one company has type: main
main_company_cannot_have_source_refThe main company declares a relation with a source_company_ref
not_unique_sourceThe resulting graph has more than one root
source_not_mainThe root of the graph is not the main company
main_not_sourceThe main company is not the root of the graph
graph_has_cyclesThe relations form a cycle
duplicate_relationTwo relations connect the same pair of entities
max_companies_exceeded, max_individuals_exceededMore than 50 companies or 50 individuals
multiple_signatories, multiple_delegatorsMore than one individual carries the flag
invalid_company, invalid_individualField-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.

Case summary, with the Dotfile case ID and external ID in the case details panel

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

A case moves from draft to open, then a case review sets it to approved, rejected or closed; a closed or rejected case can be reopened to open
StatusMeaning
draftPre-created case, typically completed through the client portal
openUnder investigation: data collected, checks run and reviewed
approvedClosed by an approved review
rejectedClosed by a rejected review
closedNo 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.

FieldDescription
statusapproved, rejected or closed
commentOptional. May be required, depending on workspace settings
reviewed_atOptional. Defaults to the current time; an earlier value back-dates the decision
next_review_atapproved 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.

FlagSet when
all_checks_approvedThe case has at least one check and all of them are approved
for_reviewThe reviewer has actions and the contact has none
for_first_collectThe contact has actions and no initial collection has completed
for_recollectionAn initial collection has completed and the contact has actions again, typically rejected or expired checks
first_collect_completedAn 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.

Contact and reviewer actions alternate — for_first_collect, for_review, for_recollection — until all_checks_approved; first_collect_completed marks the first pass done

Custom Properties

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

Workspace settings, Properties — the custom properties configured for cases, companies and individuals

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"]
  }
}
TypeJSON typeConstraint
Choicesarray of stringsOption keys. One element unless Allow Multiple is set
Countriesarray of stringsISO 3166-1 alpha-2, e.g. FR. One element unless Allow Multiple is set
DatestringISO 8601 yyyy-MM-dd, e.g. 2023-01-31
EmailstringValid email address, 250 characters maximum
NumericnumberWithin the property's min and max. The numeric format is a display setting only
Phone numberstringE.164, e.g. +33654489875
Textstring250 characters, or 10 000 when isMultiLine is set
URLstringValid URL, 2 000 characters maximum
Yes/Nobooleantrue 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.

Manual risk becomes current immediately; an automatic risk submitted while a manual one is current is stored but not current, until re-enabled from the console
FieldDescription
levelnot_defined, low, medium, high, critical or prohibited
modeautomatic or manual
scoreInteger between -2 147 483 648 and 2 147 483 647
componentsObject of string keys to numbers: the breakdown of the score
flagsObject of string keys to booleans: the breakdown of the level
commentOptional 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.

Jane Doe (shareholder and legal representative, 20%) and Acme Holding (shareholder, 80%) both relate to Acme Corp, the main company

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

A relation from an individual carries from_individual_id, roles, position and ownership_percentage; a relation from a company carries from_company_id and roles; both carry to_company_id naming the company held

relations on a company or an individual is an array of:

FieldDescription
to_company_idThe company the relation points to: the entity being held
from_company_id, from_individual_idThe holder. Exactly one of the two is set
ownership_percentage0 to 100, or null
voting_rights_percentage0 to 100, or null
rolesArray of shareholder and legal_representative. No other value is accepted
positionString, 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 the relations arrays on companies and individuals name it to_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 payloadrelations on a nested company or individual, referencing
source_company_ref. See the example above.

At entity creationrelations 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:

EndpointBehaviour
Create a relationPOST /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 relationUpdates 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 relationRejected 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

Robert Klein holds 50% of Nova Holding, which holds 60% of Acme Corp — Robert Klein's total interest in Acme Corp is 30%, while Nova Holding's direct interest is 60%

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