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

# Message versions and branching

> Revise a sent message on a conversation, render the version switcher, and select which version the conversation serves.

A conversation lets an end user revise a message they already sent. The
revision does not overwrite the original: it is stored as a second **version**
of that message, the reply streams on the new **branch**, and the conversation
serves that branch from then on. The user can switch back at any time.

The server owns all of the lineage. It stores the parent of every message,
assembles the branch a fetch returns, and records which branch the conversation
currently serves. A client holds no message tree, computes no ordering, and
never sends a lineage pointer of its own.

You adopt the whole capability through three touchpoints. They are additive on
the wire, so **a client that sends no `edit_of_message_id`, ignores `branch`,
and never calls the branch route keeps exactly today's behavior**: the same
payloads, the same insertion-ordered transcript.

| # | Touchpoint                                                | Where                                             |
| - | --------------------------------------------------------- | ------------------------------------------------- |
| 1 | Send `edit_of_message_id` alongside `message`             | `POST .../conversations/{conversation_id}/turns`  |
| 2 | Render the `branch` object on messages that have versions | `GET .../conversations/{conversation_id}`         |
| 3 | Send `message_id` to select a version                     | `POST .../conversations/{conversation_id}/branch` |

## 1. Revise a message

Take a normal turn, and add `edit_of_message_id` naming the user message the
new text replaces. `message` is required with it.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.usenexio.com/api/v1/conversation-instances/support-copilot/conversations/8f3d1c02-6b41-4a2e-9d77-2c5b0e1a44f9/turns \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Accept: text/event-stream" \
    -d '{
      "end_user": "u_8412",
      "message": "Compare the two quotes on deductible, not on premium.",
      "edit_of_message_id": "0f4c9a71-2d38-4b6c-8f10-71b2c9d43a55"
    }'
  ```

  ```python Python theme={null}
  import httpx

  with httpx.stream(
      "POST",
      "https://api.usenexio.com/api/v1/conversation-instances/support-copilot/conversations/8f3d1c02-6b41-4a2e-9d77-2c5b0e1a44f9/turns",
      headers={"Authorization": f"Bearer {api_key}", "Accept": "text/event-stream"},
      json={
          "end_user": "u_8412",
          "message": "Compare the two quotes on deductible, not on premium.",
          "edit_of_message_id": "0f4c9a71-2d38-4b6c-8f10-71b2c9d43a55",
      },
      timeout=None,
  ) as resp:
      for line in resp.iter_lines():
          print(line)
  ```

  ```typescript TypeScript theme={null}
  const resp = await fetch(
    "https://api.usenexio.com/api/v1/conversation-instances/support-copilot/conversations/8f3d1c02-6b41-4a2e-9d77-2c5b0e1a44f9/turns",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        Accept: "text/event-stream",
      },
      body: JSON.stringify({
        end_user: "u_8412",
        message: "Compare the two quotes on deductible, not on premium.",
        edit_of_message_id: "0f4c9a71-2d38-4b6c-8f10-71b2c9d43a55",
      }),
    },
  )
  ```
</CodeGroup>

What the server does with it:

* Stores the new text as a sibling version of the target message, not as a new
  message at the end of the transcript.
* Leaves the superseded exchange out of the history the model sees, so the
  reply answers the revised message alone.
* Streams the reply exactly like any other turn, over the same Server-Sent
  Events frames.
* Serves the new branch from then on.

The `conversation` frame at the start of the stream carries `user_message_id`,
the id of the message the turn just stored. Keep it if you want to offer a
second revision without refetching the conversation.

Only a `user` message can be revised. An assistant message target returns 400.

## 2. Render the version switcher

Fetch the conversation as usual. Every message now carries
`parent_message_id`, and a message that has sibling versions also carries
`branch`.

```json theme={null}
{
  "id": "0f4c9a71-2d38-4b6c-8f10-71b2c9d43a55",
  "role": "user",
  "turn_id": "6d2a4e88-1f57-4c93-b0aa-3e7d5c81b204",
  "config_version_hash": "b1c4f0e9d7a2",
  "parent_message_id": "9ab1f6c3-40de-4b21-8a77-15e0c2d9f381",
  "branch": {
    "index": 2,
    "count": 2,
    "siblings": [
      "7d21b8e4-59c0-42a7-91ff-6a3e08b4c7d2",
      "0f4c9a71-2d38-4b6c-8f10-71b2c9d43a55"
    ]
  },
  "content": [{ "type": "text", "text": "Compare the two quotes on deductible, not on premium." }],
  "created_at": "2026-08-14T16:04:11Z"
}
```

| Field               | Type          | Required | Meaning                                                                                   |
| ------------------- | ------------- | -------- | ----------------------------------------------------------------------------------------- |
| `parent_message_id` | uuid or null  | yes      | The message this one follows on its branch. Null when it starts the conversation.         |
| `branch`            | object        | no       | Present only where versions exist. Absent means one version, so there is nothing to draw. |
| `branch.index`      | integer       | yes      | 1-based position of the served version among its siblings, in creation order.             |
| `branch.count`      | integer       | yes      | How many versions exist at this point. Always greater than 1.                             |
| `branch.siblings`   | array of uuid | yes      | Every version's id, in creation order. Element 1 is `index` 1.                            |

Draw `branch` as a version switcher under the message: the pair `index` and
`count` reads as `2/2`, and the previous and next controls address
`siblings[index - 2]` and `siblings[index]`. Absence of the field is the whole
"no fork here" signal, so branch on presence rather than counting anything
yourself.

The transcript a fetch returns is the branch the conversation currently
serves, not every version. Superseded versions are reachable through
`branch.siblings` and are kept in full by the conversation export, which is an
audit artifact and stays version-complete.

## 3. Switch versions

Post the id of any message on the branch you want served. The server resolves
that message's ancestry, follows the newest reply from there, records the
selection, and returns the whole conversation detail payload for that branch.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.usenexio.com/api/v1/conversation-instances/support-copilot/conversations/8f3d1c02-6b41-4a2e-9d77-2c5b0e1a44f9/branch \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "end_user": "u_8412",
      "message_id": "7d21b8e4-59c0-42a7-91ff-6a3e08b4c7d2"
    }'
  ```

  ```python Python theme={null}
  detail = httpx.post(
      "https://api.usenexio.com/api/v1/conversation-instances/support-copilot/conversations/8f3d1c02-6b41-4a2e-9d77-2c5b0e1a44f9/branch",
      headers={"Authorization": f"Bearer {api_key}"},
      json={
          "end_user": "u_8412",
          "message_id": "7d21b8e4-59c0-42a7-91ff-6a3e08b4c7d2",
      },
  ).json()

  for message in detail["messages"]:
      print(message["role"], message["id"])
  ```

  ```typescript TypeScript theme={null}
  const resp = await fetch(
    "https://api.usenexio.com/api/v1/conversation-instances/support-copilot/conversations/8f3d1c02-6b41-4a2e-9d77-2c5b0e1a44f9/branch",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        end_user: "u_8412",
        message_id: "7d21b8e4-59c0-42a7-91ff-6a3e08b4c7d2",
      }),
    },
  )
  const detail = await resp.json()
  ```
</CodeGroup>

The 200 body is the same shape the conversation GET returns
(`conversation`, `messages`, `truncated`), already assembled for the selected
branch. One round trip both switches and renders, so you never preload a tree
and never reconcile an optimistic swap.

## What persists

The selection is stored on the conversation, not in your client:

* A later fetch returns the selected branch.
* The next turn continues the selected branch, and its messages are stored
  there.
* A reload, a second device, or a different client of the same conversation all
  see the branch that was selected last.

Switching back to an earlier version lands on that branch's newest reply.

## Errors

| Condition                                                                                   | Status | Code                    |
| ------------------------------------------------------------------------------------------- | ------ | ----------------------- |
| `edit_of_message_id` sent without `message`, or not a UUID                                  | 400    | Standard error envelope |
| Edit target is an assistant message                                                         | 400    | Standard error envelope |
| Edit target is unknown, or belongs to another org, conversation, or end user                | 404    | Standard error envelope |
| Branch `message_id` is not in this conversation, or the `end_user` assertion does not match | 404    | Standard error envelope |
| Conversation is archived                                                                    | 409    | `conversation_archived` |
| A turn segment is already running                                                           | 409    | `turn_in_progress`      |
| A confirmation gate is open and undecided                                                   | 409    | `pending_turn`          |

Two client-side consequences follow from the last two rows. A turn cannot be
cancelled, so disable the revise action while a turn streams rather than
sending into a `turn_in_progress` refusal. And a confirmation gate must be
resolved before a revision is accepted, so let the user answer the gate first.

The branch route takes no turn claim, so a switch and a running turn are not
serialized against each other. Disable the switcher while a turn streams.

## Compatibility

Both new fields and the new route are additive:

* `edit_of_message_id` is optional on the turn request.
* `parent_message_id` is new on every message; treat it as optional if you
  parse responses strictly and want to keep reading older stored payloads.
* `branch` is present only at a fork.
* The branch route is new. Not calling it means the conversation keeps serving
  the branch its last edit created, which for a conversation with no edits is
  the original single thread.
