A text agent is reached over ordinary HTTP. Nothing joins a LiveKit room — you POST a message and read the reply back, either by polling the items list or by holding an event stream open. There is no SDK you must use and no media stack to install; curl is enough. That also makes text the cheapest surface to build on. The prompt, the tools, the knowledge bases and the handoffs behave the same here as they do on a call, so a text agent is the quickest way to get a script working before you put a voice in front of it.

The three things you are handling

A thread has many items and, over time, several sessions: the agent stays warm for a minute after each reply and then closes, so a conversation that runs all afternoon is a series of sessions on one thread. See the window lifecycle below. Which conversation a message opens follows the answering agent’s conversation.context. A text agent is always transcript — the setting is forced when you save, and summary is refused outright, because the analysis pass that writes those summaries is voice and video only. So the same contact_key always reaches the same thread, and the agent always has the whole history. See conversation memory.

contact_key

contact_key is your own stable id for the person: a user id, an account number, a session id if that is all you have. It is what makes the same key reach the same thread, and the only way to list somebody’s whole history back.
  • Unique in the workspace, 1–256 characters.
  • Must not start with a reserved prefix: telegram:, sip:, agent_copilot:, tool_copilot:, knowledge_copilot: or task_copilot:. Those name threads the platform builds itself, and the check is case-insensitive.
Pick something you will still be able to produce next month. A key that changes per page load turns one person into a hundred strangers, and an agent set to carry past conversations has nothing to carry.

Opening a conversation

POST /v1/conversations binds a contact_key to an agent. It sends nothing and starts nothing.
string
required
Your own id for the person. See the rules above.
uuid
The published text agent that answers. agent, agent_version, agent_override and agent_team are accepted here too, exactly as they are on a call — see per-call configuration. A resolved agent whose channel is not text is refused with this endpoint runs text agents; '<name>' resolves to a voice agent - place a voice or video call with POST /v1/calls/token instead.
object
What the agent already knows about the person, read as {{userdata.field}} from the prompt and every tool. It is merged onto the contact’s record, so it survives into their next thread and a tool may overwrite it mid-conversation. See userdata.
object
Values for the {{vars.*}} the agents on this thread declare. Fixed for the thread’s whole life — whatever turn 1 was opened with is what turn 40 reads — and never written onto anybody’s record. Strings only. See variables.
Re-posting to POST /v1/conversations with a key you already have does not reset the thread’s vars; it replaces that bag whole. Posting a message never touches it — POST /v1/conversations/messages takes no vars at all. If a value has to change per turn, it is userdata, not a variable.
Both endpoints need the editor role. A personal access token is a workspace credential — never put one in browser code. Proxy these two routes through your own server, the same way web calls do.

Sending a turn

POST /v1/conversations/messages is addressed by contact_key, not by conversation id.
string
required
Which thread. A key with no conversation is a 404, so opening one is not optional.
uuid
required
A UUID you generate. Re-sending with the same one returns the existing item rather than posting a second copy, which is what makes a retry after a timeout safe.
string
Up to 100,000 characters. May be empty only when images is not — a request with neither is refused with a message must carry text, an image, or both.
array
Up to four images, as base64 data: URLs. See images.
The response is 202 and carries only your own item. The agent has not answered yet and will not answer inside this request. Two ways to get the reply:

Reading the reply

Poll the items

GET /v1/conversations/{conversation_id}/items?order=desc returns the newest window, chronological within the page. Give it a few seconds. Expect several items rather than one when the agent calls tools: a function_call and a function_call_output land beside the message. Every item the agent produced carries trigger_item_id pointing back at the message that caused it, which is how you pick out the answer to this turn.

Or hold the stream open

GET /v1/conversations/{conversation_id}/events is a server-sent event stream that pushes the reply token by token. Open it before you send, or you miss that turn’s frames.

Every frame

Each frame repeats its own name in an event field, so a client narrows the union with a plain switch rather than pairing each frame with its name by hand. turn.status is one of running, done, error or canceled. Two rules that decide whether your client is correct:
  • assistant.delta frames append. They are not cumulative. Concatenate them; do not replace.
  • assistant.completed.text is every delta joined, which is there precisely so a client that missed some can replace what it accumulated instead of trying to reconcile it. Prefer it when it is present.
Delivery is best effort. Nothing on this stream is canonical: a dropped frame is recovered by reconnecting — the snapshot re-syncs the whole thread — or by reading GET /v1/conversations/{conversation_id}/items, which is the record.

In React

Items

Every row in a conversation is an item, and there are five kinds. Three fields say what to do with an item:
On a thread you drive over the API, outbound messages are not_applicable — there is no provider to deliver to; you read them from the items list or the stream. pending → sending → sent is what a Telegram thread shows. skipped is a trigger whose reply_mode told the platform not to send.
Items also carry trigger_item_id (which inbound message caused this), session_id, agent_id and agent_version (so a thread that handed off reads correctly line by line), attachments, and per-turn metrics. The full field reference is on conversations.

Images

Attach up to four images per message, as base64 data: URLs beside the text:
An image and its caption arrive as one turn and one item — one row, one bubble, one thing the model reads whole. A photo the agent has to reconcile with a separate sentence is a worse conversation than the one the sender wrote. The images come back on the item as attachments, each with a URL signed per request and an url_expires_at; the stream carries the signed URLs too, so a browser can render the photo it just sent without a reload. Sending an image to an agent whose model cannot read one is refused with a 409 naming the model — <provider>/<model> cannot read images — pick a model that canbefore anything is decoded or uploaded. Formats, sizes and the per-conversation ceiling are on vision and images. If you are building the sender in a browser, @talqing/sdk/browser exports talqingImageDataUrl(file), which validates, downscales and encodes a File into exactly the data_url above.

The window lifecycle

The agent does not start and stop per message. The first message of a thread cold starts a session — compiling the agent, connecting its MCP servers, loading its knowledge — and that session then stays warm and is reused by every message that follows. Cold start is why the first reply of a quiet thread is slower than the rest. The idle timeout is 60 seconds. With nothing pending for a minute, the session finalizes with close_reason: "idle_timeout", its usage is sealed and its cost is priced. The next message opens a new session on the same thread — and a text agent carries the whole transcript, so nothing is forgotten. The first four are normal endings and the session’s status is completed. Only orphaned is a failure — the session is marked failed, and its usage is whatever had been recorded when the worker died. None of the five loses the thread: the conversation and every item on it are untouched, and the next message picks up where the last one left off. Jobs are latest-wins. Send a second message while the agent is still working on the first and the in-flight turn is superseded: the turn frame reports status: "canceled" with superseded_by_item_id naming the message that displaced it, and the agent answers the newer message. That is what makes a chat box where someone corrects themselves mid-sentence behave the way they expect. A handoff keeps the window warm. Unlike a cold restart, the session stays open and runs the new compiled agent from the next turn; the move is recorded as an internal agent_handoff item. See handoffs.

What it costs

Text carries no platform fee. A text conversation costs your workspace only what your own language-model key was charged for the tokens — Talqing bills per-minute only on voice and video. See pricing and credits. Per-session usage and cost are on GET /v1/conversations/{conversation_id}/sessions; cost is null until billing has run.

Next

Telegram

Put the same published text agent in front of a Telegram bot.

Conversations

Reading threads back: sessions, versions, and a person’s whole history.

Conversation memory

What an agent carries from one conversation to the next.