<!-- https://missblue.dev/docs/conversations-api -->

# Conversations

A conversation is what a person sees: one thread with one handle, or a group. Distinct from the message list, which is a log.

Base URL: `https://api.missblue.dev`

Every request carries `Authorization: Bearer $MISS_BLUE_KEY`.

### GET /v1/conversations/sync

Read conversation sync availability and the latest result.

Limited to Macs behind numbers this credential can access. The cooldown is shared per Mac, not per caller. Optional last_started_at, last_completed_at, last_count, and last_error describe the latest observed work.

Example:

```bash
curl -X GET https://api.missblue.dev/v1/conversations/sync \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{ "limit_per_agent": 100, "cooldown_seconds": 300, "connected_agents": 1, "eligible_agents": 1, "syncing_agents": 0 }
```

### POST /v1/conversations/sync

Queue a fresh conversation history sync from eligible Macs.

Requests up to 100 recent conversations per eligible Mac. A 202 means queued, not complete; poll GET /v1/conversations/sync for progress. Offline Macs, active syncs, and Macs in the five-minute cooldown may be skipped.

Example:

```bash
curl -X POST https://api.missblue.dev/v1/conversations/sync \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{ "queued_agents": 1, "skipped_agents": 0 }
```

### GET /v1/threads

Every conversation on your project's numbers, most recent first.

contact_is_typing is transient presence reported by the Mac and expires automatically. unread is always 0 for a key. A key is not a person and has no place in a conversation it has read up to, so reporting every inbound message ever would have read as a backlog nobody has. number_id and search are applied in the query, before the page is cut — filtering the returned page client-side would search the newest few hundred conversations and report that as the whole answer. link_code and url are each conversation's short link (url is https://missblue.dev/c/ and the code). It opens only for somebody who may read the conversation, so it is safe to pass on.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `sort` | `string` | no | newest (default), oldest, or unreplied. |
| `number_id` | `uuid` | no | One of your lines, to see only what arrived on it. Use GET /v1/threads/lines for the ids and what each is carrying. |
| `search` | `string` | no | A handle, part of one, or a saved contact's name. Matches both, so somebody found by name does not have to be found again by number. |

Example:

```bash
curl -X GET https://api.missblue.dev/v1/threads \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{
  "total": 2,
  "data": [
    {
      "chat_id": "iMessage;-;+15555550100",
      "handle": "+15555550100",
      "is_group": false,
      "last_text": "Perfect, see you then",
      "last_at": "2026-08-23T09:12:04Z",
      "last_direction": "inbound",
      "message_count": 14,
      "unread": 0,
      "contact_is_typing": true,
      "last_replied_by": "Sam Okafor",
      "link_code": "k7Qm3xT9pa",
      "url": "https://missblue.dev/c/k7Qm3xT9pa"
    }
  ]
}
```

### PUT /v1/threads/{chat_id}/pin

Pin or unpin a conversation for the signed-in member.

Member sessions only. Pins stay ahead of unpinned conversations under every sort and do not affect other members. Current number and organization access is required; a pin never grants access.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `agent_id` | `string` | yes | The agent_id returned by the conversation list. |
| `pinned` | `boolean` | yes | true to pin; false to unpin. |

Request body:

```json
{
  "agent_id": "d8f25b47-dda6-41ec-a303-4bdfb9d6953e",
  "pinned": true
}
```

Example:

```bash
curl -X PUT https://api.missblue.dev/v1/threads/iMessage%3B-%3B%2B15555550100/pin \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "d8f25b47-dda6-41ec-a303-4bdfb9d6953e",
    "pinned": true
  }'
```

### GET /v1/threads/lines

Each of your numbers, and what it is carrying right now.

The counts behind a by-number filter, asked as its own question rather than derived from the conversation list. The list is a page: counting what came back would report the lines present in the newest few hundred conversations and call it the total. unread is 0 for a key, for the same reason it is 0 in the conversation list.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `project_id` | `uuid` | no | Narrow to one project. Omit for every project this credential reaches. |

Example:

```bash
curl -X GET https://api.missblue.dev/v1/threads/lines \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{
  "total": 2,
  "data": [
    { "number_id": "9c1e4f0a-3d7b-4a21-9f38-7c2d5e6b8a14", "unread": 0, "conversations": 214 },
    { "number_id": "b47d2a90-5e13-4c8f-a6b2-19d3f7c04e85", "unread": 0, "conversations": 37 }
  ]
}
```

### GET /v1/threads/unread

Unread totals across everything this credential can see.

Every visible unread message, not the unread on one page of conversations. notification_unread counts only what a person has asked to be notified about, so a badge and an inbox count can differ honestly. Both are 0 for an API key, which has read nothing and has no place to have read up to.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `project_id` | `uuid` | no | Narrow to one project. Omit for every project this credential reaches. |

Example:

```bash
curl -X GET https://api.missblue.dev/v1/threads/unread \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{
  "unread": 0,
  "latest_unread_at": null,
  "notification_unread": 0,
  "latest_notification_at": null
}
```

### POST /v1/read-receipts

Tell the customer their message was seen.

The "Read" that appears under their message on their phone. Distinct from POST /v1/threads/{chat_id}/read, which records where a person has read to so their own unread count is right and which nobody outside this system sees. Nothing sends this automatically: it is a claim that a human looked, and a receipt that fired because a dashboard was open is a lie told at scale.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `chat_id` | `string` | yes | The conversation to acknowledge. |
| `read` | `boolean` | no | Defaults to true. False marks it unread again. |

Request body:

```json
{
  "chat_id": "iMessage;-;+15555550100",
  "read": true
}
```

Example:

```bash
curl -X POST https://api.missblue.dev/v1/read-receipts \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chat_id": "iMessage;-;+15555550100",
    "read": true
  }'
```

Errors:

- `404` — No such conversation on a number your key holds.
- `409` — The number is temporarily unavailable (agent_offline). Nothing was acknowledged; retry shortly.

### POST /v1/typing

Show or hide the typing bubble in a conversation.

Do not block a reply on this. A bubble that arrives after the message it was meant to precede is worse than no bubble — send it, ignore the outcome, and reply. There is no guaranteed stop either: Messages.app clears it on its own after a few seconds of silence, which is the behaviour to rely on.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `chat_id` | `string` | yes | The conversation. Not a bare handle — guessing which of several threads was meant would show a stranger that somebody is typing. |
| `active` | `boolean` | no | true to start, false to stop. |

Request body:

```json
{
  "chat_id": "iMessage;-;+15555550100",
  "active": true
}
```

Example:

```bash
curl -X POST https://api.missblue.dev/v1/typing \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chat_id": "iMessage;-;+15555550100",
    "active": true
  }'
```

Errors:

- `404` — No such conversation on a number your key holds.
- `409` — The number is temporarily unavailable (agent_offline). Retry shortly, or skip it — a late typing bubble is worse than none.

### POST /v1/typing-indicators

Show a bounded typing bubble to a contact.

Use this form when your integration knows the contact rather than the chat_id. The one-to-one iMessage conversation must already exist. Start defaults to 60 seconds and is always cleared by five minutes; another start extends the active timer, while stop is a safe no-op when nothing is active.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `number` | `E.164 string` | yes | The contact who sees the bubble. |
| `number_id` | `uuid` | no | Which of your numbers sends it. Preferred over a mutable handle. |
| `from_number` | `E.164 string` | no | Handle alternative to number_id. Omit both only when your key holds one number. |
| `state` | `start | stop` | no | Defaults to start. |
| `max_duration_ms` | `integer` | no | 1–300000. Defaults to 60000. |

Request body:

```json
{
  "number": "+15555550100",
  "from_number": "+16465550142",
  "state": "start",
  "max_duration_ms": 60000
}
```

Example:

```bash
curl -X POST https://api.missblue.dev/v1/typing-indicators \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "+15555550100",
    "from_number": "+16465550142",
    "state": "start",
    "max_duration_ms": 60000
  }'
```

Response:

```json
{
  "status": "ACTIVE",
  "number": "+15555550100",
  "from_number": "+16465550142",
  "chat_id": "iMessage;-;+15555550100",
  "max_duration_ms": 60000
}
```

Errors:

- `400` — The number or duration is invalid, the source is ambiguous, or no prior iMessage conversation exists.
- `404` — The source number is not held by this credential.
- `409` — The source number is temporarily unavailable (agent_offline). Retry shortly.
