You are going to put a text agent inside your own React application: a chat panel for signed-in visitors, streaming replies token by token, showing when the agent is calling a tool, accepting photos, and answering from data only your app knows. There is no drop-in chat widget, no script tag and no iframe. This is an SDK integration you build — Talqing gives you a published agent, a conversation API and an event stream, and you own every pixel and every route in front of them. When it is finished you will have three small endpoints of your own and one React component.

What you need

Text carries no platform fee. A text conversation costs the workspace only what its own LLM key was charged; there is no per-minute charge of ours on this channel at all. See pricing and credits.

The build

1

Build and publish the text agent

channel: "text" removes the speech slots — no stt, no tts, no greeting, no turn handling. What is left is a prompt, a model, and whatever you attach.
Nothing can talk to a draft. Opening a conversation against an unpublished agent is refused.
A text agent’s memory is always the whole thread, and that is not configurable. On voice you choose what a new call knows about earlier ones — none, summary or transcript. On text, conversation.context is normalized to transcript on save, and asking for summary is refused outright: a text agent cannot summarize past conversations - call analysis, which writes those summaries, is voice and video only.The reason is that a chat thread has no boundary anyone would recognise. A phone call starts and ends; a message from the same person three weeks later is the same conversation to them. So every message that visitor ever sent is in the model’s context, and your token bill grows with the length of the thread. See conversation memory.
2

Open the conversation from your server, never from the browser

A personal access token is a workspace credential. It can read, change and delete every agent, call, number and conversation in the workspace, and opening a conversation needs the EDITOR role on top. It must never reach a browser — not in a bundle, not in a public environment variable, not behind a fetch the browser makes directly. Every call below runs on your server, behind your own sign-in.
contact_key is your own stable id for the person, up to 256 characters. Post the same key again and you get the same thread back, with everything the agent has learned about them still in userdata — so this endpoint is safe to call on every page load.
app/api/chat/open/route.ts
Two things are being decided here, and they are worth separating:
  • contact_key is identity. Derive it from your own session, never from anything the browser sends. A key taken from a query string is an invitation to read someone else’s thread. Do not use the prefixes telegram:, sip:, agent_copilot:, tool_copilot:, knowledge_copilot: or task_copilot: — they are reserved.
  • userdata is what the agent knows about them. It is readable as {{userdata.name}} from the prompt and from tools, and it is merged onto that contact’s record, so it is there again in their next thread. Because your server writes it, the agent’s idea of who it is talking to comes from your session and not from anything the visitor typed. See userdata.
3

Send a message through your own endpoint

POST /v1/conversations/messages is addressed by contact_key, not by conversation id. client_message_id is a UUID you generate; re-sending with the same one is ignored rather than duplicated, which is what makes a retry on a flaky network safe.
app/api/chat/send/route.ts
It answers 202 as soon as the message is accepted, carrying only the visitor’s own item. The agent replies asynchronously, on the stream you are about to open.
4

Proxy the event stream

GET /v1/conversations/{conversation_id}/events is a server-sent event stream, and it needs the same workspace token — so it is proxied too. Resolve the conversation from the visitor’s session rather than trusting an id from the query string; re-opening is idempotent, so it costs one round trip and closes the hole.
app/api/chat/stream/route.ts
5

Render it

Each frame arrives as event: <name> followed by data: <json>, so subscribe per name rather than to onmessage. Six frames matter:
components/Chat.tsx
Two rules the stream gives you and you should not fight:
  • Nothing on the stream is canonical. Delivery is best effort. A dropped frame is recovered by reconnecting — the snapshot re-syncs — and GET /v1/conversations/{conversation_id}/items is the record. Rebuild from the snapshot rather than trying to patch a gap.
  • Open the stream before you send, or you will miss the frames for that turn.

Tool activity

Tool use is not hidden. When the agent calls a tool, two rows arrive on the stream as item.created: a function_call and then a function_call_output, both with direction: "internal", both with text: null. The tool’s name — as the model called it, namespaced if it came from an MCP server — is at metadata.data.name.Show something. A visitor watching a blank panel for four seconds assumes it is broken; “Looking that up” for the same four seconds reads as work. If naming the tool is too much of your plumbing on show, key the indicator off the type alone and say nothing more than “one moment”.
6

Let the visitor attach a photo

An image and its caption arrive as one turn, one item and one bubble — send them together rather than as two messages, because a photo the agent has to reconcile with a separate sentence is a worse conversation than the one the visitor wrote.
components/attach.ts
Post them alongside the text, and your /api/chat/send route passes them straight through as images:
The limits, all enforced server-side: at most 4 images per message, JPEG, PNG or WebP, 10 MB each, and at most 100 images across one conversation. Anything larger than 1568 pixels on its longest edge is downscaled for you before the model sees it.Whether the agent can read an image is decided by its model, not by a setting. There is no image toggle on an agent; the vision flag on the model’s catalog entry is where it says so, and a message with images to an agent on a model without it is refused, naming the model. See vision and images.
7

Give the agent a tool that reads your app's data

The agent can only be useful about this visitor if it can look them up. One tool, one capability, keyed on the account_id your server put in userdata — so identity comes from your session and never from something the model or the visitor supplied.
Then attach it — "tools": [{ "tool_id": "…" }] on the whole config — and publish the agent again. Publishing pins the tool version live at that moment, so a later tool edit changes nothing until you publish the agent again.The tool has an empty json_schema on purpose: there is nothing for the model to decide. Every argument you can avoid is an argument it cannot get wrong. Saving it warns that {{userdata.account_id}} is read but not published by this tool — that warning is correct and expected here, because it is your /api/chat/open route that puts it there. See templating and data.
8

Decide what happens when the agent cannot help

Read this before you promise a visitor a human, because the honest answer is short.On text, Talqing has no transfer to a person. The transfer operation is phone only — on a web or text session it fails with transfer is only available on phone calls. A handoff moves the conversation to another agent, not to a colleague. And there is no API that writes a human’s reply into the thread: POST /v1/conversations/messages creates the visitor’s message and triggers the agent, which is the only thing it does.What you have instead is the fact that your server is in the middle. Handover on this surface is a routing decision you own: stop posting that visitor’s messages to Talqing and route them to your own support desk instead. The agent’s part is to file the escalation and say so.
The response comes back to the model rather than being hidden, so it can tell the visitor the ticket number your endpoint returned. When your desk picks the thread up, flip your own routing flag and stop calling /api/chat/send. The Talqing thread stays where it is, and the agent’s session goes idle on its own.

Test it

  1. Talk to it before you wire anything. Open the agent in the dashboard and use the Test chat panel. It runs the published version with the same prompt and tools a real visitor gets.
  2. Send from your own endpoint with the stream open. Watch for the order: item.created for your message, turn running, assistant.started, deltas, assistant.completed.
  3. Break the stream on purpose. Kill the connection mid-reply and let EventSource reconnect. You should get a fresh conversation.snapshot and a correct panel, not a duplicated bubble. If you get a duplicate, you are appending the snapshot instead of replacing from it.
  4. Sign in as a second user. Two contact_key values, two threads, no leakage. If both see the same history, your key is not coming from your session.
  5. Ask something only the tool knows. Then check the call actually happened: the function_call row is on the stream and in GET /v1/conversations/{conversation_id}/items.

What to watch

  • The conversations inbox. Monitor → Conversations in the dashboard is a contact-centric inbox with a live timeline: every thread, every item, and which agent version handled each turn. That is where you read what your agent actually said to somebody. See conversations.
  • The idle window. One warm session is kept per conversation and reused across messages. After 60 seconds with nothing pending it finalizes with close_reason: "idle_timeout". The other two ways a text session ends are the end_call operation and a worker shutting down. That is a session ending, not the conversation — the next message opens a new session on the same thread, and the agent still carries the whole transcript.
  • Latency. Turns are latest-wins: a newer message supersedes an in-flight one, and the turn frame reports canceled with superseded_by_item_id. What Talqing measures on text is llm_node_ttft — time to the model’s first token — reported per conversation rather than per turn, under Observability. Add your own two hops to it: the browser to your server, and your server to us. Do not compare it to voice latency, which is measured per turn and includes speech.
  • Cost. Every turn re-reads the whole thread, so a long-running thread is a growing input-token bill on your own key. There is no platform fee to add to it.

Where it falls short

  • No widget. No <script> tag, no embed, no hosted bubble, no themeable component. Everything above is code you write and maintain.
  • No unauthenticated visitors without a server of yours. There is no browser-safe, scoped token for text: the only credential is a workspace personal access token. A public “chat with us” box on a marketing page still needs your own endpoint in front of it, and your own way of telling one anonymous visitor from another.
  • No read receipts and no typing indicator from us on this surface. delivery_status on an item is about delivery to a messaging provider; on web it is not_applicable, because you are the surface. The “thinking” state in the component above is inferred from the turn and assistant.* frames, not reported. On Telegram, where we own delivery, the typing indicator and delivery status are real; here they are yours to draw.
  • No transfer to a human, and no way to write into the thread as one. Covered in step 8. If your product needs a shared inbox where agents and people take turns in one thread, the thread has to live in your system and Talqing is one participant in it.
  • No call analysis on text. Summaries, outcomes and structured fields are voice and video only, so there is no automatic verdict on how a chat went. Read the thread, or have the agent write what matters into userdata while it still has it.

Next

Text conversations

Items, delivery status, every frame of the event stream, and the window lifecycle.

Tools overview

What a tool is, and the operation tree behind one.

Conversations

Reading a person’s whole history across every surface.