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.
Conversations are member-mode: every call here uses the short-lived member token your backend mints, not the Org-API-Key. 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.

The flow

List the member’s conversations, read a thread’s history, then send a reply:
import { initialize } from "@expys/sdk";

const token = process.env.EXPYS_MEMBER_TOKEN;
if (!token) {
  throw new Error(
    "Set EXPYS_MEMBER_TOKEN (a member token from your backend's /v1/auth/exchange)",
  );
}

const expys = initialize({
  baseUrl: process.env.EXPYS_BASE_URL,
  environment: "sandbox",
  token,
});

const externalUserID = process.env.EXPYS_EXTERNAL_USER_ID;

async function main(): Promise<void> {
  const { conversations } = await expys.listConversations({ externalUserID });
  console.log(`found ${conversations.length} conversations`);

  const conversation = conversations[0];
  if (!conversation) {
    return;
  }
  console.log(`reading: ${conversation.title ?? conversation.id}`);

  const { messages } = await expys.listMessages(conversation.id, {
    externalUserID,
    limit: 50,
  });
  for (const message of messages) {
    console.log(`[${message.authorID}] ${message.body ?? "(no body)"}`);
  }

  // Writes auto-send an Idempotency-Key so a retry replays rather than double-posts.
  const result = await expys.sendMessage(conversation.id, "Hello from the SDK");
  console.log(`message sent: ok=${result.ok}`);
}

Operations

OperationSDK methodReturns
List conversationslistConversations({ externalUserID? }){ conversations: Conversation[] }
List messageslistMessages(id, { limit?, cursor?, externalUserID? }){ messages: Message[], nextCursor }
Send a messagesendMessage(id, message){ ok }

List conversations

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

List messages

GET /v1/conversations/{id}/messages returns one page of a conversation’s messages with cursor pagination.
id
string
required
The conversation id.
limit
integer
Page size. Defaults to the server’s default if omitted.
cursor
string
The nextCursor from the previous page. Omit for the first page.
externalUserID
string
Names the member when a machine token calls on their behalf.
The response is a ListMessagesResponse:
messages
Message[]
required
The page of messages.
nextCursor
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.
id
string
required
The conversation id.
message
string
required
The message text to send.
curl -X POST https://api.expys.com/v1/conversations/CONVERSATION_ID/messages \
  -H "Authorization: Bearer YOUR_MEMBER_TOKEN" \
  -H "Idempotency-Key: 0f3a9c2e-4b1d-4e6a-9c7b-1f2e3d4c5b6a" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Hello from the concierge" }'
The response is a SendMessageResponse:
ok
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

FieldTypeMeaning
idstringThe conversation id. Use it for listMessages and sendMessage.
typestringThe conversation kind.
titlestring | nullA display title. May be null for untitled threads.
lastMessageAtstring | nullISO-8601 time of the most recent message, or null if empty. Useful for sorting threads.

Message

FieldTypeMeaning
idstringThe message id.
typestringThe message kind.
authorIDstringIdentifies the sender. Compare it against the member to tell incoming from outgoing.
bodystring | nullThe message text. May be null (for example, a non-text or system message).
createdAtstringISO-8601 timestamp.
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.

Streaming messages

Receive live incoming messages over SSE.

Errors

The error taxonomy and stable codes for failed calls.