> ## Documentation Index
> Fetch the complete documentation index at: https://docs.axiomancer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Beneficial ownership

> Ultimate Beneficial Owner (UBO) graph and sanctions network endpoints for vessel ownership chain analysis — multi-hop ownership traversal with confidence decay.

Trace the ownership chain of any vessel from registered owner through to ultimate beneficial owner. The UBO graph resolves corporate structures and cross-references ownership entities against sanctions lists, showing where a vessel sits in the broader network.

## `GET` `/api/v1/ubo/vessel/{imo}`

<Note>Requires API key.</Note>

Returns the full beneficial ownership graph for a vessel, including the sanctions network — all entities in the ownership chain that have sanctions matches.

**Parameters**

| Name  | Type   | Required | Description                     |
| ----- | ------ | -------- | ------------------------------- |
| `imo` | string | ✓        | 7-digit IMO number (path param) |

**Response**

```json theme={null}
{
  "vessel": {
    "imo_number": "9622629",
    "name": "MV Meridian"
  },
  "graph": {
    "nodes": [
      { "id": "registered_owner:ABC Shipping Ltd", "type": "company", "role": "registered_owner" },
      { "id": "operator:XYZ Management", "type": "company", "role": "operator" },
      { "id": "ubo:John Doe", "type": "individual", "role": "beneficial_owner" }
    ],
    "edges": [
      { "from": "registered_owner:ABC Shipping Ltd", "to": "operator:XYZ Management", "relation": "managed_by" },
      { "from": "operator:XYZ Management", "to": "ubo:John Doe", "relation": "owned_by" }
    ]
  },
  "sanctions_network": {
    "matches": [
      {
        "entity": "XYZ Management",
        "list_source": "OFAC SDN",
        "match_type": "exact",
        "sanctioned_entity": "XYZ Management LLC"
      }
    ],
    "flagged_count": 1
  },
  "tier": "pro"
}
```

**Example**

```bash theme={null}
curl -H "X-API-Key: YOUR_KEY" \
  https://axiomoverwatch.io/api/v1/ubo/vessel/9622629
```

## `GET` `/api/v1/ubo/vessels`

<Note>Requires API key.</Note>

Batch lookup of vessels with sanctions-flagged ownership. Returns a summary for each vessel indicating whether any entity in its ownership chain has a sanctions match.

**Response**

```json theme={null}
{
  "vessels": [
    {
      "imo_number": "9622629",
      "name": "MV Meridian",
      "has_sanctions_match": true,
      "match_count": 1
    }
  ]
}
```

**Example**

```bash theme={null}
curl -H "X-API-Key: YOUR_KEY" \
  https://axiomoverwatch.io/api/v1/ubo/vessels
```

***

## GLEIF ultimate parent chain

Overwatch ingests the [GLEIF Level 2 Relationship Records](https://www.gleif.org/en/lei-data/gleif-golden-copy/download-the-golden-copy) golden copy weekly. Every active **direct** and **ultimate** accounting-consolidation relationship between Legal Entity Identifiers (LEIs) lands in the `gleif_relationships` table, and a Postgres function walks the consolidation graph from a child LEI up to its ultimate parents.

Use this when you have an LEI on an entity in a vessel's ownership chain — registered owner, beneficial owner, or anything in between — and need to resolve the accounting-consolidation parent chain above it. The output is the canonical input to manual UBO investigations and to the [sanctions screening](/overwatch/api/sanctions) pipeline.

**Source**: GLEIF RR-CDF (Relationship Record Common Data Format) JSON golden copy. CC0 Public Domain. Refreshed weekly.

### `get_ultimate_parent_chain(p_lei)`

Recursive Postgres function. Follows `ACTIVE` GLEIF relationships of type `IS_DIRECTLY_CONSOLIDATED_BY` and `IS_ULTIMATELY_CONSOLIDATED_BY` from the input LEI up to each terminal parent. Returns one row per hop.

| Behavior            | Value                                         |
| ------------------- | --------------------------------------------- |
| Direction           | Child → parent (upward)                       |
| Relationship filter | `IS_%CONSOLIDATED_BY`, `status = 'ACTIVE'`    |
| Depth cap           | 10 hops (hard)                                |
| Cycle guard         | Stops re-entering any LEI already on the path |
| Input normalization | `upper(trim(p_lei))`                          |

**Parameters**

| Name    | Type | Required | Description                                                                                          |
| ------- | ---- | -------- | ---------------------------------------------------------------------------------------------------- |
| `p_lei` | text | ✓        | 20-character ISO 17442 LEI of the child entity. Case-insensitive; surrounding whitespace is trimmed. |

**Returns** (table)

| Column              | Type        | Description                                                       |
| ------------------- | ----------- | ----------------------------------------------------------------- |
| `depth`             | integer     | Hop count from the input LEI. `1` = direct parent.                |
| `relationship_id`   | text        | GLEIF relationship record identifier.                             |
| `start_node_lei`    | text        | LEI of the child in this hop.                                     |
| `end_node_lei`      | text        | LEI of the parent in this hop.                                    |
| `relationship_type` | text        | `IS_DIRECTLY_CONSOLIDATED_BY` or `IS_ULTIMATELY_CONSOLIDATED_BY`. |
| `status`            | text        | Always `ACTIVE` (inactive relationships are filtered out).        |
| `path`              | text\[]     | LEIs visited on the way to this row, starting with the input LEI. |
| `occurred_at`       | timestamptz | Publication timestamp of the underlying GLEIF record.             |
| `source_uri`        | text        | URI of the source CDF JSON file.                                  |

**Example (Supabase client)**

```ts theme={null}
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

const { data, error } = await supabase.rpc('get_ultimate_parent_chain', {
  p_lei: '529900T8BM49AURSDO55',
});

// data: [
//   { depth: 1, end_node_lei: '...', relationship_type: 'IS_DIRECTLY_CONSOLIDATED_BY', ... },
//   { depth: 2, end_node_lei: '...', relationship_type: 'IS_ULTIMATELY_CONSOLIDATED_BY', ... },
// ]
```

**Example (Postgres / SQL)**

```sql theme={null}
SELECT depth, end_node_lei, relationship_type, path
FROM public.get_ultimate_parent_chain('529900T8BM49AURSDO55')
ORDER BY depth;
```

<Note>
  The function is exposed to `anon`, `authenticated`, and `service_role`. Call it via the Supabase REST/PostgREST RPC interface, the Supabase JS / Python client, or directly in SQL against a read replica. There is no REST endpoint wrapper yet — the vessel-scoped `/api/v1/ubo/vessel/{imo}` graph above remains the recommended starting point when you are working from an IMO rather than an LEI.
</Note>
