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

# Manage Relationships

> Connect records with typed and generic relationship endpoints.

<Warning>
  **Restricted access.** Public API v2 is currently available only to allowlisted
  organizations. Requests from workspaces that have not been enabled are rejected.
  Contact your Reevo representative to have your organization added to the allowlist.
</Warning>

# Manage Relationships

Use relationship endpoints when you need to list, add, update, or remove links between records.

```text theme={null}
/api/v2/public/{record_type}/{record_id}/relationship/{api_name}
```

## Direct vs junction relationships

Every relationship is one of two kinds, reported by the `type` field on each entry in a record type's [`_schema`](/Public-API-v2/Schema-and-Resources) response. The kind decides which operations apply to its edges:

* **Direct** — a single outbound reference, such as a foreign key or an owned association. There is no edge record between the two sides, so a direct relationship has **no per-edge attributes**: you add or remove the reference, but there is nothing to `PATCH`.
* **Junction** — a many-to-many link mediated by a junction object that carries **per-edge attributes** of its own, such as `role` or `is_primary`. Junctions are the only relationships with an editable edge.

| Operation             | Direct | Junction |
| --------------------- | ------ | -------- |
| List (`GET`)          | Yes    | Yes      |
| Add (`POST`)          | Yes    | Yes      |
| Remove (`DELETE`)     | Yes    | Yes      |
| Update edge (`PATCH`) | No     | Yes      |

Read a relationship's `type` from [`GET /_schema/{record_type}`](/Public-API-v2/Schema-and-Resources) to tell the two apart. The typed junction relationships on this page — a contact's `accounts` and `opportunities`, and an account's or opportunity's `contacts` — are the only ones that expose a per-edge `PATCH`.

## Read a relationship

```bash theme={null}
curl "https://api.reevo.ai/api/v2/public/contact/<contact_id>/relationship/accounts" \
  -H "Authorization: Bearer <your-api-key>"
```

Responses include the parent, relationship name, related items, and pagination metadata.

## Add a relationship

Provide exactly one of `related_record_id` or `related_record_match`. Sending both, or neither, returns `field_value_invalid`.

Add a target by ID:

```json theme={null}
{
  "related_record_id": "<account_id>"
}
```

Or match the target by a unique field. `related_record_match` takes a `{ "field", "value" }` envelope — the same match shape used by `_upsert` and reference-field writes — where `field` is a match key on the **target** record type:

```json theme={null}
{
  "related_record_match": {
    "field": "domains.domain_name",
    "value": "acme.com"
  }
}
```

`related_record_match` is useful when an upstream system knows a unique attribute of the target, such as an account domain, but not the Reevo record ID. The allowed `field` values depend on the target record type, not on the junction:

| Target record type                       | Allowed `field` match keys              |
| ---------------------------------------- | --------------------------------------- |
| `account`                                | `domains.domain_name`                   |
| `contact`                                | `primary_email`, `contact_emails.email` |
| `opportunity`                            | None — address by `related_record_id`   |
| Custom objects and generic relationships | None — address by `related_record_id`   |

Every record type is always addressable by ID through `related_record_id`. A match must resolve to exactly one record: an unsupported `field` returns `invalid_matching_field`, a value that matches more than one record returns `ambiguous_match`, and a value that matches nothing returns `target_record_not_found`.

**Linking a contact to an opportunity has a precondition.** The contact must already be associated with that opportunity's account. If it is not, the add returns `409 contact_account_association_required` — link the contact to the account first, then add the contact-to-opportunity edge.

## Update a relationship edge

`PATCH` applies **only to junction relationships** — the ones whose edge carries its own attributes. It updates those per-edge attributes in place (for example a contact-on-account edge's `role` and `is_primary`) without unlinking and re-adding the target:

```bash theme={null}
curl -X PATCH "https://api.reevo.ai/api/v2/public/contact/<contact_id>/relationship/accounts/<target_id>" \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "CTO",
    "is_primary": true
  }'
```

Direct relationships have no edge attributes, so there is nothing to `PATCH`, and the generic `/{record_type}/{id}/relationship/{api_name}` surface exposes only `GET`, `POST`, and `DELETE`. To change a direct link, remove and re-add it.

## Remove a relationship

```bash theme={null}
curl -X DELETE "https://api.reevo.ai/api/v2/public/contact/<contact_id>/relationship/accounts/<target_id>" \
  -H "Authorization: Bearer <your-api-key>"
```

Edge `DELETE` is idempotent **for absent targets and non-primary edges**: it returns `204 No Content` whether or not such an edge existed. Deleting an absent, already-removed, or non-existent target still returns `204` and never returns `target_record_not_found`, so for these cases the status alone cannot tell you whether an edge was actually removed.

One exception: you cannot delete the **primary** contact on a contact-to-opportunity edge. `DELETE` of that edge returns `400 validation_error` — reassign the opportunity's primary contact to another contact first, then remove the edge.

## Relationship support by resource

| Resource       | Relationship support                                                                                                                                                                           |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contact`      | Typed `accounts`, typed `opportunities`, plus generic relationship names                                                                                                                       |
| `account`      | Typed `contacts`, plus generic relationship names                                                                                                                                              |
| `opportunity`  | Typed `contacts`, plus generic relationship names                                                                                                                                              |
| `task`         | Generic relationship names                                                                                                                                                                     |
| `note`         | Generic relationship names                                                                                                                                                                     |
| `meeting`      | Generic relationship names; the meeting record itself is read-only. Relationship-edge writes require a meeting **write** scope — see [Meeting relationship edges](#meeting-relationship-edges) |
| Custom objects | Generic relationship names                                                                                                                                                                     |

### Meeting relationship edges

A meeting record is read-only, but its relationship edges are governed by your key's meeting scope, not by the record's writability. Every edge `POST`/`DELETE` on a meeting requires a meeting **write** scope: with a read-only meeting key the scope check runs before relationship validation and returns `403 insufficient_scope` for any edge write — even for a relationship name that does not exist (you get `403`, not `404 unknown_relationship`).

`GET /api/v2/public/_schema/meeting` is the source of truth for which meeting relationships are writable; today it marks meeting's relationships non-creatable and non-updatable. Confirm writability there and ensure your key carries the meeting write scope before adding or removing meeting edges.

## Cardinality and junction records

Typed relationships such as contacts on accounts and contacts on opportunities are many-to-many: a contact can link to many accounts, and an account can link to many contacts. The link itself can carry data, stored on a junction record. For example, a contact-on-account edge can hold a `role` and an `is_primary` flag, while a contact-on-opportunity edge holds only `is_primary`.

Use this to decide how to change an edge:

* If the relationship has its own attributes (a junction edge), update those attributes with a per-edge `PATCH`, as shown above.
* If the relationship is a plain link with no edge attributes, there is nothing to patch. Remove and re-add the edge to change it.

### Primary edges and `is_primary`

`is_primary` is scoped differently for the two typed junctions, so the side that holds the "at most one primary" constraint depends on the relationship.

**Contact-to-account is contact-scoped.** Here `is_primary` means "this is the *contact's* primary account," and a contact has at most one primary account. Promoting a different account edge to primary demotes the previous one, and demoting the current primary by patching `is_primary: false` auto-promotes one of the contact's sibling account edges so the contact still has a primary — do not assume a demote leaves the contact with no primary. Because the flag belongs to the contact, listing from the account side can legitimately show several `is_primary: true` rows: `GET /account/{id}/relationship/contacts` returns one row per linked contact, and each contact may have flagged *this* account as its primary. Multiple `is_primary: true` rows on the account side are expected, not corruption.

**Contact-to-opportunity is opportunity-scoped.** Here `is_primary` means "this is the *opportunity's* primary contact," and an opportunity has at most one primary contact; promoting a different contact to primary demotes the previous one. A single contact can be the primary contact on many opportunities, so listing from the contact side (`GET /contact/{id}/relationship/opportunities`) can legitimately show several `is_primary: true` rows — one per opportunity where this contact is primary. Multiple `is_primary: true` rows on the contact side are expected, not corruption.

You do not address junction records directly. Reevo's canonical junction record types, such as `contact_account_role` and `contact_opportunity_role`, are not exposed as top-level Public API v2 records. Manage them through the parent-scoped relationship endpoints on this page. See [Not Supported Yet](/Public-API-v2/Not-Supported-Yet) for the canonical junction endpoints that are intentionally not available.

## Common errors

| Error code                             | Usually means                                                                                                                                                          | How to recover                                                                                                                             |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `unknown_relationship`                 | Relationship API name is not declared for this record type                                                                                                             | Check `/_schema/{record_type}`                                                                                                             |
| `target_record_not_found`              | On add, the target record ID or match value did not resolve. Edge `DELETE` is idempotent and never returns this — see [Remove a relationship](#remove-a-relationship). | Re-query or create the target first                                                                                                        |
| `record_not_found`                     | On a typed-junction `PATCH`, the edge could not be resolved (returned as `404`, not `target_record_not_found`)                                                         | Re-query the edge and confirm the `target_id` in the `PATCH` path                                                                          |
| `invalid_matching_field`               | The `related_record_match` `field` is not an allowed match key for the target record type                                                                              | Use an allowed match key for the target type (see [Add a relationship](#add-a-relationship)), or address the target by `related_record_id` |
| `ambiguous_match`                      | The `related_record_match` value matched more than one record                                                                                                          | Narrow to a unique value, resolve duplicate records, or address the target by `related_record_id`                                          |
| `field_value_invalid`                  | The request sent both `related_record_id` and `related_record_match`, or neither                                                                                       | Send exactly one target reference on the request                                                                                           |
| `contact_account_association_required` | Adding a contact-to-opportunity edge while the contact is not yet associated with the opportunity's account                                                            | Link the contact to the opportunity's account first, then retry the contact-to-opportunity add                                             |
| `field_not_writable`                   | The relationship maps to a read-only or singleton field that cannot be changed through this path                                                                       | Use the supported parent field or endpoint                                                                                                 |
| `insufficient_scope`                   | Your API key lacks the scope this edge write needs — for example, a meeting edge `POST`/`DELETE` with a read-only meeting key returns `403`                            | Use a key that carries the required write scope                                                                                            |
