# Related companies

A relation links two companies: a parent and a subsidiary. It can carry an ownership percentage, a voting percentage, notes, and a list of company roles and reasons.

These endpoints manage the relations of one company, given by `companyId` in the path:

| Endpoint | What it does |
|---|---|
| `GET /companies/{companyId}/related-companies` | Lists the company's relations, in both directions |
| `POST /companies/{companyId}/related-companies` | Creates relations |
| `PUT /companies/{companyId}/related-companies` | Replaces all of the company's relations, in both directions |
| `GET /companies/{companyId}/related-companies/{relationId}` | Returns one relation |
| `PATCH /companies/{companyId}/related-companies/{relationId}` | Changes some fields of one relation |
| `PUT /companies/{companyId}/related-companies/{relationId}` | Replaces the fields of one relation |
| `DELETE /companies/{companyId}/related-companies/{relationId}` | Deletes one relation |

`companyId` is the company's Avallone ID. `relationId` is the relation's own ID, returned as `id`. It is not the ID of either company.

## Before you start

Reading relations needs the `VIEW_COMPANY` permission. Writing them needs `UPDATE_COMPANY`. See [Permissions](/authentication#permissions).

Company roles are set up in Avallone, under **Settings → Company Roles**. A relation can only use roles that already exist in your tenant.

## The relation object

```json
{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "type": "parent",
  "companyReferenceId": "acme-holding",
  "companyId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "ownership": 51,
  "voting": 51,
  "notes": "Majority shareholder",
  "details": [
    { "role": "General Partner", "reason": "Manages the fund" },
    { "reason": "Common management" }
  ],
  "reasons": ["Common management"],
  "url": "/api/v1/companies/7c9e6679-7425-40de-944b-e07fc1f90ae7/related-companies/3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
```

| Field | Description |
|---|---|
| `id` | The relation's ID |
| `type` | Where the other company sits, seen from `companyId`. `parent` means it is above, as an owner. `subsidiary` means it is below. |
| `companyId` | The other company's Avallone ID |
| `companyReferenceId` | The other company's reference ID, when it has one |
| `ownership` | Ownership percentage, from `0` to `100` |
| `voting` | Voting percentage, from `0` to `100` |
| `notes` | Free text |
| `details` | The relation's company roles and reasons. See [Roles and reasons](#roles-and-reasons). |
| `reasons` | **Deprecated.** The reasons that have no role. `details` lists them too. Removed on 2027-06-30. |
| `url` | The path of this relation |

A field without a value is left out of the response.

## Roles and reasons

`details` is a list. Each entry is a company role, a reason, or a role with a reason:

```json
"details": [
  { "role": "General Partner", "reason": "Manages the fund" },
  { "role": "Guarantor" },
  { "reason": "Common management" }
]
```

These rules apply when you send `details`:

- `role` is the name of a company role in your tenant. Case, surrounding spaces, and the difference between a space and a dash are ignored, so `general-partner` matches `General Partner`.
- A role that doesn't exist returns `404`, and nothing is written.
- A relation can have each role only once. Listing a role twice returns `400`.
- Each entry needs a `role`, a `reason`, or both.

How each method uses `details`:

| Method | With `details` | Without `details` |
|---|---|---|
| `POST` | The relation gets these roles and reasons. | The relation has no roles. |
| `PUT` | Replaces every role and reason on the relation. | The roles are kept. `reasons` replaces the reasons that have no role. |
| `PATCH` | Replaces every role and reason on the relation. Send `null` or `[]` to remove them all. | The roles are kept. `reasons`, when sent, replaces the reasons that have no role. |

### Deprecated `reasons`

`reasons` is a list of strings, and still works. It is deprecated and will be removed on 2027-06-30. Use `details` instead.

When a request sends both, `reasons` must list the same reasons as the entries in `details` that have no role. Otherwise the request returns `400`, and in sync the relation fails. Reasons are compared without regard to case and surrounding spaces. A response lists those reasons in both fields, so you can send a relation back unchanged.

## List relations

```http
GET /api/v1/companies/{companyId}/related-companies
```

Returns the company's relations in both directions: its parents and its subsidiaries. Use `type` to tell them apart.

**Permission:** `VIEW_COMPANY`

**Query parameters:**

| Param | Description |
|---|---|
| `type` | Optional. `parent` or `subsidiary` returns only that direction. Any other value returns `400`. |

**Response:** `200` with a list of [relation objects](#the-relation-object).

## Create relations

```http
POST /api/v1/companies/{companyId}/related-companies
```

Creates a relation for each item in the body. If one item fails, none are created.

**Permission:** `UPDATE_COMPANY`

**Request body:** a list of items with these fields:

| Field | Required | Description |
|---|---|---|
| `companyId` | One of the two IDs | The other company's Avallone ID |
| `companyReferenceId` | One of the two IDs | The other company's reference ID |
| `type` | Yes | `parent` or `subsidiary`: where the other company sits |
| `ownership` | No | Ownership percentage, from `0` to `100` |
| `voting` | No | Voting percentage, from `0` to `100` |
| `notes` | No | Free text |
| `details` | No | Company roles and reasons. See [Roles and reasons](#roles-and-reasons). |
| `reasons` | No | **Deprecated.** Use `details`. |

When you send both `companyId` and `companyReferenceId`, they must name the same company.

```http
POST /api/v1/companies/{companyId}/related-companies
Content-Type: application/json

[
  {
    "companyReferenceId": "acme-holding",
    "type": "parent",
    "ownership": 51,
    "voting": 51,
    "details": [{ "role": "General Partner", "reason": "Manages the fund" }]
  }
]
```

**Response:** `201` with the relations that were created.

A relation that already exists returns `409`. So does one that would make a company own itself, directly or through other companies.

## Replace all relations

```http
PUT /api/v1/companies/{companyId}/related-companies
```

Makes the company's relations match the body, in both directions. The body has the same items as [Create relations](#create-relations).

- An item that matches an existing relation, with the same other company and `type`, updates it. The relation keeps its `id`.
- An item with no match creates a relation.
- An existing relation that isn't in the body is deleted.

Sending `[]` deletes every relation of the company.

**Permission:** `UPDATE_COMPANY`

**Response:** `200` with all of the company's relations after the change.

## Get one relation

```http
GET /api/v1/companies/{companyId}/related-companies/{relationId}
```

**Permission:** `VIEW_COMPANY`

**Response:** `200` with the [relation object](#the-relation-object).

## Change one relation

```http
PATCH /api/v1/companies/{companyId}/related-companies/{relationId}
PUT /api/v1/companies/{companyId}/related-companies/{relationId}
```

`PATCH` changes only the fields you send. `PUT` replaces all of them, and clears any field you leave out. The exception is the roles: a `PUT` without `details` keeps them.

The two companies and the direction can't change. These fields can:

| Field | Description |
|---|---|
| `ownership` | Ownership percentage, from `0` to `100` |
| `voting` | Voting percentage, from `0` to `100` |
| `notes` | Free text |
| `details` | Company roles and reasons. See [Roles and reasons](#roles-and-reasons). |
| `reasons` | **Deprecated.** Use `details`. |

`delete` on a `details` entry returns `400`. To remove a role, send the roles to keep.

**Permission:** `UPDATE_COMPANY`

**Response:** `200` with the [relation object](#the-relation-object).

## Delete one relation

```http
DELETE /api/v1/companies/{companyId}/related-companies/{relationId}
```

**Permission:** `UPDATE_COMPANY`

**Response:** `204` with no body.

## Relations in `PUT /sync`

`PUT /sync` also writes relations, in `relatedCompanies` on each company. It accepts the same `details`, with these differences:

- Without `cleanup`, the entries are added to the roles and reasons the relation already has. An entry with `delete: true` removes one.
- With `cleanup`, `details` replaces every role and reason, as `PUT` does here.
- A role listed twice keeps the last entry, instead of returning `400`.

## Errors

| Status | When |
|---|---|
| `400` | The body or a parameter is invalid. This includes a missing `type`, two IDs that name different companies, `reasons` and `details` that disagree, and a role listed twice. |
| `401` | The token is missing, malformed, expired or revoked. |
| `403` | The token doesn't have the permission. |
| `404` | `companyId` doesn't exist, a company or a role named in the body doesn't exist, or `relationId` isn't a relation of this company. |
| `409` | The relation already exists, or it would make a company own itself. |

These endpoints don't follow every [API design guideline](/api-design-guidelines) yet. For example, a list is returned as a bare array, and errors don't use the new error format.
