# List contacts

> Your contacts, the most recently texted first.

`GET /v1/contacts`

Your contacts, the most recently texted first. `q` finds people by name, company, phone number or email, and a few digits of a number work. Filter by `tag`, or by `stopped` for the stop list.

Needs a key with the `contacts:read` scope. [Keys and scopes](https://usebluebot.com/docs/concepts/api-keys)

## Query parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | No | Find people by name, company, phone number or email. Part of one works, and each word can match a different one, so `Dana Oak` finds Dana at Oak Realty. Up to 100 characters |
| `tag` | string | No | Only people with this tag. Up to 50 characters |
| `stopped` | string | No | `true` for only people on your stop list, `false` for only people who aren’t. One of `true`, `false` |
| `limit` | integer | No | How many to return, 1 to 100. Default 25. 1 to 100. Default `25` |
| `cursor` | string | No | The `next_cursor` of the page before. Leave it out for the first page. Up to 200 characters |

## Example

```bash
curl "https://usebluebot.com/api/v1/contacts" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const page = await client.contacts.list();
console.log(page.data);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

page = client.contacts.list()
print(page.data)
```

## Response

`200`: A page of contacts.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | "list" | Yes |   |
| `data` | array of Contact | Yes |   |
| `data[].id` | string | Yes | A ct_ id. Starts with `ct_` |
| `data[].object` | "contact" | Yes |   |
| `data[].handle` | string | Yes | A phone number in E.164 (`+15555550123`) or an email address an Apple ID uses (iMessage only). |
| `data[].kind` | string | Yes | `phone` or `email`. One of `phone`, `email` |
| `data[].first_name` | string or null | Yes |   |
| `data[].last_name` | string or null | Yes |   |
| `data[].company` | string or null | Yes |   |
| `data[].tags` | array of strings | Yes | The contact’s tags. |
| `data[].custom_fields` | map of string | Yes | Your own fields, for templates. |
| `data[].stopped` | boolean | Yes | True when the person is on your stop list. Texts to them are canceled. |
| `data[].stopped_at` | timestamp or null | Yes | When they were put on the stop list. |
| `data[].stop_source` | string or null | Yes | How: `reply` (they texted a stop word), `dashboard`, `api`, `import` or `admin`. One of `reply`, `dashboard`, `api`, `import`, `admin` |
| `data[].last_service` | string or null | Yes | The service of the newest text with them. One of `imessage`, `sms` |
| `data[].first_message_at` | timestamp or null | Yes | The first text either way. Null for somebody you have never texted with, who counts as a new person. |
| `data[].last_message_at` | timestamp or null | Yes | The newest text either way. |
| `data[].created_via` | string | Yes | What first added them. One of `message`, `inbound`, `import`, `api`, `dashboard`, `widget`, `hook`, `mcp`, `assistant` |
| `data[].created_at` | timestamp | Yes | When the contact was added. |
| `has_more` | boolean | Yes | Whether another page follows. |
| `next_cursor` | string or null | Yes | Send as `cursor` for the next page; null on the last. |

The answer, `200`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "ct_1Hq2LxW9cVbN3mKpRtYs0a",
      "object": "contact",
      "handle": "+15555550123",
      "kind": "phone",
      "first_name": "Maria",
      "last_name": "Alvarez",
      "company": null,
      "tags": [
        "buyer"
      ],
      "custom_fields": {
        "showing": "412 Oak St"
      },
      "stopped": false,
      "stopped_at": null,
      "stop_source": null,
      "last_service": "imessage",
      "first_message_at": "2026-11-02T15:20:00.000Z",
      "last_message_at": "2026-11-13T19:41:00.000Z",
      "created_via": "message",
      "created_at": "2026-11-02T15:20:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## Errors

Every refusal has the same envelope and a page of its own. These are the ones this operation can give.

| Code                                                    | Status | What it means                                                                                                                                    |
| ------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`unauthorized`](https://usebluebot.com/docs/errors/unauthorized)             | 401    | The request carried no key, or a key BlueBot doesn’t know: mistyped, revoked, or a test key sent to the live API.                                |
| [`insufficient_scope`](https://usebluebot.com/docs/errors/insufficient_scope) | 403    | The key doesn’t carry the scope the endpoint needs. Each endpoint’s reference names its scope.                                                   |
| [`rate_limited`](https://usebluebot.com/docs/errors/rate_limited)             | 429    | Too many requests. A key may make 120 requests a minute. The `RateLimit` header says how many are left, and `Retry-After` how long to wait.      |
| [`internal`](https://usebluebot.com/docs/errors/internal)                     | 500    | Something went wrong on BlueBot’s side. It was reported. Send the request again; with an Idempotency-Key that is always safe.                    |
| [`invalid_request`](https://usebluebot.com/docs/errors/invalid_request)       | 400    | Something in the request doesn’t fit: a missing or malformed field, a bad id, or a combination the endpoint refuses. `details` names each field. |

## Try it
