Skip to main content
The concierge lets a member hold a conversation - a support thread, a booking chat, a notification feed. You list a member’s conversations, read message history, and send new messages, all from the app with the member token.
Reading works with either credential; writing does not. Listing conversations and paging message history accept your Org-API-Key (name the member with externalUserID) or a member token. Sending a message and opening the SSE stream are member-token only - a server cannot post as its own member.See Authentication for the two-token model and refresh contract.
For live incoming messages, do not poll. Each SDK exposes a streaming subscription over Server-Sent Events that delivers new messages as they arrive.

Stream live messages

Subscribe to new concierge messages over SSE, consumed as an AsyncIterable, AsyncStream, or Flow. The right way to receive incoming messages.

Finding a conversation

You never create a conversation - they are created for you. Every member has a permanent concierge thread from the moment they exist, before they have redeemed anything, plus a dedicated thread per booking. So the only question is which id to use:
A booking thread is type: "CONCIERGE" and titled with the experience; the permanent thread is type: "GENERAL" with a null title, so label it yourself. Pass either id to listMessages, sendMessage or the SSE stream.
conversationId on the redemption is available from 0.6.0. On earlier versions, find a booking’s thread through listConversations instead.

The flow

List the member’s conversations, read a thread’s history, then send a reply:

Attachments

A concierge can send tickets, an itinerary or a photo. Those arrive as a message whose attachments array is non-empty - and whose body is null, because a media message carries no text.
Attachment[]
required
Always present. Empty for a text message, so you can map over it without a guard.
The links are short-lived and must not be stored. Each one is minted when you read the message and expires about fifteen minutes later. Fetch the message again for a fresh link rather than caching the URL.This is deliberate: these URLs appear in message payloads, which get logged, and a long-lived signed link in a log file is a publicly readable ticket for as long as it lives.
url can be null if a link could not be minted. That degrades one attachment; it never fails the page of messages around it. Attachments arrive over the SSE stream too, on the same message, so a live conversation shows them without a refresh.
Sending attachments is not supported. Members send text; attachments are concierge-to-member only.

Operations

List conversations

GET /v1/conversations returns the member’s conversations.
string
Names the member when a machine token calls on their behalf.
The response is a ListConversationsResponse:
Conversation[]
required
The member’s conversations.

List messages

GET /v1/conversations/{id}/messages returns one page of a conversation’s messages with cursor pagination.
string
required
The conversation id.
integer
Page size. Defaults to the server’s default if omitted.
string
The nextCursor from the previous page. Omit for the first page.
string
Names the member when a machine token calls on their behalf.
The response is a ListMessagesResponse:
Message[]
required
The page of messages.
string | null
required
Pass this back as cursor to fetch the next page. null marks the end of the history.

Send a message

POST /v1/conversations/{id}/messages posts a message to the conversation.
string
required
The conversation id.
string
required
The message text to send.
The response is a SendMessageResponse:
boolean
required
true when the message was accepted.
Idempotency on sendMessage is supported via the Idempotency-Key header
  • a retried send replays rather than double-posting. The API reference omits the header on the {id} route because of an emitter limitation, not because it is unsupported, so set the header yourself when retrying. The SDKs send one automatically on every write. See Retries and idempotency.

Schemas

Conversation

Message

Messages paginate by cursor, newest history reachable page by page. To follow a thread live instead of re-fetching, print the recent backlog with listMessages, then stream everything that follows.

Knowing when a message arrives

Three ways, with genuinely different latency. Pick by where your code runs. In an app, use the stream. It relays new messages as they are written, and it is the only option that is genuinely live. On a backend, use the webhook. The stream needs a member token, so a server cannot hold one on a member’s behalf. The webhook is how your backend learns our concierge has replied - to send a push notification, or to mirror the thread into your CRM. See Webhooks.
Message history is a mirror, and it lags. GET /v1/conversations/{id}/messages reads a store synced from the live conversation on a one-minute cycle, so a message written seconds ago may not be there yet. Use it for backlog, never as a way to detect new messages.

Testing in the sandbox

In the sandbox, the concierge is fully self-contained: a message you send is never forwarded to our Ops team, and a sandbox concierge bot replies automatically. The bot’s reply streams back to you over GET /v1/conversations/{id}/stream exactly like a real concierge message, so you can build and test the whole send -> stream -> reply loop end to end without paging a human. In LIVE, messages reach a real concierge as usual.

Streaming messages

Receive live incoming messages over SSE.

Errors

The error taxonomy and stable codes for failed calls.