# Shared Thread Read State

Read and advance **your own** reading position on a shared thread, so a client can put you back where you stopped and draw an "N new messages" divider instead of always landing at the newest message.

The owner read routes are ownership-scoped and return `404` for a share-link recipient. These are the slug-scoped equivalents. Authorization is the same as reading the thread's messages — the slug must resolve for you — so both a view-only visitor and a joined participant can use them.

The marker is **per-viewer**: it is keyed on the authenticated user and is never shared between people. It is also the same read cursor the thread's unread badge uses, so marking read here clears the thread's unread indicator everywhere rather than keeping a separate position that drifts.

## Authentication

Requires **user** authentication (Bearer token). An API key has no recipient identity and is rejected with `403`.

## Path Parameters

**`slug`** string required

The share-link slug, from the share URL.

## Request Body (PUT only)

**`last_read_message_id`** string required

The newest message you have now read. It must be a message this thread actually shows you; an unknown, deleted, hidden, or other-thread id returns `404` rather than being stored, because a marker pointing at nothing is a restore position you could never clear.

The write is **monotonic**: a message older than your stored marker leaves the stored one in place. Scroll-driven calls, a second tab, and late retries can therefore never rewind your progress.

## Returns

Both methods return the read state as it stands **after** the call, so a client never has to re-read to learn the outcome.

- **last_read_message_id** `string | null` — the message you last read. `null` is normal: you have never opened the thread, or your cursor was set by a whole-thread mark-read that named no message.
- **last_read_at** `string | null` — timestamp of the cursor. Set whenever any read state exists, even when `last_read_message_id` is `null`.
- **unread_count** `integer` — messages after the cursor you have not seen. Counts only rows the transcript renders, and **never your own messages**.
- **first_unread_message_id** `string | null` — the message to draw the divider above, and to pass as the `around` anchor when jumping there. Computed server-side, so a deleted or invisible row right after the cursor cannot become a dead anchor. `null` exactly when `unread_count` is `0`.
- **unread_count_capped** `boolean` — `true` when more messages are unread than `unread_count` reports (the count stops at 999 so the scan stays bounded). Render `999+` rather than an exact number when set.


## Code Examples

### cURL


```bash
# Where did I stop reading?
curl -X GET "https://api.aitronos.com/v1/shared-threads/abc123slug/read-state" \
  -H "Authorization: Bearer YOUR_TOKEN"

# I have now read up to this message.
curl -X PUT "https://api.aitronos.com/v1/shared-threads/abc123slug/read-state" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"last_read_message_id": "msg_abc123"}'
```

### Python SDK


```python
from aitronos import Aitronos

client = Aitronos(api_key="", token="YOUR_TOKEN")

state = client.threads.shared.get_read_state(slug="abc123slug")
if state.unread_count:
    print(f"{state.unread_count} new since {state.last_read_at}")
    print(f"divider goes above {state.first_unread_message_id}")

client.threads.shared.set_read_state(
    slug="abc123slug",
    last_read_message_id="msg_abc123",
)
```

### JavaScript


```javascript
const state = await (
  await fetch(
    "https://api.aitronos.com/v1/shared-threads/abc123slug/read-state",
    { headers: { Authorization: "Bearer YOUR_TOKEN" } },
  )
).json();

await fetch(
  "https://api.aitronos.com/v1/shared-threads/abc123slug/read-state",
  {
    method: "PUT",
    headers: {
      Authorization: "Bearer YOUR_TOKEN",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ last_read_message_id: "msg_abc123" }),
  },
);
```

## Response Examples

### Success — you have unread messages (200)


```json
{
  "last_read_message_id": "msg_abc123",
  "last_read_at": "2026-08-05T20:07:04Z",
  "unread_count": 12,
  "first_unread_message_id": "msg_def456",
  "unread_count_capped": false
}
```

### Success — never opened (200)

Open at the bottom with no divider. This is a normal state, not an error.


```json
{
  "last_read_message_id": null,
  "last_read_at": null,
  "unread_count": 0,
  "first_unread_message_id": null,
  "unread_count_capped": false
}
```

### Error — the message is not part of this thread (404)


```json
{
  "success": false,
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "The requested resource could not be found.",
    "system_message": "That message is not part of this thread",
    "type": "client_error",
    "status": 404,
    "details": { "last_read_message_id": "msg_ghost" },
    "trace_id": "...",
    "timestamp": "..."
  }
}
```

### Error — unknown / unauthorized / expired link (404)

The same uniform deny as opening the link — it never reveals whether the link or thread exists.


```json
{
  "success": false,
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "The requested resource could not be found.",
    "system_message": "Share link not found",
    "type": "client_error",
    "status": 404,
    "details": {},
    "trace_id": "...",
    "timestamp": "..."
  }
}
```

## Related Resources

- [Get Shared Thread Messages](/api-reference/shared-threads/get-shared-thread-messages) — Read the transcript the marker points into
- [Mark Shared Thread Read](/api-reference/shared-threads/mark-shared-thread-read) — Clear the whole thread at once, without naming a message
- [Get Shared Thread](/api-reference/shared-threads/get-shared-thread) — Open the share link
- [Join Shared Thread](/api-reference/shared-threads/join-shared-thread) — Join a live-shared thread