---
title: "Group structure: who controls whom"
method: GET
path: "/company/{number}/group-structure"
tags: ["PSC"]
---

# Group structure: who controls whom

`GET /company/{number}/group-structure`

## Request

- Base URL: `https://api.companystack.co.uk`
- URL: `https://api.companystack.co.uk/company/{number}/group-structure`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-Api-Key`

Who controls a company and which companies it controls, from the Companies House register of persons with significant control (PSC), as a graph: `nodes` and `edges`, the same in sentences (`text`) and laid out for drawing (`layout`). Every company held gets a chart: `kind` says whether it is a group, the company's own persons with significant control, or (where no person is on the register) what the register says instead.

Above the company: every current person with significant control (individuals, companies, legal persons, protected entries), then the owners of its corporate owners, until the chain reaches an individual, an owner registered outside the UK, a statement that there is no registrable person, a company not held, or an entry that cannot be matched. Each of those is a node with a `stop` giving the reason in plain words. A company with no person on the register has one `end` node above it: the PSC statement it filed in the register's words, the exemption for a public company whose shares trade on a regulated market, the rule that puts its type outside the PSC regime, or that nothing is held. Below it: the companies whose PSC entries name it. A company can have several owners and an owner several subsidiaries, so this is a graph, not a tree: a company reached twice is one node, and circular control is reported in `cycles`.

Matching: A company is linked to its owner only where the owner's entry on the register gives a company number and names a UK register, and we hold a company with that number. Names are never matched: an owner that cannot be linked is shown as the register describes it.

Each link shows the nature of control as filed, in the register's own bands. A band is not an exact holding. The register does not show owners registered outside the UK, the beneficiaries of trusts, or anyone below the thresholds for registration, so this may not be the whole structure. Ceased entries are not included (`omitted.former_links` counts them across the chain, `counts.former_owners` for the company asked about). Shareholders are a different register and are never mixed in.

Individuals are shown as the register shows them: name, and month and year of birth (never the day). A person who has objected is shown as "Name withheld" or left out, as in the PSC endpoints.

Read from the register copy held: no Companies House request is made. `as_at` is the date that copy is current to. Limits of one answer: 8 levels above and 5 below, 60 companies in all (24 of them above), 10 registered owners and 25 controlled companies for any one company. Where a limit is reached `truncated` is true and a `more` node says where and how many.

Plans: every plan may call this. API Business and Enterprise, and web-app Pro and Team, get every level (`level: full`). API Free and Starter, and web-app Free, get the first level each way (`level: first_level`): the company's registered owners and the companies it controls directly, with `locked` counting the levels and companies beyond and `upgrade` saying where they are. A chart with one level (a company owned by individuals that controls nothing) is the same on every plan.

Compatibility (2026-10-13): `has_group` keeps its meaning. `has_chart`, `kind`, `layout_narrow`, `former_note`, `counts.owners`, `counts.former_owners`, `limits.ownersOfSubject`, a node's `note` and `share_register` are new. `layout` is no longer null for a company with has_group false: test `has_group` (or `kind`), not `layout`, to tell a group from a company with no group.

## Path parameters

- `number` string, required

## Response `200`

The chart. has_group: false, with kind owners or statement, for a company with no corporate owner that controls nothing

- object
  - `title` string: "Group structure" for kind group, "Ownership chart" otherwise
  - `company_number` string, required
  - `company_name` string, required
  - `root_id` string: The node id of the company asked about
  - `level` 'full' | 'first_level', required: full: every level, to where the register stops. first_level: the registered owners and the companies controlled directly, with `locked` counting what lies beyond
  - `has_group` boolean, required: true when the company has a corporate owner or controls a company. false otherwise (unchanged in meaning). Since 2026-10-13 a company with has_group false still has a chart of its own: see has_chart, kind and layout
  - `has_chart` boolean, required: true when there is a chart to draw (layout is not null). Every company held has one (added 2026-10-13)
  - `kind` 'group' | 'owners' | 'statement', required: group: a corporate owner or a controlled company exists (has_group true). owners: the company's own persons with significant control and nothing else. statement: no person is on the register, and one `end` node says what the register says instead: the PSC statement filed, the exemption or rule for that type of company, or that nothing is held (added 2026-10-13)
  - `as_at` string, date, nullable: The date the register copy is current to (the last change applied from the Companies House PSC stream)
  - `as_shown` string
  - `source` string
  - `matching` string: The rule a link is drawn by
  - `bands_note` string
  - `completeness_note` string
  - `nodes` object[], required
    - `id` string, required: c:<company number> for a company held; r:<record> for a person or an owner that is not linked; end:, more: and locked: for the notes about where the chart stops
    - `type` 'company' | 'individual' | 'corporate' | 'legal_person' | 'protected' | 'end' | 'more' | 'locked', required: company: a company on the register copy held. individual: an individual PSC. corporate: a corporate PSC that is not linked to a company held (registered outside the UK, a UK number not held, or an entry that cannot be matched), shown as the register describes it. legal_person: a legal person that is not a company. protected: a person whose details Companies House protects. end: why the chain stops above a company. more: owners or companies not included (a limit). locked: levels the plan does not include.
    - `level` integer, required: 0 for the company asked about; -1 for its registered owners, -2 for theirs; 1 for the companies it controls, 2 for theirs
    - `root` boolean: true on the company asked about
    - `name` string, required: As the register shows it. "Name withheld" where the person named has objected
    - `company_number` string, nullable: company: its number. corporate: the UK company number the entry gives, when it gives one that is not held
    - `href` string, nullable: company: the path of its page on companystack.co.uk
    - `status` string, nullable: company: its status on the register
    - `status_label` string, nullable
    - `status_class` 'active' | 'closed' | 'insolvency' | 'other' | 'null', nullable
    - `born` object, nullable: individual: month and year of birth as the register gives them. Never the day
      - `month` integer
      - `year` integer
    - `registered` string, nullable: corporate and legal_person: the place of registration and registration number as filed
    - `detail` string, required: One short line: "01234567 · Active", "Individual · born Mar 1968", "Registered: Jersey, no. 123456"
    - `note` string: end: what the register says where no person is named, short enough for a box: the PSC statement in its own words, or the rule that applies to the company's type
    - `stop` object, nullable: Why the chain stops at this node, in plain words. null for a company whose owners are shown
      - `code` 'individual' | 'protected' | 'overseas' | 'not_held' | 'unlinked' | 'legal_person' | 'name_withheld' | 'no_registrable_person' | 'psc_not_identified' | 'statement_filed' | 'no_psc_recorded' | 'outside_psc_rules' | 'possibly_exempt_plc' | 'level_limit' | 'chart_limit'
      - `label` string
      - `text` string
    - `other_owners` integer: A company below the one asked about: its other current PSCs, which are not included
    - `count` integer: more and locked: how many are not included
  - `edges` object[], required
    - `id` string, required
    - `from` string, required: The node id of the person or company with significant control
    - `to` string, required: The node id of the company controlled
    - `kind` 'control' | 'stop' | 'more', required: control: a PSC entry on the register. stop and more: the link from a note to the company it is about
    - `natures` string[], required: Nature of control codes as filed, e.g. ownership-of-shares-75-to-100-percent
    - `natures_text` string[], required: The same in words, in the register's bands: "75% or more of shares". A band, never an exact holding
    - `label` string[]: Up to three short lines for a chart: the main nature of control and how many more were filed
    - `notified_on` string, date, nullable
    - `in_cycle` boolean: Both ends are in a circle of control (see cycles)
  - `levels` object
    - `above` integer
    - `below` integer
  - `counts` object
    - `companies_above` integer
    - `companies_below` integer
    - `individuals` integer
    - `other_owners` integer
    - `owners` integer: Current persons with significant control of the company asked about, after suppressions
    - `former_owners` integer: Its ceased PSC records, which are not included
  - `stops` object[]: Every place the chain stops above the company, with the reason
    - `node_id` string
    - `name` string
    - `code` string
    - `text` string
  - `cycles` array[]: Circles of control on the register: the company numbers in each. Each company is one node however it is reached
    - string[]
  - `notes` string[]
  - `omitted` object
    - `former_links` integer: Ceased PSC records, which are not included
    - `unconfirmed_links` integer: Entries that carry a company's number as an owner without naming a UK register: not links
  - `truncated` boolean: A limit was reached; `more` nodes say where
  - `limits` object: The limits of one answer
    - `levelsUp` integer
    - `levelsDown` integer
    - `companiesUp` integer
    - `companies` integer
    - `ownersPerCompany` integer
    - `ownersOfSubject` integer: Registered owners of the company asked about that are included
    - `subsidiariesPerCompany` integer
  - `text` object: The same structure in sentences
    - `above` object[]
      - `owner` string
      - `owner_detail` string
      - `company` string
      - `company_detail` string
      - `natures` string[]
      - `since` string, date, nullable
      - `level` integer
      - `sentence` string: "<owner> is on the register as a person with significant control of <company>: <natures as filed>."
    - `below` object[]
      - `owner` string
      - `owner_detail` string
      - `company` string
      - `company_detail` string
      - `natures` string[]
      - `since` string, date, nullable
      - `level` integer
      - `sentence` string: "<owner> is on the register as a person with significant control of <company>: <natures as filed>."
    - `stops` string[]
    - `notes` string[]
  - `layout` object, nullable: Where to draw each box and link, in pixels, computed once so a web page and a PDF draw the same chart. Owners are above the companies they control. A company's many childless subsidiaries (or individual owners) are listed in one box (kind stack); the company asked about has up to four owners side by side, and with more the rest are listed in one box headed "and N more". No text is smaller than 12 pixels. Until 2026-10-13 this was null when has_group was false; it is now given for every company held (null only when has_chart is false)
    - `width` number
    - `height` number
    - `boxes` object[]
      - `id` string
      - `kind` 'node' | 'stack'
      - `type` string
      - `x` number
      - `y` number
      - `w` number
      - `h` number
      - `root` boolean
      - `status_class` string, nullable
      - `href` string, nullable
      - `title` string
      - `lines` object[]
        - `text` string
        - `y` number
        - `size` number
        - `weight` 400 | 600
        - `tone` 'ink' | 'muted'
      - `items` object[]
        - `node_id` string
        - `href` string, nullable
        - `x` number
        - `y` number
        - `w` number
        - `h` number
        - `lines` object[]
          - `text` string
          - `y` number
          - `size` number
          - `weight` 400 | 600
          - `tone` 'ink' | 'muted'
        - `title` string
    - `links` object[]
      - `id` string
      - `from` string
      - `to` string
      - `kind` string
      - `points` array[]
        - number[]
      - `label` object, nullable
        - `x` number
        - `y` number
        - `w` number
        - `h` number
        - `size` number
        - `lines` string[]
      - `in_cycle` boolean
      - `title` string
  - `layout_narrow` object, nullable: The same chart for a narrow screen, in the shape of `layout`: two or more owners of one company (or childless subsidiaries) are listed in one box. null when `layout` already fits a phone (added 2026-10-13)
  - `former_note` string, nullable: The sentence that says the company's ceased PSC records are not in the chart, with the count. null when there are none (added 2026-10-13)
  - `share_register` object: Present only when a reconstructed share register is held for the company (GET /company/{number}/shareholders). Shareholders are a different register from the PSC register and are never mixed into this answer (added 2026-10-13)
    - `held` boolean
    - `as_at` string, date, nullable: The date of the latest list of shareholders it was rebuilt to
    - `label` string
    - `note` string
  - `locked` object: Level first_level, when the full structure has more
    - `levels_above` integer
    - `levels_below` integer
    - `companies` integer
    - `message` string
  - `upgrade` object: Level first_level only
    - `message` string
    - `upgrade_url` string, uri
  - `attribution` object
  - `notice` string

## Other responses

- `400`: Not a valid company number
- `401`: API key missing, malformed or revoked
- `403`: The key's plan does not include this endpoint (`code: plan_upgrade_required`, with `upgrade_url`), the key may not call it, or the key is beyond the plan's key limit after a downgrade (`code: key_limit_exceeded`: the newest keys are paused until keys are deleted or the plan upgraded)
- `404`: The company is not on the register copy held
- `429`: Per-minute or per-day limit reached (`code: rate_limited`, `window: minute|day`). Wait `Retry-After` seconds; when both windows are full the day is reported. A refused request does not count against either limit. A bulk lookup (`POST /company/bulk`) counts each company number in it as one request, so one that is more than you have left is refused whole, and the body adds `requested` and `fits`.

---

[API](https://skmtc.dev/companystack/apis/companystack-api.md) · [All operations](https://skmtc.dev/companystack/apis/companystack-api/llms.txt) · [OpenAPI document](https://skmtc.dev/companystack/apis/companystack-api/revisions/8f3234237d2d?raw)
