> ## 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, 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 later.

The server owns 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 does not need to hold a message tree or compute an ordering.

You adopt the capability through three touchpoints. A client that never sends `edit_of_message_id` and never calls the branch route sees one linear transcript in the order messages were stored.

| # | 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` |

All three accept an organization API key or a scoped key with `conversations:use`.

## 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 -N -X POST https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/turns \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Accept: text/event-stream" \
    -d '{
      "end_user": "u_dana_ortiz",
      "message": "Summarize only the failed runs, not all of them.",
      "edit_of_message_id": "0f4c9a71-2d38-4b6c-8f10-71b2c9d43a55"
    }'
  ```

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

  with httpx.stream(
      "POST",
      "https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/turns",
      headers={"Authorization": "Bearer " + os.environ["NEXIO_API_KEY"], "Accept": "text/event-stream"},
      json={
          "end_user": "u_dana_ortiz",
          "message": "Summarize only the failed runs, not all of them.",
          "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/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/turns",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.NEXIO_API_KEY}`,
        "Content-Type": "application/json",
        Accept: "text/event-stream",
      },
      body: JSON.stringify({
        end_user: "u_dana_ortiz",
        message: "Summarize only the failed runs, not all of them.",
        edit_of_message_id: "0f4c9a71-2d38-4b6c-8f10-71b2c9d43a55",
      }),
    },
  )
  console.log(await resp.text())
  ```
</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 frames. See [Turns and streaming](/conversations/turns-and-streaming).
* Serves the new branch from then on.

The `conversation` frame carries `user_message_id`, the id of the message this turn stored. It is the first frame of the stream unless the message carries attachments, in which case an `attachment` frame comes first. Keep `user_message_id` 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 invalid_edit_target`.

## 2. Render the version switcher

Fetch the conversation as usual. Every message 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",
  "content": [{ "type": "text", "text": "Summarize only the failed runs, not all of them." }],
  "turn_id": "6d2a4e88-1f57-4c93-b0aa-3e7d5c81b204",
  "config_version_hash": "4f1c9a7e2b8d3065",
  "created_at": "2026-09-23T16:04:11.201884Z",
  "parent_message_id": "9ab1f6c3-40de-4b21-8a77-15e0c2d9f381",
  "branch": {
    "index": 2,
    "count": 2,
    "siblings": [
      "7d21b8e4-59c0-42a7-91ff-6a3e08b4c7d2",
      "0f4c9a71-2d38-4b6c-8f10-71b2c9d43a55"
    ]
  }
}
```

| 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: its ancestors, then the newest reply at each step below. Superseded versions are reachable through `branch.siblings` and are kept in full by the [conversation export](/conversations/export-and-retention), which keeps every version.

## 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/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/branch \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "end_user": "u_dana_ortiz",
      "message_id": "7d21b8e4-59c0-42a7-91ff-6a3e08b4c7d2"
    }'
  ```

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

  detail = httpx.post(
      "https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/branch",
      headers={"Authorization": "Bearer " + os.environ["NEXIO_API_KEY"]},
      json={
          "end_user": "u_dana_ortiz",
          "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/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/branch",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.NEXIO_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        end_user: "u_dana_ortiz",
        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, so one request both switches and returns what to render.

## 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 the conversation currently serves.

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

## Errors

| Condition                                                                           | Status | Code                     |
| ----------------------------------------------------------------------------------- | ------ | ------------------------ |
| Body is not JSON, or `end_user` is missing                                          | 400    | `invalid_request`        |
| `edit_of_message_id` sent without `message`, or not a UUID                          | 400    | `invalid_turn_request`   |
| Edit target is an assistant message                                                 | 400    | `invalid_edit_target`    |
| Edit target is not a message in this conversation                                   | 404    | `message_not_found`      |
| Branch `message_id` is not a message in this conversation, or not a UUID            | 404    | `message_not_found`      |
| The conversation does not exist for this org, environment, instance, and `end_user` | 404    | `conversation_not_found` |
| Conversation is archived (turns and branch switches)                                | 409    | `conversation_archived`  |
| The instance has no published version (turns)                                       | 409    | `instance_not_published` |
| A turn segment is already running                                                   | 409    | `turn_in_progress`       |
| The previous turn is paused, waiting for a confirmation or for client tool results  | 409    | `pending_turn`           |

`pending_turn` fires for both kinds of pause: an open confirmation and an open client tool handoff. Its `details.pause_kind` is `confirm` or `handoff`, and `details.pending_call_ids` lists the calls to resolve. See [Client tools and confirmations](/conversations/client-tools).

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 paused turn must be resolved before a revision is accepted, so let the user answer the confirmation or let your application post the tool results first.

The branch route does not take the turn lock, so a switch and a running turn are not serialized against each other. Disable the switcher while a turn streams.

## Compatibility

Both fields and the branch route are additive:

* `edit_of_message_id` is optional on the turn request.
* `branch` is present only at a fork.
* Not calling the branch route means the conversation keeps serving the branch its last edit created, which for a conversation with no edits is the original single thread.
