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

# Contacts

> List and read org-scoped contacts, and update contact status.

# Contacts

Contacts are org-scoped records of the people your agents have talked to, built from channel identity (a WhatsApp phone number, an email). One contact can span multiple agents and multiple conversations. See the [Contacts guide](/platform-guide/conversations/contacts) for the concept, and `docs/architecture/CONTACT_IDENTITY.md` in the repo for the full design.

<Note>
  All API requests require a valid JWT token in the `Authorization: Bearer <token>` header. The API Gateway decodes the JWT and forwards auth context (`user-id`, `organization-id`, `user-email`, `x-platform-role`, `x-org-role`) as headers to downstream services.
</Note>

There is no create/delete endpoint here — contacts are created automatically when a channel identity resolves (an inbound WhatsApp message, or the `update_contact` tool recording an email/phone), never directly through this API.

## List Contacts

<ParamField method="GET" path="/api/contacts" />

Paginated, filterable list of contacts in your organization.

### Query Parameters

<ParamField query="search" type="string">
  Case-insensitive match against display name, phone, or email.
</ParamField>

<ParamField query="channel" type="string">
  Filter to contacts with an identity on this channel. One of `whatsapp_business`, `web`, `widget`, `email`. (`whatsapp` — the WAHA channel — is deliberately absent: it stores full JIDs rather than phone numbers, so it never produces a contact identity and the filter would always return zero rows.)
</ParamField>

<ParamField query="botId" type="string">
  Filter to contacts who have at least one conversation with this agent.
</ParamField>

<ParamField query="status" type="string">
  One of `new`, `engaged`, `qualified`, `closed`.
</ParamField>

<ParamField query="limit" type="number" default="20">
  Max rows per page. Capped at 50.
</ParamField>

<ParamField query="offset" type="number" default="0">
  Pagination offset.
</ParamField>

### Response (200)

<ResponseField name="contacts" type="object[]">
  <Expandable title="Contact list item">
    <ResponseField name="id" type="string">Contact UUID.</ResponseField>
    <ResponseField name="displayName" type="string | null">Resolved display name.</ResponseField>
    <ResponseField name="primaryPhone" type="string | null">E.164 phone number.</ResponseField>
    <ResponseField name="primaryEmail" type="string | null">Lowercased email.</ResponseField>
    <ResponseField name="status" type="string">`new` | `engaged` | `qualified` | `closed`.</ResponseField>
    <ResponseField name="channels" type="string[]">Every channel this contact has an identity on.</ResponseField>
    <ResponseField name="conversationCount" type="number">Total conversations across every agent.</ResponseField>
    <ResponseField name="agentCount" type="number">Number of distinct agents this contact has talked to.</ResponseField>
    <ResponseField name="lastSeenAt" type="string">ISO 8601 timestamp.</ResponseField>
    <ResponseField name="attributes" type="object">Free-form fields recorded by the `update_contact` tool or an operator.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="total" type="number">Total matching contacts (for pagination), independent of `limit`/`offset`.</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl "https://your-domain.com/api/contacts?search=rahul&status=engaged&limit=20" \
    -H "Authorization: Bearer $TOKEN"
  ```

  ```javascript JavaScript theme={null}
  const params = new URLSearchParams({ search: "rahul", status: "engaged", limit: "20" });
  const response = await fetch(`https://your-domain.com/api/contacts?${params}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const { contacts, total } = await response.json();
  ```
</CodeGroup>

## Get Contact

<ParamField method="GET" path="/api/contacts/:id" />

Full contact detail: the contact record, its field-write history (with provenance), and every channel identity tied to it.

### Path Parameters

<ParamField path="id" type="string" required>Contact UUID.</ParamField>

### Response (200)

<ResponseField name="contact" type="object">The bare contact record — `id`, `organizationId`, `displayName`, `displayNameSource`, `primaryPhone`, `primaryEmail`, `sourceChannel`, `status`, `attributes`, `firstSeenAt`, `lastSeenAt`. Note this is **narrower** than a list item: `channels`, `conversationCount` and `agentCount` are list-only aggregates and are not returned here.</ResponseField>

<ResponseField name="fieldHistory" type="object[]">
  Newest first. Every write to this contact, applied or rejected.

  <Expandable title="Field history entry">
    <ResponseField name="field_key" type="string">e.g. `name`, `email`, `phone`, or a custom attribute key.</ResponseField>
    <ResponseField name="value" type="string | null">The value written (or attempted).</ResponseField>
    <ResponseField name="previous_value" type="string | null">What it replaced, or `null` on first set.</ResponseField>
    <ResponseField name="source" type="string">`tool` | `extraction` | `operator` | `import`.</ResponseField>
    <ResponseField name="source_ref" type="string | null">Reference to what produced the write (e.g. an operator's user id). Currently always `null` for tool-sourced writes — see the note below.</ResponseField>
    <ResponseField name="recorded_at" type="string">ISO 8601 timestamp.</ResponseField>
    <ResponseField name="rejected_reason" type="string | null">Set (e.g. `rejected_duplicate_identity`) when this write was refused rather than applied — see below.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="identities" type="object[]">
  <Expandable title="Identity">
    <ResponseField name="channel" type="string">e.g. `whatsapp_business`, `email`.</ResponseField>
    <ResponseField name="identifier" type="string">Normalized value (E.164 phone, lowercased email).</ResponseField>
    <ResponseField name="verified" type="boolean">Reserved for future use; always `false` today.</ResponseField>
  </Expandable>
</ResponseField>

<Note>
  A row with a non-null `rejected_reason` was never applied to the contact — it records an attempt where the field's value (an email or phone) already belonged to a *different* contact in the organization. Nothing was overwritten and nothing was merged; these rows are a work-list for a future contact-merge feature, which does not exist yet.
</Note>

### Errors

<ResponseField name="404" type="error">Contact not found, or belongs to a different organization.</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl https://your-domain.com/api/contacts/CONTACT_UUID \
    -H "Authorization: Bearer $TOKEN"
  ```
</CodeGroup>

## Get Contact Conversations

<ParamField method="GET" path="/api/contacts/:id/conversations" />

Every conversation this contact has had, across every agent — the view a single agent's own conversation list can't provide.

### Path Parameters

<ParamField path="id" type="string" required>Contact UUID.</ParamField>

### Response (200)

<ResponseField name="conversations" type="object[]">
  <Expandable title="Conversation">
    <ResponseField name="id" type="string">Conversation UUID.</ResponseField>
    <ResponseField name="bot_id" type="string">Agent UUID.</ResponseField>
    <ResponseField name="bot_name" type="string">Agent name.</ResponseField>
    <ResponseField name="platform" type="string">Channel platform.</ResponseField>
    <ResponseField name="title" type="string | null">Conversation title, if set.</ResponseField>
    <ResponseField name="created_at" type="string">ISO 8601 timestamp.</ResponseField>
    <ResponseField name="updated_at" type="string">ISO 8601 timestamp.</ResponseField>
  </Expandable>
</ResponseField>

Capped at 50 conversations, most recently updated first.

### Errors

<ResponseField name="404" type="error">Contact not found, or belongs to a different organization.</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl https://your-domain.com/api/contacts/CONTACT_UUID/conversations \
    -H "Authorization: Bearer $TOKEN"
  ```
</CodeGroup>

## Export Contacts

<ParamField method="GET" path="/api/contacts/export" />

Streams every matching contact to a CSV file, ready for other CRMs and outbound tools. The response is chunked and has no row cap — large contact lists download completely.

### Query Parameters

<ParamField query="search" type="string">
  Case-insensitive match against display name, phone, or email. Same semantics as [List Contacts](#list-contacts).
</ParamField>

<ParamField query="channel" type="string">
  Filter to contacts with an identity on this channel. One of `whatsapp_business`, `web`, `widget`, `email`.
</ParamField>

<ParamField query="botId" type="string">
  Filter to contacts who have at least one conversation with this agent.
</ParamField>

<ParamField query="status" type="string">
  One of `new`, `engaged`, `qualified`, `closed`.
</ParamField>

<ParamField query="organizationId" type="string">
  Ignored unless the caller is a platform admin. Non-admin callers always export their own organization.
</ParamField>

### Response (200)

<ResponseField name="Content-Type" type="string">`text/csv; charset=utf-8` (with a UTF-8 BOM).</ResponseField>
<ResponseField name="Content-Disposition" type="string">`attachment; filename="contacts.csv"`.</ResponseField>
<ResponseField name="Transfer-Encoding" type="string">`chunked` — streamed, no `Content-Length`.</ResponseField>

Each row is one contact. Columns, in order:

* Base columns: `id`, `organizationId`, `displayName`, `displayNameSource`, `primaryPhone`, `primaryEmail`, `sourceChannel`, `status`, `firstSeenAt`, `lastSeenAt`, `organizationName`, `conversationCount`, `agentCount`, `channels`, `identities`.
* Attribute columns: every collected attribute key (e.g. `company`, `title`, or any custom field), one column per key, sorted alphabetically.

<CodeGroup>
  ```bash curl theme={null}
  curl "https://your-domain.com/api/contacts/export?status=qualified" \
    -H "Authorization: Bearer $TOKEN" \
    --output contacts.csv
  ```
</CodeGroup>

## Developer Key Export

<ParamField method="GET" path="/v1/contacts/export" />

The same CSV export for API-key integrations. Requires the **`contacts:read`** scope.

### Authentication

```
Authorization: Bearer brs_live_YOUR_KEY
```

### Query Parameters

Same as `GET /api/contacts/export`, except `organizationId` is **not accepted** — the export is always scoped to the key's own organization.

### Errors

<ResponseField name="403" type="error">Key is missing the `contacts:read` scope — `{ "error": "insufficient_scope" }`.</ResponseField>

## Update Contact Status

<ParamField method="PATCH" path="/api/contacts/:id/status" />

Set a contact's status — the only field this API lets you write directly (everything else is written by an agent via the `update_contact` tool, or resolved from channel identity).

### Path Parameters

<ParamField path="id" type="string" required>Contact UUID.</ParamField>

### Request Body

<ParamField body="status" type="string" required>
  One of `new`, `engaged`, `qualified`, `closed`.
</ParamField>

### Response (200)

<ResponseField name="contact" type="object">The updated contact — the same bare contact record as `GET /contacts/:id` (no list-only aggregates).</ResponseField>

A successful status change also appends a `fieldHistory` row with `source: "operator"`, attributing the change to the authenticated user.

### Errors

<ResponseField name="400" type="error">`status` missing or not one of the four valid values.</ResponseField>
<ResponseField name="404" type="error">Contact not found, or belongs to a different organization.</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X PATCH https://your-domain.com/api/contacts/CONTACT_UUID/status \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"status": "qualified"}'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    `https://your-domain.com/api/contacts/${contactId}/status`,
    {
      method: "PATCH",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ status: "qualified" }),
    },
  );
  const { contact } = await response.json();
  ```
</CodeGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.