# JavaScript API reference - Docs

**Support is in private alpha**

Support is currently in private alpha. [Request early access](https://us.posthog.com/settings/user-feature-previews#in-app-messenger) and we'll invite you when ready.

**Currently only available on the web. Requires `posthog-js` >= v1.337.0.**

The JavaScript API at `posthog.conversations` gives you full programmatic control over support conversations. Use it to build custom support interfaces or integrate support into your existing UI.

## Checking availability

Before using the API, check if conversations are available:

JavaScript

PostHog AI

```javascript
if (posthog.conversations.isAvailable()) {
  // Conversations API is ready to use
}
```

`isAvailable()` returns `true` when:

-   Conversations are enabled in your project settings
-   The conversations module has loaded successfully

## Sending messages

JavaScript

PostHog AI

```javascript
// Send a message (creates ticket if none exists)
const response = await posthog.conversations.sendMessage('Hello, I need help!')
// Send with user identification
const response = await posthog.conversations.sendMessage(
  'Hello, I need help!',
  {
    name: 'John Doe',
    email: 'john@example.com'
  }
)
// Force start a new conversation (new ticket)
const response = await posthog.conversations.sendMessage(
  'Starting a new conversation',
  undefined,  // userTraits
  true        // newTicket
)
```

**Parameters:**

-   `message` (string) – The message text to send
-   `userTraits` (optional) – Object with `name` and/or `email` for user identification
-   `newTicket` (optional, boolean) – If `true`, creates a new ticket even if one exists

**Response:**

typescript

PostHog AI

```typescript
interface SendMessageResponse {
  ticket_id: string      // ID of the ticket
  message_id: string     // ID of the created message
  ticket_status: string  // 'new' | 'open' | 'pending' | 'on_hold' | 'resolved'
  created_at: string     // ISO timestamp
  unread_count: number   // Unread messages from team (0 after sending)
}
```

## Fetching messages

JavaScript

PostHog AI

```javascript
// Get messages for the current active ticket
const response = await posthog.conversations.getMessages()
// Get messages for a specific ticket
const response = await posthog.conversations.getMessages('ticket-uuid')
// Get messages after a specific timestamp (for pagination)
const response = await posthog.conversations.getMessages(
  'ticket-uuid',
  '2024-01-15T10:30:00Z'
)
```

**Response:**

typescript

PostHog AI

```typescript
interface GetMessagesResponse {
  ticket_id: string
  ticket_status: string
  messages: Message[]
  has_more: boolean      // Whether more messages exist
  unread_count: number   // Unread messages from team
}
interface Message {
  id: string
  content: string
  author_type: 'customer' | 'AI' | 'human'
  author_name?: string
  created_at: string     // ISO timestamp
  is_private: boolean    // Internal notes (not shown to customer)
}
```

## Marking messages as read

JavaScript

PostHog AI

```javascript
// Mark messages as read for current ticket
await posthog.conversations.markAsRead()
// Mark messages as read for a specific ticket
await posthog.conversations.markAsRead('ticket-uuid')
```

**Response:**

typescript

PostHog AI

```typescript
interface MarkAsReadResponse {
  success: boolean
  unread_count: number   // Should be 0 after marking as read
}
```

## Fetching tickets

JavaScript

PostHog AI

```javascript
// Get all tickets
const response = await posthog.conversations.getTickets()
// Get tickets with filters
const response = await posthog.conversations.getTickets({
  status: 'open',
  limit: 10,
  offset: 0
})
```

**Parameters:**

typescript

PostHog AI

```typescript
interface GetTicketsOptions {
  status?: string   // Filter by status: 'new' | 'open' | 'pending' | 'on_hold' | 'resolved'
  limit?: number    // Number of tickets to return (default: 20)
  offset?: number   // Pagination offset (default: 0)
}
```

**Response:**

typescript

PostHog AI

```typescript
interface GetTicketsResponse {
  count: number      // Total count of tickets
  results: Ticket[]  // Array of tickets
}
interface Ticket {
  id: string
  status: string
  last_message?: string
  last_message_at?: string
  message_count: number
  created_at: string
  unread_count?: number
}
```

## Getting current context

JavaScript

PostHog AI

```javascript
// Get the current active ticket ID (null if no conversation started)
const ticketId = posthog.conversations.getCurrentTicketId()
// Get the widget session ID (persistent browser identifier)
const sessionId = posthog.conversations.getWidgetSessionId()
```

The **widget session ID** is a persistent UUID that:

-   Stays the same across page loads and browser sessions
-   Is used for access control (only this browser can access its tickets)
-   Survives user identification changes (`posthog.identify()`)

## User identification

Conversations work with both anonymous and identified users.

### Anonymous users

Messages are associated with the widget session ID. Access to the conversation persists across page loads.

### Identified users

When you call `posthog.identify()`, the conversation seamlessly continues:

-   Widget session ID remains the same (user keeps access)
-   Backend links the ticket to the identified Person
-   User traits from PostHog are used if not provided in `sendMessage()`

### User traits priority

When sending messages, user traits are resolved in this order:

1.  Explicitly provided in `sendMessage(message, { name, email })`
2.  PostHog person properties (`$name`, `$email`, `name`, `email`)
3.  Previously saved traits from the identification form

## Building a custom chat UI

You can build a completely custom chat UI using the API while disabling the default widget:

JavaScript

PostHog AI

```javascript
// In PostHog settings: set widgetEnabled to false
// Your custom implementation
async function initCustomChat() {
  // Wait for conversations to be available
  const checkAvailable = setInterval(() => {
    if (posthog.conversations.isAvailable()) {
      clearInterval(checkAvailable)
      loadExistingMessages()
    }
  }, 100)
}
async function loadExistingMessages() {
  const ticketId = posthog.conversations.getCurrentTicketId()
  if (ticketId) {
    const response = await posthog.conversations.getMessages()
    renderMessages(response.messages)
  }
}
async function sendMessage(text, userEmail) {
  const response = await posthog.conversations.sendMessage(text, {
    email: userEmail
  })
  // Add optimistic UI update
  addMessageToUI({
    id: response.message_id,
    content: text,
    author_type: 'customer',
    created_at: response.created_at
  })
}
// Poll for new messages
setInterval(async () => {
  if (posthog.conversations.getCurrentTicketId()) {
    const response = await posthog.conversations.getMessages()
    updateMessagesUI(response.messages)
  }
}, 5000)
```

## Recover tickets across browsers

Tickets are tied to the browser's widget session ID, so switching browsers or clearing storage means losing access. You can recover tickets by requesting a recovery link via email.

### Request a recovery link

JavaScript

PostHog AI

```javascript
await posthog.conversations.requestRestoreLink('user@example.com')
```

This sends an email containing a recovery link to the provided address. The link includes a `ph_conv_restore` token as a query parameter and expires after one hour.

**Parameters:**

-   `email` (string) – The email address used in previous conversations

The method is rate limited. If you send too many requests, it throws an error with a 429 status.

### Restore tickets from a recovery link

JavaScript

PostHog AI

```javascript
const result = await posthog.conversations.restoreFromUrlToken()
```

Reads the `ph_conv_restore` query parameter from the current URL and migrates the associated tickets to the current browser session. After processing, the query parameter is removed from the URL.

**Response:**

typescript

PostHog AI

```typescript
interface RestoreResult {
  status: 'success'
  migrated_ticket_ids: string[]  // IDs of tickets migrated to this session
}
```

The default widget calls `restoreFromUrlToken()` automatically on load, so you only need to call this yourself if you're building a custom UI.

## Events captured

The conversations module automatically captures these events:

| Event | Description |
| --- | --- |
| $conversations_loaded | Conversations API initialized |
| $conversations_widget_loaded | Widget UI rendered |
| $conversations_message_sent | User sent a message |
| $conversations_widget_state_changed | Widget opened/closed |
| $conversations_user_identified | User submitted identification form |
| $conversations_identity_changed | User called posthog.identify() |

### Workflow trigger events

In addition to the widget events above, the following server-side events are captured when ticket or message state changes. These events can be used as [workflow triggers](/docs/workflows/workflow-builder.md#conversation-event-triggers) to automate support processes.

| Event | Description | Properties |
| --- | --- | --- |
| $conversation_ticket_created | A new support ticket was created | – |
| $conversation_ticket_status_changed | Ticket status was updated | old_status, new_status |
| $conversation_ticket_priority_changed | Ticket priority was updated | old_priority, new_priority |
| $conversation_ticket_assigned | Ticket was assigned to a team member or AI | assignee_type, assignee_id |
| $conversation_message_sent | Team member sent a message on a ticket | message_id, message_content, author_type, author_id |
| $conversation_message_received | Customer sent a message on a ticket | message_id, message_content, author_type, customer_name, customer_email |

All workflow trigger events include these base properties: `ticket_id`, `ticket_number`, `channel_source`, `status`, and `priority`.

These events integrate with the rest of PostHog – use them in funnels, cohorts, or to trigger other actions.

## Persistence

The SDK persists the following data in `localStorage`:

-   Widget session ID (for access control)
-   Current ticket ID (to continue conversations)
-   Widget state (open/closed)
-   User traits (name/email from identification form)

This data is cleared when:

-   `posthog.reset()` is called
-   Browser storage is cleared

## Error handling

API methods return `null` if conversations are not available yet. Always check availability or handle null returns:

JavaScript

PostHog AI

```javascript
// Option 1: Check availability first
if (posthog.conversations.isAvailable()) {
  const response = await posthog.conversations.sendMessage('Hello')
}
// Option 2: Handle null response
const response = await posthog.conversations.sendMessage('Hello')
if (response) {
  console.log('Message sent:', response.message_id)
}
```

API calls may also throw errors for:

-   Network failures
-   Rate limiting (429 status)
-   Invalid ticket IDs
-   Server errors

JavaScript

PostHog AI

```javascript
try {
  await posthog.conversations.sendMessage('Hello')
} catch (error) {
  if (error.message.includes('Too many requests')) {
    // Handle rate limiting - wait and retry
  }
}
```

## API reference summary

| Method | Description | Returns |
| --- | --- | --- |
| isAvailable() | Check if conversations API is ready | boolean |
| isVisible() | Check if widget is rendered | boolean |
| show() | Show/render the widget | void |
| hide() | Hide/remove the widget | void |
| sendMessage(message, userTraits?, newTicket?) | Send a message | Promise<SendMessageResponse \\\| null> |
| getMessages(ticketId?, after?) | Fetch messages | Promise<GetMessagesResponse \\\| null> |
| markAsRead(ticketId?) | Mark messages as read | Promise<MarkAsReadResponse \\\| null> |
| getTickets(options?) | Fetch tickets list | Promise<GetTicketsResponse \\\| null> |
| getCurrentTicketId() | Get current ticket ID | string \\\| null |
| getWidgetSessionId() | Get widget session ID | string \\\| null |
| requestRestoreLink(email) | Send a recovery email to restore tickets | Promise<void> |
| restoreFromUrlToken() | Restore tickets from URL recovery token | Promise<RestoreResult \\\| null> |

### Community questions

Ask a question

### Was this page useful?

HelpfulCould be better