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

# Messaging & Support

> Reference for the MessagingService class in the Tybrite SDK.

The `MessagingService` class (accessed via `client.messaging`) handles real-time customer ↔ store communication. It manages threaded conversations, message delivery, read state, and full thread lifecycle management.

## Security Model

Messaging threads carry sensitive customer information — order inquiries, complaints, contact details, and uploaded attachments. To prevent one customer from reading or changing another customer's conversation, **every customer-facing messaging endpoint requires a signed customer session token** (`xAuthToken`) issued by `client.authentication`. Ownership is enforced on every call:

| Applies to                | Rule                                                                                                                                                           |
| :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| List / create operations  | The `customer_id` you supply in the query or request body must match the `customer_id` on your session token.                                                  |
| Thread reads & writes     | The thread must belong to the customer on the session token. Applies to `getThread`, `updateThread`, `getThreadMessages`, `sendMessage`, and `markThreadRead`. |
| Per-message edit & delete | You can only edit or delete messages the customer personally authored — not store or system messages.                                                          |

Obtain a customer session via `client.authentication.login(...)` or `client.authentication.verifyOtp(...)` and pass `customerSession.access_token` as `xAuthToken` on every call below.

<Tip>
  Each `Thread` object returned by `listThreads` and `getThread` includes an `unread_count_customer` field. Sum these client-side to compute a customer's total unread count — no separate endpoint is needed.
</Tip>

## Thread Discovery & Filtering

### `listThreads`

Retrieve message threads with advanced server-side filtering. Pinned threads are automatically returned at the top of the result set.

```typescript theme={null}
const { threads, pagination } = await client.messaging.listThreads({
  xAuthToken: customerSession.access_token, // Must match customerId claim
  customerId: 'cust_123',
  status: 'active',           // active | resolved | closed | escalated | pending
  priority: 'high',           // low | normal | high | urgent
  archived: false,            // Filter by archive state
  unread: true,               // Only show threads with unread messages
  limit: 20,
  fields: 'id,subject,status,is_pinned,unread_count_store'
});
```

```json Response theme={null}
{
  "threads": [
    {
      "id": "06084af6-7334-43d6-82c0-935f92335a25",
      "subject": "Question about my order",
      "status": "active",
      "priority": "normal",
      "thread_type": "order_inquiry",
      "last_message_at": "2026-06-20T11:28:58.338+00:00",
      "last_message_by": "customer",
      "unread_count_customer": 0,
      "unread_count_store": 1,
      "is_archived": false,
      "is_muted": false,
      "is_pinned": false
    }
  ],
  "pagination": { "limit": 50, "next_cursor": null, "has_more": false }
}
```

<Note>The `customerId` query parameter must match the `customer_id` on `xAuthToken`, otherwise the request is rejected.</Note>

#### Computing total unread

Each thread returned by `listThreads` includes `unread_count_customer`. Sum these client-side to display a "new messages" badge:

```typescript theme={null}
const { threads } = await client.messaging.listThreads({
  customerId: customer.id,
  xAuthToken: customerSession.access_token
});

const totalUnread = threads.reduce((sum, t) => sum + (t.unread_count_customer || 0), 0);
// → e.g. 3   (each thread carries its own unread_count_customer)
```

To fetch only threads with unread messages, pass `?unread=true` as a filter.

***

### `getThread`

Retrieve high-level metadata for a single thread.

```typescript theme={null}
const thread = await client.messaging.getThread({
  xAuthToken: customerSession.access_token, // Must own this thread
  id: 'thread-uuid-123',
  fields: 'subject,priority,status,is_muted,is_archived'
});
```

```json Response theme={null}
{
  "id": "06084af6-7334-43d6-82c0-935f92335a25",
  "customer_id": "88ad3683-d291-4c70-8f8b-9903a42bde1b",
  "customer_name": "Jane Doe",
  "customer_email": "jane.doe@example.com",
  "customer_phone": null,
  "store_id": "ef7b07d3-424b-4b03-b1d4-30a2d2073b61",
  "store_name": "Galactic Test Store",
  "order_id": null,
  "product_id": null,
  "subject": "Question about my order",
  "thread_type": "order_inquiry",
  "status": "active",
  "priority": "normal",
  "last_message_at": "2026-06-20T11:28:58.338+00:00",
  "last_message_by": "customer",
  "unread_count_customer": 0,
  "unread_count_store": 1,
  "is_archived": false,
  "is_muted": false,
  "is_pinned": false
}
```

<Note>The thread must belong to the customer on your session token, otherwise the request is rejected.</Note>

***

### `updateThread` 🔒

Update thread state including status, priority, or lifecycle flags.

<Note>Requires a **Secret Key** (for merchant lifecycle changes) **or** a customer `xAuthToken` belonging to the thread owner (for customer-driven actions like muting or archiving their own conversation). A customer request must own the thread, otherwise it is rejected.</Note>

```typescript theme={null}
await client.messaging.updateThread({
  xAuthToken: customerSession.access_token, // Required when called with a publishable key
  id: 'thread-uuid-123',
  requestBody: {
    status: 'resolved',
    priority: 'normal',
    archived: true,
    pinned: false,
    muted: true
  }
});
// → { thread: { id, status: 'resolved', priority: 'normal', is_archived: true, is_muted: true, is_pinned: false, … } }
```

***

### `markThreadRead` 🔒

Marks all customer messages in the thread as `read` and resets the store unread count to 0.

```typescript theme={null}
await client.messaging.markThreadRead({
  xAuthToken: customerSession.access_token, // Must own this thread
  id: 'thread-uuid-123'
});
// → { success: true, thread_id: '06084af6-7334-43d6-82c0-935f92335a25' }
```

<Note>The caller must own the thread. The `id` must be a valid UUID — malformed values return `400`.</Note>

***

## Message Operations

### `getThreadMessages`

Fetch paginated history for a thread. Deleted messages are automatically excluded.

| Param    | Type     | Description                                    |
| :------- | :------- | :--------------------------------------------- |
| `limit`  | `number` | Max messages per page (default 50).            |
| `cursor` | `string` | Base64-encoded `created_at` for next page.     |
| `order`  | `string` | `asc` (oldest first) or `desc` (newest first). |

```typescript theme={null}
const { messages, pagination } = await client.messaging.getThreadMessages({
  xAuthToken: customerSession.access_token, // Must own this thread
  id: 'thread-uuid-123',
  limit: 50,
  order: 'desc' // Get newest first
});
```

```json Response theme={null}
{
  "messages": [
    {
      "id": "33e2c744-37a9-42df-a9df-447715526a41",
      "message_content": "Thanks! Also, can I add gift wrapping to this order?",
      "message_type": "text",
      "sender_type": "customer",
      "sender_id": "88ad3683-d291-4c70-8f8b-9903a42bde1b",
      "sender_name": "Jane Doe",
      "attachments": [],
      "metadata": {},
      "message_status": "sent",
      "read_at": null,
      "is_edited": false,
      "is_deleted": false,
      "created_at": "2026-06-20T11:29:02.467216+00:00",
      "updated_at": "2026-06-20T11:29:02.467216+00:00"
    }
  ],
  "pagination": { "limit": 50, "next_cursor": null, "has_more": false }
}
```

<Note>Only the thread's owning customer (or a Secret Key holder) can read its messages.</Note>

***

### `sendMessage` 🔒

Reply to an existing conversation.

```typescript theme={null}
await client.messaging.sendMessage({
  xAuthToken: customerSession.access_token, // Must own this thread
  id: 'thread-uuid-123',
  requestBody: {
    message: "We've received your request and are looking into it."
  }
});
// → { message: { id: '33e2c744-37a9-42df-a9df-447715526a41', created_at: '2026-06-20T11:29:02.467216+00:00' } }
```

<Note>The caller must own the thread. Returns `201` with the newly persisted message body on success.</Note>

***

### `editMessage` 🔒

Update the content of one of the customer's own messages. Sets `is_edited: true` and records
`edited_at`. Works with a **publishable** key plus the customer's `xAuthToken` (a customer-self write,
same browser-origin pattern as cart/wishlist) — the session token proves ownership of the message.

```typescript theme={null}
await client.messaging.editMessage({
  xAuthToken: customerSession.access_token, // Must be the original sender
  id: 'cc64ce9a-51ba-4860-ba1a-85f14899f5bf',
  requestBody: {
    message_content: "Thanks! Also — can I add gift wrapping and a note?"
  }
});
```

```json Response theme={null}
{
  "message": {
    "id": "cc64ce9a-51ba-4860-ba1a-85f14899f5bf",
    "message_content": "Thanks! Also — can I add gift wrapping and a note?",
    "is_edited": true,
    "edited_at": "2026-06-21T10:16:33.064+00:00",
    "updated_at": "2026-06-21T10:16:33.082663+00:00"
  }
}
```

<Note>You can only edit or delete messages the customer personally authored. Store and system messages cannot be modified.</Note>

***

### `deleteMessage` 🔒

Soft-deletes one of the customer's own messages. The message is hidden from list responses but
retained for audit trails. Works with a **publishable** key plus the customer's `xAuthToken` (the
session token proves ownership). Returns `success`, the `message_id`, and the `deleted_at` timestamp.

```typescript theme={null}
await client.messaging.deleteMessage({
  xAuthToken: customerSession.access_token, // Must be the original sender
  id: 'cc64ce9a-51ba-4860-ba1a-85f14899f5bf'
});
// → { success: true, message_id: 'cc64ce9a-51ba-4860-ba1a-85f14899f5bf', deleted_at: '2026-06-21T10:16:33.419Z' }
```

<Note>Same ownership check as `editMessage` — you can only delete a message you authored. Soft delete only; the row is retained for audit.</Note>

***

### `createConversation` 🔒

Initiate a new support ticket with an initial message.

```typescript theme={null}
const { thread, message } = await client.messaging.createConversation({
  xAuthToken: customerSession.access_token, // Required when body.customer_id is supplied
  requestBody: {
    customer_id: 'cust_123', // Must match token claim
    customer_name: 'Jane Doe',
    customer_email: 'jane@example.com',
    subject: "Missing Item - Order #8821",
    message: "My package arrived but the headphones were missing.",
    priority: 'high',
    thread_type: 'order_inquiry',
    order_id: 'order-uuid-abc',  // Optional: link to an order
    product_id: 'product-uuid'   // Optional: link to a specific product
  }
});
```

```json Response theme={null}
{
  "thread": {
    "id": "06084af6-7334-43d6-82c0-935f92335a25",
    "subject": "Missing Item - Order #8821",
    "status": "active",
    "priority": "high"
  },
  "message": {
    "id": "c423c11e-ab5f-40ab-8e17-429a39710d60",
    "created_at": "2026-06-20T11:28:58.245993+00:00"
  }
}
```

<Note>Whenever `body.customer_id` is supplied, it must match the `customer_id` on `xAuthToken`. Returns `201` with both the new `thread` and the initial `message`.</Note>

***

## Data Schemas

### `Thread` Structure

| Field                | Type      | Description                                             |
| :------------------- | :-------- | :------------------------------------------------------ |
| `id`                 | `uuid`    | Unique thread identifier.                               |
| `subject`            | `string`  | Thread title/topic.                                     |
| `status`             | `enum`    | `active`, `resolved`, `closed`, `escalated`, `pending`. |
| `priority`           | `enum`    | `low`, `normal`, `high`, `urgent`.                      |
| `is_archived`        | `boolean` | Hidden from main list.                                  |
| `is_muted`           | `boolean` | Notifications disabled.                                 |
| `is_pinned`          | `boolean` | Always at top of list.                                  |
| `unread_count_store` | `number`  | Messages awaiting store reply.                          |

### `Message` Structure

| Field             | Type      | Description                      |
| :---------------- | :-------- | :------------------------------- |
| `id`              | `uuid`    | Unique message identifier.       |
| `message_content` | `string`  | The text body.                   |
| `sender_type`     | `enum`    | `customer`, `store`, `system`.   |
| `is_edited`       | `boolean` | Whether content was changed.     |
| `is_deleted`      | `boolean` | Whether message is soft-deleted. |

***

<Tip>
  **Performance Tip:** When building a chat UI, use `getThreadMessages` with `order: 'desc'` to load the most recent activity first, then use the `next_cursor` to load history as the user scrolls up.
</Tip>

***

## Realtime messaging

Instead of polling `getThreadMessages` on a timer, you can receive a thread's new messages the moment they are sent — over a **WebSocket**. Use the `subscribeToThread` helper, exported from the SDK package root. It works out of the box on browsers, Deno, Bun, and Node 22+ — **no extra packages to install**.

### `subscribeToThread`

Opens a WebSocket to the thread and calls `onMessage` for every new message — including the store's replies — as it arrives. Pass your publishable key plus one customer credential (the signed-in customer's session token, or a bring-your-own-auth assertion). It returns an unsubscribe function.

```typescript theme={null}
import { subscribeToThread } from '@tybrite-labs/sdk';

const unsubscribe = subscribeToThread({
  apiKey: 'tybrite_pk_live_...',
  threadId,
  authToken: customerSession.access_token, // a signed-in Galactic Core customer
  onMessage: (message) => {
    console.log('New message:', message);
  },
  onPresence: ({ type, participant, participants }) => {
    // Someone joined or left THIS conversation.
    console.log(type, participant, participants);
  },
});

// later, when leaving the conversation:
unsubscribe();
```

| Option         | Type                                            | Description                                                                                                                                                                                                                   |
| :------------- | :---------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apiKey`       | `string`                                        | Your publishable key (`tybrite_pk_*`).                                                                                                                                                                                        |
| `threadId`     | `string`                                        | The conversation to subscribe to.                                                                                                                                                                                             |
| `authToken`    | `string`                                        | The signed-in customer's session token. Supply this **or** `externalAuth`.                                                                                                                                                    |
| `externalAuth` | `string`                                        | A signed bring-your-own-auth identity assertion. Supply this **or** `authToken`.                                                                                                                                              |
| `onMessage`    | `(message) => void`                             | Called with each new message as it arrives.                                                                                                                                                                                   |
| `onPresence`   | `({ type, participant, participants }) => void` | Optional — called when a participant joins (`type: 'join'`) or leaves (`type: 'leave'`) **this** conversation. `participants` is the current connected set (each `{ pid, role }`). Use it for a "seller is online" indicator. |
| `onTyping`     | `({ participant, isTyping }) => void`           | Optional — the other participant started/stopped typing. Ephemeral (never stored).                                                                                                                                            |
| `onRead`       | `({ participant, at }) => void`                 | Optional — the other participant read the conversation (a "seen" receipt); `at` is when.                                                                                                                                      |
| `onClose`      | `({ code, reason }) => void`                    | Optional — called if the connection closes.                                                                                                                                                                                   |
| `baseUrl`      | `string`                                        | Optional — defaults to `https://api.tybritelabs.com`.                                                                                                                                                                         |

The value `subscribeToThread` returns is a **callable unsubscribe function** (call it to disconnect)
that also carries two senders for publishing **your own** state:

```typescript theme={null}
const sub = subscribeToThread({ /* …options… */ });
sub.sendTyping(true);   // tell the other participant you're typing (call sendTyping(false) to stop)
sub.markRead();         // mark the conversation read — sends a live "seen" cue AND persists it
sub();                  // unsubscribe / disconnect
```

`markRead()` both sends the instant "seen" cue over the socket and persists your read position via
`POST /v1/messaging/threads/{id}/read`, so the receipt survives a reconnect.

The connection is authorized server-side: the customer credential is validated and confirmed to be the thread's participant before the socket is accepted, so a customer only ever receives their own thread's messages. The same call works for both Galactic-Core-authenticated customers (pass `authToken`) and bring-your-own-auth customers (pass `externalAuth`).

<Note>
  This is a WebSocket, not a request that counts toward your monthly request quota.
</Note>

### Raw WebSocket protocol (without the SDK)

If you're not using the TypeScript SDK — a mobile app, another language, or your own client — connect
to the WebSocket directly and speak the frame protocol below. Everything the `subscribeToThread`
helper does (messages, presence, typing, read receipts) is available on the raw socket.

**Connect** (browsers/WS handshakes can't set headers, so credentials go in the query string):

```
wss://api.tybritelabs.com/v1/messaging/threads/{threadId}/subscribe
  ?api_key=tybrite_pk_live_...
  &auth_token=<customer session token>      # OR:
  &external_auth=<bring-your-own-auth assertion>
```

The server validates the credential + participant membership before accepting the socket, then keeps
it open. All frames are JSON strings, except the heartbeat (a bare `ping`/`pong`).

**Server → client frames** (things you receive):

| `type`            | `payload`                                             | Meaning                                                                    |
| :---------------- | :---------------------------------------------------- | :------------------------------------------------------------------------- |
| `message.created` | `{ thread_id, message }`                              | A new message (yours or the store's).                                      |
| `presence.join`   | `{ participant: { pid, role }, participants: [...] }` | Someone connected to this conversation; `participants` is the current set. |
| `presence.leave`  | `{ participant, participants }`                       | Someone disconnected.                                                      |
| `presence.typing` | `{ participant: { pid, role }, is_typing }`           | The other party started/stopped typing.                                    |
| `presence.read`   | `{ participant, at }`                                 | The other party read the conversation (`at` = ISO timestamp).              |

**Client → server frames** (things you send):

| Frame                                                    | Purpose                                                                                                                                   |
| :------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
| `ping` (bare string)                                     | Heartbeat; the server replies `pong`. Send every \~25s to keep the connection alive.                                                      |
| `{ "type": "typing", "payload": { "is_typing": true } }` | Publish your own typing state (send `false` when you stop). Ephemeral — never stored.                                                     |
| `{ "type": "read", "payload": { "at": "<ISO>" } }`       | Publish a live "seen" cue. **Also call `POST /v1/messaging/threads/{id}/read`** to persist your read position so it survives a reconnect. |

`pid` is the participant id (the shopper's customer id, or `store` for the merchant); `role` is
`customer` or `store`. Presence is **per-conversation** — it reflects who's connected to *this*
thread, not global online state. Build a "seller is online" dot from `presence.join`/`leave`, a typing
bubble from `presence.typing`, and read ticks from `presence.read`.

***

### Response Codes

| Code  | Meaning                                                                                                       |
| :---- | :------------------------------------------------------------------------------------------------------------ |
| `200` | Success on reads, `updateThread` patches, and `markThreadRead`.                                               |
| `201` | Success on `createConversation` and `sendMessage` — a new resource was persisted.                             |
| `400` | Invalid request body, or a malformed UUID supplied to `markThreadRead` (or any thread/message ID path param). |
| `401` | Missing or invalid `xAuthToken`, or missing/invalid API key.                                                  |
| `403` | Customer token does not own the target thread or message.                                                     |
| `404` | Thread or message not found (or filtered out by store scoping).                                               |
| `429` | Rate limit exceeded.                                                                                          |
| `500` | Internal server error — safe to retry idempotent reads.                                                       |
