Skip to main content

Build a signed-in support chat that survives reconnects

Section: DOC-MA-conversations-build-chat-assistant#build-an-ai-chat-assistant.

Build a chat assistant that a signed-in user can ask a question, return to for the reply, and use with files and notifications. Start with one complete turn through the native REST API, then add the optional steps as your app needs them. Use the account and application credential pair provisioned for your deployment. The authentication guide explains backend and client credentials.

Start with text chat only: sign in, create a thread, send one question and reopen its reply. Verify that complete path before adding attachment upload or push notifications. The optional sections extend the same conversation; they are not prerequisites for the first working chat.

Prerequisites​

Section: DOC-MA-conversations-build-chat-assistant#prerequisites.

  • A publishable key (pk_…) configured for client use with the required user authentication and intended permissions. Before distributing the app, qualify the allowed operations and restrict key-management access: the key's class alone does not establish a complete management-access boundary. See the current Publishable Key Restrictions.
  • An identity provider configured for the publishable key. This walkthrough uses Firebase as an optional example; other configured identity providers can be used instead.
  • For the optional push step, a device or simulator with a supported push token.

Step 1: Sign in the customer and authenticate requests​

Section: DOC-MA-conversations-build-chat-assistant#step-1-set-up-authentication.

Client apps send two credentials on every request:

HeaderValueIdentifies
X-API-Keyyour publishable key (pk_…)your app / tenant
AuthorizationBearer <Firebase ID token>the signed-in end user

The Firebase ID token carries user identity — the platform derives userId, memory scope, and per-user data from it. You do not put a user ID in request bodies.

Sign in the user​

Section: DOC-MA-conversations-build-chat-assistant#sign-in-the-user.

Users sign in with Firebase Authentication using the Firebase config for your tenant. After sign-in, get an ID token from the Firebase SDK:

let token = try await Auth.auth().currentUser?.getIDToken()

Verify credentials with your first call​

Section: DOC-MA-conversations-build-chat-assistant#verify-credentials-with-your-first-call.

Confirm your key and token work together by listing threads — an empty list is the expected response on a fresh account:

curl -X POST https://api.travila.ai/api/v1/llm/list-threads \
-H "X-API-Key: pk_your_publishable_key" \
-H "Authorization: Bearer <firebase_id_token>" \
-H "Content-Type: application/json" \
-d '{}'

Reference: List conversation threads · Request fields.

Response:

{}

Reference: List conversation threads · Response fields.

Step 2: Start one support conversation​

Section: DOC-MA-conversations-build-chat-assistant#step-2-create-a-thread.

A thread is a conversation container with its own message history. Create one before sending messages — you only need to do this once per conversation.

curl -X POST https://api.travila.ai/api/v1/llm/create-thread \
-H "X-API-Key: pk_your_publishable_key" \
-H "Authorization: Bearer <firebase_id_token>" \
-H "Content-Type: application/json" \
-d '{
"title": "My First Thread"
}'

Reference: Create a new conversation thread · Request fields.

Response:

{
"thread": {
"threadId": "b81d5345-c1f9-4fb9-b558-a6327c75b842",
"title": "My First Thread",
"createdAt": "2026-04-23T16:34:02.673Z",
"updatedAt": "2026-04-23T16:34:02.673Z"
}
}

Reference: Create a new conversation thread · Response fields.

Save the threadId — you'll pass it as conversationKey in every subsequent request on this conversation.

Step 3: Send the question and wait for its own reply​

Section: DOC-MA-conversations-build-chat-assistant#step-3-send-a-message-and-poll-for-the-reply.

Sending a message is asynchronous: retain the returned runId and follow the bounded polling and correlation recipe. Model/tool latency varies. A queued response and an idle conversation do not establish this request's success.

Send the message​

Section: DOC-MA-conversations-build-chat-assistant#send-the-message.

curl -X POST https://api.travila.ai/api/v1/llm/send-message \
-H "X-API-Key: pk_your_publishable_key" \
-H "Authorization: Bearer <firebase_id_token>" \
-H "Content-Type: application/json" \
-d '{
"conversationKey": "b81d5345-c1f9-4fb9-b558-a6327c75b842",
"userMessage": {
"role": "ROLE_USER",
"content": [
{
"type": "CONTENT_PART_TYPE_TEXT",
"content": "What can you help me with?"
}
]
}
}'

Reference: Send a message to a conversation · Request fields.

Do not retry a send after a lost response until you have reconciled it. A repeated send can create duplicate work; see API retries.

Response:

{
"runId": "64403669-5989-4ec3-ad9c-d84223f9679f"
}

Reference: Send a message to a conversation · Response fields.

Poll conversation state​

Section: DOC-MA-conversations-build-chat-assistant#poll-conversation-state.

Poll within a bounded deadline and correlate the result with the accepted run, following the generation guide. Stop on a recognized terminal outcome for that run. An idle conversation alone does not prove that your request completed:

curl -X POST https://api.travila.ai/api/v1/llm/conversation-state \
-H "X-API-Key: pk_your_publishable_key" \
-H "Authorization: Bearer <firebase_id_token>" \
-H "Content-Type: application/json" \
-d '{
"conversationKey": "b81d5345-c1f9-4fb9-b558-a6327c75b842"
}'

Reference: Get full conversation state · Request fields.

Response (generation still running):

{
"messageHistory": [
{
"role": "ROLE_USER",
"content": [
{
"type": "CONTENT_PART_TYPE_TEXT",
"content": "What can you help me with?"
}
],
"timestamp": "2026-04-23T22:43:44.123Z",
"messageId": "2a1f33ce-1abc-4a5d-9e22-1c0d1a2b3c4d",
"sequence": "1"
}
],
"activeRunId": "64403669-5989-4ec3-ad9c-d84223f9679f",
"activeRunning": true
}

Reference: Get full conversation state · Response fields.

Response (this run completed — matching generatedBy and terminal status):

{
"messageHistory": [
{
"role": "ROLE_USER",
"content": [
{
"type": "CONTENT_PART_TYPE_TEXT",
"content": "What can you help me with?"
}
],
"timestamp": "2026-04-23T22:43:44.123Z",
"messageId": "2a1f33ce-1abc-4a5d-9e22-1c0d1a2b3c4d",
"sequence": "1"
},
{
"role": "ROLE_ASSISTANT",
"content": [
{
"type": "CONTENT_PART_TYPE_TEXT",
"content": "I can assist you with a variety of tasks..."
}
],
"timestamp": "2026-04-23T22:43:51.653Z",
"messageId": "3f2d44de-8db6-4f67-8e51-5c600902491b",
"sequence": "2",
"generatedBy": "64403669-5989-4ec3-ad9c-d84223f9679f",
"usage": {
"promptTokens": 359,
"completionTokens": 65,
"totalTokens": 424
},
"model": "google/gemini-3.1-flash-lite"
}
],
"lastRunStatus": "AGENT_STATUS_COMPLETED"
}

Reference: Get full conversation state · Response fields.

See Async Generation for the full polling recipe and backoff guidance. Streaming and polling availability are deployment-specific; reconnect and outcome correlation still apply with push updates — see Stream conversation activity.

Step 4: Restore the chat display from stored history​

Section: DOC-MA-conversations-build-chat-assistant#step-4-render-the-conversation.

Render messageHistory by stable identity and numeric sequence. Use activeRunning for activity only; keep a separate pending/unknown state for the operation you submitted. An absent activity flag means no run is currently active; it does not identify the outcome of your earlier send.

For structured output (a JSON plan, a form, a classification), use send-message-sync for a bounded wait and inspect its returned status before consuming a final answer. A queued run, pending client tool or expired wait can return before completion. An Idempotency-Key alone does not establish duplicate prevention; follow the retry and reconciliation guidance.

Close and reopen your chat view, then read the same conversation ID. Merge the returned messages instead of appending duplicate bubbles. Keep the pending run identity separately while its outcome is unresolved.

Finish the first text-chat recipe​

Section: DOC-MA-conversations-build-chat-assistant#run-it.

Use the thread created above to complete this customer journey:

  1. Sign in, send one question and keep the returned conversation/run identifiers.
  2. Display the stored user message and the assistant reply only when it belongs to that run.
  3. Leave the chat view and reopen the same thread. The existing messages appear once, in numeric sequence order.
  4. Send a follow-up in that thread after the earlier turn is resolved.

Finished result: the signed-in user can ask, leave, return and continue without a duplicate conversation or duplicated reply. If the wait ends before the outcome is known, keep the turn unresolved and offer a state read/reconnect. The completed-state example in step 3 shows the result to match. Use the recovery section below for credentials and interrupted work.

Recover after reconnect, backgrounding or sign-out​

Section: DOC-MA-conversations-build-chat-assistant#client-lifecycle-and-recovery.

Keep request authentication and token refresh in a shared client layer. Use your identity provider's SDK for refresh; coalesce concurrent refreshes and clear the shared task on both success and failure. Every waiting request must resolve, including failed refreshes. Token expiry decoded locally is a scheduling hint, not signature validation.

Retry reads with a bounded deadline and backoff. On 401, refresh once only when the error indicates an expired renewable session; a disabled key, wrong audience or revoked account needs different recovery. 429 may carry Retry-After as seconds or an HTTP date. Honor it within the overall deadline. An uncertain mutation must be reconciled before another send; see API retry limits.

Merge history by stable message identity and numeric 64-bit sequence. A snapshot followed by a live subscription is not gap-free without a replay cursor or buffering contract. See reconnecting to updates.

Register a changed push token while authenticated, retry registration with a bounded policy and reconcile after reconnect. Unregister the current device on sign-out and clear its local private state; do not delete every persistent schedule. See push delivery for device and logout handling.

Log request/operation IDs and sanitized errors for diagnosis. Never log API secrets, bearer tokens, signed URLs or private message bodies as routine diagnostics.

Keep the signed-in session working as tokens expire​

Section: DOC-MA-conversations-build-chat-assistant#step-7-handle-token-refresh.

Firebase ID tokens expire after approximately one hour. Cache the token and refresh it before it expires — do not request a new token on every API call.

  • Parse exp from the token payload and treat the token as expired approximately 5 minutes early to absorb clock skew and in-flight requests.
  • When within that buffer, force a refresh (getIDToken(forcingRefresh: true)) and coalesce concurrent refreshes so a burst of requests triggers one refresh, not many.

Keep a shared in-flight refresh task so concurrent callers can await the same result. Refresh once when the error identifies an expired renewable user token; a revoked key or wrong issuer/audience needs different recovery. Retry only when the original request was rejected before execution or the endpoint explicitly documents safe repetition. Reconcile uncertain mutations before another send. Clear the shared refresh task on success and failure so waiting callers receive the result.

Choose recovery from the actual authentication error​

Section: DOC-MA-conversations-build-chat-assistant#handling-error-responses.

StatusWhat it meansWhat to do
401Authentication rejectedInspect the reason. Refresh once for an expired renewable user token; do not refresh-loop on a revoked key or wrong issuer/audience. Reconcile uncertain mutations.
429Request throttledHonor retry headers with bounded backoff and an overall deadline. Do not automatically repeat an uncertain send. See Rate Limiting.

Variant: let the customer ask about a file​

Section: DOC-MA-conversations-build-chat-assistant#step-5-upload-a-file-optional.

When the customer asks about a report, extend the working text chat with the attachment recipe:

  1. Request a signed upload URL, upload the bytes and register the file through file storage.
  2. Retain the registered fileId and add it to a question in the existing thread.
  3. Require the file when an answer without it would be misleading, then follow the same run/result checks as text chat.

Finished result: the customer receives the file-aware answer or a visible upload, file or generation failure. An issued URL or successful registration alone is not that answer.

Signed URLs expire, and a stable file ID does not guarantee perpetual retention or access. The older inline small-file upload remains a legacy option; use the signed upload flow for this recipe and larger files.

Variant: bring the user back when a notification arrives​

Section: DOC-MA-conversations-build-chat-assistant#step-6-register-for-push-notifications.

Add push only after the text-chat path works. Register the authenticated device, handle a notification as a prompt to reload the relevant application state, and unregister on logout. Device registration is not proof that a future notification was delivered.

curl -X POST https://api.travila.ai/api/v1/notifications/register-push-device \
-H "X-API-Key: pk_your_publishable_key" \
-H "Authorization: Bearer <firebase_id_token>" \
-H "Content-Type: application/json" \
-d '{
"fcmToken": "firebase-cloud-messaging-token",
"platform": "PLATFORM_IOS",
"deviceId": "device-unique-id"
}'

Reference: Register a push notification device · Request fields.

Response:

{
"subscriberId": "user_123",
"success": true
}

Reference: Register a push notification device · Response fields.

On logout, unregister the device:

curl -X POST https://api.travila.ai/api/v1/notifications/unregister-push-device \
-H "X-API-Key: pk_your_publishable_key" \
-H "Authorization: Bearer <firebase_id_token>" \
-H "Content-Type: application/json" \
-d '{
"fcmToken": "firebase-cloud-messaging-token"
}'

Reference: Unregister a push notification device · Request fields.

Connect the real-time inbox​

Section: DOC-MA-conversations-build-chat-assistant#connect-the-real-time-inbox.

For an in-app notification feed, call get-inbox-session to get a WebSocket URL and token, then connect for live badge and feed updates. If the socket drops, fall back to polling get-inbox-unseen-count every ~30 seconds. See Build an in-app inbox for the full connection recipe.

Upcoming: application project setup​

Status: Upcoming — not yet available.

Section: DOC-MA-conversations-build-chat-assistant#first-project-setup

Create a support application's project, choose a supported data region and follow setup until the project is ready. Issue the client credentials as a separate action. Reloading setup returns to the same project without creating another project or secret.

If setup needs attention, inspect the unfinished step before making the first call. The first-project setup guide describes that journey.

This walkthrough currently starts with your provisioned account and key. Continue using those credentials until the new setup flow is available.

Document ID: DOC-MA-conversations-build-chat-assistant. Section identities and revisions.