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

# Contact communication permissions

> Share contact information independently from permission to contact each address

Each identity has one whitelist or blacklist for **email** and one for **phone**. Phone covers SMS, calls, and iMessage together. The same rules govern incoming and outgoing communication.

* An active exact-address `allow` or `block` choice takes precedence in either mode.
* Without an exact choice, email uses matching `allow` domains in whitelist mode or `block` domains in blacklist mode.
* Unmatched addresses are denied in whitelist mode and allowed in blacklist mode.

Contact permission edits and standalone rules change the same exact-address choices. Each saved email or phone number has its own permission. Allowing one email does not allow the contact's other identifiers.

Standalone [email/domain rules](/docs/api/mail/contact-rules) and [number rules](/docs/api/phone/contact-rules) do not require a saved contact. Admin API keys and users in the [Inkbox Console](https://inkbox.ai/console/contacts) manage permissions. Agent keys cannot create, update, or delete rules.

## Contact visibility

Email and phone visibility is all-or-nothing for each contact and identity. A visible group exposes all of that contact's identifiers of that kind, including in search, lookup, vCard export, and contact context. Permission to communicate remains independent for each address. A fully hidden contact returns `404` by ID and is absent from listings.

Choose group visibility separately from each address's communication permission:

| Group    | What the identity can read                                                                       |
| -------- | ------------------------------------------------------------------------------------------------ |
| Email    | All email addresses when the group is visible                                                    |
| Phone    | All phone numbers when the group is visible                                                      |
| Profile  | Names, notes, postal addresses, company, other profile fields, and labels on visible identifiers |
| Memories | Contact memories and summaries, with citations subject to existing source access                 |

Without an explicit group-visibility choice, any communication-permitted address makes that group visible. Existing mixed communication choices stay mixed, but the agent can see every address in that group. Seeing an address does not authorize sending to it.

Profile is the parent permission for the contact. When Profile is hidden, Email, Phone, and Memories are also hidden, and no saved address is contactable through this contact. When Profile is visible, you can independently hide Memories or make Email and Phone view-only. Existing authorized conversation history is not erased.

<Note>
  Hosted voice in **YOLO mode** can read all contacts, fields, and memories in its organization regardless of these settings. Ordinary identity-scoped API, SDK, CLI, MCP, and webhook access still follows the contact policy. Setting an identity's default voice mode to YOLO does not change those ordinary reads.
</Note>

Suggested contacts follow the same permissions. A new interaction does not create a permission grant or a duplicate copy of an existing hidden contact.

## Read and update contact access

```text theme={null}
GET /api/v1/identities/{agent_handle}/contacts/{contact_id}/access
PATCH /api/v1/identities/{agent_handle}/contacts/{contact_id}/access
```

Both endpoints require an admin API key or the Inkbox Console. GET and successful PATCH return:

```json theme={null}
{
  "email": {"visible": true, "contactable": ["ada@example.com"]},
  "phone": {"visible": true, "contactable": []},
  "profile": true,
  "memories": false
}
```

`visible` covers the whole email or phone group. `contactable` lists only the current addresses the agent may contact. In this example, the agent can see all phone numbers but cannot contact them.

| Intended access                                  | PATCH for that group                                    |
| ------------------------------------------------ | ------------------------------------------------------- |
| Hidden                                           | `{"visible": false}`                                    |
| View only                                        | `{"visible": true, "contactable": []}`                  |
| View and contact selected addresses              | `{"visible": true, "contactable": ["ada@example.com"]}` |
| Change visibility without changing communication | `{"visible": true}`                                     |

* PATCH preserves omitted fields. An empty PATCH or an empty group object changes nothing on its own.
* A supplied `contactable` list replaces communication choices for that group's current addresses. Listed addresses become explicit allows; all other current addresses become explicit blocks. An empty list blocks all current addresses.
* A nonempty `contactable` list makes its group visible when `visible` is omitted. An empty list preserves the previous visibility when `visible` is omitted.
* `visible: false` hides the group and blocks all of its current addresses. Addresses subsequently added to an explicitly hidden group are also blocked.
* An explicit standalone exact-address allow can reveal a uniquely matched saved contact's group while Profile is enabled, without allowing its other addresses.
* `profile: false` hides Email, Phone, and Memories and clears the contactable lists. A request that enables any child permission while the resulting Profile permission is false returns `422`.
* Choices persist when the identity's whitelist/blacklist mode changes. No revision or previous-action field is required; concurrent edits to different fields preserve each other.

Use strict JSON booleans. Each `contactable` list accepts at most 50 unique normalized addresses. Phone values must be E.164. Nulls, unknown fields, duplicate normalized addresses, and `visible: false` with a nonempty `contactable` list return `422`. An address outside the contact returns `400`; an unavailable agent/contact returns `404`; insufficient authority returns `403`.

Use SDK/CLI `0.6.12` or later with admin credentials for local validation of Profile-child contradictions:

<CodeGroup>
  ```python Python theme={null}
  from inkbox import ContactChannelAccessUpdate

  current = inkbox.contacts.access.get("my-agent", contact_id)
  updated = inkbox.contacts.access.update(
      "my-agent", contact_id,
      email=ContactChannelAccessUpdate(visible=True, contactable=[]),
  )
  ```

  ```typescript TypeScript theme={null}
  const current = await inkbox.contacts.access.get("my-agent", contactId);
  const updated = await inkbox.contacts.access.update("my-agent", contactId, {
    email: { visible: true, contactable: [] },
  });
  ```

  ```rust Rust theme={null}
  use inkbox::contacts::{ContactChannelAccessUpdate, UpdateContactAccess};

  let contacts = inkbox.contacts();
  let current = contacts.access().get("my-agent", contact_id)?;
  let updated = contacts.access().update("my-agent", contact_id, &UpdateContactAccess {
      email: Some(ContactChannelAccessUpdate {
          visible: Some(true),
          contactable: Some(vec![]),
      }),
      ..Default::default()
  })?;
  ```

  ```bash CLI theme={null}
  inkbox contacts access get my-agent CONTACT_ID --json
  inkbox contacts access set my-agent CONTACT_ID --file access.json
  ```
</CodeGroup>

The CLI file contains the PATCH body, such as `{"email":{"visible":true,"contactable":[]}}`. The existing `contacts.access.list` SDK method and `contacts access list` CLI command return read-only compatibility metadata, not these selected-agent access settings.

## Read and update yes/no permissions

```text theme={null}
GET /api/v1/identities/{agent_handle}/contacts/{contact_id}/permissions
PATCH /api/v1/identities/{agent_handle}/contacts/{contact_id}/permissions
```

These compatibility endpoints require an admin API key or the Inkbox Console. Email/phone maps report communication permission, including blocked addresses; they do not report independent group visibility:

```json theme={null}
{
  "emails": {"ada@example.com": true, "personal@example.net": false},
  "phones": {"+15555550123": false},
  "profile": true,
  "memories": false
}
```

PATCH accepts any subset of these fields. Supplied choices become explicit permissions. Omitted fields and addresses keep their existing settings, except that disabling Profile also disables all child permissions. An empty object or empty address map changes nothing. Profile and Memories never grant communication access.

Each address map accepts up to 50 entries. Use JSON booleans, not strings, numbers, or null. Email keys are normalized and phone keys must be E.164. Duplicate normalized keys are rejected.

Successful PATCH returns the same effective-access shape as GET. No revision or previous-action field is required. Concurrent edits to separate fields preserve each other; the later edit to the same field wins. Other agents' settings remain unchanged.

| Status | Meaning                                                  |
| ------ | -------------------------------------------------------- |
| `400`  | A supplied address is not on the contact                 |
| `403`  | The caller cannot manage contact permissions             |
| `404`  | The agent or contact is unavailable in your organization |
| `422`  | Invalid field, address, or boolean value                 |

Use SDK/CLI `0.6.12` or later for local validation of Profile-child contradictions:

<CodeGroup>
  ```python Python theme={null}
  current = inkbox.contacts.permissions.get("my-agent", contact_id)
  updated = inkbox.contacts.permissions.update(
      "my-agent", contact_id,
      emails={"ada@example.com": True},
      profile=True,
      memories=False,
  )
  ```

  ```typescript TypeScript theme={null}
  const current = await inkbox.contacts.permissions.get("my-agent", contactId);
  const updated = await inkbox.contacts.permissions.update("my-agent", contactId, {
    emails: { "ada@example.com": true },
    profile: true,
    memories: false,
  });
  ```

  ```bash CLI theme={null}
  inkbox contacts permissions get my-agent CONTACT_ID --json
  inkbox contacts permissions set my-agent CONTACT_ID --file permissions.json
  ```

  ```rust Rust theme={null}
  use std::collections::HashMap;
  use inkbox::contacts::UpdateContactPermissions;

  let contacts = inkbox.contacts();
  let current = contacts.permissions().get("my-agent", contact_id)?;
  let updated = contacts.permissions().update("my-agent", contact_id, &UpdateContactPermissions {
      emails: Some(HashMap::from([("ada@example.com".into(), true)])),
      profile: Some(true),
      memories: Some(false),
      ..Default::default()
  })?;
  ```
</CodeGroup>

The CLI file contains the PATCH body, for example `{"emails":{"ada@example.com":true},"profile":true,"memories":false}`. To save initial choices with a new contact, use [create contact with permissions](/docs/api/contacts/manage#create-contact-with-permissions).

## Advanced policies

Use this interface when you need inherited visibility settings or guarded exact-address edits:

```text theme={null}
GET /api/v1/contacts/{contact_id}/communication-policy?identity_id={identity_id}
PUT /api/v1/contacts/{contact_id}/communication-policy
```

GET returns `contact_id`, `revision`, `identity_id`, `addresses`, `effective_visibility`, and `visibility`. Each address has `kind`, `value`, nullable `label`, stored `action`, and effective `allowed`. Without the optional `identity_id` query, `addresses` is empty and `identity_id` and `effective_visibility` are null.

Read the current policy, then supply its revision and each edited address's observed action:

```json theme={null}
{
  "expected_revision": 0,
  "identity_id": "11111111-1111-4111-8111-111111111111",
  "addresses": [
    {"kind": "email", "value": "ada@example.com", "action": "allow", "expected_action": "inherit"}
  ]
}
```

`allow` and `block` set explicit exact-address choices. `inherit` removes that exact choice so domain rules and the channel mode determine communication access. There are no contact-wide Email or Phone communication defaults.

The optional `visibility` object contains `defaults: {profile, memories}` and `identities: [{identity_id, profile, memories}]`. Decisions are `allow`, `block`, or `inherit`. An identity override inherits its contact default. When both Profile decisions inherit, Profile is enabled. A blocked effective Profile requires blocked Memories and prevents Email or Phone address allows for the selected identity.

Omitting `visibility` preserves all Profile/Memories settings. Supplying it replaces that entire portion, so retain other identities' settings. `visibility: null`, unknown fields, duplicate normalized addresses, and invalid decisions return `422`. Up to 500 address edits and 500 distinct identities are accepted. Stale revisions or changed observed actions return `409` without saving changes.

Replacing Profile/Memories settings preserves email/phone visibility while Profile stays enabled. Blocking Profile makes those visibility settings ineffective and prevents an exact-address allow from revealing contact data.

Python uses `communication_policy.get(contact_id, identity_id)` and `.replace(..., expected_revision=..., identity_id=..., addresses=[ContactAddressUpdate(...)])`. TypeScript uses `communicationPolicy.get(contactId, identityId)` and `.replace(contactId, {expectedRevision, identityId, addresses})`, with `expectedAction` on address edits. Rust uses `communication_policy().get_for_identity(contact_id, identity_id)` and `replace` with `ReplaceContactCommunicationPolicy`; `replace_with_visibility` also replaces visibility.

The CLI provides `contacts communication-policy get CONTACT_ID --identity-id IDENTITY_ID` and `set CONTACT_ID --file policy.json`. Its file uses the TypeScript names `expectedRevision`, `identityId`, and `expectedAction`, including `identityId` within optional visibility overrides.

## Preview an identity's saved view

```text theme={null}
GET /api/v1/contacts/{contact_id}/communication-preview?identity_id={identity_id}
GET /api/v1/identities/{agent_handle}/contact-communication-policies?limit=50&offset=0
```

The contact preview requires admin credentials and shows ordinary identity-scoped access, including for identities configured to use YOLO voice. It returns `contact: null` when that view cannot see the contact. Otherwise `contact` includes the permitted fields, `memory_count`, and `latest_memory`. Denied memories produce zero and null without revealing their existence.

`visibility.profile` and `visibility.memories` report access even when a group has no stored content. Memories is always false when Profile is false. Existing `email` and `phone` indicators mean their visible identifier arrays are nonempty. `full_profile` remains a conservative legacy aggregate; use the individual visibility fields for new interfaces. The identity listing returns `items`, `limit`, `offset`, and `has_more`; an agent can list only its own view.

Use `inkbox.contacts.communication_policy.preview(contact_id, identity_id)` in Python or `inkbox.contacts.communicationPolicy.preview(contactId, identityId)` in TypeScript. The CLI provides `contacts communication-policy preview CONTACT_ID IDENTITY_ID` and `contacts communication-policy list HANDLE`.

## Manage an identity's contact permissions

```text theme={null}
GET /api/v1/identities/{agent_handle}/contact-permissions
```

Use an admin API key or the Inkbox Console to list contacts in the selected identity's
organization, including contacts hidden from that identity. Agent keys cannot
call this endpoint. The ordinary identity-view listing above remains filtered.

| Query parameter | Description                                                           |
| --------------- | --------------------------------------------------------------------- |
| `q`             | Search contact details, up to 500 characters                          |
| `order`         | `name` or `recent`; default `recent`                                  |
| `review_status` | `confirmed` or `unreviewed`; repeat to include both, or omit for both |
| `limit`         | Page size, 1–200; default 50                                          |
| `offset`        | Offset, 0–10,000; default 0                                           |

The response contains `items`, `limit`, `offset`, and `has_more`. Each item has:

| Field                                     | Description                                                                                                                                |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `contact`                                 | ID, preferred/given/family names, company, review status, emails, and phones; no notes or memory content                                   |
| `revision`                                | Stored policy revision; zero only if no policy has been stored                                                                             |
| `visibility.defaults`                     | Contact-wide Profile and Memories decisions                                                                                                |
| `visibility.identity_override`            | Selected identity's Profile and Memories decisions, or `inherit`                                                                           |
| `effective.email`, `effective.phone`      | Communication coverage: `all`, `some`, `none`, or `no_identifiers`; not group visibility                                                   |
| `effective.profile`, `effective.memories` | Visibility booleans; Memories is false when Profile is false                                                                               |
| `access`                                  | Effective `email`/`phone` group visibility and contactable lists, plus Profile/Memories booleans; may be absent or null in older responses |

Effective communication access includes standalone address, domain, and number
rules. `no_identifiers` means no identifiers are stored, not that access is
denied. Resetting a policy to inherited values does not reset its revision.
Other identities' overrides are not included in this roster. Boolean permission updates preserve their settings automatically.

<CodeGroup>
  ```python Python theme={null}
  from inkbox.contacts import ContactReviewStatus

  page = inkbox.contacts.communication_policy.list_management_for_identity(
      "my-agent", q="Jane", order="name", limit=20,
      review_status=[ContactReviewStatus.CONFIRMED],
  )
  for item in page.items:
      print(item.contact.preferred_name, item.effective.email, item.effective.memories)
  ```

  ```typescript TypeScript theme={null}
  const page = await inkbox.contacts.communicationPolicy.listManagementForIdentity("my-agent", {
    q: "Jane", order: "name", limit: 20, reviewStatus: ["confirmed"],
  });
  for (const item of page.items) {
    console.log(item.contact.preferredName, item.effective.email, item.effective.memories);
  }
  ```

  ```rust Rust theme={null}
  use inkbox::contacts::ListContactsParams;

  let page = inkbox.contacts().communication_policy().list_management_for_identity(
      "my-agent",
      &ListContactsParams { q: Some("Jane".into()), limit: Some(20), ..Default::default() },
  )?;
  ```

  ```bash CLI theme={null}
  inkbox contacts communication-policy list-management my-agent --q Jane --order name --limit 20 --json
  ```
</CodeGroup>

## Phone compatibility

Existing phone and iMessage rule methods remain supported. `phone_filter_mode` and `imessage_filter_mode` are aliases: setting either changes both. Supplying conflicting values in one request returns `422`.

Identity-level permission configuration does not require a dedicated phone number. Older SDK helpers may still check for one locally; use their existing iMessage methods or upgrade. Releasing or replacing a phone number preserves the identity's permissions, and enabling another transport does not reset them.

## Contact edits

Changing the identifiers of an existing contact requires admin credentials or the Inkbox Console. Added identifiers follow their exact-address rules, email domain rules, and channel mode, except that an explicitly hidden group keeps added identifiers blocked. Humans and admin API keys can remove identifiers or delete a contact without resetting Profile or Memories settings. Exact-address rules survive contact or identifier deletion. Agent profile edits cannot grant communication permissions.

Agent profile PATCH requests must omit `emails` and `phones`, even when their values are unchanged. Agent credentials cannot delete contacts with configured Profile or Memories restrictions; use an admin API key or the Inkbox Console. Merging contacts with different permission settings returns `409` with `detail.error: "contact_policy_mismatch"` and a readable `detail.message`.

MCP users manage communication rules in the authenticated Inkbox Console. The agent can inspect its permitted contact view but cannot change its own rules.

Webhook replays refresh contact names and memories against current permissions. A visible name can accompany an empty memory list, but memories are never included without Profile access. Archived contact-scoped conversation context is omitted when it cannot be refreshed; the original event and directly scoped conversation history remain available.
