# Conversations

A conversation is the 1:1 thread between two users. There is at most one per pair of users.

## Before you start

- Both users exist. A user exists after their first session, or after you create them from your server. Opening a conversation with an unknown external id answers 404. See [Sessions and tokens](/guides/sessions-and-tokens/) and [Users](/guides/users/).
- You have a session token for the user who opens the conversation. Every route here is a user route: `Authorization: Bearer <session token>`.
- The `policy` of your settings has a row from the user's role to the peer's role with `start_conversation` on.
- The organising features need their own `workspace` key, which is off by default: `aliases`, `pins`, `queue` and `notes`. A request that uses a key that is off answers 403 `policy_denied` with the key as `reason`.

```sh
export MC=https://app.mondochat.io/v1
export SESSION=<session token>
```

## Steps

1. Open a conversation with `POST /v1/conversations`. `with` is the peer's external id.

   ```sh
   curl -X POST $MC/conversations -H "Authorization: Bearer $SESSION" \
     -H 'content-type: application/json' -d '{"with": "bob"}'
   ```

   ```js
   const mc = (path, init = {}) =>
     fetch(`https://app.mondochat.io/v1${path}`, {
       ...init,
       headers: { authorization: `Bearer ${process.env.SESSION}`, 'content-type': 'application/json', ...init.headers },
     })

   const res = await mc('/conversations', { method: 'POST', body: JSON.stringify({ with: 'bob' }) })
   const conversation = await res.json()
   ```

   The answer is 201 for a new conversation and 200 for the existing one. A new conversation sends the `conversation.created` webhook. Opening checks bans, then your policy and the safety state. If your policy sets `require_vouch` for the pair, send a `vouch` token as well; see [Embed](/guides/embed/).

2. Check whether the user may open it before you store anything. Send `"draft": true`.

   ```sh
   curl -X POST $MC/conversations -H "Authorization: Bearer $SESSION" \
     -H 'content-type: application/json' -d '{"with": "bob", "draft": true}'
   ```

   When the pair has no conversation yet, the answer is 202 with the `Conversation` a create would return. Nothing is stored: no conversation, no event, no webhook. Its `id` is unknown to every other route and answers 404. When the pair already has one, the answer is 200 with that conversation. Every refusal a create gives, a draft gives too.

3. List the inbox with `GET /v1/conversations`. Pass `limit` (at most 50) and the `next_cursor` of the last page as `cursor`.

   ```sh
   curl "$MC/conversations?limit=50" -H "Authorization: Bearer $SESSION"
   ```

   ```js
   const inbox = await (await mc('/conversations?limit=50')).json()
   ```

   The answer is `{items, next_cursor, latest_seq, removed, next, has_more, counts}`. Each item has the peer, a preview of the last message, `unread_count`, and the viewer's `alias`, `pinned`, `archived`, `waiting_since` and `spent`. A conversation with no message yet is not listed. Pinned conversations come first, then the rest by `last_message_at`, newest first. The inbox holds at most 500 conversations. Keep the `latest_seq` of the first page: it is the cursor for [sync and recovery](/realtime/sync/).

4. Read one conversation with `GET /v1/conversations/{id}`. The `can` flags (`message`, `call`, `photos`, `voice`, `block`, `buy`, `offer`, `reason`) come from your policy and the safety state, so a client never has to guess. `can.reason` is `inbound` when the peer takes messages from members only and the viewer is a guest.

5. Turn on `workspace.queue` to get inbox views. Pass `view` as `to_answer`, `follow_up` or `archived`. `all` is the default and leaves archived conversations out.

   - `to_answer`: conversations the peer is waiting on. Pinned first, then regulars (the peer has a paid purchase in it), then the rest by the peer's tier and last message.
   - `follow_up`: conversations where the viewer wrote last and got no reply for at least 24 hours. Pinned first, then those whose last message is the viewer's offer card, then regulars, then the rest.
   - `archived`: archived conversations, pinned first.

   The first page of every view carries `counts: {to_answer, follow_up}`. A `view` of any other value answers 422.

6. Search with `q` (1 to 100 characters after trimming). It needs `workspace.queue` and matches the peer's name, the viewer's alias, the viewer's note, and the text or caption of messages the viewer can see. Archived conversations are included. A view-once caption never matches. `q` with a `view` other than `all` answers 422.

   ```sh
   curl "$MC/conversations?q=invoice" -H "Authorization: Bearer $SESSION"
   ```

7. Organise a conversation with `PATCH /v1/conversations/{id}`. Every field is private to the viewer, and fields you leave out stay as they are.

   ```sh
   curl -X PATCH $MC/conversations/$ID -H "Authorization: Bearer $SESSION" \
     -H 'content-type: application/json' \
     -d '{"alias": "Anna (regular)", "pinned": true, "archived": false, "note": "Prefers evenings"}'
   ```

   | Field | Needs | Rules |
   |---|---|---|
   | `alias` | `workspace.aliases` | Trimmed, at most 64 characters. `null` or empty clears it. |
   | `pinned` | `workspace.pins` | At most 10 pinned conversations. The 11th answers 403 `limit_reached` with reason `pins`. |
   | `archived` | `workspace.queue` | A message that is not silently dropped, from either user, unarchives the conversation for both. |
   | `note` | `workspace.notes` | Trimmed, at most 200 characters, no line break. `null` or empty clears it. |

   The fields apply together or not at all. The change goes to the viewer only: the peer gets nothing, and neither do webhooks, your server API or moderators. A change publishes `conversation.updated` to the viewer.

8. Let a seller invite a guest to sign up with `POST /v1/conversations/{id}/invite`. It needs `workspace.queue` and `guests.nudge.signup_url`. It posts a `signup_invite` card to the guest and answers the `Message`. `Conversation.invitable` tells a client when the call is allowed.

9. Let sellers keep saved replies with `GET` and `PATCH /v1/me/replies`. `quick` is a list of up to 20 replies and needs `workspace.quick_replies`. `away` is `{enabled, text}` and needs `workspace.away_reply`, which in turn needs `presence.enabled`. Only users whose role has `seller: true` may call it. Sending a quick reply is an ordinary message send.

## Check it works

1. Open a conversation with the same `with` twice. The first answer is 201 and the second is 200 with the same `id`.
2. Send one message, then list the inbox as either user. The conversation is listed with that message as its preview.
3. Send `PATCH` with `{"pinned": true}` as one user. The conversation comes first in that user's inbox and the peer's inbox is unchanged.

## When it fails

| Answer | Cause |
|---|---|
| 404 | The peer was never created, or the viewer is not a participant. |
| 422 | The user opened a conversation with themselves, or a field is out of range, or `q` is blank or longer than 100 characters. |
| 403 `banned` | The user or their IP is banned. See [Reports, blocks and bans](/guides/safety/). |
| 403 `policy_denied` | Your policy refuses it. The `reason` names the rule: `require_vouch`, `queue`, `aliases`, `pins`, `notes`, `invite`, `seller`, `quick_replies` or `away_reply`. |
| 403 `limit_reached` | The user is past `new_conversations_per_day`, has 10 pins, or sent a second invite within 24 hours. |
| 410 `gone` | A `changed_since` cursor is too old. Reload the inbox and sync from the new `latest_seq`. |

## Limits

- 50 conversations per page and 500 in the inbox. The first page always holds every pinned conversation.
- Only conversations with a message are listed, for both users.
- At most 10 pins, 64 characters for an alias, 200 for a note, 20 quick replies.
- `new_conversations_per_day` in your `limits` counts only stored conversations, never drafts.
- When a guest signs up and merges into a member, the peer's alias, pin and note carry over to the surviving conversation.

## Related topics

- [Open a conversation](/api/operations/open-conversation/), [list conversations](/api/operations/list-conversations/), [get a conversation](/api/operations/get-conversation/), [patch a conversation](/api/operations/patch-conversation/), [invite a guest](/api/operations/invite/)
- [Quick and away replies](/api/operations/get-replies/)
- [Settings reference](/reference/settings/) for `workspace`, `limits` and `policy`
- [Events](/reference/events/) and [sync and recovery](/realtime/sync/)
- [Errors](/errors/)