# Add a contact

> Adds a person with their name, tags and custom fields, which templates fill in.

`POST /v1/contacts`

Adds a person with their name, tags and custom fields, which templates fill in. Somebody you text is added on their own, so this is for adding details ahead of time. A handle that is already a contact answers `conflict`; change that contact with `PATCH /v1/contacts/{id}` instead.

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

Send an `Idempotency-Key` so a retry never acts twice. [Idempotency](https://usebluebot.com/docs/concepts/idempotency)

## Headers

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | No | Any string of up to 255 characters, new for each request. A retry with the same key gets the first answer back for 24 hours, with `Idempotent-Replayed: true`. Up to 255 characters |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `handle` | string | Yes | Their phone number (US numbers may leave off +1) or the email address their Apple ID uses. 3 to 320 characters |
| `first_name` | string or null | No | The first name. Send null or an empty string to clear it. Up to 100 characters |
| `last_name` | string or null | No | The last name. Send null or an empty string to clear it. Up to 100 characters |
| `company` | string or null | No | The company. Send null or an empty string to clear it. Up to 200 characters |
| `tags` | array of strings | No | Tags, at most 50 of up to 50 characters each. Capitals are kept but a tag counts once however it is spelled. Up to 50 items |
| `custom_fields` | map of string | No | Your own fields for templates, at most 50. Names are made template-safe (`Showing address` becomes `showing_address`); an empty value drops the field. |

## Example

```bash
curl -X POST "https://usebluebot.com/api/v1/contacts" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0f6d8c2e-5b1a-4c7e-9d3f-2a8b6e1c4d70" \
  -d '{
    "handle": "+15555550123",
    "first_name": "Maria",
    "last_name": "Alvarez",
    "tags": [
      "buyer"
    ],
    "custom_fields": {
      "showing": "412 Oak St"
    }
  }'
```

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

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

const contact = await client.contacts.create({
  handle: '+15555550123',
  firstName: 'Maria',
  lastName: 'Alvarez',
  tags: ['buyer'],
  customFields: {
    showing: '412 Oak St',
  },
}, { idempotencyKey: '0f6d8c2e-5b1a-4c7e-9d3f-2a8b6e1c4d70' });
console.log(contact);
```

```python
import os
from bluebot import BlueBot

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

contact = client.contacts.create(
    handle="+15555550123",
    first_name="Maria",
    last_name="Alvarez",
    tags=["buyer"],
    custom_fields={
        "showing": "412 Oak St",
    },
    idempotency_key="0f6d8c2e-5b1a-4c7e-9d3f-2a8b6e1c4d70",
)
print(contact)
```

## Response

`201`: The new contact.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | A ct_ id. Starts with `ct_` |
| `object` | "contact" | Yes |   |
| `handle` | string | Yes | A phone number in E.164 (`+15555550123`) or an email address an Apple ID uses (iMessage only). |
| `kind` | string | Yes | `phone` or `email`. One of `phone`, `email` |
| `first_name` | string or null | Yes |   |
| `last_name` | string or null | Yes |   |
| `company` | string or null | Yes |   |
| `tags` | array of strings | Yes | The contact’s tags. |
| `custom_fields` | map of string | Yes | Your own fields, for templates. |
| `stopped` | boolean | Yes | True when the person is on your stop list. Texts to them are canceled. |
| `stopped_at` | timestamp or null | Yes | When they were put on the stop list. |
| `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` |
| `last_service` | string or null | Yes | The service of the newest text with them. One of `imessage`, `sms` |
| `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. |
| `last_message_at` | timestamp or null | Yes | The newest text either way. |
| `created_via` | string | Yes | What first added them. One of `message`, `inbound`, `import`, `api`, `dashboard`, `widget`, `hook`, `mcp`, `assistant` |
| `created_at` | timestamp | Yes | When the contact was added. |

The answer, `201`:

```json
{
  "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": null,
  "first_message_at": null,
  "last_message_at": null,
  "created_via": "api",
  "created_at": "2026-11-02T15:20:00.000Z"
}
```

## 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. |
| [`conflict`](https://usebluebot.com/docs/errors/conflict)                         | 409    | The request doesn’t fit the thing’s current state, or another request with the same Idempotency-Key is still running.                            |
| [`idempotency_conflict`](https://usebluebot.com/docs/errors/idempotency_conflict) | 409    | The Idempotency-Key was used in the last 24 hours with a different request. A key stands for one request.                                        |

## Try it
