# Contacts

> Create, retrieve, update, search and delete the people your business talks to.

Source: https://dev.ody.co/api/contacts

A **contact** is a person or company your business talks to. Ody creates contacts automatically the first time an unknown number texts or calls in. You can also create them yourself, for example to sync a CRM. Every conversation thread is attached to the contact whose phone number it involves.

## The contact object

- `id` *uuid* — Unique identifier.
- `firstName` *string | null* — Given name.
- `lastName` *string | null* — Family name.
- `company` *string | null* — Company or organisation.
- `phones` *array of {value, label?}* — Phone numbers in E.164. The first one is the primary number.
- `emails` *array of {value, label?}* — Email addresses.
- `notes` *array of strings* — Free-text notes, oldest first.
- `properties` *object* — Your own string key/value fields (for example `{"crmId": "A-1042"}`).
- `tags` *array of strings* — Labels used for filtering in the app.
- `source` *string | null* — How the contact was created: `manual` (the app or this API), `inbound` (auto-created from a text or call), or the integration it was synced from (for example `hubspot`).
- `createdAt` *timestamp* — ISO 8601.

List endpoints return a lighter **contact summary**: `id`, `firstName`, `lastName`, `company`, `phone` (the primary number) and `createdAt`.

The contact object:

```json
{
  "id": "8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90",
  "firstName": "Ada",
  "lastName": "Lovelace",
  "company": "Analytical Engines",
  "phones": [{ "value": "+15125550123", "label": "mobile" }],
  "emails": [{ "value": "ada@example.com" }],
  "notes": ["Prefers texts after 5pm"],
  "properties": { "crmId": "A-1042" },
  "tags": ["vip"],
  "source": "manual",
  "createdAt": "2026-10-14T18:02:11.000Z"
}
```

**`GET /v1/api/contacts`**

### List contacts

Returns the workspace's contacts, newest first. Pass `q` to run a full-text search over names, company, phones and emails. Results are [cursor-paginated](https://dev.ody.co/api/pagination).

**Scope:** `contacts:read`

**Query parameters**

- `q` *string* — Full-text search query.
- `limit` *integer* — 1–200, default 50.
- `cursor` *string* — `nextCursor` from the previous page.

**Returns** `{ contacts: ContactSummary[], nextCursor: string | null }`.

cURL:

```bash
curl "https://api.ody.co/v1/api/contacts?q=ada&limit=20" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const { contacts, nextCursor } = await ody.contacts.list({ q: "ada", limit: 20 });
```
Python:

```python
page = client.contacts.list(q="ada", limit=20)
contacts, next_cursor = page["contacts"], page["nextCursor"]
```

Response:

```json
{
  "contacts": [
    {
      "id": "8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90",
      "firstName": "Ada",
      "lastName": "Lovelace",
      "company": "Analytical Engines",
      "phone": "+15125550123",
      "createdAt": "2026-10-14T18:02:11.000Z"
    }
  ],
  "nextCursor": null
}
```

**`POST /v1/api/contacts`**

### Create a contact

Creates a contact. You must send at least one identity field: `firstName`, `lastName`, `company`, `phone`/`phones` or `email`/`emails`.

If the phone matches a contact Ody **auto-created** from an inbound text or call (one that has no name yet), that contact is adopted and updated instead of duplicated. Its existing threads keep their history.

**Scope:** `contacts:write`

**Body**

- `firstName` *string*
- `lastName` *string*
- `company` *string*
- `phone` *string* — Convenience field, merged into `phones`. E.164.
- `email` *string* — Convenience field, merged into `emails`.
- `phones` *array* — Strings, or `{ "value", "label" }` objects.
- `emails` *array* — Strings, or `{ "value", "label" }` objects.
- `notes` *array of strings*
- `properties` *object* — String values only.
- `tags` *array of strings*

**Returns** `201` with `{ contact }`.

cURL:

```bash
curl https://api.ody.co/v1/api/contacts \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Ada",
    "lastName": "Lovelace",
    "phone": "+15125550123",
    "properties": { "crmId": "A-1042" },
    "tags": ["vip"]
  }'
```
TypeScript:

```ts
const contact = await ody.contacts.create({
  firstName: "Ada",
  lastName: "Lovelace",
  phone: "+15125550123",
  properties: { crmId: "A-1042" },
  tags: ["vip"],
});
```
Python:

```python
contact = client.contacts.create(
    first_name="Ada",
    last_name="Lovelace",
    phone="+15125550123",
    properties={"crmId": "A-1042"},
    tags=["vip"],
)
```

Response:

```json
{
  "contact": {
    "id": "8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "company": null,
    "phones": [{ "value": "+15125550123" }],
    "emails": [],
    "notes": [],
    "properties": { "crmId": "A-1042" },
    "tags": ["vip"],
    "source": "manual",
    "createdAt": "2026-10-14T18:02:11.000Z"
  }
}
```

**`GET /v1/api/contacts/:id`**

### Retrieve a contact

Returns the full contact.

**Scope:** `contacts:read`

**Returns** `{ contact }`, or `404 not_found`.

cURL:

```bash
curl https://api.ody.co/v1/api/contacts/8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90 \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const contact = await ody.contacts.get("8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90");
```
Python:

```python
contact = client.contacts.get("8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90")
```

**`PATCH /v1/api/contacts/:id`**

### Update a contact

Changes only the fields you send. Array and object fields (`phones`, `emails`, `notes`, `properties`, `tags`) are **replaced wholesale**. To add a tag, read the contact, append to the array, then send the whole array back.

**Scope:** `contacts:write`

**Body:** any field from [Create a contact](#create-a-contact). Send `null` to clear `firstName`, `lastName` or `company`.

**Returns** `{ contact }`.

cURL:

```bash
curl -X PATCH https://api.ody.co/v1/api/contacts/8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90 \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"company":"Analytical Engines","tags":["vip","renewal-q4"]}'
```
TypeScript:

```ts
await ody.contacts.update("8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90", {
  company: "Analytical Engines",
  tags: ["vip", "renewal-q4"],
});
```
Python:

```python
client.contacts.update(
    "8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90",
    company="Analytical Engines",
    tags=["vip", "renewal-q4"],
)
```

**`DELETE /v1/api/contacts/:id`**

### Delete a contact

Permanently deletes the contact. Conversation threads are **not** deleted: they stay in the inbox under the raw phone number, and a new text from that number creates a fresh contact.

**Scope:** `contacts:write`

**Returns** `{ "deleted": true }`.

cURL:

```bash
curl -X DELETE https://api.ody.co/v1/api/contacts/8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90 \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
await ody.contacts.delete("8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90");
```
Python:

```python
client.contacts.delete("8b1d6f3e-2c4a-4e1b-9f0d-7a5c3e2b1a90")
```
