Corporate control
Trace a UK company control graph
Follow recorded corporate PSC links while preserving traversal limits, source vintages, unresolved identifiers, and partial coverage.
Updated 15 September 2026 10 min read
A company profile tells you who Companies House records as controlling one entity. The control graph is a separate, paid request for following active corporate PSC evidence beyond that first company. Use it when the registry names a company as a person with significant control and your workflow needs the evidence chain, the related UK companies found within fixed limits, and an honest account of where traversal stopped.
Check whether a graph is relevant
curl "https://api.rotuli.co.uk/v1/entity/00445790?sources=ch_psc" \
-H "Authorization: Bearer $ROTULI_API_KEY"Read ch_psc.corporate_psc_count and ch_psc.control_graph_availableon the ordinary entity response. A positive count means the active PSC set contains corporate records. Availability means the caller's tier permits the graph; it does not promise spare upstream capacity or complete coverage.
A null corporate_psc_count is also meaningful. It means an active PSC kind was absent or unrecognised, including in some legacy cached records, so do not convert null to zero. Ordinary entity requests remain non-recursive whichever value is returned. The separate graph is available to Growth and Enterprise keys.
Choose the direction before the limits
| Direction | What it discovers | Use it when |
|---|---|---|
ancestors | Follows active corporate PSCs above the subject. | You need the recorded chain of corporate controllers. |
group | Adds bounded controlled companies from the subject and its ancestors, including shared-controller relationships. This is the default. | You need a wider related-company view around the subject. |
Group mode does not ascend a sibling company's other controllers. Descendant discovery is capped at two levels from each anchor, so the result remains a bounded evidence graph rather than an open-ended crawl.
Make a bounded REST request
curl "https://api.rotuli.co.uk/v1/entity/00445790/control-graph?direction=group&max_depth=3&max_nodes=12" \
-H "Authorization: Bearer $ROTULI_API_KEY"max_depth bounds ancestor traversal and max_nodesincludes the subject. Values can be lower than the plan default, which is useful when an interactive workflow needs a small first pass. They cannot exceed the caller's tier ceiling.
| Plan | Depth | Nodes | CH attempts | Deadline | Graph calls |
|---|---|---|---|---|---|
| Growth | 4 | 12 | 20 | 8 seconds | 6 per minute |
| Enterprise | 6 | 24 | 30 | 12 seconds | 12 per minute |
Every graph also has a two-level descendant limit and a 100-edge cap. These are ceilings, not promised throughput: pagination, unsuccessful Companies House calls, shared capacity, and the deadline can stop work sooner. Cached sources do not consume the outbound-attempt allowance, and there is no cache-bypass option on this endpoint.
Call the same operation through MCP
{
"name": "get_company_control_graph",
"arguments": {
"company_number": "00445790",
"direction": "ancestors",
"max_depth": 3,
"max_nodes": 12
}
}Pass these arguments to get_company_control_graph in an MCP client. It uses the same service, tier bounds, directions, and response semantics as REST. A successful tool result contains JSON encoded as MCP text content, so parse the JSON and check for an errorfield before interpreting graph fields. Both REST and MCP graph calls count as one normal request against the key's monthly allowance; the internal fan-out is governed separately.
Read evidence before drawing the graph
{
"company_number": "<subject company number>",
"direction": "group",
"status": "partial",
"snapshot": {
"id": "<snapshot id>",
"as_of": "<PSC data date>",
"published_at": "<publication timestamp>"
},
"discovery_source": "psc_snapshot",
"nodes": [{
"company_number": "<UK company number>",
"name": "<registered name or null>",
"status": "<company status or null>",
"relationship": "corporate_psc",
"assessment_status": "assessed",
"sources_checked": ["ch", "insolvency"],
"sources_failed": {},
"observed_at": {
"ch": "<profile observation timestamp>",
"insolvency": "<insolvency observation timestamp>"
}
}],
"coverage": {
"discovery_complete": false,
"assessment_complete": true,
"assessed_count": 4,
"unassessed_count": 0
},
"frontier": [{
"company_number": null,
"edge_id": "<evidence edge id>",
"reason": "unresolved_identifier"
}]
}Start with status, coverage, and frontier, then read nodes and edges. This prevents a neat visual path from hiding incomplete discovery or an unassessed company.
| Response area | Interpretation |
|---|---|
nodes | UK companies retained in the bounded graph. Identity fields are company_number, nullable name, and nullable status. Relationship, depth, and path describe the first route discovered, not a legal hierarchy. |
edges | PSC evidence directed from controller to controlled company. Keep nature of control, identification, source URL, observation time, and discovery source with the relationship. |
coverage | Separates discovery completeness from node-assessment completeness and reports assessed and unassessed node counts. |
frontier | Every unresolved or capped continuation, with nullable company and edge identifiers plus a machine-readable reason. |
limits and usage | The effective bounds for this request and the actual outbound-request and cache-hit counts. |
Keep discovery and assessment vintages separate
Graph discovery pins one published daily Companies House PSC snapshot for the entire traversal. The snapshot object records its id, as_of date, and published_at time. A snapshot more than 48 hours beyond the end of its as_of day makes discovery partial.
Node assessment is different: company profile and insolvency data come from live Companies House calls or cached slabs, and each node reports the relevant timestamps under observed_at.ch and observed_at.insolvency. A graph can therefore contain a daily discovery relationship and a more recent company-status assessment. Store both rather than collapsing them into one checked-at value.
When the daily index is unavailable, live PSC pagination can still support ancestor discovery. A group request then reports the missing reverse coverage in its frontier rather than pretending the wider group search succeeded.
Treat unresolved edges as evidence, too
An edge can lead outside nodes. Foreign corporate PSCs, ambiguous registration numbers, unsupported PSC kinds, and companies beyond a traversal limit remain on the frontier. Rotuli retains the raw identification evidence but does not join a company by name or by a merely similar number. Ceased corporate PSCs and natural-person links are not traversed.
This means an empty continuation is not always “no controller”. Inspect the frontier reason: unresolved_identifier, psc_kind_unavailable, depth_limit, descendant_depth_limit, node_limit, edge_limit, request_budget, deadline, upstream_capacity, index freshness, pagination, and upstream failures each require a different follow-up.
Keep related risk separate from subject risk
related_risk.findings can report RELATED_COMPANY_DISSOLVED or RELATED_COMPANY_INSOLVENCYfor a related node, including the company number, relationship, evidence path, and observation time. These warning findings provide context. They are not added to the subject company's risk score and are not findings against the subject. Corporate control itself adds no risk points.
If coverage is partial, no related findings does not mean that the group is clear. Pair this section with the risk-screening guide when your workflow also consumes the subject's own risk signals.
Retry capacity errors deliberately
- On REST
429 control_graph_capacity, read theRetry-Afterheader, wait at least that many seconds, add a small random delay, and retry the same bounded request. - For MCP, read
retry_afterfrom the structured error and apply the same wait before another tool call. - Cap retries and preserve the error for later work if capacity remains unavailable. Do not increase depth or nodes while retrying.
- A
200response withstatus: "partial"is not a transport failure. Save the useful graph, inspect its frontier, and decide whether a narrower follow-up can answer the remaining question.
A 403 control_graph_tier_required needs an entitled key, a 422 means the company number or graph options must be fixed, and a 404 company_not_found means the subject was not found. Blind retries do not resolve those errors. The error reference and rate-limit guide cover the shared API behavior around them.
State the boundary with every conclusion
The graph follows active corporate PSC records within published limits. It does not guarantee exhaustive beneficial or economic ownership, group membership, control through contracts or informal arrangements, a complete path through foreign entities, or legal liability between companies. Preserve the evidence edge, the snapshot and observation times, and the coverage boundary with any conclusion your workflow makes.