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

# Contacts

> Create, find, update and delete contacts, and set their tags, stage and assignee.

A contact is a person saved in your Blujay workspace, identified by a US phone number, with an optional email address and name. Contacts carry tags, a stage and an assigned team member, and Blujay links them to their chats and messages. Use these endpoints to keep Blujay in sync with your own system: create or update contacts as they change on your side, look them up before you send, and set their tags, stage and assignee.

Opt-out endpoints are covered in [Opt-outs](/opt-outs.md).

## Create a contact

`POST /v1/contacts` creates a contact. `phone_number` is required; `email` and `name` are optional. A contact created through the API always has a phone number. Contacts created in the Blujay app can have only an email; the API returns them with no `phone_number`. Sends take phone numbers, so an email-only contact can't be messaged through the API until a number is added.

Blujay accepts any US number written with 10 digits, or 11 digits starting with `1`, with or without `+`, spaces, dashes or parentheses: `+15555550123`, `15555550123`, `(555) 555-0123` and `555-555-0123` are the same number. A `+` followed by anything other than `1` is rejected. Blujay stores and returns the number in E.164 format (`+15555550123`). Anything else returns `400` [`VALIDATION_ERROR`](/errors/validation-error).

```bash curl theme={null}
curl https://api.blujay.io/v1/contacts \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15555550123",
    "email": "ava@example.com",
    "name": "Ava Chen"
  }'
```

```json Response (201) theme={null}
{
  "status": "success",
  "data": {
    "contact_id": "contact_abc123",
    "phone_number": "+15555550123",
    "email": "ava@example.com",
    "name": "Ava Chen",
    "tags": [],
    "stage": "Active",
    "assigned_team_member": null,
    "opted_out": false,
    "messaging_allowed": true,
    "created_at": "2026-10-14T17:00:00.000Z",
    "updated_at": "2026-10-14T17:00:00.000Z"
  },
  "trace_id": "trc_1a3c5e7b9d0f",
  "timestamp": "2026-10-14T17:00:00.050Z"
}
```

If the phone number or email already belongs to a contact in your workspace, you get `409` [`CONTACT_ALREADY_EXISTS`](/errors/contact-already-exists). To create or update in one call, use import instead.

Creating a contact doesn't send a webhook and doesn't start workflows that run when a contact is created. This endpoint doesn't use an `Idempotency-Key`; a repeated create returns `409`, so treat that as success when you are retrying.

## Create or update with import

`POST /v1/contacts/import` takes the same body as create and matches on the phone number:

* If no contact has this phone number, Blujay creates one and returns `201` with `"created": true`.
* If a contact has this phone number, Blujay updates its `email` and `name` with the values you send and returns `200` with `"updated": true`.
* If the email you send already belongs to another contact, one that doesn't have this phone number, Blujay returns `409` [`CONTACT_MATCH_CONFLICT`](/errors/contact-match-conflict) and changes nothing.

```bash curl theme={null}
curl https://api.blujay.io/v1/contacts/import \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15555550123",
    "email": "ava.chen@example.com",
    "name": "Ava Chen"
  }'
```

```json Response (200) theme={null}
{
  "status": "success",
  "data": {
    "contact_id": "contact_abc123",
    "phone_number": "+15555550123",
    "email": "ava.chen@example.com",
    "name": "Ava Chen",
    "tags": ["VIP"],
    "stage": "Active",
    "assigned_team_member": null,
    "opted_out": false,
    "messaging_allowed": true,
    "created_at": "2026-10-14T17:00:00.000Z",
    "updated_at": "2026-10-14T17:30:00.000Z",
    "created": false,
    "updated": true
  },
  "trace_id": "trc_7d9f1b3a5c2e",
  "timestamp": "2026-10-14T17:30:00.040Z"
}
```

Like create, import doesn't send a webhook and doesn't start workflows that run when a contact is created. Import is safe to repeat: sending the same body again updates the same contact.

## Find contacts

List every contact, most recently updated first, with `GET /v1/contacts`. Pages hold 25 contacts by default and up to 100 with `limit`. When `has_more` is `true`, pass `next_cursor` as `cursor`.

```bash curl theme={null}
curl "https://api.blujay.io/v1/contacts?limit=50" \
  -H "Authorization: Bearer blu_your_api_key"
```

To look someone up, use `GET /v1/contacts/search` with at least one of these:

* `phone_number`: an exact match, in any accepted US format.
* `email`: an exact match, case-insensitive.
* `q`: a case-insensitive substring match against name, phone number (as stored, `+1…`) and email. `q=555-0123` finds nothing; `q=5550123` does.

A contact matches when any of the parameters you send matches it. Search returns up to `limit` matches (10 by default, 25 at most), most recently updated first. Results aren't paginated, so narrow the search rather than page it.

```bash curl theme={null}
curl "https://api.blujay.io/v1/contacts/search?phone_number=%2B15555550123" \
  -H "Authorization: Bearer blu_your_api_key"
```

```json Response (200) theme={null}
{
  "status": "success",
  "data": {
    "data": [
      {
        "contact_id": "contact_abc123",
        "phone_number": "+15555550123",
        "email": "ava.chen@example.com",
        "name": "Ava Chen",
        "tags": ["VIP"],
        "stage": "Active",
        "assigned_team_member": null,
        "opted_out": false,
        "messaging_allowed": true,
        "created_at": "2026-10-14T17:00:00.000Z",
        "updated_at": "2026-10-14T17:30:00.000Z"
      }
    ]
  },
  "trace_id": "trc_3e5a7c9b1d0f",
  "timestamp": "2026-10-14T17:31:00.000Z"
}
```

> [!WARNING]
>
> URL-encode `+` as `%2B` in query strings. A raw `+` arrives as a space, and `phone_number= 15555550123` returns [`VALIDATION_ERROR`](/errors/validation-error).

Read one contact with `GET /v1/contacts/{contact_id}`.

### Fields to check before you send

| Field | Meaning |
| - | - |
| `opted_out` | `true` when the person has opted out, through the API, in Blujay, or by texting STOP to a Dedicated number. `opted_out_at` and `opted_out_source` say when and how; see [Opt-outs](/opt-outs.md). |
| `messaging_allowed` | `true` when Blujay will accept a send to this person. Today it is `false` exactly when `opted_out` is `true`. Check `messaging_allowed`: it is the field that will reflect any other block Blujay adds, while `opted_out` stays specific to opt-outs. |
| `stage` | The contact's stage, as a string. New contacts start as `Active`. Stages are set in the Blujay app; the API can only set `Completed` (see below). |

Neither field tells you about Free Plan verification or the pre-reply rules. See [Sending rules](/messaging/sending-rules.md#who-you-can-message).

## Update a contact

`PATCH /v1/contacts/{contact_id}` changes `phone_number`, `email` or `name`. Send at least one field; fields you leave out keep their values. A phone number or email that already belongs to another contact returns `409` [`CONTACT_ALREADY_EXISTS`](/errors/contact-already-exists).

```bash curl theme={null}
curl -X PATCH https://api.blujay.io/v1/contacts/contact_abc123 \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Ava M. Chen" }'
```

Tags, stage and assignee have their own endpoints, described below.

## Delete a contact

`DELETE /v1/contacts/{contact_id}` deletes the contact and returns `204 No Content`.

```bash curl theme={null}
curl -X DELETE https://api.blujay.io/v1/contacts/contact_abc123 \
  -H "Authorization: Bearer blu_your_api_key"
```

What deleting does:

* The contact record is removed. Reading it again returns `404` [`NOT_FOUND`](/errors/not-found).
* Chats and messages with that person stay, and you can still read them. They no longer link to a contact, so they stop carrying this `contact_id`. A contact you create later with the same number gets a new `contact_id`.
* An opt-out stays in place for their phone number or email, so you still can't message them. See [Opt-outs](/opt-outs.md).
* On the Free Plan, the person's verification is removed with the contact. They have to text your number and be verified again before you can message them; see [Plans](/plans.md#free-plan).

## Add a tag

`POST /v1/contacts/{contact_id}/tags` adds one tag (up to 100 characters) and returns the contact. `tag_added` is `true` when the tag was new to the contact and `false` when the contact already had it.

```bash curl theme={null}
curl https://api.blujay.io/v1/contacts/contact_abc123/tags \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "tag": "VIP" }'
```

When the tag is new, Blujay sends the `contact.tag_added` webhook and starts any workflows that run when that tag is added. Adding a tag the contact already has does neither. There is no endpoint to remove a tag or to list the tags in your workspace; remove tags in the Blujay app.

## Mark a contact completed

`PATCH /v1/contacts/{contact_id}/stage` sets the contact's stage. `Completed` is the only stage you can set through the API; any other value returns `400` [`VALIDATION_ERROR`](/errors/validation-error). `stage_changed` tells you whether the stage changed. There is no way to set a contact back to an earlier stage through the API.

```bash curl theme={null}
curl -X PATCH https://api.blujay.io/v1/contacts/contact_abc123/stage \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "stage": "Completed" }'
```

## Assign a contact

First get the team members you can assign with `GET /v1/team-members/assignable`. It returns the active members of your workspace.

```bash curl theme={null}
curl https://api.blujay.io/v1/team-members/assignable \
  -H "Authorization: Bearer blu_your_api_key"
```

```json Response (200) theme={null}
{
  "status": "success",
  "data": {
    "team_members": [
      { "id": "member_abc123", "name": "Sam Rivera", "email": "sam@example.com" }
    ]
  },
  "trace_id": "trc_9b1d3f5a7c0e",
  "timestamp": "2026-10-14T17:40:00.000Z"
}
```

Then pass the member's `id` as `team_member_id` to `PATCH /v1/contacts/{contact_id}/assignee`. The response is the contact with `assigned_team_member` set and `assignment_changed`, which is `true` when the assignee changed. A change also sends the `contact.assigned` webhook.

```bash curl theme={null}
curl -X PATCH https://api.blujay.io/v1/contacts/contact_abc123/assignee \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "team_member_id": "member_abc123" }'
```

There is no endpoint to unassign a contact. `contact.assigned` carries `assigned_team_member: null` only when someone unassigns the contact in the Blujay app.

## Limitations

* US phone numbers only. Any other number returns `400` [`VALIDATION_ERROR`](/errors/validation-error).
* One contact per phone number and one per email in a workspace: `409` [`CONTACT_ALREADY_EXISTS`](/errors/contact-already-exists) or [`CONTACT_MATCH_CONFLICT`](/errors/contact-match-conflict).
* No tag removal, no tag list, no unassign, and no stage other than `Completed` through the API.
* No filter on `GET /v1/contacts` by tag, stage, assignee or `opted_out`. Search covers name, phone number and email only, up to 25 results.
* Creating or updating a contact doesn't make them messageable. Free Plan verification ([`CONTACT_NOT_VERIFIED`](/errors/contact-not-verified)), the AI Agent inbound-first rule ([`CONTACT_MUST_TEXT_FIRST`](/errors/contact-must-text-first)) and opt-outs ([`RECIPIENT_SUPPRESSED`](/errors/recipient-suppressed)) still apply. See [Plans](/plans.md).
* Creating a contact sends no webhook and starts no workflow. Only a new tag does.

## Next steps

> **[Opt-outs](/opt-outs.md)**
>
> Stop messages to a contact or an address, and lift an opt-out.

> **[Workflows](/workflows.md)**
>
> Enroll a contact in a manual workflow.

> **[Sending rules](/messaging/sending-rules.md)**
>
> Who you can message, and what a first message can contain.

> **[Webhook events](/webhooks/events.md)**
>
> Hear about tag and assignment changes.


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