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

# Bulk recommended paths (persons)

> 
Body: `uuids` (person UUIDs, max 100) plus optional `limit` (top contacts
per target, default 25, max 100), `sort_by`, and `order`. `results` holds
one entry per input UUID, in order; each carries that target's `total` and
its top contacts. Targets that are not found come back with empty `items`
rather than failing the whole batch. (`include_items: false` is accepted
but only useful on the company batch, where `top_external_contacts`
remains populated.)



## OpenAPI

````yaml POST /v1/persons/recommended-paths/batch
openapi: 3.1.0
info:
  description: >

    ## Rings MCP server


    The Rings MCP server exposes tenant data to AI tools that support the

    [Model Context Protocol](https://modelcontextprotocol.io) (e.g. Claude,

    ChatGPT, Cursor). AI tools authenticate with your Rings account via OAuth
    2.0

    and can read and update data within what the authenticated user can already
    see

    and do in Rings. For server-side integrations, an API key may be used on MCP

    requests with the same tenant and rules as the REST API (see
    **Authentication**

    below).


    * **Server URL:** `https://production-api.joinrings.com/mcp`

    * **Auth:** OAuth 2.0. Actions are performed on behalf of the authenticated
    user;
      access is scoped to the same data and permissions as in the Rings app.
    * **Transport:** Streamable HTTP, **stateless** (no server-side MCP
    sessions).

    * **ChatGPT app:** [Rings
    AI](https://chatgpt.com/apps/rings-ai/asdk_app_6a32575772f481918f88503de126c411)


    ### Tools and this document


    MCP **tools** are generated from the **`/v1` operations** defined in this
    OpenAPI

    document. Each tool maps to an HTTP method and path; parameters and bodies
    match

    the schemas in the tagged sections below. For exact tool names and JSON
    schemas

    at runtime, use the MCP **`list_tools`** request in your client.


    ### What you can do with MCP today


    | **Area** | **Description** |

    | -------- | ---------------- |

    | **Person** | People, PathPower, incremental lists, recommended paths |

    | **Company** | Orgs, affiliations, PathPower, incremental sync, recommended
    paths |

    | **Opportunity** | Deals, custom field mapping and values |

    | **Activity** | Email and meeting activity for persons and companies |

    | **Recommended paths** | Ranked contacts and multi-hop path context to a
    target |

    | **PathPower** | Relationship strength scores for persons and companies |

    | **Note** | Notes, rich content, entity associations |

    | **Task** | Tasks, assignees, due dates, reminders, associations |

    | **Users** | Tenant users (e.g. to set per-user context) |

    | **Meetings** | Calendar meetings, date and participant filters |

    | **AI summary** | Cached AI summaries for persons and companies |

    | **Entity List** | Named collections of companies or persons and their
    membership |

    | **Relationships** | Ring-wide and per-user relationships ranked by
    RQ/PathPower |


    ### Rate limiting


    MCP calls the same REST API underneath. **Per-second and monthly** limits
    apply

    (see **Rate Limiting** below). Responses may include **`429 Too Many
    Requests`**

    and the usual rate-limit headers.


    ### Use-case examples


    * **Meeting prep:** From chat, load a person, recent activity, and upcoming
    meetings.

    * **Capture follow-ups:** Tasks and notes via natural language (tenant
    rules).

    * **Relationship mapping:** Ask for PathPower or recommended paths before an
    intro.

    * **Create an opportunity:** List opportunity types, pick a type and one of
    its
      stages, then create the deal linked to a company or person.

    For a fuller walkthrough — meeting prep briefings, network navigation and

    analysis, pipeline management, workflow automation (inbound email → deal,

    call debriefs, handoffs), and searching firm notes/account history — see

    [Examples of Rings workflows users can do in Claude and
    ChatGPT](https://rings-ai-knowledge-base.help.usepylon.com/articles/2808798956-examples-of-rings-workflows-users-can-do-in-claude-and-chatgpt)

    in the knowledge base.


    ---


    ## Authentication


    All API requests require authentication using an API key provided in the

    `x-api-key` header.


    ### Getting an API Key


    API keys can be created through your account settings. Each API key is

    associated with a specific tenant and provides access to that tenant's data.


    ### Using Your API Key


    Include your API key in the `x-api-key` header for all requests:


    ```

    x-api-key: pk_your_api_key_here

    ```


    ### API Key Format


    API keys must start with the prefix `pk_` followed by a secure random
    string.

    Invalid or inactive API keys will return a `401 Unauthorized` response.


    ### Security


    - Keep your API keys secure and never commit them to version control

    - Rotate your API keys regularly

    - Revoke unused or compromised keys immediately

    - Each API key is scoped to a specific tenant and can only access that
      tenant's data

    ## Per-User Context


    API keys support two scopes that control how user context is resolved:


    ### Tenant-scoped keys (default)


    Include an optional `x-rings-user-id` header with the UUID of a user
    belonging

    to the API key's tenant:


    ```

    x-rings-user-id: <user-uuid>

    ```


    Use `GET /v1/users` to discover available user UUIDs. If the header is
    omitted,

    endpoints return tenant-wide data. If the user UUID is invalid or belongs to
    a

    different tenant, the API returns `403 Forbidden`.


    ### User-scoped keys


    User-scoped keys are permanently bound to a single user. The user context is

    automatically populated from the bound user on every request — no

    `x-rings-user-id` header is required.


    If `x-rings-user-id` is provided, it **must** match the bound user UUID;

    otherwise the API returns `403 Forbidden`. The API also returns `403
    Forbidden`

    if the bound user is missing or no longer active in the tenant.


    ## Rate Limiting


    The API enforces tiered rate limits to ensure fair usage and system
    stability.

    Limits vary by plan:


    | Tier | Per-Second | Monthly |

    |------|-----------|---------|

    | Standard | 5 req/s | 10,000 req/month |

    | Professional | 20 req/s | 100,000 req/month |

    | Enterprise | 50 req/s | 500,000 req/month |

    | Custom | Configured per tenant | Configured per tenant |


    Both per-second and monthly limits are enforced simultaneously. Limits are

    configured at the tenant level. Contact support to request a custom tier
    with

    specific per-second and monthly limits.


    ### Per-Second Limit


    Enforced per API key on a per-second basis to prevent burst traffic.


    ### Monthly Limit


    Tracked per tenant on a discrete calendar month basis (e.g., January 1-31,

    February 1-28/29), not a rolling 30-day window.


    ### Rate Limit Headers


    All API responses include rate limit information in the following headers:


    **Per-Second Headers:**

    - `X-RateLimit-Limit`: Maximum requests allowed per second for your tier

    - `X-RateLimit-Remaining`: Requests remaining in the current second

    - `X-RateLimit-Reset`: Unix timestamp (seconds) when the per-second limit
      resets (start of the next second)

    **Monthly Headers:**

    - `X-RateLimit-Limit-Monthly`: Maximum requests allowed per month for your
    tier

    - `X-RateLimit-Remaining-Monthly`: Requests remaining in the current month

    - `X-RateLimit-Reset-Monthly`: Unix timestamp (seconds) when the monthly
    limit
      resets (start of the next calendar month)

    ### Rate Limit Exceeded


    When you exceed either limit, the API returns a `429 Too Many Requests`

    response with the following information:


    - **Status Code**: `429`

    - **Response Body**: Error message indicating which limit has been exceeded

    - **Headers**: Rate limit headers with the exceeded limit's remaining count
      set to `0`
    - **Retry-After**: Header indicating seconds to wait before retrying (for
      per-second limits)

    ### Rate Limit Reset


    - **Per-Second Limit**: Resets at the start of each second

    - **Monthly Limit**: Resets automatically at the start of each calendar
    month
      (00:00:00 UTC on the 1st of each month)

    ### Best Practices


    - Monitor both `X-RateLimit-Remaining` headers to track your usage

    - Implement exponential backoff if you receive a 429 response

    - Respect the `Retry-After` header when provided

    - Plan your API usage to stay within both limits

    - Consider caching responses when appropriate to reduce API calls

    - Use request batching when possible to minimize per-second usage


    ## Request Correlation


    Every API response includes an `X-Request-Id` header containing a UUID that

    uniquely identifies the request. This header is useful for correlating
    client

    requests with server-side logs when troubleshooting failures.


    If your client or gateway already generates a request ID, you may forward it
    by

    including an `X-Request-Id` header on the incoming request — the same value

    will be echoed back in the response. If no header is provided, the API
    generates

    a fresh UUID automatically.
  title: Rings AI API
  version: v1.2.2
servers:
  - description: API server
    url: https://production-api.joinrings.com
security:
  - ApiKeyAuth: []
tags:
  - description: >-
      Operations related to persons.


      ## Strongest relationships


      Person detail and list responses include a `strongest_relationships` array
      — up to two tenant internal users with the strongest relationship to the
      subject, ranked by `priority_management.rq_pretty` (desc). Values are
      precomputed every ~3 hours by a background job so the field is cheap to
      surface on list endpoints. Tenants with no `Signal` rows are skipped
      during precomputation and will see `strongest_relationships = null`.


      `last_engaged_at` on each row is sourced from the tenant-wide activity
      stats table and may lag while the activity-stats pipeline is in
      remediation.


      ## Filtering by modification time


      List endpoints accept `modified_since` (ISO 8601); it filters on
      `rings_updated_at`, the system-wide updated timestamp on each record. Use
      this for incremental sync workflows rather than polling the full page.


      ## Last contact timestamp


      `last_contact_at` is tenant-wide and aliases `last_activity_date`; both
      fields are populated from the same `ActivityDateByEntity` source.


      ## Next meeting


      `next_meeting_at`, `next_meeting_title`, and `next_meeting_with_user_uuid`
      reflect the earliest future calendar event (Activity.kind=EVENT,
      non-cancelled, start_date >= now) involving this person, tenant-wide.
      `my_next_meeting_at` and `my_next_meeting_title` narrow to events where
      the calling user (identified via `x-rings-user-id`) is a participant; they
      are `null` when no user context is provided. Backed by materialized tables
      refreshed by event listeners plus an hourly Celery sweep.


      ## Bulk import


      For importing many people at once:


      - `POST /v1/persons/lookup/batch` resolves up to 100 match criteria
      (email/linkedin_url/domain) in one call — use this first to find existing
      records.

      - `POST /v1/persons/batch` match-or-creates up to 50 persons in one call:
      each entry that matches an existing tenant-visible person is returned
      as-is (status `MATCHED`); unmatched entries are created (status
      `CREATED`). Items are processed independently, so one failing row (status
      `ERROR`) doesn't sink the batch. Results are returned in input order.

      - Typical import flow: `POST /v1/persons/lookup/batch` → `POST
      /v1/persons/batch` for the unmatched remainder → `POST /v1/notes/batch` to
      attach notes to the results (see the **Note** tag).


      ## Merging duplicate persons


      `POST /v1/persons/{uuid}/merge` merges a superseded person into the
      canonical person named in the path, reassigning notes, associations,
      activity, and other references before removing the superseded record.
      Merging is always scoped to the caller's own tenant — there is no option
      for a GLOBAL-scope or cross-tenant merge, and no way to force
      global-record promotion or other admin-only controls. Set `preview: true`
      for a read-only dry run: nothing persists, and the response's `preview`
      field describes the canonical/superseded/merged entities as they would
      look post-merge. Returns `404` if either UUID doesn't exist, `409` if the
      two UUIDs are already merged (or identical), and `400` for other merge
      validation errors (e.g. using a shared global record as the canonical
      side).
    name: Person
  - description: >-
      Operations related to companies.


      ## Strongest relationships


      Company detail and list responses include a `strongest_relationships`
      array — up to two tenant internal users with the strongest relationship
      into the company, computed by taking the best
      `priority_management.rq_pretty` an internal user has against any person
      currently affiliated with the company. Values are precomputed every ~3
      hours on the same cadence as the person endpoints, so list pages issue a
      single batched lookup rather than per-item work.


      `last_engaged_at` on each row is sourced from the tenant-wide activity
      stats table (keyed on the company entity) and may lag while the
      activity-stats pipeline is in remediation.


      ## Filtering by modification time


      List endpoints accept `modified_since` (ISO 8601); it filters on
      `rings_updated_at`, the system-wide updated timestamp on each record.


      ## Filtering the company list


      `GET /companies` accepts the same filter set as the web app's company
      explorer (see each query parameter for exact semantics). Filters compose
      with AND:


      - **Identity / text** — `name`, `domain`, `linkedin_url`, `facebook_url`,
      `twitter_url`, `description` (case-insensitive substring).

      - **Location** — `city` (substring); `country` and `state` (resolved via
      pycountry, matched on the canonical name).

      - **Status (comma-separated, OR within a field)** — `operating_status`,
      `acquisition_status`, `ipo_status`.

      - **Tags / investors** — `rings_industry_tags` with
      `rings_industry_tags_operator` (`AND`/`OR`); `investor_types`,
      `last_investment_types`, `npi_types`.

      - **Numeric ranges (inclusive)** — `founded_on_from/to`, `score_from/to`,
      `employee_count_from/to`, `overall_operating_score_from/to`,
      `overall_investor_score_from/to`.

      - **Date ranges (ISO 8601, both bounds required)** —
      `last_investment_date_from/to`, `npi_date_from/to`.

      - **By identity** — `uuids` (comma-separated).


      ## Last contact timestamp


      `last_contact_at` is tenant-wide and aliases `last_activity_date`; both
      fields read from the same `ActivityDateByEntity` source used by the
      list/detail endpoints.


      ## Next meeting


      `next_meeting_at`, `next_meeting_title`, and `next_meeting_with_user_uuid`
      reflect the earliest future calendar event involving this company,
      tenant-wide. `my_next_meeting_at` and `my_next_meeting_title` narrow to
      events where the calling user (identified via `x-rings-user-id`) is a
      participant; they are `null` when no user context is provided. Same
      materialized source as the person endpoints.
    name: Company
  - description: >-
      Operations related to sales opportunities.


      ## Filtering the opportunity list


      `GET /v1/opportunities` filters compose with AND (unless noted). See each
      query parameter for exact semantics.


      - **Identity / text** — `name` (substring match on the opportunity's own
      name); `search` (fuzzy match across name and associated person/company
      names — broader than `name`).

      - **Type / stage** — `opportunity_type_uuid`; `opportunity_stage_uuid`
      (single) or `opportunity_stage_uuids` (comma-separated IN filter).

      - **Ownership** — `owner_user_uuid` (comma-separated user UUIDs, OR'd
      together) and `include_no_owner` (OR'd with `owner_user_uuid` when both
      are set); `intro_person_uuid`.

      - **Hierarchy** — `parent_uuid` (sub-opportunities of this parent) or
      `hierarchy_type` (`PARENT`/`CHILD`, ignored when `parent_uuid` is set —
      with neither set, both parents and children are returned);
      `related_company_uuid` / `related_person_uuid` (parent opportunities with
      a *sub*-opportunity associated with this entity — distinct from
      `company_uuid`/`person_uuid` below).

      - **Associated entities** — `company_uuid` / `person_uuid`
      (comma-separated; union of opportunities directly associated with any of
      them).

      - **Numeric ranges (inclusive)** — `value_from`/`value_to`,
      `probability_from`/`probability_to`.

      - **Date ranges** — `updated_after`/`updated_before` (ISO 8601
      date/datetime, independent bounds); `source_date_from`/`source_date_to`
      and `last_activity_date_from`/`last_activity_date_to` (ISO 8601, **both
      bounds required together** — a single-sided value is a no-op).


      ### Sorting


      `sort_by` accepts `name`, `created_at`, `updated_at`, `value`,
      `weighted_value`, `probability`, `source_date`, `intro_date`,
      `last_activity_date`, `details`, `next_step`, `stage` (pipeline stage
      order), `owner` (by name), `intro_person` (by name), `days_in_stage`, and
      `notes_and_files_count`, plus `order` (`asc`/`desc`). Sorting by `owner`,
      `intro_person`, `days_in_stage`, or `notes_and_files_count` is more
      expensive than the other keys since those columns only exist after
      enriching the full filtered set (not just the returned page).


      ### Totals


      The list response includes `total_value` and `total_weighted_value` — sums
      of `value`/`weighted_value` across the *entire filtered set*, not just the
      current page.


      ## Custom field mapping


      `GET /v1/opportunities/{uuid}` and each list response include a
      `custom_field_mapping` object containing:


      - `fields[]` — all tenant custom field definitions, each with `uuid`,
      `key`, `name`, `type`, and optional `choices`

      - `standard_key_to_uuid` — explicit `{key: uuid}` map for the 16 standard
      column keys that are resolved for this tenant (null when none are
      resolved)

      - `unresolved_keys` — standard column keys that have no matching custom
      field for this tenant (null when all are present)


      On `GET /v1/opportunities` (list), `custom_field_mapping` is carried once
      at the top level of the response rather than repeated on every item —
      items themselves do not carry the field.


      ### Custom field lookup


      Use `standard_key_to_uuid` to translate a web-app column name (e.g.
      `targetSize`) to the `uuid` used as the key in the opportunity's
      `custom_fields` JSONB. The full `fields[]` list provides additional
      metadata (display name, type, choices) for every custom field.


      ### Standard column keys


      The following 16 keys are standard columns in the Rings web app. Resolved
      keys appear in `standard_key_to_uuid`; any that are not configured appear
      in `unresolved_keys`:


      `targetSize`, `segment`, `priority`, `industry`, `postMoneyValuation`,
      `dealSize`, `fundCost`, `noteDiscount`, `fundFmvAsOf`,
      `preMoneyValuation`, `indicativePrice`, `source`, `dataRoomLink`,
      `result`, `resultDetails`, `nextStepDate`
    name: Opportunity
  - description: >
      Activity history — emails, meetings, and other interactions associated
      with a person or company.


      ## Response field names


      | Field | Description |

      |---|---|

      | `activity_id` | Internal activity identifier |

      | `object_id` | External source identifier |

      | `activity_type` | `"email"` or `"meeting"` |

      | `activity_timestamp` | ISO-8601 date/time of the activity |

      | `start_time` | Meeting start (ISO-8601, meetings only) |

      | `end_time` | Meeting end (ISO-8601, meetings only) |

      | `subject` | Email subject line (emails only) |

      | `from_email` | Sender email address (emails only) |

      | `body` | Email body text or meeting notes |

      | `participants` | All participants |

      | `cc_participants` | CC recipients (emails only) |
    name: Activity
  - description: >
      Recommended relationship paths — ranked contacts that connect you to a
      target person or company via multi-hop relationships.


      Results are ranked by PathPower score (highest first by default). Each
      contact includes a `strength` label and `connection_score` derived from
      PathPower. See the **PathPower** tag for score interpretation details.


      ## How paths are derived


      ```
        ┌─────────────────────────────────────────────────┐
        │           Recommended Paths Pipeline            │
        └─────────────────────────────────────────────────┘

        1. Query affiliations for the target entity
        2. Score each affiliated person with PathPower
        3. Enrich with priority_management signals
        4. Build paths from signals with rq_pretty > 0

        ┌──────────┐    signal (RQ>0)    ┌──────────────┐
        │ Colleague├───────────────────►  │ Target Person│
        │ (hop 0)  │    rq_pretty=2       │   (hop 1)    │
        └──────────┘                      └──────────────┘

        Each signal with rq_pretty > 0 produces one 2-hop path:
          paths[i] = [ colleague_hop, target_hop ]

        Example with 2 qualifying signals:

        ┌─────────┐
        │  Alice   │──► ┐
        │  RQ = 3  │    │    ┌──────────────┐
        └─────────┘    ├──► │ Target Person │
        ┌─────────┐    │    │  pp_score=0.7 │
        │   Bob    │──► ┘    └──────────────┘
        │  RQ = 2  │
        └─────────┘

        paths = [
          [ {Alice, score=3}, {Target, score=3} ],
          [ {Bob,   score=2}, {Target, score=3} ]
        ]
        path_count = 2
      ```


      ## Response structure


      Each `RecommendedContact` includes:


      - **Identity**: `person_uuid`, `name`, `job_title`, `email`, `logo_url`,
      `linkedin_url`, `facebook_url`, `twitter_url`

      - **PathPower**: `connection_score` (RQ-style integer 1-3), `strength`
      label, `path_count`

      - **Paths**: `paths[]` — array of hop arrays representing relationship
      paths to the target

      - **Company**: `company_name`, `primary_company` object with `uuid`,
      `name`, `logo_url`, and `is_primary_company_recommended`

      - **Role tags**: `tags[]` — deduplicated role labels (e.g. "founder",
      "ceo")

      - **Emails**: `emails[]` — all known email addresses with `address`,
      `is_primary`, `last_seen_at`

      - **Signal metadata**: `signal_metadata` — counts of `lists` and
      `associations` linked to this contact

      - **Priority management**: `priority_management[]` — per-user relationship
      quality signals


      ## paths[] and path_count


      `paths` is an array of arrays. Each inner array is a sequence of `PathHop`
      objects representing one relationship path from an organisation colleague
      to the target contact.


      Paths are derived from `priority_management` signals: each signal with
      `rq_pretty > 0` produces a 2-hop path `[colleague → target]`. `path_count
      = len(paths)`.


      ### No qualifying signals


      When no signals with `rq_pretty > 0` exist for a contact, the contact is
      still returned but with `paths=[]` and `path_count=0`. The contact card is
      still shown, just without an expandable paths section.


      ### PathHop fields


      | Field | Type | Description |

      |---|---|---|

      | `person_uuid` | string | Person UUID for this hop |

      | `name` | string | Full name |

      | `job_title` | string | Current title (target hop only) |

      | `company_name` | string | Current company (target hop only) |

      | `logo_url` | string | Avatar/photo URL |

      | `connection_score` | int | RQ score (1-3) — from PathPower on target
      hop, from signal `rq_pretty` on colleague hop |

      | `associations` | string[] | Association count labels |

      | `ring_name` | string | Organisation display name (colleague hop only) |

      | `ring_key` | string | Stable tenant slug (colleague hop only) |

      | `linkedin_url` | string | LinkedIn profile URL |

      | `last_interaction_at` | string | ISO timestamp of the most recent
      activity between this hop and the next one in the path (colleague/"you"
      hop only — `null` on the target hop, which has no single-pair signal) |

      | `interaction_count` | int | Count of activities backing that same pair
      (colleague/"you" hop only). Use with `last_interaction_at` to tell a
      thin-but-live RQ 1 connection (recent date, count > 1) from a stale one
      (old date, count = 1) — `connection_score` alone can't make that
      distinction |


      ## connection_score semantics


      `connection_score` on the top-level `RecommendedContact` is an RQ-style
      integer (1-3) derived from the PathPower score:


      | connection_score | PathPower range | strength label |

      |---|---|---|

      | 1 | 0.00 - 0.20 | weak |

      | 2 | 0.21 - 0.60 | moderate / strong |

      | 3 | 0.61 - 1.00 | very_strong |


      ## Priority management signals


      Each entry in `priority_management` represents one user's assessment of
      the relationship with the contact:


      | Field | Description |

      |---|---|

      | `user_name` | Name of the user who set the signal |

      | `user_uuid` | UUID of that user |

      | `rq_pretty` | Relationship Quality score (1-3) |

      | `priority_score` | Priority Score set by the user |

      | `rq_reason` | Reason for the RQ rating |

      | `priority_context` | Priority context tags |

      | `last_activity_date` | ISO timestamp of the most recent activity between
      this user and the contact |

      | `interaction_count` | Count of activities backing this signal. Both
      fields are scoped to this specific (user, contact) pair, not tenant-wide —
      use them to distinguish a thin-but-live RQ 1 from a single stale email,
      since `rq_pretty` alone can't |


      ## top_external_contacts (company endpoint only)


      `GET /v1/companies/{uuid}/recommended-paths` additionally returns a
      top-level `top_external_contacts` array: the company's top external
      contacts (`is_internal=false`) ranked by PathPower, capped at 5. This
      array is independent of pagination — it always reflects the strongest
      external contacts across the whole result set, not just the current page.
      The person endpoint omits this field (`null`).


      Each entry contains:


      | Field | Description |

      |---|---|

      | `person_uuid` | Person UUID of the external contact |

      | `name` | Contact name |

      | `job_title` | Contact title |

      | `tags` | Deduplicated company roles (e.g. employee, investor, advisor,
      board_member) — same values as `items[].tags` |

      | `strength` | PathPower band label (weak … very_strong) |

      | `connection_score` | RQ-style integer (1-3) |

      | `internal_connectors` | Up to 2 internal users with the strongest
      relationship to this contact — same shape as `strongest_relationships`
      (`name`, `path_power_score`, `last_engaged_at`) |
    name: Recommended Paths
  - description: >-
      PathPower measures the strength of your organisation's relationship to a
      person or company.


      ```
        ┌─────────────────────────────────────────────────┐
        │              PathPower Overview                 │
        └─────────────────────────────────────────────────┘

        Person PathPower:

          User A ──RQ signal──►┐
          User B ──RQ signal──►├──► Person X ──► score 0.0-1.0
          User C ──RQ signal──►┘

          Score derived from Relationship Quality (RQ) signals
          set by users in the organisation.

        Company PathPower:

          Person A (score 0.8) ──►┐
          Person B (score 0.6) ──►│
          Person C (score 0.5) ──►├──► avg(top 5) ──► Company score
          Person D (score 0.4) ──►│
          Person E (score 0.3) ──►┘

          Average of top 5 person scores affiliated
          with the company.

        Score mapping:

          0.0 ──────────── 0.2 ──── 0.4 ──── 0.6 ──────── 1.0
          │     Weak       │ Moderate│ Strong │ Very Strong │
      ```


      ## Score format


      - **Raw score**: `0.0`–`1.0` (stored in `score`)

      - **Display score**: `0`–`100` (stored in `score_display`, equals `score *
      100`)

      - **Strength label**: band key string in `strength_label`: `weak`,
      `moderate`, `strong`, `very_strong` (lowercase, underscores—these are
      stable API values, not title-cased prose).


      ## Strength labels and thresholds


      | strength_label value | Meaning | Score range |

      |---|---|---|

      | weak | Weak | 0.00 - 0.20 |

      | moderate | Moderate | 0.21 - 0.40 |

      | strong | Strong | 0.41 - 0.60 |

      | very_strong | Very strong | 0.61 - 1.00 |


      A `null` score means no PathPower data exists for that entity.


      ## How scores are computed


      **Person scores** are derived from Relationship Quality (RQ) signals — the
      strength ratings that users in your organisation have assigned to their
      relationships with that person.


      **Company scores** are the average of the top 5 person PathPower scores
      among people affiliated with that company.


      ## Refresh cadence


      Scores are recomputed automatically every 3 hours. They are not
      triggerable via the API. If a score appears stale, it will update on the
      next refresh cycle.


      ## Related endpoints


      - PathPower is also returned inline on `GET /v1/persons` and `GET
      /v1/companies` responses (in the `pathpower` field).

      - `GET /v1/*/recommended-paths` returns contacts ranked by PathPower
      score.
    name: PathPower
  - description: >-
      Operations for tenant notes — rich-text notes that can be linked to
      people, companies, opportunities, files, and tasks via associations.


      ## Lifecycle


      - `POST /v1/notes` creates a note. The HTML body is stored in S3 keyed by
      note UUID; a plain-text excerpt is persisted on the row for cheap
      list/search access.

      - `PUT /v1/notes/{uuid}` updates note fields. Only supplied fields are
      written; passing `content` re-uploads the S3 object and refreshes the
      excerpt.

      - `DELETE /v1/notes/{uuid}` soft-deletes a note. Only the owner or a
      tenant **ringmaster** may delete; other users receive `403 Forbidden`.

      - `POST /v1/notes/batch` creates up to 50 notes in one call, each with its
      own inline `associations` and optional `idempotency_key` (re-submitting
      the same key returns the existing note, status `SKIPPED`, instead of
      duplicating it). One failing row (status `ERROR`) doesn't sink the rest of
      the batch — this is the note half of the bulk import flow described under
      the **Person** tag's `POST /v1/persons/batch`.


      ## Associations


      Use the dedicated sub-resource to manage links to other entities after
      creation:


      - `POST /v1/notes/{uuid}/associations` attaches a
      person/company/opportunity/file/task. Idempotent on duplicates — the
      existing row is returned.

      - `DELETE /v1/notes/{uuid}/associations/{association_uuid}` removes the
      link. `204 No Content` on success, `404 Not Found` if the association
      doesn't exist on the note.


      Associations can also be seeded at create-time via the `associations`
      array on `POST /v1/notes`.


      ## Semantic search


      `POST /v1/notes/search` performs hybrid vector + full-text search over
      tenant notes, grouped by source note with matching snippets. Behavior
      depends on `search_mode`:


      - **Direct/precise lookups** (default `recency`, or
      `historical`/`relevance`/`balanced`): fast, sub-second, ranked by raw
      hybrid search score. Use for narrow asks like 'what did I discuss with
      this person.'

      - **`comprehensive`**: for broad, ambiguous asks — e.g. 'give me everyone
      I've researched who fits this profile' — additionally reranks the full
      candidate set with a cross-encoder, scored against each note's associated
      person/company context (job title, company description, mentions) in
      addition to raw note text, so a note whose text never mentions a role or
      company type can still surface via its associated person/company record.
      This takes ~4-5s and has a small per-request cost — reach for it only when
      the query is genuinely broad, not for direct lookups.


      `limit` accepts 1-200 (default 10).
    name: Note
  - description: >-
      Operations for tenant tasks — actionable items with optional assignees,
      due dates, reminders, and entity associations.


      ## Visibility


      Tasks have two visibility modes:


      - `RING` (default) — visible to any active user in the tenant.

      - `PERSONAL` — visible only to the creator. PERSONAL tasks resolve `404
      Not Found` for other tenant users.


      ## Lifecycle


      - `POST /v1/tasks` creates a task. Assignees and associated entities are
      attached transactionally — partial failures are not possible.

      - `PUT /v1/tasks/{id}` updates fields. `due_date` and reminders can be
      cleared via the explicit `clear_due_date` / `clear_reminder` flags.

      - `DELETE /v1/tasks/{id}` soft-deletes a task. Visibility is enforced —
      PERSONAL tasks can only be deleted by their creator.


      ## Associations


      After creation, link tasks to other entities via the associations
      sub-resource:


      - `POST /v1/tasks/{id}/associations` attaches a
      person/company/opportunity/note/file. Visibility is preflighted — PERSONAL
      tasks belonging to other users return `404 Not Found`.

      - `DELETE /v1/tasks/{id}/associations/{association_uuid}` removes the
      link. `204 No Content` on success.
    name: Task
  - description: >-
      Tenant user management.


      Use `GET /v1/users` to list active users belonging to the API key's
      tenant. User UUIDs can then be passed in the `x-rings-user-id` header to
      enable per-user context on supported endpoints (recommended paths,
      activity history).
    name: Users
  - description: >-
      Calendar meetings for the authenticated user.


      `GET /v1/meetings` and `GET /v1/meetings/{activity_id}` return only
      meetings where that user is an internal calendar participant (the same
      scope as in-app meeting tools). User context is required:
      `x-rings-user-id` with a TENANT-scoped key, or a USER-scoped key.


      Meetings are calendar events (Google Calendar, Outlook, etc.) synced via
      connected email/calendar grants. They are stored as activity records with
      type `meeting`.


      ## Filtering


      - **Date range**: use `date_from` / `date_to` (ISO `YYYY-MM-DD`) to scope
      by meeting start time

      - **Participant**: use `person_uuid` or `company_uuid` to further narrow
      to meetings where that entity is also a participant

      - **Search**: use `search` (min 3 characters) to filter by title


      ## Participant fields


      | Field | Description |

      |---|---|

      | `kind` | Participant role: `ORGANIZER` or `PARTICIPANT` |

      | `status` | RSVP status: `accepted`, `declined`, `tentative`,
      `needsAction` |

      | `person_uuid` | Matched person UUID (if resolved) |

      | `company_uuid` | Matched company UUID (if resolved) |

      | `avatar_url` | Profile photo URL (if available) |

      | `is_current_user_participant` | Whether the requesting user is a
      participant (requires user context) |
    name: Meetings
  - description: >-
      AI-generated summaries for persons and companies.


      These endpoints return **cached/prior** summaries only — they do not
      trigger on-demand generation.


      ## Response shape


      | Field | Type | Description |

      |---|---|---|

      | `entity_uuid` | string | UUID of the person or company |

      | `entity_type` | string | `"person"` or `"company"` |

      | `title` | string\|null | Summary title (e.g. "Jane Doe Summary") |

      | `bio` | string\|null | Person biography (person only, always null for
      companies) |

      | `sections[]` | array | Structured summary sections |

      | `sections[].heading` | string | Section heading |

      | `sections[].content` | string | Section content |

      | `generated_at` | string\|null | ISO-8601 timestamp of generation, or
      `null` |


      When no cached summary is available the response is still `200 OK` with
      `title` set to `<Entity Name> Summary`, `bio` null, `sections` empty, and
      `generated_at` null.
    name: AI Summary
  - description: >-
      Named collections of entities (e.g. "LP Feedback for Fund II"), and their
      membership.


      A list is single-kind — `kind` is one of `PERSON`, `COMPANY`, `NOTE`,
      `ACTIVITY`, `FILE`, `TASK`, `OPPORTUNITY`, never mixed. In practice
      `PERSON` and `COMPANY` are the common cases.


      ## Resolving a list


      `GET /v1/lists` searches by `name` (exact) or `search` (case-insensitive
      substring), optionally filtered by `kind`. Each result includes
      `entities_count`.


      ## Fetching membership


      `GET /v1/lists/{list_uuid}/entities` returns the paginated
      `entity_uuid`/`entity_kind` of each member. Returns `404` if the list
      doesn't exist or isn't accessible to the API key's tenant.


      ## Hydrating members


      These endpoints intentionally return UUIDs only. To get full records for a
      brief or export, pass the UUIDs to `GET /v1/companies?uuids=...` or `GET
      /v1/persons?uuids=...` (comma-separated), which already support batched
      lookup by UUID and return the same rich fields as their single-entity
      endpoints.


      ## Lifecycle


      - `POST /v1/lists` creates a list. List names are unique per tenant per
      `kind` — returns `409` if `name` is already in use for that `kind`. Pass
      `entity_uuids` to seed membership at creation time (each becomes a member
      with `entity_kind` = the list's `kind`).

      - `PUT /v1/lists/{list_uuid}` updates `name`, `description`, and/or
      `legacy_explorer_state` — only fields supplied are written. Membership is
      not affected by this endpoint. Only the list's owner or a tenant
      **ringmaster** may update it; other callers receive `404 Not Found` (the
      same response as a nonexistent list, to avoid leaking existence to
      non-owners).
    name: EntityList
  - description: >-
      Ring-wide and per-user relationships, ranked by relationship quality (RQ)
      / PathPower / last activity.


      Both endpoints are thin REST wrappers over precomputed parquet caches
      (MRR: My Ring Relationships) rather than live queries, so they return
      `503` while the precompute hasn't been built yet for the tenant (or user).


      - `GET /v1/relationships` — the tenant's external relationships (internal
      ring members excluded), refreshed every ~2h. Each item is one external
      person with their embedded primary company, RQ, PathPower, priority
      signals, AI scores, entity lists, and emails.

      - `GET /v1/relationships/mine` — the calling user's own relationships,
      sorted by that user's own `relationship_quality`, skipping the team
      aggregation/overlay enrichment the ring-wide endpoint does. Requires a
      user-bound API key (USER-scoped, or a TENANT-scoped key with
      `x-rings-user-id`) — returns `400` otherwise.


      ## Filtering and sorting


      Both accept `sort_by` (any MRR schema field, e.g. `relationship_quality`,
      `priority_score`, `last_activity_date`, `company_score`, `person_name`,
      `company_name`), `order`, and pagination. `relationship_quality` and
      `priority_score` also act as filters when passed as comma-separated
      integers (1-3); `company_score_from`/`company_score_to` bound by
      `company_score` with **exclusive** bounds (strictly greater-than /
      less-than, not >=/<=).
    name: Relationships
paths:
  /v1/persons/recommended-paths/batch:
    post:
      tags:
        - Recommended Paths
      summary: Bulk recommended relationship paths for up to 100 target persons.
      description: >-

        Body: `uuids` (person UUIDs, max 100) plus optional `limit` (top
        contacts

        per target, default 25, max 100), `sort_by`, and `order`. `results`
        holds

        one entry per input UUID, in order; each carries that target's `total`
        and

        its top contacts. Targets that are not found come back with empty
        `items`

        rather than failing the whole batch. (`include_items: false` is accepted

        but only useful on the company batch, where `top_external_contacts`

        remains populated.)
      operationId: post_bulk_person_recommended_paths
      parameters: []
      requestBody:
        content:
          application/json:
            schema:
              properties:
                include_items:
                  default: true
                  description: >-
                    When false (companies only), skip the expensive `items[]`
                    recommended-paths list and return just
                    `top_external_contacts` per company. Resolved with batched
                    queries, so large batches stay fast. Use this if you only
                    consume `top_external_contacts`.
                  title: Include Items
                  type: boolean
                limit:
                  default: 25
                  description: Max contacts returned per target (1-100).
                  title: Limit
                  type: integer
                order:
                  default: desc
                  description: 'Sort order. Default: ''desc'' (highest score first)'
                  enum:
                    - asc
                    - desc
                  title: Order
                  type: string
                sort_by:
                  anyOf:
                    - enum:
                        - score
                        - name
                      type: string
                    - type: 'null'
                  default: null
                  description: 'Field to sort by. Valid values: ''score'', ''name'''
                  title: Sort By
                uuids:
                  items:
                    type: string
                  title: Uuids
                  type: array
              required:
                - uuids
              title: RecommendedPathsBatchRequest
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  results:
                    items:
                      $ref: '#/components/schemas/RecommendedPathsResponse'
                    title: Results
                    type: array
                required:
                  - results
                title: RecommendedPathsBatchResponse
                type: object
          description: >-
            Bulk recommended-paths response.


            `results` holds one `RecommendedPathsResponse` per input UUID, in
            the same

            order as the request. Targets that are not found come back with
            empty

            `items` rather than failing the whole batch.
components:
  schemas:
    RecommendedPathsResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/RecommendedContact'
          title: Items
          type: array
        page:
          title: Page
          type: integer
        per_page:
          title: Per Page
          type: integer
        target_logo_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Target Logo Url
        target_name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Target Name
        target_uuid:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Target Uuid
        top_external_contacts:
          anyOf:
            - items:
                $ref: '#/components/schemas/TopExternalContact'
              type: array
            - type: 'null'
          default: null
          title: Top External Contacts
        total:
          title: Total
          type: integer
      required:
        - items
        - page
        - per_page
        - total
      title: RecommendedPathsResponse
      type: object
    RecommendedContact:
      properties:
        company_name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Company Name
        connection_score:
          anyOf:
            - type: integer
            - type: 'null'
          default: null
          title: Connection Score
        email:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Email
        emails:
          anyOf:
            - items:
                $ref: '#/components/schemas/ContactEmail'
              type: array
            - type: 'null'
          default: null
          title: Emails
        facebook_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Facebook Url
        is_internal:
          anyOf:
            - type: boolean
            - type: 'null'
          default: null
          title: Is Internal
        is_primary_company_recommended:
          anyOf:
            - type: boolean
            - type: 'null'
          default: null
          title: Is Primary Company Recommended
        job_title:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Job Title
        linkedin_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Linkedin Url
        logo_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Logo Url
        name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Name
        path_count:
          default: 0
          title: Path Count
          type: integer
        paths:
          anyOf:
            - items:
                items:
                  $ref: '#/components/schemas/PathHop'
                type: array
              type: array
            - type: 'null'
          default: null
          title: Paths
        person_uuid:
          title: Person Uuid
          type: string
        primary_company:
          anyOf:
            - $ref: '#/components/schemas/PrimaryCompany'
            - type: 'null'
          default: null
        priority_management:
          anyOf:
            - items:
                $ref: '#/components/schemas/PrioritySignal'
              type: array
            - type: 'null'
          default: null
          title: Priority Management
        signal_metadata:
          anyOf:
            - $ref: '#/components/schemas/SignalMetadata'
            - type: 'null'
          default: null
        strength:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Strength
        tags:
          anyOf:
            - items:
                type: string
              type: array
            - type: 'null'
          default: null
          title: Tags
        twitter_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Twitter Url
      required:
        - person_uuid
      title: RecommendedContact
      type: object
    TopExternalContact:
      properties:
        connection_score:
          anyOf:
            - type: integer
            - type: 'null'
          default: null
          title: Connection Score
        email:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Email
        internal_connectors:
          anyOf:
            - items:
                $ref: '#/components/schemas/StrongestRelationship'
              type: array
            - type: 'null'
          default: null
          title: Internal Connectors
        job_title:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Job Title
        name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Name
        person_uuid:
          title: Person Uuid
          type: string
        strength:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Strength
        tags:
          anyOf:
            - items:
                type: string
              type: array
            - type: 'null'
          default: null
          title: Tags
      required:
        - person_uuid
      title: TopExternalContact
      type: object
    ContactEmail:
      properties:
        address:
          title: Address
          type: string
        is_primary:
          default: false
          title: Is Primary
          type: boolean
        last_seen_at:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Last Seen At
      required:
        - address
      title: ContactEmail
      type: object
    PathHop:
      properties:
        associations:
          anyOf:
            - items:
                type: string
              type: array
            - type: 'null'
          default: null
          title: Associations
        company_name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Company Name
        connection_score:
          anyOf:
            - type: integer
            - type: 'null'
          default: null
          title: Connection Score
        interaction_count:
          anyOf:
            - type: integer
            - type: 'null'
          default: null
          title: Interaction Count
        job_title:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Job Title
        last_interaction_at:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Last Interaction At
        linkedin_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Linkedin Url
        logo_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Logo Url
        name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Name
        person_uuid:
          title: Person Uuid
          type: string
        ring_key:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Ring Key
        ring_name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Ring Name
      required:
        - person_uuid
      title: PathHop
      type: object
    PrimaryCompany:
      properties:
        logo_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Logo Url
        name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Name
        uuid:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Uuid
      title: PrimaryCompany
      type: object
    PrioritySignal:
      properties:
        interaction_count:
          anyOf:
            - type: integer
            - type: 'null'
          default: null
          title: Interaction Count
        last_activity_date:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Last Activity Date
        person_uuid:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Person Uuid
        priority_context:
          anyOf:
            - items:
                additionalProperties: true
                type: object
              type: array
            - type: 'null'
          default: null
          title: Priority Context
        priority_score:
          anyOf:
            - type: number
            - type: 'null'
          default: null
          title: Priority Score
        rq:
          anyOf:
            - type: number
            - type: 'null'
          default: null
          title: Rq
        rq_manual:
          anyOf:
            - type: number
            - type: 'null'
          default: null
          title: Rq Manual
        rq_pretty:
          anyOf:
            - type: number
            - type: 'null'
          default: null
          title: Rq Pretty
        rq_reason:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Rq Reason
        signal_metadata:
          anyOf:
            - $ref: '#/components/schemas/SignalMetadata'
            - type: 'null'
          default: null
        user_logo_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: User Logo Url
        user_name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: User Name
        user_uuid:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: User Uuid
      title: PrioritySignal
      type: object
    SignalMetadata:
      properties:
        associations:
          default: 0
          title: Associations
          type: integer
        lists:
          default: 0
          title: Lists
          type: integer
      title: SignalMetadata
      type: object
    StrongestRelationship:
      properties:
        internal_user_person_uuid:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          description: >-
            Person UUID for the internal user, usable as the `{uuid}` path
            parameter on other person endpoints.
          title: Internal User Person Uuid
        internal_user_uuid:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          description: UUID of the internal tenant user. Matches `UsersPrivate.uuid`.
          title: Internal User Uuid
        last_engaged_at:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          description: >-
            ISO 8601 timestamp of the most recent all-time activity between this
            internal user and the subject entity, sourced from
            `ActivityStatsByEntity`. May lag behind true activity while the
            activity-stats pipeline is in remediation.
          title: Last Engaged At
        name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          description: Display name of the internal user.
          title: Name
        path_power_score:
          anyOf:
            - type: number
            - type: 'null'
          default: null
          description: >-
            Strength of the relationship, sourced from
            `priority_management.rq_pretty`. Higher is stronger. The existing
            `pathpower.score` field remains the preferred numeric score for the
            person/company itself; this field reflects the specific internal
            user's relationship.
          title: Path Power Score
      title: StrongestRelationship
      type: object
  securitySchemes:
    ApiKeyAuth:
      in: header
      name: x-api-key
      type: apiKey

````