# Actions Source: https://docs.thinnest.ai/agent/actions Letting the agent do things, not only say them — safely. An agent that can only talk is a search box with manners. Actions let it look up an order, check stock, or start something in your own systems. **The tools we wrote arrive on; anything that reaches outside your business arrives off.** Knowledge search, lead capture and escalation are what people set an agent up for, so making you go and find three switches would be a checklist before the thing works at all. Web search, your own API and every connected service start off, because those are reachable from a public chat by a stranger who may be trying to talk the model into using them — and that is a decision worth making one at a time. ## How the Actions page is laid out The page has three tabs. The one you have open is kept in the address, so a reload, or a link you send a colleague, lands on the same tab. * **Built in** — the tools we wrote, in three cards: **What Agent can do** (knowledge search, lead capture, handing over to a person, booking a call back, web search), **Reaching the customer elsewhere**, and **Remembering customers across channels**. * **On calls** — **Hand calls to another agent**, the **Who-is-calling lookup** and **After the call**. * **Connect a service** — webhooks, a helpdesk, your own API, then calendar, spreadsheet, CRM and records, and the directory of other services. Under the title, one line shows what is on: how many tools are switched on, how many services are connected, and how many agents a call can be handed to. A tool you have switched on that is missing something it needs shows an amber **Not in use yet**, with what is missing, rather than quietly doing nothing. ## Built-in tools Switchable per agent. Off means the tool is **removed**, not that the model is asked not to use it. * **Knowledge search** — your own material, always searched first. **Two switches**: one for chat and WhatsApp, one for calls. Both on. * **Web search** — runs on the model provider's side, so it is only available on models that offer it. * **Capture lead** — see [Leads](/agent/leads). * **Escalate** — hand the conversation to a person. See [Escalation](/agent/escalation). * **Hand calls to another agent** — on calls only. Under this heading the agent can pass a live call to another of your agents, say sales to support, on the same line and in the same conversation. Each agent on the list is handed over **warm** (with a short summary and the conversation so far) or **cold** (it starts fresh and greets the caller). See [Voice](/channels/voice#handing-a-call-on) for the rules. * **End the call** — on calls only. The agent hangs up once the conversation is genuinely finished. See [Voice](/channels/voice#ending-the-call). * **Book a call back** — somebody asks to be called later and the agent books it; your number rings them at that time. See [Voice](/channels/voice#booking-a-call-back). ## Booking into your own calendar Different from **Book a call back** above, and worth keeping straight: that has your number ring the customer, this puts a real meeting in your diary at a time you were genuinely free. Connect Cal.com and the agent can read your availability, offer real times, and book — with Cal.com sending the confirmation. Twelve switches, all starting off. See [Calendar](/agent/calendar). ## Filling in your own spreadsheet The agent takes a caller's details and writes them into a row of your spreadsheet as it goes, and can look somebody up in the same table to answer from what you already hold. Connect your own Grist account; two switches, both starting off. See [Spreadsheet](/agent/spreadsheet). **Your calendar and spreadsheet switches share one budget: eight on at once.** An agent given too many tools gets worse at choosing between them, so we stop you at eight rather than let it quietly degrade. ## Reaching the customer on a different channel A caller needs a link sent to them. A customer on WhatsApp would rather be phoned. These are the tools for that, and they have their own block on the Actions page because they share something the others do not: each one reaches the customer **outside** the conversation the agent is in. **All four start off**, and three of them cost money each time. Turning one on is not quite enough on its own — each also needs something only you can give it, and the row says so when it is missing. | Tool | You turn it on, and it needs | What it does | | - | - | - | | **Ring the customer now** | a number that can dial out, and the customer inside your calling hours | Rings that customer straight away on a number we are allowed to use, and tells them it is coming | | **Send the customer a WhatsApp** | an approved **utility** template | Sends that template to the customer, blanks filled — never free text, and never while the agent is already on WhatsApp with them | | **Send the customer an SMS** | your own [SMS provider](/channels/sms) connected, and at least one DLT template | Sends one of your DLT templates by SMS, blanks filled — never free text. Indian mobile numbers only; your provider bills you | | **Reply to the customer by email** | a connected helpdesk that can send — Zendesk today | Replies on that customer's existing email thread. See [Helpdesk](/workspace/helpdesk) | **"Ring the customer now" is not the same switch as "Book a call back".** One is a customer asking to be rung later; the other is the agent ringing them while it is already talking to them somewhere else. You can have either, both, or neither. **The agent never chooses the destination.** It cannot be told "email this to another address" or "call this number instead" — there is no such thing to say. The number or address is resolved from your records at the moment of sending, from the same evidence that [links a person across channels](/concepts#how-two-channels-become-one-person). So the worst a visitor can talk the agent into is contacting the right person at the wrong moment. A few consequences worth expecting: * **A WhatsApp template costs money**, the same as any other template send. The agent may send **two an hour in one conversation**, so a customer who keeps asking — or tries to make it loop — cannot run up your bill. * **SMS is capped the same way**, two an hour in one conversation, and is billed by your own provider, not from your wallet. * **Emails are capped the same way**, at three an hour in one conversation. Enough for the invoice and the corrected invoice, not enough to flood an inbox. * **The agent will refuse in words** when it has no usable number or address — it says so and carries on helping, rather than pretending it sent something. ### Which templates the agent may send Two separate decisions, both yours: 1. **Whether this agent may send at all** — the switches above, per agent. 2. **Which templates any agent may send** — a tick on each template, once for the whole workspace. Only templates you have ticked **Agents may send this during a call or chat** are shown to the agent. It picks the one that fits what the customer asked for, and fills the blanks. It can't send anything you didn't tick, and it can't write its own words. | Tool | Where you tick | What can be ticked | | - | - | - | | **Send the customer a WhatsApp** | The template's card on [Templates](/whatsapp/templates#letting-an-agent-send-a-template) | Approved **Utility** templates whose link, if any, is fixed | | **Send the customer an SMS** | The template in [SMS settings](/channels/sms) | Any of your DLT templates | New templates start **unticked**. A tool with nothing ticked says so on its row, and isn't given to the agent at all. The agent is shown at most **ten** ticked templates, the first ten alphabetically, so every turn stays quick and the same ones appear each time. With more than ten ticked, untick the ones you don't want it to reach for. The SMS page tells you when you are over. Want a message that is always the same one, sent at a known moment? Use [After the call](/agent/after-the-call). There you choose the exact template and what fills every blank, and no tick is needed. ## A message after the call Separate from the tools above: the agent can also send the caller a WhatsApp or an SMS when a call ends — a thank-you with your booking link, or a sorry-we-missed-you. It's set in the **After the call** card on the same page. See [After the call](/agent/after-the-call). ## Call your own API Point the agent at an HTTPS endpoint of yours, describe what it does and what it takes, and the agent can call it mid-conversation. **[Full guide, including how to write the endpoint safely](/agent/custom-api).** * Credentials are encrypted at rest and are not readable from the dashboard. * The URL is validated **and DNS-resolved** before we fetch it, so an endpoint cannot be used to reach inside our network. * Your header secret is never shown again after you save it. ## Connect a service (MCP) 21 services with addresses read from the official registry and grouped by what they do, plus any address you want to paste. A further 1,434 services are listed in the directory. Sign-in happens in a popup and tokens are sealed and renewed automatically, so nobody signs in twice. **Asana, Xero and Shopify need a manual step.** Those vendors do not support dynamic client registration, so connecting them currently means registering an app on their side yourself. It works; it is not one-click, and we would rather say so here than have you discover it mid-setup. ## Send what happens somewhere else Signed outbound webhooks to any HTTPS endpoint. This is the lane that reaches Google Sheets, Zoho and effectively any CRM through Zapier or Make today. Paste an address — or an email address, if your helpdesk opens tickets that way — and pick what it should be told about. | Event | Fires when | | - | - | | A lead was captured | The agent collected a name, email or phone from somebody interested | | A conversation was escalated | It handed a conversation to a person | | A conversation was resolved | A teammate marked one done | | **A call finished** | Any call placed or answered — with the outcome, how long it lasted, and which campaign it belonged to | | **A call's results are ready** | After a call ends and has been read — its summary, the details it collected, the transcript and a recording link ([shape](/api-reference/calls/get-call)) | | **A campaign finished** | Everybody on a broadcast or [calling campaign](/channels/voice-campaigns) has been reached or given up on | | **A caller asked not to be called again** | During a call, somebody asked not to be called again and their number went on your do-not-call list | | **Something went wrong on a live call** | While a call is going: the agent could not answer, your own model key failed, a reply was very slow, or the voice connection failed. Sent once per kind of problem per call | A finished call carries whether it was answered, missed or refused, so a missed call is something your own system can act on rather than something you find out by opening ours. Want what was *said* as well? Tick **A call's results are ready** — it arrives a few seconds later, once the summary and details are written. See [Call leads from your CRM](/guides/leads-from-your-crm). Every delivery is signed with a secret shown once when you add the endpoint, so your receiver can prove the request came from us. A delivery that fails is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (six attempts over about 8½ hours). An endpoint is switched off, and says so, only after five events in a row that each failed every attempt; a failure that a retry then delivers does not count. What failed can be sent again for 7 days. See [Webhooks](/api-reference/webhooks) for the headers, the retries and how to dedupe. **Choose the events you want.** An endpoint with no events chosen receives every event — the ones that exist today and any we add later. A receiver built for leads should not be woken by a phone call it has never seen the shape of, so tick only the events it is built for. ## Two limits worth knowing **Eight tools per agent.** Every tool's schema is re-sent on every turn, and models get measurably worse at choosing as the list grows. The cap is a quality decision, not a licensing one. **Every discovered tool arrives off.** Enabling is one deliberate act per tool. A newly connected service does not silently gain the agent thirty new capabilities. ## Name protection Third-party tools are namespaced, so nothing you connect can register itself as your own knowledge search and quietly take over your own material. # After the call Source: https://docs.thinnest.ai/agent/after-the-call Send the caller a WhatsApp or an SMS when a call ends — a thank-you, a booking link, a sorry-we-missed-you. When a call ends, the agent can send the caller a message: a WhatsApp, an SMS, or both. It's the same message every time, from a template you chose, with blanks filled from the call. Set it in the **After the call** card on the agent's **Actions** page. Things it's good for: * **Answered calls** — "Thanks for calling, . Book your visit here: …" * **Missed calls** — "Sorry we missed you, . Reply here or call us back on …" * **Every call** — your address and opening hours, after anyone rings. ## Set it up * **WhatsApp:** an approved **Utility** template. See [Templates](/channels/whatsapp). * **SMS:** a DLT template, with your SMS provider connected. See [SMS](/channels/sms). **Send after**: *Calls somebody answered*, *Missed calls*, or *Every call*. A call that failed before it rang never gets one. For each blank, choose what goes in it: | Choice | Fills with | | - | - | | Customer's first name | The caller's first name, or "there" if we don't know it | | Business name | Your workspace's name | | Agent's name | This agent's name | | Date of the call | Like "7 Oct", in your workspace's timezone | | Fixed text… | Whatever you type, like your booking link | An SMS blank holds at most 30 characters, so fixed text there is kept short. Choose **Don't send** for a channel to turn it off. **A WhatsApp template with a blank in its link button can't be used here.** A button like `yoursite.com/{{1}}` needs a value for the blank, and nothing can fill it after a call. Those templates don't appear in the list, and saving one through the API is refused. Use a template whose link is fixed, or put the changing part in the message text. ## What keeps it polite * **Once per call.** A call that ends twice in our records still sends once. * **Never to anyone on your do-not-contact list**, including someone who asked the agent on the call not to be contacted again. * **At most once a day to the same number**, per channel. A caller who rings three times in a morning gets one message, not three. * **Only to a number we know is theirs**, the one they called from or that we called. Never to a number someone said out loud on the call. * **WhatsApp must be a Utility template.** Marketing templates are refused here. They need marketing consent, and Meta pauses them quickly when people don't want them. ## What it costs * **WhatsApp** is charged like any other template you send. See [WhatsApp](/channels/whatsapp). * **SMS** goes through your own provider, which bills you directly. Nothing is taken from your wallet here. ## Seeing what went out Each WhatsApp appears in the customer's WhatsApp conversation, marked as sent by the agent after the call. Each SMS appears as a note on the call's conversation. A message that couldn't be sent (no WhatsApp on that number, a template no longer approved, your SMS provider refused it) is simply not sent. The call itself is unaffected. ## From the API The same setting is `followUp` on [Update Post-call Settings](/api-reference/post-call/update-post-call-settings). It needs a **full** key, because it makes the agent message customers. ```bash theme={null} curl -X PATCH https://app.thinnest.ai/api/v1/agents/ag_5cbb2cb9-41aa-4181-9e37-78914bd7b4f9/post-call \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "followUp": { "when": "missed", "whatsapp": { "templateId": "3f1c2b7e-9a41-4f0b-8e2d-5c6a7b8d9e01", "values": ["customer_name", "business_name"] }, "sms": { "templateId": "8d2e4f6a-1b3c-4d5e-9f70-a1b2c3d4e5f6", "values": ["customer_name", "fixed:acme.example/book"] } } }' ``` `values` fill the template's blanks in order, one each. Anything you leave out of `followUp` keeps what is saved, and a channel set to `null` stops it: ```json theme={null} { "followUp": { "sms": null } } ``` # Behaviour Source: https://docs.thinnest.ai/agent/behaviour Tone, language, opening line, model and when to fetch a human. ## Where to set it Most of this is on the agent's **Playground**, in four cards: **Language and model**, **Voice**, **What Agent says** (the opening line and the instructions) and **Reply length**. Nothing changes until you press **Save changes** at the top right; after that it applies to the next message, including for real customers, so the Playground is not a sandbox. **Test a call** and a chat panel sit beside the cards (above them on a narrower screen), so you can try a change straight away. ## Name Rename an agent from the pencil beside its name at the top of its sidebar, or under **Settings → Identity**. Customers see the name in the chat. ## Business description The facts the agent falls back on when a knowledge search finds nothing — what you sell, to whom, and anything true of the business as a whole. Keep it factual. This is grounding, not instructions: it is treated as information to work from, never as commands. ## Instructions Tone and subject matter. "Be warm but brief." "Never discuss competitors." "Always mention that delivery is free over ₹2,000." Tune these in the **Playground**, against a live reply. Guessing at wording and shipping it is how you find out a week later that the agent has been ending every message with an emoji. Instructions can be up to **20,000 characters** — the counter under the box shows how many you have used, and the same limit applies through the API. Conversation steps are limited separately (up to 12 steps, each with an 80-character title and a 600-character detail) and don't count toward the 20,000. Your instructions are treated as guidance on tone and subject matter, not as a command channel. The platform's own safety rules always win — which is what stops a customer talking the agent out of them too. **Your agent never pretends to be a person.** Give it any name and persona you like, but if someone asks whether they are talking to an AI, a bot or a real person, it says it is an AI assistant — on every channel, in every language, whatever your instructions say. On a call, it also answers truthfully whether the call is being recorded. ## Opening line The first thing a visitor reads in the widget. Write it in the business's own voice; "How can I help you today?" is a wasted sentence. ## Reply language Choose it when you create the agent, or any time on the **Playground** or **Settings**. Either **\[Auto] Match the customer** — the agent replies in whatever language the customer speaks in — or tick the languages it speaks: one, or up to four. Through the API, send `languages` on Create Agent or Update Agent: a list of language names. `[]` is auto, one name is that language alone, and two to four are the languages the agent expects, the first being the one it falls back to, for example `["Telugu", "English"]`. More than four, a repeat, a name the console does not offer, or `languages` together with `language` is refused with a `400`. Get Agent returns `languages` too. Sending `language` (one language) still works and replaces any list. In chat (website chat, WhatsApp, Telegram, the Playground and test chat), **Match the customer** reads the script of the customer's latest message. Write in Telugu, Hindi, Tamil, Kannada, Malayalam, Bengali, Gujarati, Punjabi or Odia script and the agent replies in that language, even if its opening line was in another. Once a conversation is under way, a few words in another script do not move it: the agent switches when the customer's first message is in that script, or when a later message is a real sentence in it. Some cases are left to the agent's own judgement: a message written in English letters (including Telugu or Hindi typed in English letters), a message too short to tell, and one that is mostly English. A customer can always ask for a different language and the agent follows. Pick one language and the agent answers **only** in it. On a phone call that also tells the speech recogniser which language to listen for. **Match the customer** leaves the recogniser to work it out afresh for every sentence, and a short or mixed reply can be heard as the wrong language — a name from another language is enough. If your callers use one language, choose it. Pick **several** — Hindi and English, say, or Tamil and English — and the agent answers in whichever of them the customer is speaking, and in the **first** one you ticked when they use any other. On a phone call the recogniser still works out the language for itself, but only among yours: a reply it hears as some other language is listened to again in the language the call is in. That second listen adds about half a second, and only to those replies. Up to four languages; the first one you tick is the one the agent falls back to. 37 languages, including all 22 of the Eighth Schedule, searchable, each in its own script, so there is no "other languages" section to scroll past. ## Speaking speed In the **Voice** card, under the voice itself: how fast the agent talks on calls, on a slider from slower to faster. See [Speaking speed](/channels/voice#speaking-speed). ## Answer style Presets rather than a raw temperature number. Precise for policy and pricing questions; warmer where the conversation is a sales one. ## Model Every new agent starts on **Prana \[Voice]** — from scratch, from a pre-built agent, or through the API (`prana-voice`) — and answers chat and calls with it until you choose another. The choice is checked against your plan at request time, so downgrading a plan cannot leave a more expensive model quietly running. If your workspace brings only its own voice key, calls run on Prana, Prana \[Voice], GPT-OSS 120B or another low-cost model; an agent set to anything else answers its calls on GPT-OSS 120B. If it brings only its own language model key, your key answers on any model it offers, and the call is billed like any other call, by the voice. See [Bring your own keys](/api-reference/bring-your-own-keys). A model marked **premium** in the picker puts every call on the premium per-minute rate, even with a standard voice. `GET /api/v1/models` gives the same thing for each model: `callTier` (`standard` or `premium`) and `callPricePerMinuteMicro`, the price of one call minute in millionths of `currency` (the currency of the wallet your calls are charged to). Both cover the model only, with a standard voice: a premium or studio voice can raise a call's price, and one call is billed at one rate, never a stacked surcharge. If your workspace brings its own keys, calls bill at the [BYOK per-minute rate](/api-reference/bring-your-own-keys) whatever the model, so these prices apply only to an agent you've set to run on our stack. The exception is a workspace that brings only its own language model: there the voice plan still applies, set by the voice alone, and the model you run never raises it. Typical response latency per model isn't published. **The agent does not go dark.** If a model provider fails mid-conversation, the reply falls through to another model rather than to silence. A customer looking at a dead chat window is a worse outcome than a slightly slower answer. ## Handing over to a person Two triggers, each switchable — and [Escalation](/agent/escalation) covers what happens when one fires, including when a custom action fails: * **When the material does not cover the question.** The honest escalation. * **When the customer asks for a person**, or is clearly upset. Turning one off removes the capability entirely rather than asking the model to refrain — the difference matters, because a model that has a tool will eventually use it. A business with nobody watching the inbox should turn escalation off. An escalation nobody answers is worse than a straight "I don't know", because the customer waits. # Calendar Source: https://docs.thinnest.ai/agent/calendar Your agent reads your real availability and books meetings into your Cal.com. Someone asks when you are free. The agent reads your actual calendar, offers two or three real times, takes their name and email, and books it. Cal.com sends the confirmation itself. It works on every channel — website chat, WhatsApp, Telegram and phone calls. This is not the same as **Book a call back**. That has your number ring the customer at a time they chose. This puts a meeting in your diary at a time you were actually free. You can have both on; they do not know about each other. ## Connecting your calendar You need a Cal.com account. Go to **Actions**, find **Calendar**, and click **Cal.com**. In Cal.com, open **Settings → Developer → API keys** and create one. It must be a **live** key — one beginning `cal_live_`. Cal.com also issues test keys, which begin `cal_` and work perfectly against a separate test world, so a booking made with one never reaches the calendar you are looking at. We refuse those rather than let you find out from an empty diary. Leave the expiry off unless you have a reason to set one — a key that expires will stop your agent booking, and the first sign is usually a customer who could not get a slot. We call Cal.com once to check the key works and to read back which account it belongs to. If it does not work you are told immediately, rather than finding out from a missed meeting. Everything starts **off**. Connecting your calendar on its own gives the agent nothing. ## Choosing what it can do Twelve things, each its own switch. **An agent can have eight on at once** — past that it gets worse at picking the right one, so we stop you rather than let it quietly degrade. That eight is shared with anything else the agent connects, so a [spreadsheet](/agent/spreadsheet) switch and a calendar switch come out of the same budget. ### What a customer can ask for | Switch | What happens | | - | - | | **See what can be booked** | Reads your meeting types — a 15-minute intro, a demo — so the agent offers real ones instead of inventing them | | **Check when you are free** | Reads your genuinely open slots and offers a couple of them | | **Hold a slot while you talk** | Reserves the time for a few minutes so nobody else takes it while the customer is still spelling their email | | **Book the meeting** | Puts a real booking in your Cal.com. Cal.com emails the confirmation | | **Find their existing booking** | Looks up what this customer already has, by their email | | **Move a booking** | Reschedules to a time they picked | | **Cancel a booking** | Cancels it, with the reason they gave | Most people want the first four. Add **Find** before **Move** or **Cancel** — those two need it, because it is where the agent gets the booking to act on. ### Acting as you Five more decide something on your behalf: accepting or declining a booking that is waiting on you, marking a no-show, and reading your working hours or time off. **Three of these need Find their existing booking turned on as well** — accepting, declining and marking a no-show all act on a specific booking, and Find is the only thing that can identify one. Marking a no-show needs one more thing: the person's email address. Cal.com identifies an attendee by email and by nothing else, so without one the agent cannot say who failed to turn up. **Leave these off unless the agent only ever talks to your own staff.** A phone call has no login, so the agent cannot tell you from a caller who has worked out what to say. It is asked to refuse strangers, but the reliable protection is the switch being off. ## What your customer experiences > **Them:** Can I get a demo this week? > **Agent:** Let me check the diary. I have Tuesday at 11, or Thursday at 3. > **Them:** Thursday. > **Agent:** Can I take your name and email? Then a Cal.com confirmation lands in their inbox, exactly as if they had used your booking page. The agent will **not** invent a time. If nothing is free it says so and offers to look at another week, rather than promising a slot you do not have. ## Things worth knowing **It always asks for an email.** Cal.com will accept a booking without one; we will not. A booking with no email gets no confirmation and cannot be found again if the customer rings back to change it. **Testing in the Playground books for real.** The Playground runs your actual agent, not a simulation — so if you ask it to book you a demo, a real meeting appears in your Cal.com and a real confirmation email goes out. Cancel it the same way, or leave these switches off until you are ready. **Times are read in your calendar's timezone**, unless the customer says where they are — then it converts. **Hidden meeting types are never offered.** If you took something off your public booking page, the agent will not offer it either. **Disconnecting leaves your bookings alone.** They are your customers' appointments, not ours to cancel. ## If it stops working The card says **Needs attention** and shows what Cal.com said. Almost always the key expired or was revoked. Create a new one in Cal.com and use **Replace key** — your switches are remembered, so you do not have to set them up again. ## Deliberately not included The agent cannot **create, change or delete your meeting types**. It can book into them, and that is all. Nobody phoning your business has a good reason to reshape what your business offers, and the safest way to guarantee that is not to give the agent the ability at all. # Call your own API Source: https://docs.thinnest.ai/agent/custom-api Give the agent one narrow endpoint of yours, and every rule that keeps it safe. A custom action is an HTTPS endpoint of yours that the agent can call mid-conversation — an order lookup, a stock check, a booking. It is the most powerful thing you can give an agent and the one with the sharpest edges, because it is reachable from a public chat by a stranger who may be trying to talk the model into using it. ## Defining one | Field | What it is | | - | - | | **Name** | 3–40 characters: lowercase letters, numbers and underscores, starting with a letter | | **Description** | What the model reads to decide whether to call this at all | | **Method** | `GET` or `POST` | | **URL** | Must be `https://`. May contain `{{placeholders}}` | | **Parameters** | Each with a name and a description | | **Body template** | For `POST`. JSON, with `{{placeholders}}` | | **Header secret** | Sent on every call so your endpoint can verify it is us | ### The description is the whole thing The model has nothing else to go on. It decides whether to call your action, and with what, entirely from what you write here. **Weak:** ``` Gets order info ``` **Strong:** ``` Look up a customer's order. Requires BOTH the order number and the email address on the order — if the customer has given only one, ask for the other before calling this. Returns found: false when the pair does not match: say you could not find it and ask them to check both, never say the order does not exist. ``` The second one tells the model when to call it, when *not* to, what it needs first, and how to phrase the failure. Every sentence prevents a specific bad answer. Write the description, then read it back as though you knew nothing about your own systems. Anything you had to already know is a sentence that belongs in it. ### Parameters need descriptions too Every parameter must say what it is, or the model will invent a value. `order_id` with no description gets filled with something plausible-looking and wrong. ## Placeholders Use `{{parameter_name}}` in the URL or the body: ``` https://shop.example.com/api/orders/{{order_number}} ``` ```json theme={null} { "orderNumber": "{{order_number}}", "email": "{{email}}" } ``` Two rules the editor enforces before you can save: * **Every placeholder must have a parameter behind it.** A `{{order_id}}` with no matching parameter would reach your API as the literal text `{{order_id}}` — failing in a way that looks like your bug rather than ours. * **The body must still be valid JSON once the blanks are filled.** Values are escaped when substituted, so a customer cannot break out of a string and reshape your request. ## Values the platform fills Some placeholders are filled by us from the conversation, not by the model, so a caller cannot talk the agent into sending a different one. They have a dot in the name, and you do not declare them as parameters. | Placeholder | Value | | - | - | | `{{call.id}}` | The call's id — the same id `GET /api/v1/calls` lists. Empty outside a call | | `{{call.from}}` | The number the call came from, E.164. Empty outside a call | | `{{call.to}}` | The number that was dialled, E.164. Empty outside a call | | `{{call.direction}}` | `inbound` or `outbound`. Empty outside a call | | `{{conversation.id}}` | The conversation (`conv_…`) | | `{{contact.id}}` | The contact (`cust_…`) | | `{{contact.phone}}` | The contact's phone number, E.164, when known | | `{{agent.id}}` | The agent (`ag_…`) | | `{{workspace.id}}` | The workspace (`org_…`) — useful when you run customer workspaces | | `{{test.chat}}` | `true` during a test chat, empty otherwise | ``` https://crm.example.com/calls/{{call.id}}/notes?caller={{call.from}} ``` * They work in the path, the query, the body and in header values. The host must be written out in full — a placeholder before the path is refused. * Header values take only these, never a model parameter. * An unknown one, such as `{{call.nope}}`, is refused when you save. * On text chat (WhatsApp chat and web chat) the `call.*` values are empty; the rest still fill. Voice calls on every surface, WhatsApp calls and web voice included, fill them. A test from the editor or `POST …/actions/{id}/test` has no conversation, so only `{{agent.id}}` and `{{workspace.id}}` fill. Every call also carries `X-Call-Id`, `X-Conversation-Id`, `X-Agent-Id` and `X-Workspace-Id` headers (each only when known), so you get them without editing a template. A header you set yourself with the same name wins, whatever its capitalisation (`x-call-id` replaces `X-Call-Id`). ### During a test chat A test chat (`POST /api/v1/agents/{id}/test-chat`, or `test_chat` over MCP) runs your actions for real. So your endpoint can tell, every action request in a test chat carries an `X-Test-Chat: true` header and `{{test.chat}}` fills as `true`. There is no call, so `{{call.id}}` and `{{call.from}}` are empty and `X-Call-Id` is not sent. Check for the header and answer as normal without writing anything — no order placed, no record created — so testing never changes your real data. Outside a test chat the header is never sent. Only the API's test chat is marked. A chat in the console's **Playground**, a live conversation and a test of a single action from the editor are not, and the tools of an MCP server you have connected to the agent are not marked either. ## Reserved names These clash with the agent's built-in tools and are refused: `search_knowledge`, `capture_lead`, `escalate_to_human`, `send_media`, `send_link`. Anything else you connect is namespaced, so no third-party tool can impersonate one of the agent's own. ## Security Once when you save it — so you hear about an `http://` address while looking at the form — and again at call time. Validated **and DNS-resolved**, so an address cannot be used to reach a private network from inside ours. Stored encrypted, not readable from the dashboard, and never displayed again after you save it. Custom actions are the only tool that spends *your* money, so the cap is a per-conversation one. A conversation is the unit a visitor controls, so it is the unit that gets bounded — a workspace-wide cap would be one a single visitor could exhaust for everybody. Thirty is far above any honest conversation. Checking an order, a booking and a balance twice over is under ten. ## Writing the endpoint Four rules, in order of how much trouble they save. Before parsing anything. ```js theme={null} if (req.get("X-Agent-Secret") !== process.env.AGENT_SECRET) { return res.status(401).json({ error: "unauthorised" }); } ``` An order number alone is guessable — they are usually sequential. Require the number **and** something only the real customer knows, like the email on the order. ```js theme={null} // Same response either way. A different one for each is an oracle telling a // stranger which order numbers are real. if (!order) return res.json({ found: false }); ``` Everything you return may end up spoken to the customer. Return the status and the tracking link, not the whole order record with the customer's address and payment details in it. ```js theme={null} return res.json({ found: true, status: order.status, carrier: order.carrier ?? null, trackingUrl: order.trackingUrl ?? null, }); ``` **Never write an action that changes something irreversible.** Cancelling an order, issuing a refund, deleting a booking — these should not be one conversation away from a stranger who is good at persuasion. Read actions first; for writes, [escalate to a person](/agent/escalation) instead. ## When it fails A timeout, a 500, or an unreachable host does not crash the conversation. The agent is told the lookup failed and answers accordingly — normally by [handing over to a person](/agent/escalation) rather than guessing. Which is the argument for having escalation switched on wherever you have actions: the failure mode of "I could not check that" is much better when there is somebody to pass it to. ## Sending leads somewhere The other direction: signed outbound webhooks to any HTTPS endpoint, fired when something happens rather than when the model asks. That is the lane that reaches Google Sheets, Zoho and effectively any CRM through Zapier or Make. ## A full worked example [Answer "where is my order?"](/guides/order-status) builds one end to end — the endpoint, the action config, the description text, and the four cases to test. # Escalation Source: https://docs.thinnest.ai/agent/escalation How the agent hands a conversation to a person — including when an action fails. The counterpart to knowledge search: one finds an answer, this one admits there isn't one. Without it the agent's only options are to guess or to apologise into the void, and you never learn it happened. ## The two triggers Each switchable, per agent. | Trigger | Fires when | | - | - | | **When the material does not cover it** | A knowledge search found nothing and guessing would be worse than waiting | | **When the customer asks** | They ask for a person, or are clearly upset | Turning one off **removes the capability**, rather than asking the model to refrain. A model that has a tool will eventually use it; a model without one cannot. **A business with nobody watching the inbox should turn escalation off entirely.** An escalation nobody answers is worse than a straight "I don't know", because the customer waits for something that is not coming. ## What happens when it fires Status changes to **escalated** and it surfaces in the inbox. One sentence on what the customer wants and what the agent could not find — written for a colleague, not for a log. Outbound webhooks are delivered, and if you have connected one, a [helpdesk ticket](/workspace/helpdesk) opens in Zendesk or Salesforce with the transcript. The agent says a person will follow up. It does not go quiet. ## On a phone call: putting the caller through With **Call transfer** chosen in **Hand over to a person**, a caller on the phone is also put through to the number you give. Choose how: * **Cold** (the default) — the phone rings and the caller is connected as soon as someone answers. * **Warm** — whoever answers first hears a short spoken summary of who is calling and what they need, then the caller is connected. The caller hears the phone ringing meanwhile. Add **Press 1 to accept** and the caller is connected only once that person presses 1, so a voicemail never takes the call; choose **Connect after the summary** to connect as soon as it has played. Press 1 to accept is not available on Telnyx numbers yet. The details are on [Voice](/channels/voice#handing-a-call-on). ## Escalation from a failed action The case worth planning for. When a [custom action](/agent/custom-api) times out, returns a 500, or cannot be reached, the agent is told the lookup failed. It does **not** invent an answer and it does not pretend the tool succeeded. What it does next depends on whether escalation is available: | Escalation | What the customer gets | | - | - | | **On** | "I can't reach our order system right now — I've passed this to the team and someone will come back to you." | | **Off** | "I can't check that at the moment. Please try again shortly." | **Switch escalation on wherever you have custom actions.** The failure mode of "I could not check that" is far better when there is somebody to pass it to — and your API will be down at some point, whatever your uptime target says. The same applies when a customer hits the [per-conversation action limit](/agent/custom-api#security): the agent stops calling and hands over rather than continuing without the information. ## Once a person takes over The agent **stops replying to that thread entirely**. Two voices answering the same customer, contradicting each other, is worse than a slower answer. When a teammate replies, the agent later reads that reply as its own prior turn — so if the conversation returns to it, it does not re-answer something a person just handled. ## It escalates once The agent is told to call it once per conversation. Calling it again does not make anybody arrive sooner, and a thread that escalates four times is four notifications about one problem. ## If flagging fails If the escalation cannot be recorded for some reason, the agent still tells the customer a person will follow up — because the conversation is in the inbox either way, and promising nothing is worse than promising something you will honour by reading it. ## Related Where escalations land, and how taking over works. Escalations as Zendesk or Salesforce tickets, with replies routed back. The actions whose failures escalation catches. "Needed a person" over time — not a failure metric, a working one. # Find leads Source: https://docs.thinnest.ai/agent/find-leads Describe the businesses you want. Your agent finds them and brings back their phone numbers and emails. Find leads is the first tab on **Leads**. You describe the kind of business you want to reach — "real-estate developers in Pune", "dental clinics in Bengaluru" — and an agent finds them and brings back their phone numbers and email addresses. The people who came to *your* agents are on the second tab, [Inbound leads](/agent/leads). ## Find your first leads Open **Leads**. Type who you want to reach, or start from one of the examples. Add a place: "in Pune" finds far better than "developers". A broad request ("builders") gets one question with a few answers to tick — which city, what kind. Tick, add your own, or skip it. The agent shows what it will look for. Pick 25, 50, 100, 250 or 500 leads and **Start search**. Before it starts you see the most the search can cost. The chat on the left shows each search the agent runs; the table on the right fills in as leads are found. You can leave the page — the search carries on — and **Stop** it whenever you like. ## Ask for more, or something different After a search, just talk to the agent in the box under the chat. * **"2 more"**, **"20 more please"**, **"keep going"** — it searches again for different businesses and adds them to the same table, up to 500 leads in a search. You only pay for the new leads. * **"Only the ones with an email"**, **"now in Mumbai"**, **"also include orthodontists"** — it changes the search and starts the next one in the same chat. * **"Interior designers in Hyderabad"** — a different search, in the same chat. * Say how many in the same message — **"10 dentists in Pune"** — and it starts straight away. Your earlier searches stay in the chat, each with a **View leads** button that brings its table back. **Back to new search** (the arrow beside **Leads**) starts a fresh chat. ## What you get for each lead A row in the table: the business, its website, phone, email, location and what kind of business it is. * **Only what the website itself prints.** A number or email is included only if it is on the business's own page. The link beside the website opens that exact page, so you can check any of them. * **The business's public details, not a person's.** Expect a main line, a sales number, a toll-free number or a general address such as `info@` — not the direct number of a named person. * **More than one number or email?** The first is shown, with **+2** or similar; hover it to see the rest. ## What it costs * **₹1 for each lead** (one cent if your workspace is billed in dollars), taken from your balance. No GST is added. * **The first 50 leads are free** — in your workspace's first month of Find leads, counted from your first search. They are counted per workspace and per person. * **You only pay for a lead:** a business with a phone number or email on its own website that you don't already have. Sites that were not a match, sites with no contact details, businesses already in your contacts and businesses an earlier search found are never charged. * **When the balance runs out,** the search pauses. Add money and press **Continue**. A workspace with no balance — the free trial — is asked to choose a plan once its free leads are used. The price is also on each plan card in **Settings → Billing**. ## Do something with them Tick the leads you want, then: * **Save to Contacts** — they become contacts in your workspace, and go into a [group](/agent/leads#groups) you choose or create. * **Send WhatsApp** — saves them to a group and opens a [broadcast](/whatsapp/broadcasts) already aimed at it. * **Call with an Agent** — saves them to a group and opens a [calling campaign](/channels/voice-campaigns) already aimed at it. Saving asks you to confirm that **contacting these businesses is your responsibility**. These businesses have not asked to hear from you, so read the next section before you send anything. ## Before you contact them * **Calls.** Calling businesses that have not asked to be called is covered by India's telemarketing rules, including Do Not Disturb. Your calling campaign still respects calling hours and your do-not-contact list; the rules themselves are yours to follow. * **WhatsApp.** WhatsApp expects people to have opted in. A broadcast to found businesses is allowed, but complaints and blocks lower your WhatsApp account's quality rating and can get it limited. Send only what they would expect from a business like yours. * **Anyone who says stop is never contacted again,** on any campaign, whichever way they were found. ## Save a search, run it again On a finished search, **Save search** gives it a name and keeps it at the top of the start screen under **Saved searches**. **History** lists your recent searches. **Run again** repeats a search: businesses you already have come back as duplicates and are not charged, so a rerun pays only for new ones. ## Limits * One search at a time in a workspace, and 30 a day. * Up to 500 leads a search. A search ends early if it is finding nothing, and says so. * Businesses in India. * Leads stay in your workspace. They are not in the Contacts download or in the contacts API. ## Not available yet * Workspaces of a white-label client, and customers of a developer: their usage is billed through somebody else, and this is not part of that yet. * Finding leads from the API or the MCP server. * Repeating a saved search automatically every week. # Knowledge Source: https://docs.thinnest.ai/agent/knowledge What the agent answers from, and what it will refuse to guess at. The agent answers from material you give it. When a search finds nothing, it says it does not know rather than inventing an answer — that behaviour is the point, and it is what makes the answers worth trusting. ## Ways to add material Paste a URL. Good for a returns policy or a pricing page you want indexed on its own. Paste your homepage and choose a depth of 1 to 3 levels. You see the list of pages found and **pick which ones to index before anything is ingested** — a crawl is a proposal, not a commitment. Reaches pages a crawl cannot: unlinked pages, pages only reachable from search, anything not in your navigation. If a crawl missed something important, this is usually why. For the policies that live in someone's head rather than on a page. Often the highest-value thing you will add. `.txt`, `.md`, `.csv` and PDFs with a text layer. CSVs are chunked **row by row**, so a price never gets separated from its product. ## What is not supported **Scanned documents do not work.** There is no OCR. A PDF that is really a photograph of a page will ingest and contribute nothing, which is worse than failing loudly — so check that you can select the text in your PDF before uploading it. Also unsupported: images, video, and Word or Excel files. Export to PDF, CSV or text first. ## Getting good answers **Fewer, better pages.** Four hundred pages of navigation chrome make retrieval worse, not better. Index the pages that answer questions. **Write the answer down somewhere.** If three customers a week ask something your site never states, no amount of crawling will find it. Paste it in as text. **Check the gaps report.** [Analytics](/workspace/analytics) lists the questions the agent searched for and found nothing on. Each row is something worth adding — it is the most directly useful page in the product. ## Knowledge and language You do not need a knowledge base per language. Keep your material in one language; the agent translates as it answers, in whichever of the 37 supported languages the customer is using. # Leads and contacts Source: https://docs.thinnest.ai/agent/leads Ending the conversation with somebody to call, not just a closed ticket. A support bot answers a question and the visitor leaves. This one answers the question **and** leaves you a name and a reason to call. **Leads** has two tabs. **Inbound leads** is this page: people who came to your agents. **Find leads** goes the other way — your agent finds businesses you want to reach. See [Find leads](/agent/find-leads). ## How a lead gets captured When a visitor shows real interest, the agent asks for their details in the flow of the conversation and records: * name * email * phone * **a one-line note on what they actually wanted** That last field is the one that matters. "Priya, kitchen quote, 12ft run, budget around ₹4L" is a callback worth making. A bare phone number is not. ## Partial details are still a lead A phone number on its own is recorded. Later turns add to the same record rather than starting a new one, so a customer who gives their name first and their number three messages later ends up as one contact. ## Switching it off Per agent. Off removes the capability entirely — the agent has no way to ask rather than being asked not to. Worth turning off for a pure support agent, where asking for contact details reads as a sales tactic and annoys people. ## Contacts Everyone who has spoken to an agent, **leads first**, with what they asked for. One click goes from a contact to the conversation that produced them, so whoever calls back can read what was actually said. ## Groups A group is a named list of contacts you put together yourself — "Wholesale buyers", "Pune walk-ins" — so you can send a broadcast or run a voice campaign to exactly those people. On **Contacts**, choose **New group** and name it. Names are unique in your workspace, whatever the capitals. Tick contacts in the list (the box at the top ticks everyone on the page), then **Add to group** and pick the group. Somebody already in it is left as they are. A contact can be in any number of groups. Each group is a chip above the list with how many people are in it. Open one to see only its members; tick people there and **Remove from** the group to take them out. The **⋯** beside an open group renames or deletes it. Deleting a group never deletes its contacts. A group decides *who you are aiming at*, never *who agreed*. Consent still applies to everybody in it — see [narrowing by group](/whatsapp/broadcasts#narrowing-by-tag-or-group). ## Contact language Each contact can carry a language. Where a WhatsApp template exists in several languages, the contact's language decides which one they receive. A contact with no language set gets exactly what they would have got before — your default. Setting the field is an improvement, never a prerequisite. ## Consent Contacts carry a marketing consent state, and it is enforced in the database rather than in the interface. A campaign audience is computed at send time from consent — never from a list somebody exported last week. Businesses you found with [Find leads](/agent/find-leads) have not opted in, and saving them asks you to confirm that contacting them is your responsibility. They are the one exception to the opt-in rule for WhatsApp broadcasts; calls and everything else here still apply, and anyone who says stop is never contacted again. Somebody who opts out stops receiving **marketing**. Utility and authentication messages still reach them, and must: an order confirmation is not something anybody opted out of. # Spreadsheet Source: https://docs.thinnest.ai/agent/spreadsheet Your agent writes what it learns into your spreadsheet while it is still talking, and can look things up in it. Someone rings and gives their name, their number and what they want. The agent writes each answer into a row of your spreadsheet as it arrives, so a call that ends after two questions still leaves those two answers behind. It can also look somebody up — an order, a booking, a customer record — and answer from what you already hold. It works on every channel: website chat, WhatsApp, Telegram and phone calls. We use **Grist**, a spreadsheet you can sign up for free at [getgrist.com](https://www.getgrist.com). You connect your own account; we never put your data in ours. This is not the same as **Lead capture**, which stores names and numbers in your inbox here. This puts them in **your** spreadsheet, in columns you chose, where your team already works. You can have both on, and when you do the agent records the details in both places rather than choosing between them. ## Connecting your spreadsheet Go to **Actions**, find **Spreadsheet**, and click **Grist**. In Grist, click your profile picture → **Account settings** → **Developer**, and create a key in the **API keys** section. It does not expire until you remove it, so unlike a calendar key there is nothing here that quietly stops working next year. We call Grist once to check the key really belongs to someone. If it does not work you are told immediately. Open the document in Grist, go to **Settings** and find the **API** section. Copy the **Document ID** from there. The id in your browser's address bar is a **shorter** one, and the API cannot use it. If you paste that, we tell you so rather than letting you discover it as "document not found". We show you every table in the document with how many fields each has. Pick one. We then read that table's real column names and remember them. That is what the agent fills in. It cannot invent a column, and it cannot reach any other table or any other document. ## What the agent may do Two switches, both **off** until you turn them on. Connecting your spreadsheet on its own gives the agent nothing. | Switch | What happens | | - | - | | **Fill in a row** | Writes what it has learned into your table — one row per conversation, filled in as the answers arrive | | **Look something up** | Finds rows by matching one column exactly, so the agent can answer from what you already hold | Every switch you turn on — here, on your calendar, anywhere — is described to the agent on **every turn of every call**. A handful is sharp and quick. A long list is a slower reply from an agent less sure which one you meant. Nothing stops you, so the restraint is yours. ## Filling the lead's own row, not a new one A lead list usually arrives with a row per person already in it — imported from an ad, a form or a CSV — and the job of the call is to fill in the rest. Left alone, every call adds a fresh line underneath and the row you already had stays half empty. On the spreadsheet card, pick the column that identifies each person under **Update an existing row**, and how to compare it: | Comparing as | Use it for | What matches | | - | - | - | | **Phone number** | Mobile numbers | The last ten digits, so `+91 98018 05137`, `919801805137` and `98018-05137` are one person | | **Exact value** | An application id, an email, a name | The same text, ignoring case and spacing | When a caller matches, their answers go into **that** row. When nobody matches, a new row is added as before. We never guess the column. A column called *Contact* might hold an email or a phone number, and a wrong guess writes one customer's answers over another's. ### Filling in what the call already knew On a campaign you already know things the customer is never asked — their name and number came from your own list. A model told not to repeat those back never volunteers them either, so they would stay blank. **Fill from campaign variables** on the same card maps a column to a variable your campaign supplies, one per line: ``` MOBILE_NO=phone CANDIDATE_NAME=name ``` Anything the agent did learn wins; this only fills what was left blank. ## When to save: during the call, or after it A switch on the spreadsheet card, and it only changes **phone calls**. | | | | - | - | | **During the call** | Rows fill in as the customer answers, so the sheet is current while the call is still going. Each save costs the caller a short pause. | | **After the call** | The agent just asks its questions. The whole conversation is read once it ends and the row is written in one go. | Reading afterwards is usually the better call, and not only for the pause: * An answer given in the first minute lands in the right column even if the question came in the fourth. * Someone who corrects themselves — *"sorry, it's 42, not 41"* — is recorded once, correctly, instead of twice. * A call that ends abruptly still leaves everything that was said. What you give up is liveness: nothing appears until the call is over. **Chat and WhatsApp always save as they go.** Nothing there ends the way a call does, so there is no "after" to wait for. The switch is about calls. ## Save in the background A switch on the same card. With it on, the agent **stops waiting for Grist**. The answer is kept here the moment the customer gives it, and reaches your spreadsheet within a minute. Nothing is lost if Grist is slow, and nothing is lost if you have used up a free plan's calls for the month — the work waits and goes through when it can. **Turn it on if your agent answers the phone.** A caller hears every second the agent spends waiting, and this removes all of them. **Leave it off if you watch the sheet live** — during a campaign, say — and want the row to appear as the words are spoken. Either way the agent never claims something is saved when it is not. ## What your customer experiences > **Them:** Hi, I'd like a quote for a kitchen. > **Agent:** Of course. Can I take your name? > **Them:** Priya Nair. > **Agent:** Thanks Priya. And the best number to reach you on? By the end of that call there is one row in your spreadsheet with both answers in it, plus anything else you have columns for. ## A column for what happened Make a column called **Summary** — or Notes, Remarks, Comments — and turn on **Summarise each call** on the Voice page, and it fills with two or three sentences about what the customer said and how the call ended. It is the one column that is filled even when nothing else could be: somebody who says only *"not interested, I'm busy"* answers no question on your list, and that sentence is the whole useful record of the call. **Both halves are needed.** The switch is what starts it; making the column does not. And after adding any column in Grist, go back to the spreadsheet card and pick your table again — we cache the column list when you pin it, so a column added since is one the agent has never been told about. ## Things worth knowing **One row per conversation.** Every answer goes into the same row, so you get one line per customer rather than one line per thing they said. **The agent fills in only what it was told.** A field it never heard is left empty. It will not guess. **Formula columns are read, not written.** Grist computes those itself. If *every* column in the table is a formula we say so when you pick it — the agent can read that table but cannot save anything into it. **Looking something up is exact and case-sensitive.** `PRIYA` does not match `Priya`. Use it for order numbers, phone numbers and reference codes, which is what customers give verbatim anyway. **It will not search for nothing.** If the agent has no value to match on it asks the customer for one, rather than returning whatever happens to be at the top of your table. **At most five rows come back from a lookup.** More than that read aloud is a paragraph nobody can follow on a phone. **If you rename or delete a column in Grist, tell us.** We remember your columns from when you picked the table. Rename one and saves start failing. Fixing it takes ten seconds: open the card and choose the table again. **Free Grist has limits, and they are Grist's rather than ours.** A free site allows **3,000 API calls a month** and **5,000 rows per document**. A busy phone line can use those, and when they are gone Grist refuses — your card says so. *Save in the background* handles the smaller bumps for you. **Disconnecting keeps your data.** Rows already written stay in your spreadsheet; they are your records, not ours to remove. Anything still queued and not yet written is discarded, and we tell you how many that is before you are left wondering. ## If it stops working The card says **Needs attention** and shows what Grist said. Usually it is one of three things: the key was removed in Grist, the document is full or out of monthly calls, or a column was renamed. Create a new key and use **Replace key** if it is the first — your switches and your chosen table are remembered. ## Deliberately not included The agent cannot reach **any other document or table**, even though your API key could. It cannot **create or delete columns, tables or documents**, and it cannot **delete rows**. It fills the one table you pointed it at, and reads from it. Nobody phoning your business has a good reason to reshape your spreadsheet, and the safest way to guarantee that is not to give the agent the ability at all. # Website & app agent Source: https://docs.thinnest.ai/agent/website-and-app-agent Let your agent see the screen your customer is on, fill it in, point at the right button and step in when they get stuck. Switch this on and your agent stops being a chat in the corner. On your website — and, with the mobile SDKs, inside your app — it can see the screen your customer is on: the form, which fields are empty, the error under a field, the buttons. It answers about what is in front of them, fills fields in, points at the next step, and offers help on its own when someone gets stuck. By chat or by voice. ## Switch it on Go to **Agents**, open the agent on your website, and choose **Website & app** under **Deploy**. Switch on **Let the agent see and use the screen** and **Save**. If your chat widget is already on your site, there is nothing else to add: it works on the next page load. Every action is on until you switch it off: point at a field or button, scroll to it, put the cursor in a field, type into fields, choose from drop-downs, tick boxes, press buttons and links, open another page of your site, and run your own actions. Under **When it steps in**, add a moment: what happens, on which pages, after how long, and what the agent says. Set **At most … per visit**. ## What it always does — whatever you switch on * It **never reads or fills** passwords, card numbers, CVV, OTP, Aadhaar or PAN fields. * It sees **whether** a field is filled, not **what** is in it — unless you mark the field readable (below). * PAN, Aadhaar and card numbers shown on the page are masked before anything leaves it, and page addresses leave without their query string. * A button that **pays, submits or confirms** is pressed only after your customer taps **Allow** in the chat. If they tap **Not now**, the agent tells them so and leaves it to them. * It never touches anything you mark off-limits. ## The moments it steps in | Moment | What it means | | - | - | | The person has not done anything for a while | No typing, tapping or scrolling for the time you set | | A field shows an error | An error stays on screen for the time you set | | The same field fails twice | They fixed it, and it failed again | | Clicks the same thing again and again | Three or more clicks on one spot within a second | | Clicks something that does nothing | Nothing on the page changed after the click | | Moves to leave the page | Desktop only: the pointer leaves the top of the window | | Has been on the page for a while | Time on the page passes what you set | **On pages containing** takes part of the page address (`checkout`, `/kyc`) or the screen name your site sets (below). Leave it empty for every page. Each moment shows at most once per page, never while the chat is open, and never more than your per-visit limit. The message appears beside the chat button; a tap opens the chat with the agent already talking. ## Tell it more (optional) Add these after the widget tag: ```html theme={null} ``` Action names are lowercase with underscores. Whatever your function returns is what the agent is told. Nothing your function does passes through ThinnestAI. ## Control element by element (optional) ```html theme={null}
…
``` ## Voice If the agent takes calls on your website (**Voice → where it answers → Website**), it can act on the screen during a call too. The call shrinks to a small bar with the agent's name, a live caption, mute and hang-up, so your customer sees their details go in while they talk. Anything that pays or submits still waits for **Allow** in the chat. ## Mobile apps SDKs for **Android, iOS, React Native and Flutter** are in early access. They show the same agent — chat, voice, cards and forms — in a sheet over your app. Your app describes each screen to the agent (its fields, their state and errors) and carries out the actions in its own code. [Ask us for access](mailto:support@thinnest.ai). ## FAQ The extra script loads only when this is switched on for the agent, and it reads the page only while the chat is open or a call is on. It can fill the form. Pressing a button that submits, pays or confirms waits for your customer to tap Allow. To never let it press a particular button, mark it `data-ta-no-touch`. By design: values stay on your customer's device. Add `data-ta-allow` to a field the agent may read, such as a city or plan name — never to anything personal. # Create Action Source: https://docs.thinnest.ai/api-reference/actions/create-action /api-reference/openapi.json post /agents/{id}/actions Give the agent a call to your own API, with placeholders it fills from what the customer said. Dotted placeholders are filled by the platform, never the model: `{{call.id}}` (the id `GET /calls` lists), `{{call.from}}`, `{{call.to}}`, `{{call.direction}}` (empty outside a call), `{{conversation.id}}`, `{{contact.id}}`, `{{contact.phone}}`, `{{agent.id}}`, `{{workspace.id}}` and `{{test.chat}}` (`true` during a test chat, empty otherwise) — in the path, query, body or a header value, never the host; an unknown one is refused. Every call also sends `X-Call-Id`, `X-Conversation-Id`, `X-Agent-Id` and `X-Workspace-Id` when known, and `X-Test-Chat: true` during a test chat (`POST /agents/{id}/test-chat`), where the call values are empty and `X-Call-Id` is absent — so your endpoint can answer without writing anything. A new action is always **off**: test it with `POST …/test`, then `PATCH` `enabled: true` — sending `enabled` here is refused. Header values are sealed on arrival and never returned. A **build** key may do this. # Delete Action Source: https://docs.thinnest.ai/api-reference/actions/delete-action /api-reference/openapi.json delete /agents/{id}/actions/{actionId} Remove the action and its sealed headers. The agent stops being able to call it at once. A **build** key may do this. # Get Action Source: https://docs.thinnest.ai/api-reference/actions/get-action /api-reference/openapi.json get /agents/{id}/actions/{actionId} One action. Header values never appear; only `headerNames`. # List Actions Source: https://docs.thinnest.ai/api-reference/actions/list-actions /api-reference/openapi.json get /agents/{id}/actions The agent's actions — calls to your own HTTPS API that it may make mid-conversation — newest first. Header values never appear; only `headerNames`. # Test Action Source: https://docs.thinnest.ai/api-reference/actions/test-action /api-reference/openapi.json post /agents/{id}/actions/{actionId}/test Make the call once, for real, through the same address checks and credentials the agent uses — and it works while the action is still off, which is the point. A test has no conversation, so only `{{agent.id}}` and `{{workspace.id}}` fill; the rest are empty. A failed call is still `200`, with `ok: false`. On a `POST`, `PUT`, `PATCH` or `DELETE` action the test **does** the write. A **build** key may do this. # Update Action Source: https://docs.thinnest.ai/api-reference/actions/update-action /api-reference/openapi.json patch /agents/{id}/actions/{actionId} Change any field, or switch it on or off with `enabled`. The action is validated as it will be saved, so a new `url` is checked against the parameters already there. Leaving `headers` out keeps the saved credential; sending any replaces the whole set. `enabled: true` makes it callable from every conversation at once. A **build** key may do this. # Create Agent Source: https://docs.thinnest.ai/api-reference/agents/create-agent /api-reference/openapi.json post /agents Makes an agent exactly as the console does: a starter persona when you send no `instructions`, a website widget key, and (where your deployment has voice) a voice channel that answers on the web and WhatsApp. Only `name` is required. The plan's agent allowance, its model list and its voice tiers apply; a refused voice undoes the whole create. A **build** key may do this. # Delete Agent Source: https://docs.thinnest.ai/api-reference/agents/delete-agent /api-reference/openapi.json delete /agents/{id} Deletes the agent and everything under it: its conversations, knowledge, actions and channels. It cannot be undone. A **build** key may do this. Refused with `409` `agent_on_live_call` while the agent is on a call (ringing or connected): deleting it then would lose that call's recording, transcript, analysis and usage. Delete it after the call ends. # Get Agent Source: https://docs.thinnest.ai/api-reference/agents/get-agent /api-reference/openapi.json get /agents/{id} One agent: its instructions, model, languages, script, widget, call settings and the details it collects from calls. Which engine speaks its voice or runs its model is never part of the answer. # Get Agent BYOK Model Source: https://docs.thinnest.ai/api-reference/agents/get-agent-byok-model /api-reference/openapi.json get /agents/{id}/byok-model On a workspace that brings its own keys: the model this agent runs on your own LLM key. `model` is the agent's own choice (null when it runs the key's model), and `stale` names an earlier choice your key no longer reaches, which the key's model answers in place of. Refused with `409` when the workspace is not using its own keys, or when this agent is set to `byok: "off"` (it then runs on our models, and a choice you set is not available to read until you put it back on the workspace's keys). # Get Agent BYOK Voice Source: https://docs.thinnest.ai/api-reference/agents/get-agent-byok-voice /api-reference/openapi.json get /agents/{id}/byok-voice On a workspace that brings its own keys: the voice this agent speaks with on your own voice provider. `voice` is null for the provider's default voice. A choice belongs to the provider it was made on; after you switch your voice key to another provider, `stale` holds the old choice and calls speak the new provider's default until you choose again. On a Cartesia key the response also holds how the voice sounds: `speed`, `volume`, `emotion`, `agentModel` (this agent's own Sonic model; `model` is always your key's) and `pronunciationDictId`, each null while unset. Refused with `409` when the workspace is not using its own voice key (it brings no keys, or only its language model), or when this agent is set to `byok: "off"` (it then speaks in our voices). # List Agents Source: https://docs.thinnest.ai/api-reference/agents/list-agents /api-reference/openapi.json get /agents Every agent in your workspace, newest first, in the same shape Get Agent returns. Page with `limit` and `cursor`. # Set Agent BYOK Model Source: https://docs.thinnest.ai/api-reference/agents/set-agent-byok-model /api-reference/openapi.json put /agents/{id}/byok-model Chooses which model of your own LLM key this agent runs. It must be one the key reaches — listed under `byok.models` by List Models — or `null` to run the key's own model (the one set under Settings → Your keys). The next reply uses it. A **build** key may do this. # Set Agent BYOK Voice Source: https://docs.thinnest.ai/api-reference/agents/set-agent-byok-voice /api-reference/openapi.json put /agents/{id}/byok-voice Chooses the voice this agent speaks with on your own voice provider: an id from List BYOK Voices, checked against the provider's list for your key, or `null` for the provider's default. On Cartesia an id that is not on the list yet, such as a voice cloned a moment ago, is checked with Cartesia directly and accepted if your key owns it. The agent needs a voice channel first. A **build** key may do this. On a Cartesia key you can also set how this agent's voice sounds: `speed` (0.6 to 1.5), `volume` (0.5 to 2.0), `emotion` (one of Cartesia's values; English only), `model` (`sonic-3.6`, `sonic-3.6-2026-08-27`, `sonic-3.5`, `sonic-3` or `sonic-2` for this agent alone; `sonic-3.6` follows Cartesia's latest stable snapshot, the dated one never changes) and `pronunciationDictId` (a dictionary id from your Cartesia account). Every field is optional and the request only changes what it names: a key left out stays as it is, `null` goes back to the default, a value sets it. So `{ "voice": "…" }` alone changes the voice and keeps the settings; `{ "voice": null }` goes back to the provider's default voice and clears them. The settings belong to a voice of the agent's own: sending one before a voice is chosen, or with `voice: null`, is refused. `speed`, `volume` and `emotion` need a Sonic 3 model, and `speed` does not apply to a Cartesia Professional Voice Clone. A refused field answers `400` with `code: validation_failed` and `field` naming it. # Test Chat Source: https://docs.thinnest.ai/api-reference/agents/test-chat /api-reference/openapi.json post /agents/{id}/test-chat Talks to your real agent through its website chat — the same prompt, model, knowledge, actions, limits and billing as the console's Playground — and answers with its reply and every tool it used. It runs as a sandbox: anything that would phone, message, email or send a code to a person, or change a calendar booking, is switched off and the agent says what it would have done instead; your own actions still run for real, and every request they make carries an `X-Test-Chat: true` header (and `{{test.chat}}` fills as `true`) so your endpoint can answer without writing anything. The conversation appears in your inbox; send `session` back to continue it. A **build** key may do this. # Update Agent Source: https://docs.thinnest.ai/api-reference/agents/update-agent /api-reference/openapi.json patch /agents/{id} Changes any subset of the fields Create Agent takes and answers with the whole agent. Fields you leave out are untouched, and unknown fields are ignored — but a body that names no field this API knows is refused. A model or voice is checked against your plan only when it changes, and a refused voice refuses the whole patch before anything is written. A **build** key may do this. # Get Agent Analytics Source: https://docs.thinnest.ai/api-reference/analytics/get-agent-analytics /api-reference/openapi.json get /agents/{id}/analytics What the agent's Analytics page shows: totals, conversations/replies/leads per day, conversations by hour and by channel, the questions it could not answer and the questions asked most. Windows are rolling `days`; hours and days are UTC. `resolved` and `escalated` count conversations by their current status. Any key may call this, a **read-only** key included. # Get Dashboard Source: https://docs.thinnest.ai/api-reference/analytics/get-dashboard /api-reference/openapi.json get /dashboard The console Dashboard's figures for the whole workspace: how many agents, numbers, knowledge documents, campaigns, keys, members, contacts and leads; conversations unread and waiting on a person; calls in the last 30 days; and the balance as the account card shows it. The period charts are `GET /calls/summary` and `GET /whatsapp/summary`. Any key may call this, a **read-only** key included. # Authentication Source: https://docs.thinnest.ai/api-reference/authentication Bearer keys, and the one place they must never go. ``` Authorization: Bearer ta_live_… ``` Create a key in **Settings → API keys**. It is shown **once**, at creation, and stored only as a hash — nothing can retrieve it afterwards, including us. ## This is not your widget key Your **public key** (`pk_…`) identifies which agent answers and is meant to be visible in your page source. Your **API key** (`ta_live_…`) can message any of your customers. It belongs in a server's environment. Putting it in your HTML hands anybody the ability to message your entire contact list. ## Choose what each key can do When you create a key, you pick one of three access levels. The level applies everywhere the key is used — your own code calling the API, and the MCP server. A key's access cannot be changed later — create a new key and revoke the old one. | Access level | What the key can do | | - | - | | **Full access** | Everything, including messaging your customers and placing calls. Keys created before access levels existed have full access. | | **Build** | See everything, and set up agents, knowledge, actions, contacts, webhooks, sequences and their steps, WhatsApp forms, template and one-time-code drafts, projects, teams, saved replies and notification settings, and connect a Telegram bot. It can also test-chat an agent, resolve, take over and assign conversations, and pause a sequence or pause or cancel a campaign. It **cannot** send messages or codes, place or end calls, reply to a customer, report events, add anyone to a sequence, start a campaign or sequence, publish a form, rent or release a number, clone a voice, or change the workspace, its members or invitations. Test chats and adding knowledge still count towards your usage. | | **Read-only** | See everything. Change nothing. | If a key tries something its access level does not allow, the request is refused with `403` and a message saying what kind of key it is: ```json theme={null} { "error": "This API key is a build key: it can set up agents, actions, contacts and webhooks, but not message customers, send codes or place calls. Use a full key for that." } ``` Connecting an AI assistant such as Claude Code or Cursor to the [ThinnestAI MCP server](/mcp/overview)? The MCP server can do exactly what its key can do, so choose **Build** to let it set things up without being able to contact a customer, or **Read-only** if it only needs to report. See [Connect your assistant to the MCP server](/mcp/connect). ## Acting for one of your customers If you build your own product on the API, your key reaches each of your [customers](/api-reference/customers): add their id in a header. ``` Thinnest-Workspace: org_1f2e… ``` The request runs inside that customer, with your key's access level. An id that is not one of your customers is `404`, whether or not it exists. ## Revoking Immediate. Revoked keys are kept along with their last-used time, because that is the first thing anybody wants to know after a leak. ## Rate limits Per organization, per minute. ```http theme={null} HTTP/1.1 429 Too Many Requests Retry-After: 60 ``` Honour `Retry-After`. The limit exists because a loop in an integration can spend a month of WhatsApp budget before anybody looks at a dashboard — and because Meta rate-limits the number afterwards. The damage is money first and reputation second. Idempotency protects you from repeating **one** request. It does nothing about a thousand different ones. These are separate problems and both are guarded. ## Storing the key ```bash Environment theme={null} THINNEST_API_KEY=ta_live_xxxxxxxxxxxx ``` ```js Node theme={null} const res = await fetch("https://app.thinnest.ai/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.THINNEST_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); ``` ```python Python theme={null} import os, requests res = requests.post( "https://app.thinnest.ai/api/v1/messages", headers={"Authorization": f"Bearer {os.environ['THINNEST_API_KEY']}"}, json=payload, ) ``` # Bring your own keys Source: https://docs.thinnest.ai/api-reference/bring-your-own-keys Run your agents on your own speech-to-text, LLM and voice accounts, on your own voice and language model, on your own voice alone, or on your own language model alone. Each agent can follow the workspace or choose its own. With **bring your own keys** (BYOK), your workspace uses your own accounts with the providers. The three jobs are speech-to-text, the LLM and the voice. You pay those providers directly. Depending on what you bring, we charge a flat platform fee or our usual per-minute voice plan. You choose what you bring: * **All three of my keys** (scope `all`). Speech-to-text, the LLM and the voice are all yours. * **Only my voice** (scope `voice`). Your voice account speaks. Our speech-to-text hears the caller, our model answers, and the phone line is ours. * **My voice and my language model** (scope `llm_voice`). Your voice account speaks and your LLM account answers, on any model your key offers. We keep the speech recogniser (our speech-to-text hears the caller), and the phone line is ours. * **Only my language model** (scope `llm`). Your LLM account answers, on any model your key offers. We keep the speech recogniser and the voice, and the phone line is ours. You pay the same per-minute voice plan as any other call: Standard, Premium or Studio, by the voice the agent speaks with. | | All three of my keys | My voice and my language model | Only my voice | Only my language model | | - | - | - | - | - | | Call minute | **₹1.00** (\$0.012) a minute, all-in | **₹1.00** (\$0.012) a minute, our speech-to-text and phone line included | **₹1.50** (\$0.02) a minute, our phone line included | Our usual voice plan: Standard, Premium or Studio, with your plan's discount | | Chat reply | **₹0.02** per reply | **₹0.02** per reply | Our usual price, because the model is ours | **₹0.02** per reply, as with all three | Calls are billed in 30-second pulses, at the rate set when the call starts. With only your language model, the model you run never changes the price of the minute: the price follows the voice alone, however costly your model is on your own account. With all three, BYOK turns on only when a speech-to-text key, an LLM key and a voice key are all added and checked. With your voice and your language model, it needs a checked LLM key and a checked voice key. With only your voice, it needs only a checked voice key. With only your language model, it needs only a checked LLM key, with a model chosen. Any plan can turn it on. With all three keys, your voice and language model, or only your language model, each agent can also run its own **model** from your LLM key. On the agent's **Playground**, the Model list shows every model your key reaches. Leave it on your key's model, which is the one set in **Settings → Your keys**, or pick another for this agent alone. Each agent speaks with a voice you choose from your own voice provider. Open the agent's **Voice** page: it lists every voice your key can use, and **Play** speaks a short line in the agent's language. The preview runs on your account, so the provider charges you for those few characters. If a provider hosts its own sample of a voice, that sample plays instead, at no cost. An agent without a chosen voice speaks with a standard voice from your provider. On a call with all three, your speech-to-text hears the caller, your LLM answers, and your voice speaks. With your voice and your language model, our speech-to-text hears the caller, and your LLM answers and your voice speaks. With only your voice, only the speaking is yours. With only your language model, only the answering is yours: our speech-to-text hears the caller and our voice speaks. If one of your keys cannot be used when a call starts, the call is not answered on our providers instead. ### Which models answer calls with only your voice With only your voice, the call minute includes our model, so a call runs only on a low-cost model. Today those are: * Prana * Prana \[Voice] * GPT-OSS 120B * GPT-5 Nano * GPT-4.1 Nano The rule is Prana, Prana \[Voice] and GPT-OSS 120B by name, plus any voice model (other than our call-only Composer, which runs a second model each turn) whose price per token is at or below our reference low-cost voice model on both input and output, so a new model that is cheap enough joins the list on its own. `GET /api/v1/models` marks each model with `voiceOnlyByok`. An agent already set to another model keeps it for chat, which is billed as usual, and runs on GPT-OSS 120B on calls. The agent's model picker lists only the allowed models while you use only your voice. Setting another model through `POST` or `PATCH /api/v1/agents` returns a 400 that says why. With all three of your keys, with your voice and language model, with only your language model, or without BYOK, nothing changes: with only your language model the model is your key's, so any model your key offers can answer calls. The limit applies only to agents that use your keys: an agent you have switched off your keys (below) answers on our stack, so it may use any model. ### Each agent chooses how it uses your keys BYOK is switched on for the workspace, and the scope you choose in **Settings → Your keys** is the **default**: every agent follows it until it is told otherwise. A workspace holds its keys once, and each agent can choose for itself. An agent's `byok` is one of: * `null` (or `"workspace"`): follow the workspace's default scope. This is how every agent starts. * `"off"`: never use your keys (below). * `"all"`, `"voice"`, `"llm"` or `"llm_voice"`: run this agent on that scope's keys, whatever the workspace default is. For example, with an LLM key and a voice key saved, one agent can run on `"llm"` (your model, our voice) while another runs on `"llm_voice"` (your model and your voice). Choosing a scope is refused with `409` unless the workspace holds checked keys, each with a model, for everything the scope needs. The message names the missing key, for example "no verified voice key (tts)". Switching BYOK off in the workspace is the master switch: with it off, no agent uses a key, whatever it chose. The workspace default decides which keys turning BYOK on needs; an agent's own scope needs its keys too, so a key an agent's scope needs cannot be removed while BYOK is on (set the agent to follow the workspace first, or replace the key). A call or reply is priced and run under the scope that applies to the agent when it starts, so two agents in one workspace can bill differently: the price follows the agent's scope. If an agent's scope can no longer be served when a call starts (a key was removed while BYOK was off), the agent falls back to the workspace's scope and the call goes ahead; it is never refused for that, and never billed at a rate that does not match how it ran. A call can be handed to another agent only when that agent would run on the same stack the call started on. A hand-off to an agent on a different scope, or switched off your keys, is refused. #### An agent on ours Give an agent `byok: "off"` and it never uses your keys, whatever the workspace does: * calls run on our voices, our speech-to-text and our model, and the minute is billed at our normal rate for that voice, not the BYOK rate; * chat replies are billed as normal replies, not the flat BYOK reply charge. With only your language model, the minute is already our normal rate, so what changes for an agent set to `"off"` is the model (ours, not yours) and the chat reply price; * the voice-only model limit does not apply to it; * the model and voice choices you saved for your own keys are kept and ignored until you switch the agent back to `"workspace"`. The other agents in the workspace keep their own choice and price. A customer of yours follows the same rule on its own agents. The setting is read once when a call or a reply starts, so a call is never priced at one rate and run on another stack. A call already ringing keeps the stack it was started on: if you switch its agent off your keys in those few seconds, the call is refused rather than moved onto ours. An agent set to `"off"` is billed at our rate for the voice it uses. A new agent starts on a Premium voice, which bills at the Premium rate, so choose a standard voice for an agent whose minutes should be cheaper. ## In the console Open **Settings → Your keys** and choose the default: **All three of my keys**, **My voice and my language model** (we keep the speech recogniser, ₹1/min), **Only my voice** or **Only my language model**. All three key cards are always shown, so you can save a key for an agent that chooses a different scope. Keys for jobs your default does not cover are not used by agents that follow it. On an agent's **Playground** or **Voice** page, **How Agent uses your keys** chooses between following the workspace, **Off**, and each set your saved keys allow. Each card has: * **Provider.** Choose who does that job. * **Model.** Every model the provider offers. Before you add a key, the list shows the models the provider offers. Once your key is checked, it shows the models **your** key reaches. Long lists are grouped by family, such as Flux, Aura-2 and Aura for Deepgram's voices. * **API key**, plus any other field the provider needs. Azure, for example, needs a region. **Verify and save** checks the key with the provider before storing it. A key the provider rejects is not saved. Keys are stored encrypted, and are only ever shown by their last four characters. Once the cards you need are filled, switch on **Use my own keys**. ## Providers | Job | Providers | | - | - | | Speech-to-text | Deepgram, AssemblyAI, Soniox, Speechmatics, Gladia, Sarvam, ElevenLabs, Cartesia, OpenAI, Groq, Mistral, Azure Speech | | LLM | OpenAI, Anthropic Claude, Google Gemini, Groq, Mistral, Sarvam, DeepSeek, xAI Grok, Together AI, Fireworks AI, Cerebras, OpenRouter, or any OpenAI-compatible URL | | Voice | Deepgram, ElevenLabs, Cartesia, OpenAI, Sarvam, Soniox, Mistral, Azure Speech, Rime, Inworld, Hume, Murf, Speechify | **Azure Speech** takes your key and region. If your Azure resource accepts its key only at its own address, as Azure AI Foundry resources do, also give its **resource endpoint**, such as `https://my-resource.cognitiveservices.azure.com`. **Any OpenAI-compatible URL** takes a base URL that starts with `https://`. Azure OpenAI's `https://.openai.azure.com/openai/v1` is one. If the server does not list its models, send the model id with the key, and we check the two together with a one-token request. On calls, a Google Gemini 2.5 or 3 model is asked to think less before it speaks, so the caller is not left waiting (a Pro model cannot switch thinking off, so it is asked for the least it allows). Chat replies are not changed. ## When a key fails There is no fallback to our providers for a job that is yours. If your provider refuses a request (a revoked key, no credit, rate limits), the reply fails, or the voice does not speak. We do not answer on our own models and bill you for it. You get a notification naming the provider and the reason, at most once a day per provider. With only your voice, speech-to-text and the LLM are ours, so our usual backups for them stay on. With only your language model, the speech-to-text and the voice are ours and keep our usual backups, but the model never has one: if your key fails, the caller hears our apology and the call is flagged, and we do not answer on our model. To hear about changes to your keys in your own systems, subscribe a webhook to [`byok.changed`](/api-reference/webhooks#workspace-events): it is sent when a key is added, replaced, removed or refused by its provider, and when your own keys are switched on or off or set to cover different jobs. It names the job and the provider, never any part of the key. Each call's [Get Call](/api-reference/calls/get-call) also says which keys it ran on, in `billing.keys`. ## Customers If you build on our API with [customers](/api-reference/customers), your customers use **your** keys while BYOK is on, with your choice of all three, your voice and language model, only the voice or only the language model. A customer can bring a complete set of its own for that choice (three keys, an LLM and voice key, a voice key or an LLM key), which then overrides yours. Turning BYOK off in your workspace turns it off for every customer that inherits your keys. A customer with its own complete set and its own switch on keeps using its own. ## API Every request below also works for a customer, with the `Thinnest-Workspace` header. ### Status ```http theme={null} GET /api/v1/byok ``` ```json theme={null} { "enabled": true, "scope": "all", "usingScope": "all", "using": "own", "complete": true, "credentials": [ { "kind": "stt", "provider": "deepgram", "label": "Deepgram", "key": "…k9z2", "fields": { "model": "nova-3-general" }, "model": "nova-3-general", "models": ["flux-general-en", "nova-3-general", "…"], "verifiedAt": "2026-10-06T10:00:00Z", "updatedAt": "2026-10-06T10:00:00Z" } ] } ``` `scope` is what this workspace brings by default: `all`, `voice`, `llm` or `llm_voice`; each agent may choose its own (below). `usingScope` is the scope that applies right now (a customer runs on its developer's), or `null`. `complete` says whether the keys for `scope` are ready. `using` is `own`, `developer` (a customer using its developer's keys) or `none`. `models` is what the provider offered this key, and `model` is the one it runs. ### Add or replace a key ```http theme={null} PUT /api/v1/byok/credentials ``` ```json theme={null} { "kind": "llm", "provider": "openai", "credentials": { "apiKey": "sk-…", "model": "gpt-5.4-mini" } } ``` * **`kind`.** One of `stt`, `llm` or `tts`. * **`provider`.** One of the provider ids in the table above, in lower case. An error lists the ones that do that job. * **`credentials.model`.** Optional. Without it, a speech key starts on the provider's usual model. An LLM key starts on the provider's usual model when it has one; otherwise choose one. * **Replacing a key.** If you replace a key with one for the same provider, its model is kept. | Status | Why | | - | - | | `400` | The provider rejected the key, or does not offer that model on it. | | `409` | BYOK is on, and this key would leave a job without a model. | | `502` | The provider could not be reached to check the key. Try again. | ### Change the model, or refresh the list ```http theme={null} PATCH /api/v1/byok/credentials/llm ``` ```json theme={null} { "model": "gpt-5.4-nano" } ``` The key is not sent again. The model must be on the key's `models` list. For a model the provider launched after you added the key, send `"refreshModels": true`, alone or together with the new `model`. ### Turn BYOK on or off ```http theme={null} PATCH /api/v1/byok ``` ```json theme={null} { "enabled": true, "scope": "voice" } ``` Send `enabled`, `scope`, or both. `scope` is `all`, `voice`, `llm` or `llm_voice`. The request is refused with `409` until the keys for the scope are added, checked, and each has a model: all three for `all`, the voice key for `voice`, the LLM key for `llm`, the LLM key and the voice key for `llm_voice`. While BYOK is on, moving to a scope needs that scope's keys first, for example the other two keys to move from `voice` to `all`. ### Voices ```http theme={null} GET /api/v1/byok/voices ``` ```json theme={null} { "provider": { "id": "elevenlabs", "label": "ElevenLabs" }, "model": "eleven_flash_v2_5", "items": [ { "id": "EXAVITQu4vr4xnSDxMaL", "name": "Sarah", "language": "en", "gender": "female", "sample": "https://…", "own": false } ] } ``` These are the voices your voice provider offers your key. `sample` is a clip the provider hosts, when it has one. On Deepgram, a voice is a model, such as `aura-2-helena-en` or `flux-alexis-en`. On Cartesia the list starts with your own cloned voices. `own` is `true` for a voice that belongs to your own account with the provider — one you cloned or designed there — and `false` for the provider's library: | Provider | What `own` means | | - | - | | Cartesia | The voices your Cartesia account owns (listed first) | | ElevenLabs | The voices your account owns: your cloned and designed voices | | Soniox | Always `true`: Soniox lists only your account's own voices | | Every other provider | Always `false`: the provider does not say which voices are yours | ### Preview a voice ```http theme={null} POST /api/v1/byok/voices/preview ``` ```json theme={null} { "voice": "EXAVITQu4vr4xnSDxMaL", "agentId": "agt_…" } ``` The response is the audio itself: `audio/mpeg`, or `audio/wav` for Sarvam. Without `text`, the voice speaks a short line of ours in the agent's language, or in `language` if you send it. Any `text` you send is capped at 200 characters, because your provider charges you for every character. Soniox has no preview here: it speaks only on a live call. ### Choose an agent's model `GET /api/v1/models` lists our models in `items`, as before. On a workspace using its own keys it also returns `byok`: ```json theme={null} { "byok": { "provider": { "id": "openai", "label": "OpenAI" }, "workspaceModel": "gpt-5.4-nano", "models": ["gpt-5.4-mini", "gpt-5.4-nano", "…"] } } ``` ```http theme={null} PUT /api/v1/agents/{id}/byok-model ``` ```json theme={null} { "model": "gpt-5.4-mini" } ``` The model must be one your key reaches. Send `null` to go back to your key's model. `GET` on the same path returns the agent's choice. It also returns `stale`, the agent's earlier choice, if your key no longer reaches it; until you choose again, the agent runs your key's model. ### Choose an agent's voice ```http theme={null} PUT /api/v1/agents/{id}/byok-voice ``` ```json theme={null} { "voice": "EXAVITQu4vr4xnSDxMaL" } ``` The voice must be one that `GET /byok/voices` lists. Send `null` to go back to the provider's standard voice. `GET` on the same path returns the agent's current choice. Your own cloned voices are listed and can be used. On Cartesia, `GET /byok/voices` lists your account's own voices first, then the whole library (up to 2,000 voices), so a clone is always on the list however large the library is. If Cartesia is slow to answer, the list can come back short: the response then says `"complete": false`, is not cached, and you should ask again. A voice you cloned a moment ago is accepted here by its id even before it shows up in the list. To use a different voice for a single call, put it in that call's `overrides`, as `"overrides": { "voice": "EXAVITQu4vr4xnSDxMaL" }` on `POST /api/v1/calls` or in a batch. On a workspace using its own keys, the voice must be one of yours from `GET /byok/voices`. A per-call voice is checked like an agent's voice: if it is not on the list we ask the provider about that one voice directly, so a clone made a moment ago is accepted. If the provider cannot be reached to check, the request fails with `502` and can be retried; only a provider that says it has no such voice gives a refusal. A choice belongs to the provider it was made on. If you switch your voice key to another provider, the agent speaks with the new provider's standard voice until you choose again. `GET` shows the old choice as `stale` until then. ### Tune a Cartesia voice If your voice key is Cartesia, each agent can also set how its voice sounds. A client who finds calls quiet or a little fast can fix it for that agent alone, without touching the others. In the console, open the agent's **Voice** page and choose a voice. A **How it sounds** panel appears under the list, with a **Hear it** button that speaks a short line with the settings as they are now (on your account, so Cartesia charges you for those few characters) and a **Save settings** button. Through the API, send the same fields to the voice call above: ```http theme={null} PUT /api/v1/agents/{id}/byok-voice ``` ```json theme={null} { "speed": 0.95, "volume": 1.4, "emotion": "calm", "model": "sonic-3.5", "pronunciationDictId": "pdict_123" } ``` | Field | Values | Default | | - | - | - | | `speed` | 0.6 to 1.5 | 1, Cartesia's own | | `volume` | 0.5 to 2.0. Raise it if calls sound quiet. | 1 | | `emotion` | One of Cartesia's emotions, such as `neutral`, `calm`, `content`, `sad`, `angry`, `scared`, `happy`, `excited`. | the words decide | | `model` | `sonic-3.6`, `sonic-3.6-2026-08-27`, `sonic-3.5`, `sonic-3` or `sonic-2`, for this agent alone. `sonic-3.6` is Cartesia's own name for its newest 3.6 version; the dated one is a fixed release. | your key's model | | `pronunciationDictId` | The id Cartesia gave a pronunciation dictionary in your account | none | Every field is optional, and a request changes only the fields it names. A field you leave out stays as it is, `null` puts one back to its default, and a value sets it. So `{ "voice": "…" }` alone changes the voice and keeps the settings. `{ "voice": null }` goes back to the standard voice and clears them, because the settings belong to a voice of your own: sending one before a voice is chosen, or together with `"voice": null`, is refused. A setting that is not allowed answers `400` with `code: validation_failed` and `field` naming it. `GET` on the same path returns the settings, each `null` while unset. It returns the agent's own model as `agentModel`, because `model` there is always your key's. A few things to know: * `emotion` works in English only. In other languages Cartesia ignores it. * `speed`, `volume` and `emotion` need a Sonic 3 model. With `sonic-2` they are refused, and if your key's model is later changed to `sonic-2` they are simply not sent. * `speed` does not apply to a Professional Voice Clone, which keeps the pace of its recording. Use `volume`, or an Instant Voice Clone, to adjust it. * Cartesia treats these as guidance, not exact changes. Listen with **Hear it** or the preview below before you rely on a value. * A call that names its own voice in `overrides` keeps the agent's settings. * Only an agent speaking with your Cartesia voice has them. Other providers are not affected. To hear a setting without saving it, add the same fields to the preview: ```http theme={null} POST /api/v1/byok/voices/preview ``` ```json theme={null} { "voice": "a0e99841-438c-4a64-b679-ae501e7d6091", "speed": 0.95, "volume": 1.4 } ``` Only the fields you send are spoken; the agent's saved settings are not read. ### Choose how one agent uses your keys `byok` is a field of the agent, on `POST /api/v1/agents` and `PATCH /api/v1/agents/{id}`, and comes back on every agent: ```http theme={null} PATCH /api/v1/agents/{id} ``` ```json theme={null} { "byok": "llm_voice" } ``` * Leave `byok` out and the agent's choice is unchanged. * `null` (or `"workspace"`) follows the workspace's default scope. An agent that follows the workspace comes back as `"workspace"`. * `"off"` keeps this agent on our voices, models and pricing. * `"all"`, `"voice"`, `"llm"` or `"llm_voice"` runs this agent on that scope's keys. It is refused with `409` if the workspace has no checked key for something the scope needs, and the error names it. * Anything else is a 400. In the console, the same choice is **How Agent uses your keys** on the agent's Playground and Voice pages, shown only while your workspace uses its own keys. Changing it needs an API key that can edit agents, as for the agent's model and voice. While an agent is `"off"`, `GET /api/v1/agents/{id}/byok-model` and `byok-voice` answer `409`, because it uses neither of your providers. `PUT` still saves a choice for when you switch it back on. ### Remove a key ```http theme={null} DELETE /api/v1/byok/credentials/tts ``` The request is refused with `409` while BYOK is on and the key is one the scope needs. Under scope `all` that is every key; under scope `voice` only the voice key is required, under scope `llm` only the LLM key, and under scope `llm_voice` the LLM key and the voice key, so a leftover key for a job that is ours can be deleted while BYOK is on. It is also refused while an agent is set to a scope that needs the key. Replace a needed key instead, set that agent to follow the workspace, or turn BYOK off first. Changing keys or switching BYOK needs an API key with **full** access. A read-only key can see the status. # Add BYOK Key Source: https://docs.thinnest.ai/api-reference/bring-your-own-keys/add-byok-key /api-reference/openapi.json put /byok/credentials Adds or replaces your key for one job. The key is checked with the provider before anything is stored — a key the provider rejects is not saved — then stored encrypted and only ever shown by its last four characters. The provider's model list is pulled at the same time. Without `credentials.model`, a speech key starts on the provider's usual model and an LLM key on its usual one where it has one; replacing a key with one for the same provider keeps the model already chosen. Needs a **full** key. # Delete BYOK Key Source: https://docs.thinnest.ai/api-reference/bring-your-own-keys/delete-byok-key /api-reference/openapi.json delete /byok/credentials/{kind} Removes your key for one job. Refused while BYOK is on, since a workspace on its own keys always has all three — turn BYOK off first, or replace the key with Add BYOK Key. Needs a **full** key. # Get BYOK Status Source: https://docs.thinnest.ai/api-reference/bring-your-own-keys/get-byok-status /api-reference/openapi.json get /byok Whether your workspace runs on its own keys, whose keys are in use (`own`, or `developer` for a customer using its developer's), which keys you bring (`scope`: all three, only your voice, only your language model, or your language model and your voice), whether your keys for that scope are complete enough to switch on, and each key, masked to its last four characters. # List BYOK Keys Source: https://docs.thinnest.ai/api-reference/bring-your-own-keys/list-byok-keys /api-reference/openapi.json get /byok/credentials Your workspace's own provider keys, one per job, masked to their last four characters, with the model each runs and every model its provider offered it. # List BYOK Voices Source: https://docs.thinnest.ai/api-reference/bring-your-own-keys/list-byok-voices /api-reference/openapi.json get /byok/voices The voices your own voice provider offers your key — what Set Agent BYOK Voice accepts. `sample` is a clip the provider hosts, free to play, when it has one. On Cartesia the list starts with your own cloned voices, then the whole library (up to 2,000 voices); if Cartesia is slow to answer the list can come back short: `complete` is then `false`, it is not cached, and you should ask again. A per-call `overrides.voice` is checked the same way as an agent's voice: if it is not on the list we ask the provider about that one voice directly, so a voice missing from a short list is still accepted. Refused with `409` when the workspace is not using its own keys; its voices are then List Voices. These are the voices of the agents that use your keys: an agent set to `byok: "off"` (Update Agent) speaks in our voices from List Voices, at our normal pricing, while your other agents stay on your keys. # Preview BYOK Voice Source: https://docs.thinnest.ai/api-reference/bring-your-own-keys/preview-byok-voice /api-reference/openapi.json post /byok/voices/preview Speaks one short line in a voice of your own voice provider, on your key, and answers with the audio itself. Your provider charges you for the characters, so `text` is capped at 200; without it the voice says a short greeting of ours in `language`, or the agent's language when you name an agent. Soniox has no preview: it speaks only on a live call. On a Cartesia key, `speed`, `volume`, `emotion`, `model` and `pronunciationDictId` are spoken too, so a setting is heard before it is saved (they work as in Set Agent BYOK Voice; the agent's saved settings are not read). Needs a **full** key. # Turn BYOK On or Off Source: https://docs.thinnest.ai/api-reference/bring-your-own-keys/turn-byok-on-or-off /api-reference/openapi.json patch /byok Switches your workspace onto your own keys, or back onto ours, and/or chooses what you bring (`scope`). Scope `all` needs all three keys — speech-to-text, LLM and voice — added, verified and each with a model chosen; scope `voice` needs only the voice key, and speech-to-text, the LLM and telephony stay ours; scope `llm` needs only the LLM key, and speech-to-text and the voice stay ours (calls are billed on the normal voice plan, whatever your model); scope `llm_voice` needs the LLM key and the voice key, and our speech recogniser stays (a call is a flat ₹1/min like `all`). `scope` is the default for your agents: any agent may follow it, be switched off, or choose another scope your keys allow (Update Agent `byok`). Any plan may turn it on. On a developer's workspace, turning it off turns it off for every customer using your keys, and a customer inherits your scope. Needs a **full** key. # Update BYOK Key Model Source: https://docs.thinnest.ai/api-reference/bring-your-own-keys/update-byok-key-model /api-reference/openapi.json patch /byok/credentials/{kind} Changes the model a stored key runs without sending the key again. `model` must be on the key's `models`; for a model the provider launched after you added the key, send `refreshModels: true` — alone, or with the new `model`, which is then checked against the fresh list. While BYOK is on, a refreshed list that drops the chosen model is refused. Needs a **full** key. # Cancel Call Source: https://docs.thinnest.ai/api-reference/calls/cancel-call /api-reference/openapi.json delete /calls/{id} Calls off a call you placed through the API (`out_…` or `sch_…`). A call still waiting to ring — booked, queued for its hours, or a retry — is cancelled and answers `200` with its report, `status: "cancelled"`. A call ringing or connected is ended on the line and answers `202` with the report as it stands; its ending arrives a moment later with `call.analysed`, and no retry follows. A call that has already ended answers `409`. Needs a **full** key. # Get Call Source: https://docs.thinnest.ai/api-reference/calls/get-call /api-reference/openapi.json get /calls/{id} One call and everything it learned: status, timings, how it ended, the summary, the collected `fields`, the transcript, a signed `recording` link and the call's `quality` figures (reply times, silences, interruptions, model errors). It is exactly the object `call.analysed` delivers, so a missed delivery can be read back here; `analysedAt` null means the summary and fields are not final yet. Any id List Calls hands out works — `out_…` and `sch_…` for calls you placed, the call reference for any other. # Get Call Transcript Source: https://docs.thinnest.ai/api-reference/calls/get-call-transcript /api-reference/openapi.json get /calls/{id}/transcript What was said on one call, oldest first: this call's spoken turns only, internal notes left out — the same words `call.analysed` delivers. `?format=txt` returns a plain-text file to save (`call-{id}.txt`, one `[mm:ss] Who: what` line per turn). A turn's `offset` is when it was written down, not where it falls in the audio, so it suits ordering and rough position but not subtitles. # List Calls Source: https://docs.thinnest.ai/api-reference/calls/list-calls /api-reference/openapi.json get /calls Every call your workspace made or took — inbound, website, WhatsApp, a campaign's or one you placed — newest first, without transcripts or recordings. Each item's `id` opens the call with Get Call. A call booked for later appears here only once it has rung; read a booked call by the `sch_…` id Place Call gave you. # Place Batch Calls Source: https://docs.thinnest.ai/api-reference/calls/place-batch-calls /api-reference/openapi.json post /calls/batch Rings up to 200 people with one request. Every option Place Call takes may be set once for the batch and again on an entry, the entry winning (an entry's `variables` sit on top of the batch's); `purpose` and `agent` are the batch's. Every entry is queued and paced inside your plan's lines and balance — none rings inline — and each gets its own `sch_…` id that reports through `call.analysed` and Get Call. A bad entry is listed under `refused` and does not stop the rest. Needs a **full** key. # Place Call Source: https://docs.thinnest.ai/api-reference/calls/place-call /api-reference/openapi.json post /calls Your agent rings a customer, opens with your `purpose` (spoken aloud, so 300 characters or fewer), then holds a normal conversation. A number that is not a contact yet becomes one with the `name` and `source` you send; someone who opted out or is on your do-not-call list is refused. Outside the calling hours — never wider than 9am–9pm in the customer's own time — or with `scheduledAt`, the call is queued and answers `status: "scheduled"` with a `sch_…` id; otherwise it rings now with an `out_…` id. Each call is billed from your balance, refused with `402` when it cannot pay, held to your plan's concurrent lines and 60 requests a minute; what it learned arrives as `call.analysed` or from Get Call. Needs a **full** key. # Cancel Campaign Source: https://docs.thinnest.ai/api-reference/campaigns/cancel-campaign /api-reference/openapi.json post /campaigns/{id}/cancel Stops a draft, sending or paused campaign for good and sets `finishedAt`. Messages already handed to WhatsApp, and calls already placed, still complete. This cannot be undone, and it is also how you get rid of a draft you do not want: campaigns cannot be edited or deleted, so cancel it and create a new one. A **build** key may do this, since it only ever stops sends. # Create Campaign Source: https://docs.thinnest.ai/api-reference/campaigns/create-campaign /api-reference/openapi.json post /campaigns Creates a WhatsApp broadcast (`kind: "whatsapp"`) or a calling campaign (`kind: "voice"`) as a draft, with the same checks as the console's builder: WhatsApp connected and the template approved with every blank filled, or for calls an agent with a number that answers phone calls and is licensed for this kind of call. The audience is built from your contacts right away, by consent; people who asked not to be contacted are always left out. Send `launch: true` to start it at once, or `scheduledAt` to start it later; a launch that is refused (no paid plan, not enough balance) still leaves the draft and says why in `message`, and an audience of nobody is never launched. Needs a **full** key. # Get Campaign Source: https://docs.thinnest.ai/api-reference/campaigns/get-campaign /api-reference/openapi.json get /campaigns/{id} Returns one broadcast or calling campaign with its current state and counts. Poll it while a campaign is sending to follow its progress; `finishedAt` is set when it ends or is cancelled, and `lastError` says why a campaign stopped, when it did. # Launch Campaign Source: https://docs.thinnest.ai/api-reference/campaigns/launch-campaign /api-reference/openapi.json post /campaigns/{id}/launch Starts a draft, as the console's Send or Start button does, re-running its checks at the moment they matter: your plan must include campaigns, a calling campaign's agent must still have a number that answers phone calls (and, for promotional calls, is licensed for them), and your balance must cover the send. If the balance covers only part of it, the campaign starts and `message` says how far it goes; the rest stays queued until you top up. Messages go out in the background; calls are paced by your free call lines. A campaign already sending answers `200` and is left alone, and a scheduled draft launched here starts now instead of waiting. Needs a **full** key. # List Campaigns Source: https://docs.thinnest.ai/api-reference/campaigns/list-campaigns /api-reference/openapi.json get /campaigns Lists your workspace's broadcasts and calling campaigns, newest first, each with its live counts: recipients, sent, delivered, read, replied, failed and skipped. Filter by `status` to watch only the ones that are sending. # Pause Campaign Source: https://docs.thinnest.ai/api-reference/campaigns/pause-campaign /api-reference/openapi.json post /campaigns/{id}/pause Stops a sending campaign from sending any more. Messages already handed to WhatsApp, and calls already placed, still complete. Resume Campaign picks up where it stopped, without messaging anyone twice. Only a `sending` campaign can be paused. A **build** key may do this, since it only ever stops sends. # Resume Campaign Source: https://docs.thinnest.ai/api-reference/campaigns/resume-campaign /api-reference/openapi.json post /campaigns/{id}/resume Sends a paused campaign again, to everyone it has not reached yet; nobody is messaged or called twice. It clears `lastError`, and what it sends is charged as usual. Only a `paused` campaign can be resumed; a draft is started with Launch Campaign. Needs a **full** key. # Connect Telegram Bot Source: https://docs.thinnest.ai/api-reference/channels/connect-telegram-bot /api-reference/openapi.json post /agents/{id}/channels/telegram Connects a Telegram bot to the agent with the token @BotFather gave you: the token is checked with Telegram, stored encrypted and never shown again, and Telegram is told where to deliver messages. Telegram needs a paid plan, and a bot can belong to one agent only. Telegram calls back on the address this request arrived on, so call the public API address. A **build** key may do this. # Disconnect Telegram Bot Source: https://docs.thinnest.ai/api-reference/channels/disconnect-telegram-bot /api-reference/openapi.json delete /agents/{id}/channels/telegram Disconnects the agent's Telegram bot (a 404 when none is connected) and tells Telegram to stop delivering to us. Its conversations stay in your inbox, and the bot itself still exists in Telegram — delete it in @BotFather if you want it gone. A **build** key may do this. # Get Agent Channels Source: https://docs.thinnest.ai/api-reference/channels/get-agent-channels /api-reference/openapi.json get /agents/{id}/channels Where the agent answers, as the Channels page shows it: the widget key, the WhatsApp number and its health, the Telegram bot and the voice line. No token or secret is ever part of the answer. The widget's look and the call settings are changed with Update Agent (`widget`, `voice`). # Add Contacts To Group Source: https://docs.thinnest.ai/api-reference/contact-groups/add-contacts-to-group /api-reference/openapi.json post /contact-groups/{id}/members Adds up to 1,000 contacts to the group. Contacts already in it are left as they are (`added` counts only the new ones). Every id must be a contact in this workspace, or nothing is added and the answer is `404` naming them. A **build** key may do this. # Create Contact Group Source: https://docs.thinnest.ai/api-reference/contact-groups/create-contact-group /api-reference/openapi.json post /contact-groups Creates an empty group. Names are unique in the workspace whatever the capitals; a second one answers `409`. A **build** key may do this. # Delete Contact Group Source: https://docs.thinnest.ai/api-reference/contact-groups/delete-contact-group /api-reference/openapi.json delete /contact-groups/{id} Deletes the group and its memberships. Its contacts stay. A campaign not yet sent that was aimed at it no longer reaches anybody through it. A **build** key may do this. # Get Contact Group Source: https://docs.thinnest.ai/api-reference/contact-groups/get-contact-group /api-reference/openapi.json get /contact-groups/{id} One group and how many contacts are in it. # List Contact Groups Source: https://docs.thinnest.ai/api-reference/contact-groups/list-contact-groups /api-reference/openapi.json get /contact-groups Every group in the workspace, by name, each with how many contacts are in it. Returned whole: `nextCursor` is always null. # Remove Contacts From Group Source: https://docs.thinnest.ai/api-reference/contact-groups/remove-contacts-from-group /api-reference/openapi.json delete /contact-groups/{id}/members Takes up to 1,000 contacts out of the group. The contacts stay; ids not in the group are ignored (`removed` counts the ones taken out). A **build** key may do this. # Rename Contact Group Source: https://docs.thinnest.ai/api-reference/contact-groups/rename-contact-group /api-reference/openapi.json patch /contact-groups/{id} Renames a group. Campaigns aimed at it keep reaching it. A **build** key may do this. # Create Contact Source: https://docs.thinnest.ai/api-reference/contacts/create-contact /api-reference/openapi.json post /contacts Creates a contact — or, when someone is already on that number, **updates that person** instead, so a CRM that syncs the same lead twice never doubles your list. Answers `201` for a new contact and `200` for one it updated. Creating a contact is not consent: only `consent.status: "granted"` with a `source` is. A **build** key may do this. # Delete Contact Source: https://docs.thinnest.ai/api-reference/contacts/delete-contact /api-reference/openapi.json delete /contacts/{id} **Erases** the contact: their conversations and transcripts, their calls, any queued call and their campaign rows go with them, and so does any duplicate that was merged into them. This cannot be undone. Use it for a person's request to be forgotten; to stop messaging someone, set their consent to `withdrawn` instead. A **build** key may do this. **Recordings are erased too.** The audio of their calls is deleted before the call rows go: a recording we store is deleted right away, the phone provider's copy of a call on a number we rent is deleted through your workspace's own account with them, and any copy made for model training is deleted. A file that cannot be deleted at that moment is queued and retried every night until it is gone; the contact is erased regardless, and the response says how many are still queued in `recordingsPending`. **Knowing when it is finished.** Subscribe a webhook to `contact.erased`: it is sent once the erasure is complete — straight away when this answers 204, or from the nightly retry once the last queued recording is gone — with only the contact's id and when it was erased. **What is not erased.** Do-not-call entries are kept deliberately, so the person is not called again. Webhook payloads we already sent are purged on their own schedule after 7 days. Copies in your own systems, and in your vendors', are yours to delete. # Get Contact Source: https://docs.thinnest.ai/api-reference/contacts/get-contact /api-reference/openapi.json get /contacts/{id} One contact, by id. # List Contacts Source: https://docs.thinnest.ai/api-reference/contacts/list-contacts /api-reference/openapi.json get /contacts Contacts with a phone number, newest first. Filter by tag, by your CRM's id, or by phone to look one person up. Contacts folded into another by a merge are left out. # Update Contact Source: https://docs.thinnest.ai/api-reference/contacts/update-contact /api-reference/openapi.json patch /contacts/{id} Changes any of the fields Create Contact takes, consent included; fields you leave out keep their values, and `null` clears a field. `tags` replaces the whole list. A contact whose consent is `withdrawn` is never sent marketing, enrolled in a sequence or put in a campaign. A **build** key may do this. # Assign Conversation Source: https://docs.thinnest.ai/api-reference/conversations/assign-conversation /api-reference/openapi.json post /conversations/{id}/assign Make a conversation a teammate's job, by their email — or `null` to put it back in the pool. The teammate is notified when it really moves to them. Assigning does **not** silence the agent; taking it over with Update Conversation Status does. List your teammates with `GET /members`. The shared inbox needs a paid plan. A **build** key may do this. # List Conversation Messages Source: https://docs.thinnest.ai/api-reference/conversations/list-conversation-messages /api-reference/openapi.json get /conversations/{id}/messages One conversation's messages, newest first: the customer's, your agent's, and replies a teammate sent from the inbox (`author: "teammate"`). Your team's internal notes are left out. Photos, voice notes, videos and documents a customer sent come back as links valid for **eight hours** — download what you need to keep. # List Conversations Source: https://docs.thinnest.ai/api-reference/conversations/list-conversations /api-reference/openapi.json get /conversations Your workspace's conversations, newest first. `since` keeps only those a customer has written in at or after that moment — the way to find what a missed `message.received` webhook would have told you, including a conversation whose very first message you never heard about. Go back a few minutes from the last event you handled, since messages that arrive together can be delivered out of order. # Reply to Conversation Source: https://docs.thinnest.ai/api-reference/conversations/reply-to-conversation /api-reference/openapi.json post /conversations/{id}/reply Your own agent platform answers a WhatsApp customer — the other half of the `message.received` webhook. Send exactly one of `text`, `media`, `template` or `form`; text, media and forms need the customer's 24-hour window open, a template does not. Only works on a number with **Connect External Agent/CRM** turned on, and not while a teammate has taken the conversation over. Each message is charged like any WhatsApp message from that number. Needs a **full** key. # Update Conversation Status Source: https://docs.thinnest.ai/api-reference/conversations/update-conversation-status /api-reference/openapi.json post /conversations/{id}/status The inbox's three moves: `resolved` marks it done and fires the `conversation.resolved` webhook, `active` hands it back to the agent, and `human_handling` takes it over so the agent stays quiet until it is handed back. An internal note records which API key made the change; the customer never sees it. `changed: false` means it was already in that state, and nothing was written or announced. The shared inbox needs a paid plan. A **build** key may do this. # Customers Source: https://docs.thinnest.ai/api-reference/customers Build your own product on the API: a workspace for each of your customers, reached with one key. A **customer** is a workspace you create for one of your own customers — with its own agents, contacts, knowledge, phone numbers and WhatsApp. Your one API key reaches any of them by adding a header. Everything they use is billed to you. Your first customer makes your workspace a **developer workspace**. A workspace that resells our console under its own brand (white label) cannot also have API customers. ## Create a customer ```http theme={null} POST /api/v1/customers Authorization: Bearer ta_live_… Idempotency-Key: 6f1d2c9a-… Content-Type: application/json ``` ```json theme={null} { "name": "Acme Dental", "externalId": "acme-42", "metadata": { "tier": "gold", "seats": 3 }, "timezone": "Asia/Kolkata" } ``` `201 Created` ```json theme={null} { "id": "org_1f2e…", "name": "Acme Dental", "externalId": "acme-42", "metadata": { "tier": "gold", "seats": 3 }, "timezone": "Asia/Kolkata", "allottedCallLines": null, "archivedAt": null, "eraseAfter": null, "createdAt": "2026-10-06T10:00:00Z" } ``` | Field | | | - | - | | `name` | Required. 1–120 characters. | | `externalId` | Your own id for this customer: 1–128 letters, digits, `.` `_` `:` or `-`. Unique among your customers. | | `metadata` | Up to 50 keys (1–40 characters each). Values are strings (up to 500 characters), numbers or booleans — no nested objects or lists. 8 KB in all. | | `timezone` | An IANA time zone such as `Asia/Kolkata`. | Send an `Idempotency-Key` so a retried request returns the same customer instead of making a second one. Creating a customer needs a key with **full** access. | Status | Why | | - | - | | `400` | A field is wrong — the message names it. | | `402` | Your plan's customer limit is reached. | | `409` | Another customer already has that `externalId`, or your workspace uses white label. | ## Work inside a customer Add `Thinnest-Workspace` with the customer's `id` to **any** request. It runs inside that customer as if the customer had sent it. ```http theme={null} POST /api/v1/agents Authorization: Bearer ta_live_… Thinnest-Workspace: org_1f2e… ``` ```json theme={null} { "name": "Front desk" } ``` | You send | You get | | - | - | | No header, or your own workspace | Your own workspace | | One of your customers | That customer | | Someone else's customer, or an id that does not exist | `404 Workspace not found.` | | A customer you deleted | `410` — with when it was deleted and when it will be erased | | The header from a workspace with no customers | `400` | Your key's access level applies inside the customer too: a read-only key can only read there. ## List customers ```http theme={null} GET /api/v1/customers?limit=50 ``` Newest first. Pass `nextCursor` back as `cursor` for the next page. Filters: `externalId`, `archived=true` (deleted, waiting to be erased), `createdAfter`, `createdBefore` (ISO 8601 times). Manage customers from your own workspace — **without** `Thinnest-Workspace`. ## Get one customer ```http theme={null} GET /api/v1/customers/org_1f2e… ``` The customer, plus: ```json theme={null} { "counts": { "agents": 2, "phoneNumbers": 1, "contacts": 418 }, "spendThisMonthMicro": 1840000 } ``` `spendThisMonthMicro` is what this customer's usage has cost you since the 1st of the month (UTC), in millionths of your currency — `1840000` is ₹1.84. ## Update a customer ```http theme={null} PATCH /api/v1/customers/org_1f2e… ``` ```json theme={null} { "name": "Acme Dental Pvt", "metadata": { "tier": null, "region": "south" } } ``` You can change `name`, `externalId` (`null` clears it), `timezone`, `allottedCallLines` (how many of your call lines this customer may use at once; `null` for no cap) and `metadata`. Metadata is **merged**: keys you leave out are kept, and a key set to `null` is removed. ## Delete a customer ```http theme={null} DELETE /api/v1/customers/org_1f2e… ``` Release the customer's phone numbers and disconnect its WhatsApp number first. Deleting is refused (`409`, naming them) while it still holds either, because releasing a number cannot be undone. When you delete a customer: * requests into it answer `410`, and keys made for it stop working; * its webhooks are switched off, its campaigns cancelled, its sequences and scheduled callbacks stopped; * a call already in progress finishes; * **30 days later it is erased**: every conversation, contact, recording and file. Until then you can bring it back: ```http theme={null} POST /api/v1/customers/org_1f2e…/restore ``` Restoring does not restart campaigns or bring back old keys — start those again yourself. ## Keys for one customer To give a customer its own API access, or to run one customer's integration on a key that cannot reach the others: ```http theme={null} POST /api/v1/customers/org_1f2e…/keys ``` ```json theme={null} { "name": "Acme's dashboard", "scope": "read" } ``` The key (`ta_live_…`) is in the response **once**. `scope` is `full`, `build` or `read`. | | | | - | - | | `GET /api/v1/customers/{id}/keys` | The customer's keys — name, prefix, access level, last used. Never the key itself. | | `DELETE /api/v1/customers/{id}/keys/{keyId}` | Revoke it, immediately. | A key made for one customer works only in that customer. ## Move a phone number between customers A number one customer rents can be handed to another customer, or to your own workspace, with its digits kept — for a customer that is splitting in two, or a number you rented for yourself while setting a customer up. In the console it is on the **Phone Numbers** page: open the number's menu and choose **Move to another workspace…**. Through the API: ```bash theme={null} curl -X POST https://app.thinnest.ai/api/v1/phone-numbers/918041234567/move \ -H "Authorization: Bearer $KEY" \ -H "Thinnest-Workspace: org_…from…" \ -H "Content-Type: application/json" \ -d '{ "toWorkspace": "org_…to…" }' ``` Send it as the workspace that holds the number (no header when that is your own). It answers with the number, `from`, `to` and `paidThrough` — the last month already paid, after which the new workspace's rent begins. The number arrives unassigned; point it with `PATCH /api/v1/phone-numbers/{number}` as the new workspace. An Indian number needs the new workspace's own business details approved first (`number_move_needs_business_details`), and nothing moves during a live call (`number_on_live_call`). See [Move Phone Number](/api-reference/phone-numbers/move-phone-number) for every refusal. ## Billing Everything your customers use — calls, messages, replies, phone number rental — is charged to **your** balance at your plan's prices. Your customers have no balance of their own. When your balance runs out, your customers stop with you until you top up. A customer is always on your plan. Your plan decides how many customers you can have: | Plan | Customers | | - | - | | Free and Pay as you go | 3 | | Pro | 100 | | Scale | 1,000 | | Enterprise | 10,000 | Need more? Write to us. A deleted customer counts until it is erased. On the Free plan, the included replies, voice minutes and WhatsApp messages are **shared** by you and all your customers — one trial, not one each. ### What a customer can see Your balance stays yours. Inside a customer, `GET /api/v1/usage` returns `"billedTo": "developer"` and no `wallet`, and its console shows no balance and nothing to pay — so a key you give a customer, or a person you invite into it, never sees what you hold. ### Your cloned voices Every voice you clone can be used by all your customers' agents, automatically: inside a customer, [`GET /voices`](/api-reference/voices/list-voices) lists it with `"sharedFromDeveloper": true`. Only you can rename or delete it, and it counts against your clone limit. A customer's own clones are its own; you do not see them. More on the [Voice Clone](/channels/voice-clone#your-customers-can-use-your-voices) page. ### How long a customer's calls are kept Set `callRetentionDays` on a customer with [`PATCH /workspace`](/api-reference/workspace/update-workspace) and the `Thinnest-Workspace` header, the same as any other setting of theirs. See [How long calls are kept](/workspace/data-and-erasure#how-long-calls-are-kept). ### Notifications Your customers have no members of their own, so anything we would tell them — a webhook switched off, a template rejected, a campaign in trouble — comes to **you**, with the customer's name in the title, and is emailed according to your own notification settings. Its **Open** link takes you into that customer's workspace, to the template or campaign it is about. So does a low-balance warning caused by your customers' usage. ### Currency You and your customers use one currency, fixed once you have a customer. If you have not chosen one when you create your first customer, it is set to rupees — pick dollars before then if you bill in dollars. # Create Customer Source: https://docs.thinnest.ai/api-reference/customers/create-customer /api-reference/openapi.json post /customers Creates a workspace for one of your customers and answers with its `id` — send that id as `Thinnest-Workspace` to act inside it. Your first customer makes your workspace a developer workspace; everything a customer uses (calls, messages, replies, number rental) is charged to your balance at your plan's prices, and your plan caps how many customers you may have (3 on Free and pay-as-you-go, 100 on Pro, 1,000 on Scale, 10,000 on Enterprise; a deleted customer counts until it is erased). Send an `Idempotency-Key` so a retry never makes a second workspace. Needs a **full** key. # Create Customer Key Source: https://docs.thinnest.ai/api-reference/customers/create-customer-key /api-reference/openapi.json post /customers/{id}/keys Mints an API key that works in this one customer only — to give your customer its own API access, or to run one customer's integration on a key that cannot reach the others. The full key (`ta_live_…`) is in this response **once**; store it, because it is never shown again. Its usage is still charged to you. Needs a **full** key. # Delete Customer Source: https://docs.thinnest.ai/api-reference/customers/delete-customer /api-reference/openapi.json delete /customers/{id} Deletes a customer: requests into it answer `410` from now on, every key made for it is revoked, its webhooks are switched off, its campaigns cancelled and its sequences and scheduled callbacks stopped (a call already in progress finishes). **30 days later it is erased** — every conversation, contact, recording and file — unless you restore it first. Refused while the customer still holds a rented phone number or a connected WhatsApp number: release those yourself first, because that cannot be undone. Deleting an already-deleted customer answers it as it is. Needs a **full** key. # Get Customer Source: https://docs.thinnest.ai/api-reference/customers/get-customer /api-reference/openapi.json get /customers/{id} One customer, with how many agents, active phone numbers and contacts it has and what its usage has cost you since the 1st of this month (UTC). Deleted customers waiting to be erased are found too. # List Customer Keys Source: https://docs.thinnest.ai/api-reference/customers/list-customer-keys /api-reference/openapi.json get /customers/{id}/keys Every API key made for this customer, newest first, revoked ones included: name, prefix, access level and when it was last used. Never the key itself. Not paged. A deleted customer answers `404`. # List Customers Source: https://docs.thinnest.ai/api-reference/customers/list-customers /api-reference/openapi.json get /customers Your customer workspaces, newest first. Deleted customers waiting to be erased are left out unless you ask for them with `archived=true`. Call it from your own workspace: with the `Thinnest-Workspace` header it is refused, because a customer has no customers of its own. # Restore Customer Source: https://docs.thinnest.ai/api-reference/customers/restore-customer /api-reference/openapi.json post /customers/{id}/restore Brings back a deleted customer before its erase date, with everything it held. It does not restart campaigns or revive the keys that were revoked when it was deleted: start those again and mint new keys. Restoring a customer that was never deleted answers it unchanged. Needs a **full** key. # Revoke Customer Key Source: https://docs.thinnest.ai/api-reference/customers/revoke-customer-key /api-reference/openapi.json delete /customers/{id}/keys/{keyId} Revokes one of a customer's keys, immediately: its next request answers `401`. Answers with the key as it now is. A key already revoked is `404`. Needs a **full** key. # Update Customer Source: https://docs.thinnest.ai/api-reference/customers/update-customer /api-reference/openapi.json patch /customers/{id} Changes a customer's `name`, `externalId`, `timezone`, `allottedCallLines` or `metadata`; fields you leave out are kept. `metadata` is merged, not replaced: keys you do not mention stay, and a key set to `null` is removed. A deleted customer must be restored before it can be changed. Needs a **full** key. # Add Do-Not-Call Number Source: https://docs.thinnest.ai/api-reference/do-not-call/add-do-not-call-number /api-reference/openapi.json post /do-not-call Puts a number on the list; from then on no outbound call rings it. Adding a number already there changes nothing and answers `200` with the entry as it was. A **build** key may do this — it only ever stops calls. # Get Do-Not-Call Number Source: https://docs.thinnest.ai/api-reference/do-not-call/get-do-not-call-number /api-reference/openapi.json get /do-not-call/{phone} Whether a number is on the list, and how it got there. # List Do-Not-Call Numbers Source: https://docs.thinnest.ai/api-reference/do-not-call/list-do-not-call-numbers /api-reference/openapi.json get /do-not-call The workspace's do-not-call list, newest first. # Remove Do-Not-Call Number Source: https://docs.thinnest.ai/api-reference/do-not-call/remove-do-not-call-number /api-reference/openapi.json delete /do-not-call/{phone} Takes a number off the list, so it can be called again. Needs a **full** key. If the person asked on a call not to be called (`source: call`), be sure they have since agreed before removing it. # Errors Source: https://docs.thinnest.ai/api-reference/errors The status, the machine-readable code that tells refusals apart, and whether retrying will help. Every error answers a JSON body with a sentence for a person and a code for a program: ```json theme={null} { "error": "That customer has asked not to be contacted.", "code": "opted_out" } ``` ## Branch on `code`, never on the sentence The status says what kind of problem it is. The **`code`** says exactly which. A `403` on `POST /calls` can mean the customer opted out, the number is on your do-not-call list, or the key is not a full key; those are three different codes, and what you do about each is different (an opted-out customer is never rung again, a do-not-call number can be taken off the list, a key can be swapped for a full one). The sentence in `error` is for showing to a person and **may be reworded in any release**. A `code` does not change; new ones are only ever added. So match on `code`, and treat a code you do not recognise as the generic one for its status. ```js theme={null} const res = await fetch("https://app.thinnest.ai/api/v1/calls", { method: "POST", headers, body }); if (res.status === 403) { const { code } = await res.json(); if (code === "opted_out" || code === "do_not_call") markUncallable(lead); else if (code === "key_scope_build" || code === "key_scope_read") alertOps("use a full key for calls"); } ``` ### Which limit a `429` hit A `429` names the limit in `code` and sizes it in a `limit` object, next to the `Retry-After` header (seconds to wait): ```json theme={null} { "error": "Over 60 calls a minute. Slow down and retry.", "code": "rate_limit_calls", "limit": { "name": "calls", "perMinute": 60 } } ``` `limit.perMinute` is set for the limits counted per minute; `limit.max` (and `limit.windowSeconds` when it has a window) for the others, such as `concurrent_calls` and the codes one number may be sent. ### A `402` on `POST /calls` `code` is `insufficient_balance`. The existing `reason` field says which limit stopped the call: `no balance`, `trial spent`, `overdrawn`, `too many calls`, `balance unknown` or `retry buffer`. ## Statuses | Status | Means | What to do | | - | - | - | | `400` | Malformed request, or a template blank is missing | Fix and resend. Retrying unchanged fails identically | | `401` | Key missing, wrong or revoked | Check the header. **Do not retry**. The answer is the same whichever it is, so a probe learns nothing about your keys | | `402` | Money or the plan: the balance cannot pay, or the plan does not include it | Top up or upgrade. `code` says which | | `403` | The key's level, or a refusal about the person or number | `code` says which. Retrying unchanged fails the same way | | `404` | No template of that name on your account | Check the name. Names are per account, so another business's template is invisible to you | | `409` | Template not approved, WhatsApp not connected, contact opted out, or Meta has restricted the account | Look at **Templates** or **Channels**. Retrying makes a restriction worse | | `409` | The same idempotency key is still in flight | Retry in a moment; the first request will have answered by then | | `409` | Deleting an agent that is on a live call (`agent_on_live_call`: "The agent is on a live call. Delete it after the call ends.") | Nothing was deleted. Delete it after the call ends. In the console the same refusal reads "This agent is on a call right now. Delete it once the call ends." | | `429` | Rate limited | Wait for `Retry-After` | | `503` | WhatsApp is temporarily unavailable | Ours to fix, not yours. Safe to retry with the same idempotency key | ## Every code Generated from the same list the API uses, so it cannot drift from it. A status with no more specific code answers its generic one: `validation_failed` (400), `unauthorized` (401), `payment_required` (402), `forbidden` (403), `not_found` (404), `conflict` (409), `gone` (410), `unprocessable` (422), `rate_limited` (429), `internal_error` (500) and `service_unavailable` (502, 503, 504). **The key and the workspace it acts in** | `code` | Means | | - | - | | `unauthorized` | The key is missing, wrong or revoked. The answer is the same whichever it is. | | `key_scope_read` | The key is read-only and the request changes something. Use a build or full key. | | `key_scope_build` | The key is a build key and the request needs a full key: messaging customers, sending codes, placing calls, spending money or changing who has access. | | `workspace_not_found` | The `Thinnest-Workspace` header names a workspace that is unknown, is not one of your customers, or is not an `org_` id. | | `workspace_deleted` | The customer named in `Thinnest-Workspace` was deleted. Restore it before its erase date. | | `workspace_not_developer` | `Thinnest-Workspace` was sent from a workspace that is not a developer workspace. | | `workspace_header_forbidden` | A key that belongs to one customer workspace cannot act as another. | **Limits: which one was hit (a `limit` object names it)** | `code` | Means | | - | - | | `rate_limit_resources` | Over the per-minute limit on ordinary API requests for this workspace. Wait for `Retry-After`. | | `rate_limit_family` | Over the per-minute limit across all of a developer's customers. Wait for `Retry-After`. | | `rate_limit_calls` | Over the per-minute limit on `POST /calls`. Wait for `Retry-After`. | | `rate_limit_messages` | Over the per-minute limit on `POST /messages`. Wait for `Retry-After`. | | `rate_limit_codes` | Over the per-minute limit on `POST /codes`. Wait for `Retry-After`. | | `rate_limit_code_recipient` | This number has already been sent the most codes allowed in the window. Wait for `Retry-After`. | | `concurrent_calls` | Every line the plan allows is already on a call. Retry when one finishes. | | `number_daily_limit` | The `from` number you chose has placed its calls for today. Use another number. | | `rate_limited` | Too many requests of another kind. Wait for `Retry-After` when it is given. | **Placing a call** | `code` | Means | | - | - | | `opted_out` | The customer asked not to be contacted. Nothing you change in the request makes it allowed. | | `do_not_call` | The number is on the workspace's do-not-call list. Remove it from the list to call it. | | `stop_list_unknown` | The opt-out and do-not-call lists could not be read, so the call was not placed. Safe to retry shortly. | | `insufficient_balance` | The balance cannot pay for a call. `reason` says which limit stopped it: `no balance`, `trial spent`, `overdrawn`, `too many calls`, `balance unknown` or `retry buffer`. | | `outside_calling_hours` | It is outside the calling hours and the request said `ifOutsideHours: refuse`. `nextOpening` says when. | | `number_never_callable` | The calling hours never open for this number on the days given, in its own time zone. | | `call_in_progress` | The person is on a call right now, with this agent or another in the workspace. | | `call_already_scheduled` | A call to this number is already scheduled on this agent. Its `id` and `scheduledFor` come back. | | `calling_not_set_up` | The workspace has no agent that answers the phone, or its phone provider is not connected. | | `carrier_unavailable` | The phone provider would not place the call. 503 is worth retrying; 502 is this number. | | `agent_not_found` | No agent of that name or id answers the phone in this workspace. | | `agent_ambiguous` | More than one agent answers the phone. Name one in `agent`. | | `from_number_invalid` | `from` is not a number this agent can call from. | **Agents** | `code` | Means | | - | - | | `signing_unavailable` | Requests cannot be signed right now, so a setting that needs a signing secret (the who-is-calling lookup) was not saved. Try again later. | | `agent_on_live_call` | The agent is on a live call, and deleting it would lose that call's recording, transcript and usage. Delete it after the call ends. | | `phone_number_required` | `voice.surfaces` includes `phone`, but the agent has no phone number to answer or call from. Attach one first. | **Numbers** | `code` | Means | | - | - | | `invalid_number` | The phone number could not be read. | | `number_required` | Connect a WhatsApp number first; this needs one. | | `number_claimed_elsewhere` | That number is already registered here. Get in touch if it is yours. | | `needs_own_carrier_keys` | The number is on your own carrier account; add that account's API credentials to place calls from it. | | `number_cannot_forward` | Calls to this number cannot be forwarded: its phone provider does not support it here. | | `forward_destination_not_allowed` | `forwardTo` is a premium-rate, shared-cost, toll-free, satellite or high-cost international number, which a forward cannot go to. | | `forward_loop` | `forwardTo` is this number or another number on the platform, which would forward calls in a loop. | | `number_move_needs_developer` | Moving a number between workspaces needs a developer's key, from its own workspace or acting as one of its customers. | | `number_move_same_workspace` | `toWorkspace` is the workspace the number is already in. | | `number_on_live_call` | The number is on a call right now. Move it once the call ends. | | `number_move_needs_business_details` | An Indian number is held in the name of the business using it: the target workspace's business details must be approved first. | | `number_move_carrier_account` | A number on your own carrier account moves only when both workspaces have that same account's keys saved. | | `number_move_in_progress` | That number is already being moved (another request, or a retry still in flight). Wait a few minutes and read it again. | | `number_move_legacy_number` | This number was set up before workspace-level phone accounts existed and cannot be moved here. Contact support to move it. | | `number_move_unavailable` | The phone network did not take the move. The number is still where it was, answered by no agent; safe to retry shortly. | **Messages and codes** | `code` | Means | | - | - | | `template_not_found` | No template of that name in this workspace. | | `template_not_approved` | The template is not approved yet. | | `template_language_missing` | The template is not approved in that language. `available` lists the ones it is. | | `template_unsendable` | The template carries something no send can fill. Edit the template. | | `code_template_not_found` | There is no approved one-time-code template matching the request. | | `code_template_ambiguous` | More than one code template is approved; name one with `template` or `language`. | | `whatsapp_not_connected` | The workspace has no connected WhatsApp number. | | `whatsapp_blocked` | The WhatsApp account is restricted. Retrying makes a restriction worse. | | `whatsapp_not_configured` | WhatsApp is not available right now. | | `free_allowance_used` | The free plan's monthly WhatsApp allowance is used. Pay as you go lifts it. | | `send_failed` | The message provider would not take the message. Safe to retry with the same idempotency key. | **Plan and money** | `code` | Means | | - | - | | `plan_required` | The plan does not include this. Upgrade to use it. | | `plan_limit` | The plan's allowance of this is used (for example customers). Upgrade, or remove one. | | `payment_required` | Another 402: money or a plan upgrade is needed. Read `error`. | **Idempotency** | `code` | Means | | - | - | | `idempotency_in_progress` | That `Idempotency-Key` is still being processed. Retry in a moment for its answer. | **The generic code for a status that has no specific one** | `code` | Means | | - | - | | `validation_failed` | The request was malformed or a value is out of range. Retrying unchanged fails the same way. | | `forbidden` | Not allowed, for a reason with no more specific code. | | `not_found` | No such resource in this workspace. | | `conflict` | The request clashes with the resource's current state. | | `gone` | It existed and has been deleted or has expired. | | `unprocessable` | The request is well formed but cannot be acted on in the state things are in. | | `internal_error` | Something failed on our side. Safe to retry; if it persists, tell us. | | `service_unavailable` | A dependency is unavailable. Safe to retry shortly. | ## On 409 in particular When Meta restricts an account, every refused request is a signal that the business has not noticed. **A retry loop against a restriction extends it.** The console shows the reason and when it lifts. Go and look rather than retrying. `409` also covers the correct-behaviour cases: a **marketing** template addressed to somebody who opted out is refused. That is not a bug to work around. ## Retry policy that will not hurt you ```js theme={null} const RETRYABLE = new Set([429, 500, 502, 503, 504]); async function send(payload, key, attempt = 0) { const res = await post(payload, key); if (res.ok) return res.json(); if (!RETRYABLE.has(res.status) || attempt >= 4) { // 400, 401, 404 and 409 never succeed on a retry. Surface them. throw new Error(`${res.status}: ${await res.text()}`); } const after = Number(res.headers.get("Retry-After") ?? 0); const backoff = after > 0 ? after * 1000 : 2 ** attempt * 1000; await sleep(backoff); // Same idempotency key on every attempt — that is what makes this safe. return send(payload, key, attempt + 1); } ``` The important line is the last one: **the same idempotency key on every attempt**. Generating a fresh key per retry turns a safe retry into a duplicate message. Keys are honoured for 24 hours, so a backoff that runs for minutes is well inside the window. A job parked overnight and released the next morning is not — it will send. # Report Event Source: https://docs.thinnest.ai/api-reference/events/report-event /api-reference/openapi.json post /events Tells us something happened to a customer. Every sequence or tag rule subscribed to that event name runs: sequences that stop on it end first, then tags are added or removed, then sequences that start on it enrol the contact (unless they opted out, and only into an active sequence). The contact is created if the number is new. Nothing subscribed is still a `200`, with `matched: false` — you can send events before anyone has set up what they do. Only send events for people who agreed to hear from you: an event that starts a sequence is taken as their opt-in. Needs a **full** key. # Create Form Source: https://docs.thinnest.ai/api-reference/forms/create-form /api-reference/openapi.json post /forms Saves a form, **unpublished** — it cannot be sent until Publish Form succeeds. Up to 8 questions; field names are what each answer comes back under. Without `agent`, the form goes to the agent holding your WhatsApp number, else your oldest agent. No connected number is needed to build one. A **build** key may do this. # Delete Form Source: https://docs.thinnest.ai/api-reference/forms/delete-form /api-reference/openapi.json delete /forms/{id} Deletes the form; it can no longer be sent. Answers already collected stay, marked as from a removed form. A **build** key may do this. # Get Form Source: https://docs.thinnest.ai/api-reference/forms/get-form /api-reference/openapi.json get /forms/{id} One form, with its questions and whether it can be sent. # List Forms Source: https://docs.thinnest.ai/api-reference/forms/list-forms /api-reference/openapi.json get /forms Your workspace's WhatsApp forms, newest first, published or not. # Publish Form Source: https://docs.thinnest.ai/api-reference/forms/publish-form /api-reference/openapi.json post /forms/{id}/publish Publishes the form to your workspace's **own** WhatsApp Business Account, which makes it sendable. Needs a connected WhatsApp number whose connection was finished. If WhatsApp refuses (most often because the business is not verified yet), the form stays saved and you can publish again later; a failed republish leaves the live version on the air. Needs a **full** key. # Update Form Source: https://docs.thinnest.ai/api-reference/forms/update-form /api-reference/openapi.json patch /forms/{id} Changes any of `name`, `category` and `form` (`form` is replaced whole). **Any** change unpublishes the form, as saving does in the console — the published copy still asks the old questions — so publish it again before sending it. A **build** key may do this. # API reference Source: https://docs.thinnest.ai/api-reference/introduction The transactional API — the messages your systems send, not the ones your marketing does. This API exists for the messages a **system** sends: an order confirmation, a delivery update, a one-time password. One customer, one event, right now. It is not the campaigns API, and the difference is not cosmetic. | | Broadcasts | This API | | - | - | - | | Triggered by | A person, in the console | Your code, on an event | | Audience | A list computed from consent | One customer | | Typical category | Marketing | Utility or authentication | | Reaches an opted-out contact | No | Yes, for utility and authentication | If you find yourself looping this endpoint over a list of customers, you want [broadcasts](/whatsapp/broadcasts) instead. ## Base URL ``` https://app.thinnest.ai/api/v1 ``` ## Endpoints | Method | Path | What | | - | - | - | | `POST` | `/messages` | [Send a template to one customer](/api-reference/messages/send-message) | | `POST` | `/codes` | [Send a one-time code](/api-reference/one-time-codes/send-one-time-code) | | `POST` | `/calls` | [Ring one customer](/api-reference/calls/place-call), and get the results back | | `GET` | `/calls/{id}` | [A call's results](/api-reference/calls/get-call) — summary, fields, transcript, recording | | `DELETE` | `/calls/{id}` | [Cancel a waiting call, or end a live one](/api-reference/calls/get-call) | | `POST` | `/calls/batch` | [Ring up to 200 people](/api-reference/calls/place-batch-calls) | | `GET` | `/calls` | [List calls](/api-reference/calls/list-calls) | | `…` | `/agents`, `/agents/{id}` | [Agents](/api-reference/agents/create-agent) — create, read, update, delete | | `…` | `/agents/{id}/knowledge` | [What an agent knows](/api-reference/knowledge/add-knowledge-document) | | `…` | `/agents/{id}/actions` | [Actions](/api-reference/actions/create-action) — let the agent call your own API | | `POST` | `/agents/{id}/test-chat` | [Talk to your own agent](/api-reference/agents/test-chat) and read its reply | | `GET` | `/templates`, `/sequences` | [The template names and sequence ids](/api-reference/templates/list-templates) the sending endpoints take | | `…` | `/campaigns`, `/campaigns/{id}` | [Campaigns](/api-reference/campaigns/create-campaign) — results, and pause, resume, cancel | | `…` | `/conversations/{id}/status`, `/assign`, `/members` | [Resolve, hand back, take over and assign](/api-reference/conversations/list-conversations) | | `GET` | `/usage`, `/agents/{id}/analytics`, `/calls/summary`, `/whatsapp/summary` | [Usage and analytics](/api-reference/usage/get-usage) — the numbers to build a dashboard on | | `GET` | `/voices`, `/models`, `/phone-numbers` | [The lists an agent is configured from](/api-reference/voices/list-voices) | | `…` | `/contacts`, `/contacts/{id}` | [Contacts](/api-reference/contacts/create-contact) | | `…` | `/webhooks`, `/webhooks/{id}` | [Webhook endpoints](/api-reference/webhooks) | | `POST` | `/events` | Report an event, which can enrol a contact in a [sequence](/whatsapp/sequences) | | `POST` | `/sequences/{sequenceId}/enrolments` | Enrol a contact directly | Every id is prefixed by what it is — `ag_`, `act_`, `cmp_`, `cust_`, `doc_`, `wh_` — and every list takes `limit` (up to 100) and `cursor`, answering `{ "items": […], "nextCursor": "…" | null }`. Resource endpoints share a limit of 240 requests a minute per workspace. ## What it will not do * **Send an unapproved template.** Meta refuses it, so we refuse it earlier and with a clearer message. * **Send marketing to somebody who opted out.** Refused with `409`. Utility and authentication still reach them, and must. * **Send free-form text.** Outside the 24-hour window WhatsApp carries only approved templates. Inside it, the conversation belongs to the agent and the inbox. ## Start here Creating a key, and where it must not go. The one endpoint most integrations need. # Create Invitation Source: https://docs.thinnest.ai/api-reference/invitations/create-invitation /api-reference/openapi.json post /invitations Invites somebody to the workspace and emails them a link that works for 14 days (the email goes out just after the answer). Inviting an address that already has a pending invitation sends it again with a fresh 14 days and its new role, and answers `200` instead of `201`. Every seat counts — the people in the workspace plus pending invitations. Addresses at reserved test domains (`example.com`, `.test`, …) get the invitation but no email. Needs a **full** key. # Get Invitation Source: https://docs.thinnest.ai/api-reference/invitations/get-invitation /api-reference/openapi.json get /invitations/{id} One invitation, whatever its status — pending, accepted or revoked. # List Invitations Source: https://docs.thinnest.ai/api-reference/invitations/list-invitations /api-reference/openapi.json get /invitations Pending invitations, newest first. The link in each is only ever in the email, never here. # Revoke Invitation Source: https://docs.thinnest.ai/api-reference/invitations/revoke-invitation /api-reference/openapi.json delete /invitations/{id} Revokes a pending invitation: its link stops working and its seat is freed. An invitation that was accepted or already revoked is `404`. Needs a **full** key. # Add Knowledge Document Source: https://docs.thinnest.ai/api-reference/knowledge/add-knowledge-document /api-reference/openapi.json post /agents/{id}/knowledge Adds a web page (`url`) or pasted `text` to what the agent knows. A page is fetched and indexed before this answers, so a slow site is a slow request; `passages` says how much was indexed. A page that cannot be read is answered `422` with the reason and the `failed` document it left behind, which you can delete. Files are uploaded in the console. A **build** key may do this. # Delete Knowledge Document Source: https://docs.thinnest.ai/api-reference/knowledge/delete-knowledge-document /api-reference/openapi.json delete /agents/{id}/knowledge/{documentId} Removes the document and every passage indexed from it; the agent stops citing it at once. A **build** key may do this. # List Knowledge Documents Source: https://docs.thinnest.ai/api-reference/knowledge/list-knowledge-documents /api-reference/openapi.json get /agents/{id}/knowledge The agent's documents, newest first, with whether each is ready to be cited. Files uploaded in the console are listed here too. # Get Member Source: https://docs.thinnest.ai/api-reference/members/get-member /api-reference/openapi.json get /members/{id} One person in the workspace, with their role. # List Members Source: https://docs.thinnest.ai/api-reference/members/list-members /api-reference/openapi.json get /members Everybody in the workspace, with their role — so a conversation can be assigned by email, or a role changed or a person removed by `id`. Not paged: every member is in one answer, and `nextCursor` is always null. Pending invitations are listed by List Invitations. # Remove Member Source: https://docs.thinnest.ai/api-reference/members/remove-member /api-reference/openapi.json delete /members/{id} Removes a person from the workspace. Conversations assigned to them go back to the unassigned pool. The workspace's last owner cannot be removed. Needs a **full** key. # Update Member Source: https://docs.thinnest.ai/api-reference/members/update-member /api-reference/openapi.json patch /members/{id} Changes a member's role to `admin` or `member`. A key cannot make anybody an owner — do that in the console, signed in as one — and the workspace's last owner cannot be demoted. Needs a **full** key. # Send Message Source: https://docs.thinnest.ai/api-reference/messages/send-message /api-reference/openapi.json post /messages Sends one approved template to one number and records it in that customer's conversation. Marketing templates are refused for a customer who opted out; utility and authentication templates are not, since an order confirmation is not something a customer opts out of. Each send is charged like any WhatsApp template from your number, and the Free plan stops at its included messages. At most 60 sends a minute per workspace; send an `Idempotency-Key` so a retry never sends twice. Needs a **full** key. # List Models Source: https://docs.thinnest.ai/api-reference/models/list-models /api-reference/openapi.json get /models Every model an agent may answer with, by the name the console shows. `available` says whether your plan may pick it — the rest are listed so you can see what an upgrade adds — and `voice` whether it is quick enough to answer a call (an agent's model must be). On a workspace that brings its own keys there is also `byok`: your LLM provider, your key's own model and every model the key reaches, which is what agents run on while your own keys are on. `voiceOnlyByok` marks the models a call may run on while you bring only your own voice. `callTier` and `callPricePerMinuteMicro` are the per-minute call price the console shows beside each model, for your workspace (plan discount included), in `currency` micro-units: a model marked `premium` bills every call at the premium rate even with a standard voice; a premium or studio voice can raise a call's price, never lower it. These cover the model only, with a standard voice. On a workspace using its own keys, a call bills at the BYOK per-minute rate whatever the model (all three keys, or voice key only — see Bring your own keys), so these prices apply only to an agent whose `byok` is `"off"`. Typical latency is not published. # Get Notification Preferences Source: https://docs.thinnest.ai/api-reference/notifications/get-notification-preferences /api-reference/openapi.json get /notification-preferences Every notification category, with whether it is muted and what of it is emailed. A category never changed is shown, not muted and not emailed. Not paged: `nextCursor` is always null. # Update Notification Preferences Source: https://docs.thinnest.ai/api-reference/notifications/update-notification-preferences /api-reference/openapi.json patch /notification-preferences Changes any categories at once, keyed by category: `muted` hides it from the feed and the bell (a critical notice shows regardless), `email` chooses what is emailed. A field you leave out keeps its value. Emails go only through the email sender connected in the console's notification settings; until one is connected nothing is emailed, whatever this says, and that connection is not available by API. Answers with every category. A **build** key may do this. # Create Code Template Source: https://docs.thinnest.ai/api-reference/one-time-codes/create-code-template /api-reference/openapi.json post /codes/templates Saves an authentication template as a **draft**. WhatsApp writes the words; you choose how long the code lasts, whether to add "For your security, do not share this code.", and the button — `copy_code` (works everywhere) or `one_tap` (hands the code to your Android app, which needs `apps`). Nothing is sent to WhatsApp until Submit Code Template. A **build** key may do this. # Delete Code Template Source: https://docs.thinnest.ai/api-reference/one-time-codes/delete-code-template /api-reference/openapi.json delete /codes/templates/{id} Deletes the template — at WhatsApp first when it was submitted, then here. WhatsApp keeps a deleted template's name reserved for 30 days, so a replacement needs a different name. Codes stop sending at once if this was your only approved one. A **build** key may do this. # Get Code Template Source: https://docs.thinnest.ai/api-reference/one-time-codes/get-code-template /api-reference/openapi.json get /codes/templates/{id} One authentication template, with its review status and WhatsApp's reason when it was rejected. # List Code Templates Source: https://docs.thinnest.ai/api-reference/one-time-codes/list-code-templates /api-reference/openapi.json get /codes/templates Your workspace's authentication templates — drafts, those in review and approved ones — newest first. # Send One-Time Code Source: https://docs.thinnest.ai/api-reference/one-time-codes/send-one-time-code /api-reference/openapi.json post /codes Sends a code you generated to a WhatsApp number, using your approved authentication template — the code goes into the message **and** the copy-code button. We do not generate, store, expire or check the code; that stays yours. One number may be sent at most 5 codes in 10 minutes, and the workspace at most 120 sends a minute (codes keep the top of the budget it shares with Send Message). Each code is charged at WhatsApp's authentication rate, which is far higher for numbers abroad. Needs a **full** key. # Submit Code Template Source: https://docs.thinnest.ai/api-reference/one-time-codes/submit-code-template /api-reference/openapi.json post /codes/templates/{id}/submit Sends a draft to WhatsApp for review, on your workspace's own WhatsApp Business Account — so a connected number is needed first. The status moves to `pending` only when WhatsApp took it. Review usually takes minutes; a `template.*` webhook and Get Code Template tell you how it went. A **build** key may do this. # Update Code Template Source: https://docs.thinnest.ai/api-reference/one-time-codes/update-code-template /api-reference/openapi.json patch /codes/templates/{id} Changes any of a **draft**'s fields; the ones you leave out keep their values. Once a template has been submitted, WhatsApp has the text and it can no longer be edited — make a new one instead. A **build** key may do this. # Get Business Details Source: https://docs.thinnest.ai/api-reference/phone-numbers/get-business-details /api-reference/openapi.json get /phone-numbers/business-details Where this workspace's business details stand. India issues a number only to a business it has approved, in the business's own name, so there is one application per workspace — a platform with a workspace per customer sends and reads each customer's own (with the `Thinnest-Workspace` header). Returns `{ "status": "none" }` until details are sent. `canRent` is true once the details are approved (`accepted`) — the point from which an Indian number can be rented. `reviewNote` appears when the application was `rejected` and says what to correct. `businessName` is null if the application's name could not be read just now. The document itself is never returned. Any key can read this; the status is checked live, so it is current. Checks usually finish in minutes. # Get Phone Number Source: https://docs.thinnest.ai/api-reference/phone-numbers/get-phone-number /api-reference/openapi.json get /phone-numbers/{number} One number this workspace holds: who answers it, who calls out on it, and its name. A number held by another workspace is `404`, the same as one that does not exist. # Import Phone Number Source: https://docs.thinnest.ai/api-reference/phone-numbers/import-phone-number /api-reference/openapi.json post /phone-numbers/import Brings a number you own on your own carrier account. It stays on your account and you keep paying your carrier for it; we never bill it. For carriers that sign their calls with your account's secret, save the account's keys first (Save Carrier Account) — we also use them to check the number is really on that account. `setup` says what is left to do: where the number must send its calls, and whether we pointed it there ourselves. Bringing the same number again is not an error; it corrects `provider`. The number arrives answered by nobody — Update Phone Number points it at an agent. Needs a paid plan and a **full** key. # List Available Cities Source: https://docs.thinnest.ai/api-reference/phone-numbers/list-available-cities /api-reference/openapi.json get /phone-numbers/available/cities The cities Search Available Phone Numbers accepts as `city`, with how many numbers each has. Use these spellings exactly: "Bangalore" finds numbers where "Bengaluru" finds none. Empty on the Free plan or when the stock cannot be reached. Not paged. # List Carrier Accounts Source: https://docs.thinnest.ai/api-reference/phone-numbers/list-carrier-accounts /api-reference/openapi.json get /phone-numbers/carrier-accounts Your own carrier accounts we hold verified keys for — the accounts your brought numbers live on — each by the last four characters of its account id, never any part of the secret. A carrier missing from the list has no keys saved: its brought numbers can answer calls (where the carrier allows) but not call out. Not paged. # List Phone Numbers Source: https://docs.thinnest.ai/api-reference/phone-numbers/list-phone-numbers /api-reference/openapi.json get /phone-numbers Every number this workspace can call from or be called on — rented and brought — with the agent that answers each. Not paged. `label` is always `null` here for a brought number; Get Phone Number reads it. # Move Phone Number Source: https://docs.thinnest.ai/api-reference/phone-numbers/move-phone-number /api-reference/openapi.json post /phone-numbers/{number}/move In the console, developers do this from the Numbers page: open the number's menu and choose **Move to another workspace…**. Moves a number, **keeping its digits**, from the workspace the request runs in to another workspace of the same developer: the developer's own, or any of its customers. Send it from the developer's own workspace, or as the customer that holds the number with `Thinnest-Workspace`; a key made for one customer cannot. The number leaves every agent and calling pool where it was and arrives **answered by nobody** — point it with `PATCH /phone-numbers/{number}` (as the target workspace) once it is there. **Rent** continues from the next period on the target. The month already paid is not charged again, and inside one developer both workspaces bill the developer, so no money moves. **An Indian number** is held in the name of the business using it: the target workspace's business details must be approved first (`PUT /phone-numbers/business-details` as that workspace), and the move re-registers the number to that business. **A brought number** (on your own carrier account) moves only when both workspaces have that same account's keys saved. Refused while the number is on a call. **Safe to retry:** repeating a move that already succeeded to the same workspace answers the same `200` (for a day). Needs a **full** key. # Release Phone Number Source: https://docs.thinnest.ai/api-reference/phone-numbers/release-phone-number /api-reference/openapi.json delete /phone-numbers/{number} Gives a number up; either way it stops being any agent's line and leaves every agent's calling pool. A **brought** number is simply forgotten: it stays yours on your own carrier account, and calls to it are declined until you repoint it. A **rented** number is **released, permanently**: it goes back to the pool, anybody may rent it next, and this month's rent is not refunded — so it is refused unless you add `?confirm=release`. Needs a **full** key. # Rent Phone Number Source: https://docs.thinnest.ai/api-reference/phone-numbers/rent-phone-number /api-reference/openapi.json post /phone-numbers Rents a number from Search Available Phone Numbers. **This costs money:** the first month's rent (the `monthlyPrice` the search showed) is charged from your balance at once, and again every month on the same day until you release the number. Every refusal happens before anything is bought or charged. An Indian number also needs your business details sent and approved first (Send Business Details, or the console's Phone Numbers page). The number arrives answered by nobody — Update Phone Number points it at an agent. Needs a **full** key. # Save Carrier Account Source: https://docs.thinnest.ai/api-reference/phone-numbers/save-carrier-account /api-reference/openapi.json put /phone-numbers/carrier-accounts/{provider} Saves the keys of your own account with a carrier, which lets its brought numbers be confirmed, answered and called out from. The keys are checked with the carrier before anything is stored, stored sealed, and never returned. One account per carrier: switching to a different account while numbers are still brought on the first is refused. `telnyx` takes its API key as `token` alone. Needs a **full** key — a carrier credential can spend money on your account. # Search Available Phone Numbers Source: https://docs.thinnest.ai/api-reference/phone-numbers/search-available-phone-numbers /api-reference/openapi.json get /phone-numbers/available Indian local numbers you can rent, each with its monthly rent at your plan's rate in your balance's currency. Numbers somebody already holds are left out, so a page (up to twenty) can hold fewer and still have more after it: pass `nextCursor` back as `cursor`. Needs a paid plan. # Send Business Details Source: https://docs.thinnest.ai/api-reference/phone-numbers/send-business-details /api-reference/openapi.json put /phone-numbers/business-details Sends the business's details for approval, as multipart/form-data: the legal name, whether it is registered for GST, and **one** document — the GST certificate if it is, otherwise its Certificate of Incorporation or its Udyam certificate (say which in `documentKind`; incorporation is assumed). PDF, JPG or PNG, at most 5 MB, filename at most 99 characters. This is the Business details form in the console, run by the same code. The details go to India's numbering authority in the business's name, so this needs a **full** key. There is one live application per workspace: sending while one is being checked, or after it was approved, is `409`; sending after a `rejected` one corrects that application in place (`200`), and a first or expired one creates a new application (`201`). Approval usually takes a few minutes — read it with Get Business Details, then rent the number. Needs a paid plan. # Update Phone Number Source: https://docs.thinnest.ai/api-reference/phone-numbers/update-phone-number /api-reference/openapi.json patch /phone-numbers/{number} Points a number at an agent, lends it to one for calling out, names it, or forwards its calls — any of the four. `agent` makes the number that agent's line (it stops being any other agent's; the agent must have voice switched on); `callingAgent` lets an agent call out on it without answering it (a brought number needs its carrier account's keys saved first); `forwardTo` sends every call to another phone, and works whether or not an agent answers the number (in the console: Numbers page, the number's menu, **Forward calls**). They are applied in the order `label`, `agent`, `callingAgent`, `forwardTo`, and a refusal stops there with the earlier changes kept. Needs a **full** key. # Delete Call Content Source: https://docs.thinnest.ai/api-reference/post-call/delete-call-content /api-reference/openapi.json delete /calls/{id}/content Deletes ONE call's recording and transcript, and keeps its billing record. Gone: the recording (ours, the phone provider's copy of a call on a number we rent, and any copy made for model training), this call's own turns in its conversation (other calls and chats with the same person stay), its summary, its collected fields, and the summary handed on with a warm hand-off. Kept: the call itself — when it happened, who it was with, how long it lasted, what it cost and `billing`. Afterwards Get Call answers with `recording: null`, an empty `transcript`, `contentDeleted: true` and `contentDeletedAt`. A recording that cannot be deleted at that moment is queued and retried every night until it is gone, and counted in `recordingsPending`. For a call you placed that was retried, every attempt's content goes. This cannot be undone. This is not Cancel Call: `DELETE /calls/{id}` still calls a call off. Safe to repeat — again answers 200 with the first deletion's time. Needs a **full** key; a developer reaches a customer's call with `Thinnest-Workspace`. # Get Call Analysis Source: https://docs.thinnest.ai/api-reference/post-call/get-call-analysis /api-reference/openapi.json get /calls/{id}/analysis What one call came to, without the transcript: the outcome as the console's call log shows it ("Answered", "Line busy", "Answering machine"), the summary and the collected fields. `outcome` is null until the call has ended; `analysedAt` null means the summary and fields may still arrive, and `call.analysed` is sent the moment they do. Any id List Calls hands out works. # Get Post-call Settings Source: https://docs.thinnest.ai/api-reference/post-call/get-post-call-settings /api-reference/openapi.json get /agents/{id}/post-call What an agent does after each call: whether it summarises and records calls, how long your plan keeps a recording, and the details it collects. These are the agent's own settings — the same values Get Agent shows under `voice` and `collectFields` — and `followUp`, the WhatsApp and/or SMS the agent sends the caller when a call ends. # Update Post-call Settings Source: https://docs.thinnest.ai/api-reference/post-call/update-post-call-settings /api-reference/openapi.json patch /agents/{id}/post-call Changes what an agent does after each call; send only what you are changing. `collectFields` replaces the whole list, and an empty list stops collecting (details already collected are kept). Any other key is refused — the rest of the agent belongs to Update Agent. `followUp` sets the WhatsApp and/or SMS sent to the caller when a call ends, and is partial: what it leaves out keeps what is saved. A WhatsApp template must be an approved Utility one; an SMS needs a connected SMS provider. Answers with the settings as they now stand. A **build** key may do this, except `followUp`: it messages customers, so it needs a **full** key. # Create Project Source: https://docs.thinnest.ai/api-reference/projects/create-project /api-reference/openapi.json post /projects Adds a project. Every workspace starts with its default project; a second one needs a plan with teams and projects, and is refused with `402` otherwise. A **build** key may do this. # Delete Project Source: https://docs.thinnest.ai/api-reference/projects/delete-project /api-reference/openapi.json delete /projects/{id} Deletes a project and its team grants. Refused for the default project, and while any agent is still in the project — move them first. A **build** key may do this. # Get Project Source: https://docs.thinnest.ai/api-reference/projects/get-project /api-reference/openapi.json get /projects/{id} One project, with how many agents are in it. # List Projects Source: https://docs.thinnest.ai/api-reference/projects/list-projects /api-reference/openapi.json get /projects The workspace's projects, newest first, each with how many agents are in it. # Update Project Source: https://docs.thinnest.ai/api-reference/projects/update-project /api-reference/openapi.json patch /projects/{id} Renames a project. A **build** key may do this. # Get Call Recording Source: https://docs.thinnest.ai/api-reference/recordings/get-call-recording /api-reference/openapi.json get /calls/{id}/recording One call's audio as MP3, streamed — it plays inline, or with `download=1` saves as `call-{id}.mp3`. `Range` requests are honoured so a player can seek, and nothing is cached. A recording lands about a minute after the call ends (`404` with `Retry-After` until then) and is deleted when your plan's retention runs out (`410`): 30 days on Free and pay-as-you-go, 49 on Pro, 75 on Scale, 365 on Enterprise. Any id List Calls hands out works. # Get Recording By Signed Link Source: https://docs.thinnest.ai/api-reference/recordings/get-recording-by-signed-link /api-reference/openapi.json get /recordings/{callId} The signed link a call report hands out as `recording.url`, for a CRM user to click: it needs **no API key**, because the signature is the permission. Use the URL exactly as the report gives it; add `&download=1` to save rather than play. The link stops working at its `expiresAt`, which is when your plan deletes the recording. A forged link and an unknown call get the same `404`. Answers are plain text, not JSON. # Create Saved Reply Source: https://docs.thinnest.ai/api-reference/saved-replies/create-saved-reply /api-reference/openapi.json post /saved-replies Adds a saved reply, offered in the inbox composer as `/shortcut`. A leading `/` on the shortcut is dropped. A shortcut already used — in any capitalisation — is `409`. A **build** key may do this. # Delete Saved Reply Source: https://docs.thinnest.ai/api-reference/saved-replies/delete-saved-reply /api-reference/openapi.json delete /saved-replies/{id} Deletes a saved reply. A **build** key may do this. # Get Saved Reply Source: https://docs.thinnest.ai/api-reference/saved-replies/get-saved-reply /api-reference/openapi.json get /saved-replies/{id} One saved reply. # List Saved Replies Source: https://docs.thinnest.ai/api-reference/saved-replies/list-saved-replies /api-reference/openapi.json get /saved-replies The workspace's saved replies, newest first. # Update Saved Reply Source: https://docs.thinnest.ai/api-reference/saved-replies/update-saved-reply /api-reference/openapi.json patch /saved-replies/{id} Changes a saved reply's `shortcut`, `body`, or both. A **build** key may do this. # SDKs Source: https://docs.thinnest.ai/api-reference/sdks Typed TypeScript and Python clients for every endpoint, with retries, idempotency and pagination built in. Two small clients are generated from the same OpenAPI document as this reference, so every endpoint here has a method, with typed request bodies and responses. | | TypeScript | Python | | - | - | - | | Package | `@thinnestai/sdk` | `thinnestai` | | Runtime | Node 18.17+, Deno, Bun, browsers — the global `fetch`, no dependencies | Python 3.9+, `httpx` only | | Clients | `Client` | `Client` and `AsyncClient`, the same shape | The packages are **not yet published to npm or PyPI**. Until they are, install them from a copy of the repository: ```bash theme={null} npm install ./sdks/typescript # TypeScript pip install ./sdks/python # Python ``` The compiled `dist/` is not committed, so installing from a path builds it: the package's `prepare` script fetches the TypeScript compiler with `npx` (the machine needs network access the first time) and compiles `src/`. If your npm blocks install scripts, or you copied the folder instead of installing it, run `npm install && npm run build` inside it once. ## Quickstart ```ts TypeScript theme={null} import { Client, ApiError } from "@thinnestai/sdk"; const client = new Client({ apiKey: process.env.API_KEY! }); // Create an agent const agent = await client.agents.create({ name: "Skyline Sales" }); // Place a call const call = await client.calls.place({ to: "+91 98765 43210", purpose: "I'm calling from Skyline Homes about your enquiry.", agent: agent.id, }); // Act in one of your customers' workspaces const customer = await client.customers.create({ name: "Sunrise Dental Clinic" }); const sunrise = client.withWorkspace(customer.id); await sunrise.agents.list(); ``` ```python Python theme={null} import os from thinnestai import Client, ApiError client = Client(os.environ["API_KEY"]) # Create an agent agent = client.agents.create({"name": "Skyline Sales"}) # Place a call call = client.calls.place({ "to": "+91 98765 43210", "purpose": "I'm calling from Skyline Homes about your enquiry.", "agent": agent["id"], }) # Act in one of your customers' workspaces customer = client.customers.create({"name": "Sunrise Dental Clinic"}) sunrise = client.with_workspace(customer["id"]) sunrise.agents.list() ``` `withWorkspace` / `with_workspace` sends the `Thinnest-Workspace` header on every request (see [Customers](/api-reference/customers)). The `customers.*` methods and `usage.getCustomers` ignore it, because those endpoints are always called as you, never as one of your customers. To fix one workspace for a whole client, pass `workspace: "org_…"` (TypeScript) or `workspace="org_…"` (Python) when you create it. ## How methods are named One namespace per group in this reference — `agents`, `calls`, `customers`, `phoneNumbers` (`phone_numbers` in Python), `webhooks`, … — and a method per endpoint, named for what it does: `client.agents.list()`, `client.calls.place(…)`, `client.customers.restore(id)`. * Path parameters come first, then the request body. * TypeScript takes query parameters as an object after the body, then `RequestOptions` (`idempotencyKey`, `headers`, `signal`, `timeoutMs`, `maxRetries`). * Python takes query parameters as keyword arguments (`client.calls.list(status="completed")`), plus `extra_headers=`, `timeout=` and, where the endpoint accepts one, `idempotency_key=`. * Binary answers (call recordings, voice previews) come back as a `Blob` / `bytes`; text answers (a transcript as text, a CSV export) as a string. ## Pagination List endpoints answer one page, `{ items, nextCursor }`. Each has a companion that walks every page: ```ts TypeScript theme={null} for await (const agent of client.agents.listAll({ limit: 100 })) { console.log(agent.name); } ``` ```python Python theme={null} for agent in client.agents.list_all(limit=100): print(agent["name"]) # AsyncClient async for agent in client.agents.list_all(): print(agent["name"]) ``` ## Errors Any answer outside `2xx` raises `ApiError`, carrying `status`, `message` (the `error` sentence from [the error body](/api-reference/errors)), the response `headers` and the parsed `body`. No answer at all — the network failed or the request timed out — raises `ApiConnectionError`. ## Retries and idempotency The clients follow the [retry policy](/api-reference/errors) for you: * `429` is retried for every method, waiting for `Retry-After` (up to 120 seconds; a longer wait is thrown rather than retried early). * `408`, `500`, `502`, `503`, `504` and network errors are retried only for `GET` and `HEAD`, and for any request sent with an `Idempotency-Key`. A `POST`, `PUT`, `PATCH` or `DELETE` without one is never replayed, because replaying it could act twice. * Backoff is exponential with jitter, capped at 8 seconds; two retries by default (`maxRetries` / `max_retries`). * These get an `Idempotency-Key` generated when you give none: `calls.place`, `calls.placeBatch`, `conversations.reply`, `messages.send`, `codes.send` and `customers.create`. **The same key is sent on every retry**, so a retry never places a second call. A key you pass — as the `idempotencyKey` option or the `idempotencyKey` body field — is the one sent. * `400`, `401`, `403`, `404` and `409` are never retried. # Activate Sequence Source: https://docs.thinnest.ai/api-reference/sequences/activate-sequence /api-reference/openapi.json post /sequences/{sequenceId}/activate Turns a draft or paused sequence on: messages that fall due start going out, and its tags and events start enrolling people. Refused for a sequence with no steps, which would take people in and send nothing. Needs a **full** key. # Add Sequence Step Source: https://docs.thinnest.ai/api-reference/sequences/add-sequence-step /api-reference/openapi.json post /sequences/{sequenceId}/steps Appends a message at the **end** of the sequence: an approved template, sent `delayHours` after the step before (or after the person joined, for the first step). Steps cannot be edited, reordered or removed — someone partway through a live sequence would skip a message or get one twice — so to change the messages, delete the sequence and make another. A **build** key may do this. # Create Sequence Source: https://docs.thinnest.ai/api-reference/sequences/create-sequence /api-reference/openapi.json post /sequences Creates a sequence as a **draft**: it sends nothing until you add a step with Add Sequence Step and turn it on with Activate Sequence. Tags are stored lower-case and hyphenated, and event names lower-case with dots, as the console stores them. A **build** key may do this. # Delete Sequence Source: https://docs.thinnest.ai/api-reference/sequences/delete-sequence /api-reference/openapi.json delete /sequences/{sequenceId} Archives the sequence, which is how the console deletes one: it sends nothing more and leaves every list, while the record of who was sent what stays. There is no undo. A **build** key may do this. # Enrol in Sequence Source: https://docs.thinnest.ai/api-reference/sequences/enrol-in-sequence /api-reference/openapi.json post /sequences/{sequenceId}/enrolments Puts one existing contact into a sequence, with values its messages can use — an order number your system knows and the console does not. The person must already be a contact; this never creates one. `enrolled: false` is a `200`, not an error: they are already in it, the number is not a contact, they opted out, or the sequence has no first message yet. Prefer Report Event unless your system must choose the sequence itself, since this puts a sequence id in your code. Only enrol people who agreed to hear from you. Needs a **full** key. # Get Sequence Source: https://docs.thinnest.ai/api-reference/sequences/get-sequence /api-reference/openapi.json get /sequences/{sequenceId} One sequence with its steps in order, the events that start and stop it, and how many people are in it, finished it or left it. # List Sequences Source: https://docs.thinnest.ai/api-reference/sequences/list-sequences /api-reference/openapi.json get /sequences Your workspace's sequences, newest first. Deleted (archived) ones are left out. Get Sequence adds the steps, events and how many people are in each. # Pause Sequence Source: https://docs.thinnest.ai/api-reference/sequences/pause-sequence /api-reference/openapi.json post /sequences/{sequenceId}/pause Pauses an active sequence: nothing more is sent until it is activated again. The people in it keep their place. A **build** key may do this. # Update Sequence Source: https://docs.thinnest.ai/api-reference/sequences/update-sequence /api-reference/openapi.json patch /sequences/{sequenceId} Changes any of `name`, `exitOnReply`, `triggerTags`, `exitOnTags`, `startOn` and `stopOn`. A list you send **replaces** the old one; a list you leave out is kept. Works while the sequence runs, and applies to the people already in it — add a stop tag and they stop when they get it. Steps cannot be changed here. A **build** key may do this. # Add SMS Template Source: https://docs.thinnest.ai/api-reference/sms/add-sms-template /api-reference/openapi.json post /sms/templates Adds a template exactly as it was approved on your DLT portal, with `{#var#}` wherever the text changes. Each `{#var#}` takes at most 30 characters when sent. Promotional templates are not accepted: an agent may only send transactional or service messages. MSG91 and Fast2SMS send by their own template id, so with them `providerTemplateId` is required. Needs a **full** key. # Connect SMS Provider Source: https://docs.thinnest.ai/api-reference/sms/connect-sms-provider /api-reference/openapi.json put /sms/provider Connects your SMS provider, or replaces the one connected. A secret you leave out keeps the one saved, for the same provider; switching provider needs every field. The connection's `status` goes back to `untested` until Send Test SMS succeeds. Needs a **full** key. What each provider asks for in `credentials`: - `msg91`: `authKey` - `exotel`: `accountSid`, `apiKey`, `apiToken`, `region` (`in` or `sg`) - `gupshup`: `userId`, `password` - `infobip`: `baseUrl` (your own, like `xxxxx.api.infobip.com`), `apiKey` - `vonage`: `apiKey`, `apiSecret` - `fast2sms`: `apiKey` # Delete SMS Template Source: https://docs.thinnest.ai/api-reference/sms/delete-sms-template /api-reference/openapi.json delete /sms/templates/{id} Deletes a template. An agent that sent it after its calls stops sending an SMS then. Needs a **full** key. # Disconnect SMS Provider Source: https://docs.thinnest.ai/api-reference/sms/disconnect-sms-provider /api-reference/openapi.json delete /sms/provider Forgets the provider and its credentials, and switches `send_sms` off on every agent. Your templates are kept. Needs a **full** key. # Get SMS Provider Source: https://docs.thinnest.ai/api-reference/sms/get-sms-provider /api-reference/openapi.json get /sms/provider The workspace's SMS provider, sender header and DLT entity id. Secrets are never returned: `secretsSaved` names the ones held. # List SMS Templates Source: https://docs.thinnest.ai/api-reference/sms/list-sms-templates /api-reference/openapi.json get /sms/templates The workspace's DLT templates, by name. # Send Test SMS Source: https://docs.thinnest.ai/api-reference/sms/send-test-sms /api-reference/openapi.json post /sms/test Sends one template to one Indian mobile number through your provider — a real SMS, billed by your provider — to prove the provider, header and DLT ids work together. Success sets the connection's `status` to `working`; a provider's refusal sets it to `failing` with the reason. Needs a **full** key. # Update SMS Template Source: https://docs.thinnest.ai/api-reference/sms/update-sms-template /api-reference/openapi.json patch /sms/templates/{id} Changes any of a template's fields; what you leave out stays. If an agent sends this template after its calls, a body with a different number of `{#var#}` is refused until that agent's follow-up is changed. Needs a **full** key. # Add Team Member Source: https://docs.thinnest.ai/api-reference/teams/add-team-member /api-reference/openapi.json put /teams/{id}/members/{memberId} Puts a member of the workspace in the team, so they can reach the team's projects. Repeating it changes nothing. Answers with the team. A **build** key may do this. # Create Team Source: https://docs.thinnest.ai/api-reference/teams/create-team /api-reference/openapi.json post /teams Adds an empty team; put members in it and grant it projects with the team's sub-resources. Teams need a plan with teams and projects, and are refused with `402` otherwise. A **build** key may do this. # Delete Team Source: https://docs.thinnest.ai/api-reference/teams/delete-team /api-reference/openapi.json delete /teams/{id} Deletes a team and its grants. A project only this team could reach opens to the whole workspace. The people stay in the workspace. A **build** key may do this. # Get Team Source: https://docs.thinnest.ai/api-reference/teams/get-team /api-reference/openapi.json get /teams/{id} One team, with its members' ids and the projects it can reach. # Grant Team Project Source: https://docs.thinnest.ai/api-reference/teams/grant-team-project /api-reference/openapi.json put /teams/{id}/projects/{projectId} Lets the team reach a project. A project's first grant **restricts** it: from then on only the teams granted it (plus owners and admins) can see its agents. Repeating it changes nothing. Answers with the team. A **build** key may do this. # List Teams Source: https://docs.thinnest.ai/api-reference/teams/list-teams /api-reference/openapi.json get /teams The workspace's teams, newest first, each with its members' ids and the projects it can reach. # Remove Team Member Source: https://docs.thinnest.ai/api-reference/teams/remove-team-member /api-reference/openapi.json delete /teams/{id}/members/{memberId} Takes a member out of the team; they stay in the workspace. Taking out somebody who is not in the team changes nothing. Answers with the team. A **build** key may do this. # Revoke Team Project Source: https://docs.thinnest.ai/api-reference/teams/revoke-team-project /api-reference/openapi.json delete /teams/{id}/projects/{projectId} Takes a project out of the team's reach. When that was the project's last grant, the project opens to the whole workspace again. Answers with the team. A **build** key may do this. # Update Team Source: https://docs.thinnest.ai/api-reference/teams/update-team /api-reference/openapi.json patch /teams/{id} Renames a team. A **build** key may do this. # Create Template Source: https://docs.thinnest.ai/api-reference/templates/create-template /api-reference/openapi.json post /templates Saves a WhatsApp template as a draft, after the console's own checks of WhatsApp's rules for the body, header, footer, buttons and an authentication template's code. Review is a separate, deliberate step — Submit Template, or `submit: true` here — because once a name is approved it can never be reused for different words; if that submission is refused, the draft is kept and `message` says why. Limited-time offers, carousels and buttons that open a WhatsApp form are made in the console. Needs a **full** key. # Delete Template Source: https://docs.thinnest.ai/api-reference/templates/delete-template /api-reference/openapi.json delete /templates/{name} Deletes a WhatsApp template. One that was sent for review is deleted from WhatsApp first — only that language — and from your workspace only once that works, so a refusal leaves everything as it was. This cannot be undone, and WhatsApp may keep a deleted name reserved for a while, so it may not be reusable straight away. Needs a **full** key. # Get Template Source: https://docs.thinnest.ai/api-reference/templates/get-template /api-reference/openapi.json get /templates/{name} Returns one WhatsApp template by name, with its parts — header text, footer, buttons — and, when WhatsApp refused it, the reason in `reviewNote`. Poll it after Submit Template to follow the review: `status` moves from `pending` to `approved` or `rejected`. Pass `language` when the name exists in more than one. # List Templates Source: https://docs.thinnest.ai/api-reference/templates/list-templates /api-reference/openapi.json get /templates Lists your workspace's WhatsApp templates, newest first, with what a send needs from each: how many body values (`variableCount`), how many link-button values (`buttonVariableCount`) and whether the header needs a media link (`headerKind`). Only `approved` templates can be sent. # Submit Template Source: https://docs.thinnest.ai/api-reference/templates/submit-template /api-reference/openapi.json post /templates/{name}/submit Sends a **draft** template to your own WhatsApp Business Account for review, after the same last checks as the console's Submit. The template moves to `pending` only if WhatsApp accepted it. Review usually takes minutes, occasionally days; follow it with Get Template, whose `status` becomes `approved` or `rejected` (with the reason in `reviewNote`). A workspace with no connected WhatsApp number has no account to submit to. Needs a **full** key. # Update Template Source: https://docs.thinnest.ai/api-reference/templates/update-template /api-reference/openapi.json patch /templates/{name} Edits a **draft** template. Send only the fields that change; the result is checked against the same rules as Create Template. Once a template has been sent for review WhatsApp has its text, so it can no longer be edited — create a new template instead. Changing `name` or `language` renames it. A **build** key may do this. The one exception is `agentMaySend` (whether an agent may send the template in a call or chat): it goes alone and needs a **full** key. # Configure Connection Source: https://docs.thinnest.ai/api-reference/tools/configure-connection /api-reference/openapi.json patch /agents/{id}/connections/{kind} Pin a connection to its target, say how a lead's row is found, choose when it saves, and switch its tools. The account must be connected first (records excepted). Changes apply in a fixed order and stop at the first refusal, keeping what came before it. Helpdesks have nothing to configure. A **build** key may do this. # Connect Account Source: https://docs.thinnest.ai/api-reference/tools/connect-account /api-reference/openapi.json post /agents/{id}/connections/{kind} Connect (or re-key) an account. Every key, token and secret is sealed and never returned. Connecting grants nothing: tools start off and are switched with `PATCH` `tools`. `googlesheets`, `zoho` and `zoho_bigin` sign in on the provider's own consent screen — connect them once in the console (this answers `409`), then configure them here. A helpdesk connect opens one test ticket or case and answers with `inbound` **once**; a records connect that makes the first link answers with `link.passcode` **once**. A **build** key may do this. # Connect Tool Server Source: https://docs.thinnest.ai/api-reference/tools/connect-tool-server /api-reference/openapi.json post /agents/{id}/mcp-servers Connect an MCP tool server that takes a token (or none). It is asked for its tools at once and every tool arrives **off** — switch each on with `PATCH …/tools/{toolId}`. A server that cannot be reached is still saved and answers `201` with `status: "failed"` and the reason in `message`, so you can fix it and sync. A server that signs in through a browser must be connected in the console. A **build** key may do this. # Disconnect Account Source: https://docs.thinnest.ai/api-reference/tools/disconnect-account /api-reference/openapi.json delete /agents/{id}/connections/{kind} Disconnect the account; its tools leave the agent and its credential is deleted. A browser sign-in still pending is cancelled too. What already happened in the account (meetings booked, rows written) is untouched, and records already collected are kept. A **build** key may do this. # Disconnect Tool Server Source: https://docs.thinnest.ai/api-reference/tools/disconnect-tool-server /api-reference/openapi.json delete /agents/{id}/mcp-servers/{serverId} Disconnect the server; its tools leave the agent at once and its token is deleted. This works for servers connected in the console too. A **build** key may do this. # Get Connection Source: https://docs.thinnest.ai/api-reference/tools/get-connection /api-reference/openapi.json get /agents/{id}/connections/{kind} One connection, connected or not, with the shared tool budget. # Get Tool Server Source: https://docs.thinnest.ai/api-reference/tools/get-tool-server /api-reference/openapi.json get /agents/{id}/mcp-servers/{serverId} One tool server and its tools. # List Built-in Tools Source: https://docs.thinnest.ai/api-reference/tools/list-built-in-tools /api-reference/openapi.json get /agents/{id}/tools The agent's built-in tools — the Actions page's switches — with the recall, verification and hand-over settings. The three tools that reach a customer on another channel also say whether they are `ready` (on, and what they need exists) and what they `needs`. # List Connection Options Source: https://docs.thinnest.ai/api-reference/tools/list-connection-options /api-reference/openapi.json get /agents/{id}/connections/{kind}/options What a connected account can reach, read live each time, so you can choose a `target` for `PATCH …/connections/{kind}`: a Grist document's tables and columns (`?doc=`), a spreadsheet's sheets (`?spreadsheet=`), or the modules a Zoho account can write to. Other kinds have nothing to choose. # List Connections Source: https://docs.thinnest.ai/api-reference/tools/list-connections /api-reference/openapi.json get /agents/{id}/connections Every account the agent can work in, connected or not — a calendar, two spreadsheets, two CRMs, records kept here and two helpdesks — each with its state, what it is pinned to and its tools. `toolsInUse` / `maxTools` is the one budget every connection's switches share. Credentials never appear. # List Tool Servers Source: https://docs.thinnest.ai/api-reference/tools/list-tool-servers /api-reference/openapi.json get /agents/{id}/mcp-servers Every tool server the agent is connected to, oldest first, each with its tools. All come in one page. Tokens never appear; `hasSecret` says one is held. # Replace Records Link Source: https://docs.thinnest.ai/api-reference/tools/replace-records-link /api-reference/openapi.json post /agents/{id}/connections/records/link Make a new link to read the records the agent collects, with a new passcode — shown in this response and never again. The old link stops working at once, which is also how a link shared too widely is revoked. A **build** key may do this. # Sync Tool Server Source: https://docs.thinnest.ai/api-reference/tools/sync-tool-server /api-reference/openapi.json post /agents/{id}/mcp-servers/{serverId}/sync Ask the server what it offers, again — the console's Refresh, and the closest thing to a test. New tools arrive off; tools it no longer lists leave the agent. A store connection has a fixed list and is refused. A **build** key may do this. # Update Built-in Tools Source: https://docs.thinnest.ai/api-reference/tools/update-built-in-tools /api-reference/openapi.json patch /agents/{id}/tools Switch built-in tools on or off and set recall, code verification and the hand-over. `search_knowledge` is not a switch and web search cannot be switched on; both are refused. Changes apply in order and stop at the first refusal, keeping what came before. Needs a **full** key. — it can arm tools that call or message customers. # Update Tool Server Tool Source: https://docs.thinnest.ai/api-reference/tools/update-tool-server-tool /api-reference/openapi.json patch /agents/{id}/mcp-servers/{serverId}/tools/{toolId} Switch one of a server's tools on or off. On, it is callable from every conversation the agent has. Answers with the server and all its tools. A **build** key may do this. # Get Calls Summary Source: https://docs.thinnest.ai/api-reference/usage/get-calls-summary /api-reference/openapi.json get /calls/summary Call totals for the last `days` — calls, inbound, outbound, answered, missed, failed, seconds, what they cost, by surface — and a series by hour (`days=1`) or by day, bucketed in your workspace's time zone. The dashboard's calls figures. Any key may call this, a **read-only** key included. # Get Customers Usage Source: https://docs.thinnest.ai/api-reference/usage/get-customers-usage /api-reference/openapi.json get /customers/usage Developers: what each of your customers cost you over a range, in one call — spend net of refunds, how many charges, and spend by kind. Customers with no charges in the range are left out (list them with `GET /customers`). Without `from`/`to` it is this month so far; days are UTC and a range covers at most 366 days. Call it from your own workspace: the `Thinnest-Workspace` header is refused. Any key may call this, a **read-only** key included. # Get Daily Usage Source: https://docs.thinnest.ai/api-reference/usage/get-daily-usage /api-reference/openapi.json get /usage/daily Replies, calls and call minutes for every UTC day in the window — the Usage page's chart, quiet days as zeros. Give a rolling `days`, or a `from`/`to` range of up to 366 days reaching back at most 730 days; a `to` past today is cut to today. Any key may call this, a **read-only** key included. # Get Usage Source: https://docs.thinnest.ai/api-reference/usage/get-usage /api-reference/openapi.json get /usage Your plan, this calendar month's (UTC) replies against its allowance, the wallet, and replies per UTC day for the last `days` — the figures on the console's Usage page. Any key may call this, a **read-only** key included. # Get WhatsApp Summary Source: https://docs.thinnest.ai/api-reference/usage/get-whatsapp-summary /api-reference/openapi.json get /whatsapp/summary WhatsApp messages sent and received, how many were delivered, read or failed (with failure codes), a series, and what they cost you — the WhatsApp dashboard's figures. Either a rolling `days` or a `from`/`to` range (inclusive, up to 366 days), in your workspace's time zone; `from`/`to` wins when both are given. Any key may call this, a **read-only** key included. # List Call Log Source: https://docs.thinnest.ai/api-reference/usage/list-call-log /api-reference/openapi.json get /usage/calls The Usage page's call log: every call made or taken, newest first, with how it ended and what it was charged. `format=csv` returns the console's export instead — the same calls, up to 10,000, as a `calls.csv` attachment (columns Started, Direction, Surface, Agent, Number, From, Outcome, Status, Seconds, Cost); `limit` and `cursor` do not apply to it. `GET /calls` is the integration view of the same calls; this is the billing view. Any key may call this, a **read-only** key included. # List Invoices Source: https://docs.thinnest.ai/api-reference/usage/list-invoices /api-reference/openapi.json get /invoices One invoice for every payment taken, newest first — the rows under Settings → Billing → History → Invoices. Amounts are integer **minor** units (paise, cents) of `currency`, as issued — not micro-units. `from`/`to` filter on the issue date. Any key may call this, a **read-only** key included. # List Wallet Ledger Source: https://docs.thinnest.ai/api-reference/usage/list-wallet-ledger /api-reference/openapi.json get /wallet/ledger The balance history — every top-up, reply, call, WhatsApp message, number rental, refund and adjustment, newest first, with the balance after each (Settings → Billing → History). A developer's charges for its customers land on its own ledger, each naming the customer; `customer` narrows to one (read with your own key, no `Thinnest-Workspace` header). Reads only: money is added in the console. Any key may call this, a **read-only** key included. # Create Voice Clone Source: https://docs.thinnest.ai/api-reference/voice-clones/create-voice-clone /api-reference/openapi.json post /voice-clones Clones a voice from a 5–30 second recording, sent as multipart/form-data with the audio as `sample`. Both consent fields must be `true` — the two promises the console shows — or nothing is stored or sent anywhere; the refusal carries the two sentences so you can show your own user the same words. Cloning is on the Pro plan and above, up to your plan's number of voices (10 on Pro, 20 on Scale). Needs a **full** key. # Delete Voice Clone Source: https://docs.thinnest.ai/api-reference/voice-clones/delete-voice-clone /api-reference/openapi.json delete /voice-clones/{id} Forgets the voice everywhere — the voice, its recording and its preview — and moves every agent that spoke with it back to a standard voice; `movedAgents` says how many. It cannot be undone. Needs a **full** key. # Get Voice Clone Source: https://docs.thinnest.ai/api-reference/voice-clones/get-voice-clone /api-reference/openapi.json get /voice-clones/{id} One cloned voice, with a preview link that works for sixty seconds — long enough to play, too short to share. # List Voice Clones Source: https://docs.thinnest.ai/api-reference/voice-clones/list-voice-clones /api-reference/openapi.json get /voice-clones Your workspace's cloned voices, newest first, with a short-lived link to each one's preview and the consent it was made under. # Update Voice Clone Source: https://docs.thinnest.ai/api-reference/voice-clones/update-voice-clone /api-reference/openapi.json patch /voice-clones/{id} Renames, re-describes or re-tags a cloned voice. Its language, recording and consent are what it was made from and cannot be changed; sending any other field is refused. A **build** key may do this. # List Voices Source: https://docs.thinnest.ai/api-reference/voices/list-voices /api-reference/openapi.json get /voices Every voice your agents may speak with, in the picker's order: standard, then premium, then Studio. `tier` is the price band a call is billed at. Studio voices — the shared Studio catalogue and your own clones (`mine: true`) and, in a developer's customer workspace, the developer's clones (`sharedFromDeveloper: true`) — are listed only on the Pro plan and above, so every id here is one you can set. Every voice speaks every supported language; the agent's language decides which. # Webhooks Source: https://docs.thinnest.ai/api-reference/webhooks Add, list, change and remove the endpoints an agent's events are sent to. ```http theme={null} POST /api/v1/webhooks Authorization: Bearer ta_live_… Content-Type: application/json ``` ```json theme={null} { "agent": "ag_5c4a5f93-…", "url": "https://crm.example.com/hooks/thinnest", "events": ["call.analysed", "call.completed"] } ``` ```json theme={null} { "id": "wh_e1c5…", "agent": "ag_5c4a5f93-…", "url": "https://crm.example.com/hooks/thinnest", "events": ["call.analysed", "call.completed"], "enabled": true, "delivery": { "failuresInARow": 0, "lastStatus": null, "lastError": null, "lastSentAt": null }, "createdAt": "2026-09-17T09:41:00Z", "signingSecret": "s3cr3t…" } ``` **`signingSecret` is in the create response and nowhere else.** Store it. Every delivery carries `x-thinnest-signature: sha256=…`, an HMAC of the raw body under this secret, and `x-thinnest-signature-v2`, which also signs the delivery time — see [Delivery](#delivery). ## Endpoints | Method | Path | What | | - | - | - | | `GET` | `/webhooks` | List, newest first. `agent` filters | | `POST` | `/webhooks` | Add one. `url`, and `agent` — or `includeCustomers: true` | | `GET` | `/webhooks/{id}` | One endpoint, with its delivery state | | `PATCH` | `/webhooks/{id}` | `url`, `events`, `enabled` | | `DELETE` | `/webhooks/{id}` | Remove it | | `POST` | `/webhooks/{id}/test` | Send a sample event and say whether it arrived | | `GET` | `/webhooks/{id}/deliveries` | Recent deliveries, newest first, with attempts and the next retry | | `POST` | `/webhooks/{id}/redeliver` | Send one delivery again, or everything that failed since a time | ## Fields Whose events. Required on create — unless `includeCustomers` is `true`. For a workspace with [customers](/api-reference/customers): one endpoint for the events of every agent in every one of your customers. It has no `agent` (send one or the other, not both). Each delivery's `data` carries `workspaceId` — the customer it is about — inside the signed body. An `https://` address, or an email address — a helpdesk that opens tickets by mail gets one email per event. Addresses inside our own network are refused. Which events to send. Leave it out for every event, now and as new ones are added; the response then reads `["*"]`. | Event | When | | - | - | | `lead.captured` | The agent collected a name, email or phone from somebody interested | | `conversation.escalated` | It handed a conversation to a person | | `conversation.resolved` | A teammate marked one done | | `call.completed` | A call ended — outcome and duration, the moment it ends | | `call.analysed` | A call's [results are ready](/api-reference/calls/get-call) — summary, fields, transcript, recording link, and what the call cost (`costMicro` in millionths of `currency`; your reseller's price if you are a white-label client; `costMicro` is `null` if the call has not settled yet — read [Get Call](/api-reference/calls/get-call) later) | | `campaign.finished` | A broadcast or calling campaign finished | | `contact.opted_out` | During a call, somebody asked not to be called again and the agent put their number on your [do-not-call list](/channels/voice#dont-call-me-again) — `data.phone`, `data.callId`, `data.source` (always `"call"`), `data.optedOutAt`, `data.reason` (their words, or `null`) | | `call.problem` | During a call that is [still going on](#problems-on-a-live-call): the agent could not answer, your own model key failed, a reply was very slow, or the voice connection failed. At most once per kind per call | | `contact.erased` | A contact you deleted is [gone completely](#workspace-events), recordings included. Sent once | | `byok.changed` | Your [own keys](#workspace-events) changed: one was added, replaced, removed or failed its check, or they were switched on or off or set to cover different jobs | Switching an endpoint back on forgives its past failures. `call.started` is not a webhook event you subscribe to. It is the request your own system receives from an agent's [Who-is-calling lookup](/channels/voice#who-is-calling-lookup), signed the same way, which you answer with values for the agent to use. ## Problems on a live call `call.problem` is sent **while the call is still going on**, so you can alert somebody or flag the call before it ends. Each kind is sent at most once per call, and a burst across many calls is held to about 20 events a minute per agent. An event held back by that limit is not queued: it is sent if that kind happens again on the same call once the agent is back under the limit, and otherwise it is not sent at all. The same problems are also on the call itself, under **Problems on this call** in [Usage](/workspace/usage#how-a-call-went). ```json theme={null} { "id": "evt_4d8b2f6a-…", "event": "call.problem", "sentAt": "2026-10-06T04:31:02.715Z", "data": { "callId": "out_9f2c1a44-…", "agentId": "ag_5c4a5f93-…", "kind": "slow_reply", "detail": "A reply took 6.2 seconds to start.", "at": "2026-10-06T04:31:02.702Z" } } ``` | `kind` | What happened | | - | - | | `no_answer_from_model` | The agent could not get an answer — even from its backup — and apologised to the caller | | `byok_failed` | Your [own model key](/api-reference/bring-your-own-keys) failed, so the agent apologised instead of answering | | `slow_reply` | A reply took more than 5 seconds to start | | `voice_failed` | The call's voice connection failed — the caller may have heard silence or been cut off. It also rings the workspace's notification bell | `callId` is the id [Get Call](/api-reference/calls/get-call) takes. `detail` is a sentence you can show a person. ## Call quality Every call the agent ran carries `quality`, written once when the call ends — on [Get Call](/api-reference/calls/get-call) and in `data.quality` on `call.completed` and `call.analysed`: ```json theme={null} "quality": { "turns": 14, "responseMs": { "median": 1180, "p90": 1920, "max": 3410 }, "firstTokenMs": { "median": 610, "p90": 940, "max": 1730 }, "longSilences": 1, "longSilenceSeconds": 10, "longestSilenceSeconds": 12.4, "interruptions": 2, "modelErrors": 0, "rescues": 1 } ``` | Field | What it is | | - | - | | `turns` | Caller turns the agent answered — noises that got no reply, such as an echo of the agent's own voice, are not counted | | `responseMs` | How long the caller waited: from when they stopped speaking to the agent's first audio — median, 90th percentile and slowest, in milliseconds | | `firstTokenMs` | Our side of that wait: from the turn reaching the model to its first word | | `longSilences` | Silences on either side at least `longSilenceSeconds` long — the agent's "are you still there?" time, or 10 seconds if it has none | | `longestSilenceSeconds` | The longest stretch where nobody spoke | | `interruptions` | Times the caller started speaking while the agent was talking | | `modelErrors` | Turns the agent apologised for because it could not get an answer. `null` when the count was not available for the call | | `rescues` | Turns where the agent's first choice failed and a backup answered, so the caller still got a reply. `null` when the count was not available for the call | `quality` is `null` for a call that was not measured (a call from before these figures were recorded, or one that never connected), and a figure inside it is `null` when it could not be measured on that call. On `call.completed` it can also be `null` when the call ended before its figures were ready; `call.analysed` and Get Call have them by then. In the console the same figures are under **How the call went** when you open the call in [Usage](/workspace/usage#how-a-call-went). ## Calls handed to another agent When an agent passes a live call to another of your agents ([how](/channels/voice)), `call.analysed` and Get Call list it in `handedTo`, oldest first — empty when the call stayed with `agent`, which is always the agent the call was placed or answered as: ```json theme={null} "handedTo": [ { "agent": { "id": "ag_7b1d4e90-2c3a-4f6e-8d15-9a0b3c2e4f71", "name": "Support" }, "from": { "id": "ag_3f6a9c21-7d4e-4b58-9a1f-0c2e8b7d5a34", "name": "Sales" }, "at": "2026-10-09T10:04:12Z" } ] ``` A hand-off to another agent is not a hand-over to a person, so it does not send `conversation.escalated`. **Which agent an event names.** The call-level events — `call.completed`, `call.analysed` and `call.problem` — stay with the agent the call started with, which is also the one the call is billed and summarised under. Events the agents themselves raise mid-call — `lead.captured`, `conversation.escalated` — name the agent that ran the tool in `data.agentId` and carry `data.callId`, so on a handed call you can join them to the call report and to `handedTo`. **Webhooks belong to an agent.** An event raised by the agent a call was handed to goes to *that* agent's webhooks, not to the agent that took the call first. If you only subscribe the first agent, you will get `call.completed` and `call.analysed` for the whole call but miss any `lead.captured` or `conversation.escalated` the second agent raises. Add the same endpoint to every agent a call can be handed to. ## Delivery Each event is a `POST` of JSON: ```json theme={null} { "id": "evt_9d1c…", "event": "lead.captured", "sentAt": "2026-10-07T09:12:03.551Z", "data": { … } } ``` with these headers on every attempt (an email destination gets the ticket email only, without them): | Header | What it is | | - | - | | `x-thinnest-signature` | `sha256=` + hex HMAC-SHA256 of the raw body under your signing secret | | `x-thinnest-signature-v2` | `sha256=` + hex HMAC-SHA256 of `.` — the delivery time, signed | | `x-thinnest-delivered-at` | When this attempt left (ISO 8601) | | `x-thinnest-event-id` | `evt_…`, the same as the body's `id` — the same on every retry and re-send | | `x-thinnest-attempt` | `1`, then `2`, `3`, … | ### Retries Anything other than a 2xx answer — an error, a timeout after 10 seconds, an address we cannot reach — is tried again after **1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours**: six attempts over about 8½ hours. Every attempt sends the **same bytes**, so `sentAt` is when the event happened, not when the attempt left. Payloads are kept for **7 days**, so anything that still did not arrive can be [sent again](#re-send-what-failed) within that week. Re-sending a delivery starts its retry schedule afresh. Retries and re-sends go to the endpoint's **current** address — change the URL and pending retries follow it. A Slack or helpdesk destination that accepted a post but answered after the 10-second timeout can receive it twice, since neither can dedupe on the event id. ### What your receiver should do 1. **Verify** `x-thinnest-signature-v2` (or `x-thinnest-signature`) with a constant-time compare. 2. **Check freshness on `x-thinnest-delivered-at`** — reject one more than five minutes from your clock. Not on `sentAt`: a retry's `sentAt` is hours old by design. The delivery time is covered by the v2 signature, so it cannot be moved without breaking it. 3. **Dedupe on `x-thinnest-event-id`.** A retry after a timeout can reach you after you already handled the first attempt. 4. **Answer 2xx quickly**, then do the work. ```js theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; function verify(secret, rawBody, headers) { const at = headers["x-thinnest-delivered-at"]; const expected = "sha256=" + createHmac("sha256", secret).update(`${at}.${rawBody}`).digest("hex"); const given = headers["x-thinnest-signature-v2"] ?? ""; const fresh = Math.abs(Date.now() - Date.parse(at)) < 5 * 60_000; return fresh && given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected)); } ``` ### Switched off An endpoint is switched off — `enabled: false`, `delivery.lastError` says why — after **five events in a row that each failed every attempt**. A failed attempt that a retry then delivers does not count. Events that happen while it is off are recorded as not sent; fix the endpoint, `PATCH { "enabled": true }`, and re-send them — nothing within the last 7 days is lost. ## Event payloads Every delivery is the same envelope. What changes is `data`, shown here in full for each event. Ids in `data` are the same prefixed ids the API returns and takes: `conv_…` for a conversation (pass it to [List Conversation Messages](/api-reference/conversations/list-conversation-messages)), `cmp_…` for a campaign, and `out_…`, `sch_…` or the call reference for a call (pass it to [Get Call](/api-reference/calls/get-call)). `conversationId` and `campaignId` always carry their `conv_` and `cmp_` prefix, so a value from a webhook can be passed straight to the API. If your receiver stored these ids as bare identifiers before, store the prefixed value from now on. An endpoint created with `includeCustomers: true` also gets `data.workspaceId` (`org_…`), the customer the event is about. ### `lead.captured` The agent collected contact details. `name`, `email` and `phone` are each `null` when not given; `note` is what the customer wants, written for whoever follows up. `agentId` is the agent that captured it and `callId` the call it was on (the id [Get Call](/api-reference/calls/get-call) takes; `null` off a call). ```json theme={null} { "id": "evt_9d1c4b2a-7e3f-4a6b-8c1d-2e5f7a9b0c13", "event": "lead.captured", "sentAt": "2026-10-06T04:31:55.204Z", "data": { "conversationId": "conv_3f6b2d1e-8a4c-4f7b-9e21-6c0d5a7b8e94", "agentId": "ag_3f6a9c21-7d4e-4b58-9a1f-0c2e8b7d5a34", "callId": "out_6d1c2e84-3b7a-4f95-8e10-5a9b7c3d2f41", "name": "Asha Rao", "email": "asha@example.com", "phone": "+919876543210", "note": "Wants a 2 BHK at Sky Towers, budget around 80 lakh, and a site visit on Saturday." } } ``` ### `conversation.escalated` The agent handed the conversation to a person. `reason` is the agent's own words. `agentId` is the agent that handed over and `callId` the call it was on (`null` off a call). ```json theme={null} { "id": "evt_1a7f3c9e-5b2d-4e8a-9f6c-0d3b7e1a4c25", "event": "conversation.escalated", "sentAt": "2026-10-06T04:32:12.880Z", "data": { "conversationId": "conv_3f6b2d1e-8a4c-4f7b-9e21-6c0d5a7b8e94", "agentId": "ag_3f6a9c21-7d4e-4b58-9a1f-0c2e8b7d5a34", "callId": null, "reason": "The customer asked to speak to someone about a refund." } } ``` ### `conversation.resolved` A teammate marked the conversation done. Sent once, when the status actually changes. ```json theme={null} { "id": "evt_6e2b8d4f-1c7a-4f3e-a9b5-8d0c2e6f1a37", "event": "conversation.resolved", "sentAt": "2026-10-06T05:10:03.551Z", "data": { "conversationId": "conv_3f6b2d1e-8a4c-4f7b-9e21-6c0d5a7b8e94" } } ``` ### `call.completed` The moment a call ends, before it has been read. `outcome` is `completed`, `missed` or `failed`; `hangup` is `answered`, `no_answer`, `busy`, `rejected`, `unreachable`, `cancelled`, `voicemail`, `before_agent` or `failed`; `phone` is null on a web call. `campaignId` is present only for a campaign's call, and `id`, `reference` and `metadata` only for a call placed through the API. ```json theme={null} { "id": "evt_3c9a5e1b-7d4f-4b2a-8e6c-1f0d9a3b5c49", "event": "call.completed", "sentAt": "2026-10-06T04:33:12.020Z", "data": { "callRef": "out_9f2c1a44-6b0e-4c1d-8a7f-2e5b3c9d1f60", "conversationId": "conv_3f6b2d1e-8a4c-4f7b-9e21-6c0d5a7b8e94", "outcome": "completed", "hangup": "answered", "seconds": 182, "direction": "outbound", "surface": "phone", "phone": "919876543210", "campaignId": "cmp_7a1e9c3b-2d4f-4b8a-a6e0-1f5c8d2b9e47", "id": "out_9f2c1a44-6b0e-4c1d-8a7f-2e5b3c9d1f60", "reference": "LSQ-42", "metadata": { "deal_id": "D-19", "owner": "Ravi" }, "quality": { "turns": 14, "responseMs": { "median": 1180, "p90": 1920, "max": 3410 }, "firstTokenMs": { "median": 610, "p90": 940, "max": 1730 }, "longSilences": 1, "longSilenceSeconds": 10, "longestSilenceSeconds": 12.4, "interruptions": 2, "modelErrors": 0, "rescues": 1 }, "endedAt": "2026-10-06T04:33:11.902Z" } } ``` `quality` is how the call went in numbers (see [Call quality](#call-quality)); it can be `null` when the call ended before its figures were ready. `forwardedTo` appears only on a call your number forwarded to a phone, and is that phone's number; it is left out of every other call. ### `call.analysed` The call once it has been read: exactly the object [Get Call](/api-reference/calls/get-call) returns. `quality` can be `null` (see [Call quality](#call-quality)), `handedTo` is empty unless the call was [handed to another agent](#calls-handed-to-another-agent), and `forwardedTo` is `null` unless the call was forwarded to a phone. ```json theme={null} { "id": "evt_8f4d2a6c-3e9b-4c1f-b7a5-6d2e0f8c4a51", "event": "call.analysed", "sentAt": "2026-10-06T04:33:20.402Z", "data": { "id": "out_9f2c1a44-6b0e-4c1d-8a7f-2e5b3c9d1f60", "callRef": "out_9f2c1a44-6b0e-4c1d-8a7f-2e5b3c9d1f60", "reference": "LSQ-42", "metadata": { "deal_id": "D-19", "owner": "Ravi" }, "attempt": 1, "status": "completed", "direction": "outbound", "phone": "919876543210", "from": "918045678901", "agent": { "id": "ag_5c4a5f93-2b1e-4d7a-9f60-8e2d1c3b4a71", "name": "Skyline Sales" }, "scheduledFor": null, "startedAt": "2026-10-06T04:30:02.118Z", "answeredAt": "2026-10-06T04:30:09.540Z", "endedAt": "2026-10-06T04:33:11.902Z", "seconds": 182, "hangup": "answered", "variables": { "lead_name": "Asha", "project": "Sky Towers" }, "summary": "Asha is keen on a 2 BHK at Sky Towers with a budget around 80 lakh. She asked to visit the site on Saturday.", "fields": { "budget": 8000000, "interest": "Hot", "site_visit": true }, "transcript": [ { "speaker": "agent", "text": "Hello Asha, I'm calling from Skyline Homes about your enquiry for Sky Towers.", "at": "2026-10-06T04:30:10.204Z" }, { "speaker": "customer", "text": "Haan, boliye. I wanted a 2 BHK.", "at": "2026-10-06T04:30:16.880Z" } ], "recording": { "url": "https://app.thinnest.ai/api/v1/recordings/3b7e1c90-5a2d-4e8f-9c61-0d4b7a2e8f13?expires=1762403402&sig=8c1f0e7a92b4d3c6e5f1a0b9c8d7e6f5a4b3c2d1", "expiresAt": "2026-11-05T04:30:02.118Z", "ready": true }, "analysedAt": "2026-10-06T04:33:20.311Z", "retryScheduledFor": null, "costMicro": 6370000, "currency": "INR", "quality": { "turns": 14, "responseMs": { "median": 1180, "p90": 1920, "max": 3410 }, "firstTokenMs": { "median": 610, "p90": 940, "max": 1730 }, "longSilences": 1, "longSilenceSeconds": 10, "longestSilenceSeconds": 12.4, "interruptions": 2, "modelErrors": 0, "rescues": 1 }, "handedTo": [ { "agent": { "id": "ag_7b1d4e90-2c3a-4f6e-8d15-9a0b3c2e4f71", "name": "Site Visits" }, "from": { "id": "ag_5c4a5f93-2b1e-4d7a-9f60-8e2d1c3b4a71", "name": "Skyline Sales" }, "at": "2026-10-06T04:31:40.000Z" } ], "forwardedTo": null, "error": null } } ``` ### `campaign.finished` Everyone on a broadcast (`kind: "whatsapp"`) or a calling campaign (`kind: "voice"`) has been reached or given up on. `skipped` is people who withdrew consent before their turn. ```json theme={null} { "id": "evt_2b6e0c8a-9f3d-4a7e-8c4b-5e1f3d7a9b63", "event": "campaign.finished", "sentAt": "2026-10-06T13:02:44.311Z", "data": { "campaignId": "cmp_7a1e9c3b-2d4f-4b8a-a6e0-1f5c8d2b9e47", "name": "Diwali site visits", "kind": "voice", "recipients": 120, "sent": 104, "failed": 9, "skipped": 7, "finishedAt": "2026-10-06T13:02:44.180Z" } } ``` ### `contact.opted_out` During a call, somebody asked not to be called again and their number went on the do-not-call list. `callId` is the call's id; `source` is always `"call"`; `reason` is what the caller said, as recorded (up to 160 characters, in the language they spoke), or `null` when the agent did not record their words. ```json theme={null} { "id": "evt_5a3f7d1e-2c8b-4e6a-9d0f-7b4c1e5a3d75", "event": "contact.opted_out", "sentAt": "2026-10-06T04:32:40.640Z", "data": { "phone": "+919876543210", "callId": "out_9f2c1a44-6b0e-4c1d-8a7f-2e5b3c9d1f60", "source": "call", "optedOutAt": "2026-10-06T04:32:40.511Z", "reason": "please don't call me again" } } ``` ### `call.problem` Something went wrong while a call was live. `kind` is `no_answer_from_model`, `byok_failed`, `slow_reply` or `voice_failed`; `detail` says what happened in a sentence; `callId` is the id [Get Call](/api-reference/calls/get-call) takes. Sent at most once per kind per call. ```json theme={null} { "id": "evt_4d8b2f6a-1e3c-4a9d-b5f7-0c6e2a8d4b87", "event": "call.problem", "sentAt": "2026-10-06T04:31:02.715Z", "data": { "callId": "out_9f2c1a44-6b0e-4c1d-8a7f-2e5b3c9d1f60", "agentId": "ag_5c4a5f93-2b1e-4d7a-9f60-8e2d1c3b4a71", "kind": "slow_reply", "detail": "A reply took 6.2 seconds to start.", "at": "2026-10-06T04:31:02.702Z" } } ``` ## Workspace events `contact.erased` and `byok.changed` are about your whole workspace rather than one agent. They go to **every endpoint in the workspace that subscribed to them**, whichever agent you added it on, and **once per address**: if three agents post to the same URL, that URL still gets one delivery. If you are a developer, an endpoint that receives every customer's events (`includeCustomers`) gets them for your customers too, with `workspaceId`. An endpoint subscribed to every event (`["*"]`) receives them as well. Neither event carries personal details or any part of a key. ### `contact.erased` A contact you deleted with [Delete Contact](/workspace/data-and-erasure) is gone completely, recordings included. It is sent **once**: straight away when Delete Contact answered `204`, or — when it answered `recordingsPending` because a recording could not be deleted at that moment — from the nightly retry, once the last of those recordings is gone. A recording that was still being written when you deleted the contact can take up to three nights to be confirmed gone. `contactId` is the id you deleted. ```json theme={null} { "id": "evt_7c2e9a41-5d3b-4f8e-a6c0-1b9d4e7f2a58", "event": "contact.erased", "sentAt": "2026-10-07T02:30:11.402Z", "data": { "contactId": "cust_2b7e4c19-8a3d-4f60-b1e5-9c0d6a3f7e24", "erasedAt": "2026-10-06T11:42:05.118Z" } } ``` ### `byok.changed` Your [own keys](/api-reference/bring-your-own-keys) changed. `change` is one of: | `change` | What happened | | - | - | | `key_added` | A key was added for a job that had none | | `key_replaced` | A key was saved over the one you had for that job | | `key_removed` | A key was removed | | `key_failed_verification` | You tried to save a key and the provider refused it. Nothing was saved | | `byok_enabled` | Your own keys were switched on | | `byok_disabled` | Your own keys were switched off; calls and replies run on ours | | `scope_changed` | Your own keys were set to cover different jobs | `kind` (`stt`, `llm` or `tts`) and `provider` name the key for the four key changes, and are `null` for the others. `byokEnabled` and `scope` say where your workspace stands **after** the change. Choosing a different model for a key is not sent. Saved keys are not re-checked on a schedule: a model key that stops working on a call shows up as [`call.problem`](#problems-on-a-live-call) (`byok_failed`). ```json theme={null} { "id": "evt_1f6b3d8e-9a2c-4e75-b0d4-6c8a2e5f9b13", "event": "byok.changed", "sentAt": "2026-10-06T09:15:40.226Z", "data": { "change": "key_replaced", "kind": "tts", "provider": "cartesia", "byokEnabled": true, "scope": "voice" } } ``` ## Re-send what failed ```http theme={null} POST /api/v1/webhooks/{id}/redeliver ``` One delivery, now — the answer says whether it arrived: ```json theme={null} { "deliveryId": "whd_2b6f…" } ``` ```json theme={null} { "delivered": true, "status": 200, "error": null, "attempts": 4 } ``` Or everything that has not succeeded since a time, at most 7 days back — queued and sent within a minute: ```json theme={null} { "since": "2026-10-06T00:00:00Z" } ``` ```json theme={null} { "queued": 12 } ``` The same body, event id and `sentAt` as the first attempt, with `x-thinnest-attempt` one higher. The endpoint must be switched on. The console's **Re-send failed** button on the endpoint does the same for the last 7 days. A build key may do this. ## Test an endpoint ```http theme={null} POST /api/v1/webhooks/{id}/test ``` ```json theme={null} { "delivered": true, "status": 200, "error": null } ``` Sends one sample `lead.captured` — marked `"test": true`, with obviously fake details — the same way a real event goes: signed JSON, Slack's own format for a Slack URL, or a ticket email for an email address. A failed test counts towards the five failures that switch an endpoint off. ## Delivery history ```http theme={null} GET /api/v1/webhooks/{id}/deliveries ``` ```json theme={null} { "items": [ { "id": "whd_2b6f…", "eventId": "evt_9d1c…", "event": "call.analysed", "status": 500, "delivered": false, "error": "Internal Server Error", "attempts": 2, "nextRetryAt": "2026-10-05T09:18:04Z", "at": "2026-10-05T09:12:03Z", "lastAttemptAt": "2026-10-05T09:13:04Z" }, { "id": "whd_7c3e…", "eventId": null, "event": "test", "status": 200, "delivered": true, "error": null, "attempts": 1, "nextRetryAt": null, "at": "2026-10-05T08:58:41Z", "lastAttemptAt": "2026-10-05T08:58:41Z" } ] } ``` One row per event, however many attempts it took, newest first — the last 7 days (or the last 50, whichever is more). The answer to "why did leads stop reaching our CRM on Tuesday?". `status` and `error` are from the latest attempt; `0` means we could not reach it. `nextRetryAt` is when the next automatic attempt is due — null once it arrived or ran out of retries. The payloads themselves are not returned: they carry your customers' details. # Create Webhook Source: https://docs.thinnest.ai/api-reference/webhooks/create-webhook /api-reference/openapi.json post /webhooks Adds an endpoint for one agent's events — or, from a developer workspace, with `includeCustomers: true`, one endpoint for every agent in every one of your customers, each delivery's `data` carrying the `workspaceId` it is about. The response carries `signingSecret` **once**: store it to verify `x-thinnest-signature` on every delivery, because it is never shown again. `url` is an `https://` address or an email address (a helpdesk that opens tickets by mail gets one email per event); addresses inside our own network are refused. A **build** key may do this. # Delete Webhook Source: https://docs.thinnest.ai/api-reference/webhooks/delete-webhook /api-reference/openapi.json delete /webhooks/{id} Removes the endpoint. Nothing more is sent to it, and its delivery history goes with it. A **build** key may do this. # Get Webhook Source: https://docs.thinnest.ai/api-reference/webhooks/get-webhook /api-reference/openapi.json get /webhooks/{id} One endpoint, with its delivery state: how many deliveries in a row have failed, and the last status, error and time. Never the signing secret. # List Webhook Deliveries Source: https://docs.thinnest.ai/api-reference/webhooks/list-webhook-deliveries /api-reference/openapi.json get /webhooks/{id}/deliveries The endpoint's recent deliveries, newest first — one row per event, however many attempts it took; 7 days (or the last 50, whichever is more) are kept. `status` and `error` are from the latest attempt, `0` when your endpoint could not be reached. `attempts` counts them, and `nextRetryAt` says when the next automatic one is due (null once delivered or out of retries). Test sends are listed as `test` with no `eventId`. Payloads are not returned: they carry your customers' details. Returns at most 50 rows and has no cursor: `nextCursor` is always null. A delivery counts toward switching the endpoint off only when it used up all six attempts: five such events in a row switch it off, and since each takes about 8½ hours to give up, a dead endpoint can take up to about two days to be switched off. # List Webhooks Source: https://docs.thinnest.ai/api-reference/webhooks/list-webhooks /api-reference/openapi.json get /webhooks The workspace's webhook endpoints, newest first — those added by API and those added in the console alike — each with its delivery state. Filter to one agent's with `agent`. # Redeliver Webhook Events Source: https://docs.thinnest.ai/api-reference/webhooks/redeliver-webhook-events /api-reference/openapi.json post /webhooks/{id}/redeliver Sends events to this endpoint again — the same body, event id and `sentAt` they first had, with `x-thinnest-attempt` one higher. Either `{ "deliveryId": "whd_…" }` for one delivery, sent now (the answer says whether it arrived; if it fails it joins the retry schedule), or `{ "since": "" }` for every delivery that has not succeeded since then — at most 7 days back, when payloads are dropped — queued and sent within a minute (`202 { queued }`). The endpoint must be switched on. A **build** key may do this: it reaches only your own endpoint. # Test Webhook Source: https://docs.thinnest.ai/api-reference/webhooks/test-webhook /api-reference/openapi.json post /webhooks/{id}/test Sends one sample `lead.captured` event to the endpoint — its `data` marked `"test": true`, with obviously fake details — the same way a real event goes: signed JSON, Slack's own message format for a Slack URL, or a ticket email for an email address. It is recorded in the delivery history as `test`, and a failed test counts towards the five failures in a row that switch an endpoint off. The answer says whether it arrived; a failure to deliver is still a `200` here. A **build** key may do this. # Update Webhook Source: https://docs.thinnest.ai/api-reference/webhooks/update-webhook /api-reference/openapi.json patch /webhooks/{id} Changes an endpoint's `url`, `events` or `enabled`; fields you leave out are kept. Switching an endpoint back on (`enabled: true`) forgives its past failures, so after an outage: fix the receiver, catch up from the API on what it missed, then `PATCH { "enabled": true }`. The agent an endpoint belongs to cannot be changed. A **build** key may do this. # Get Workspace Source: https://docs.thinnest.ai/api-reference/workspace/get-workspace /api-reference/openapi.json get /workspace The workspace's id, name, console address (`slug`), time zone and when it was made. Plan and billing are not settings and are not here. # Update Workspace Source: https://docs.thinnest.ai/api-reference/workspace/update-workspace /api-reference/openapi.json patch /workspace Renames the workspace, changes its time zone, or sets how many days calls are kept (`callRetentionDays`) — any of them. The name appears in invitation emails, so it follows the rules for a person's name: 2 to 80 characters (a longer one is refused with a 400), no web or email address, no markup. Every date in the console is shown in the time zone. The `slug` never changes. Needs a **full** key. # Channels overview Source: https://docs.thinnest.ai/channels/overview Where customers talk to your agent, and what each channel can carry. A channel is a place a customer **talks to you**. There are four. | Channel | Inbound | Agent replies | Broadcasts | Recognises signed-in customers | | - | - | - | - | - | | [Website widget](/channels/website) | Yes | Yes | Never — a campaign reaches somebody who is not there | Yes, [with signing](/install/signed-in-customers) | | [WhatsApp](/channels/whatsapp) | Yes | Yes | Yes | Yes, by phone number | | [Telegram](/channels/telegram) | Yes | Yes | Not built | No | | [Voice](/channels/voice) | Yes, by phone | Yes, out loud | No — a call is not a broadcast | Yes, by phone number | An agent has at most one channel of each kind. **Shopify, WordPress and PrestaShop are not channels.** They are doors onto the website widget — ways to get it onto your pages without editing theme code. See [Put it on your site](/install/shopify). ## One agent across all of them The same knowledge, the same instructions, the same tools. You do not configure a separate WhatsApp bot. Each channel renders what it can. A card that appears as a rich card in the website widget arrives as a native WhatsApp card, and where a channel cannot render something it degrades to text rather than to a blank message. ## One customer across all of them A person who asks on your website and then messages on WhatsApp is one contact with one history — provided the two can be joined on evidence they did not supply themselves. [How that works](/concepts#how-two-channels-become-one-person). ## What is not built * **Instagram and Messenger.** Not available. Meta's rules also forbid marketing content on those outside the 24-hour window, so a campaign lane would not exist there even if the channel did. * **Multi-channel campaigns.** The picker is real and only WhatsApp can send. * **WhatsApp calling.** Not yet. [Voice](/channels/voice) answers on a phone number and in the website widget today. # Phone numbers Source: https://docs.thinnest.ai/channels/phone-numbers Get a number your customers ring, or point one you already own at your agent. Your agent answers on a phone number. You can get one from us — searched, bought and ready on the same screen — or use a number you already own. Numbers belong to the **workspace**, not to one agent. They live on the **Phone Numbers** page in the main sidebar, which lists every number you hold, where it came from, and two things for each: the **Inbound** agent, which answers it, and the **Outbound** agent, which calls out on it. They can be the same agent or different ones, or either can be empty — see [Inbound and outbound](#inbound-and-outbound-are-separate). A **Dial-out ready** column says whether the number can place calls as well as answer them. You can search the list and move any number to another agent. You do not need a number to try voice. The website call button works without one, on any plan, and it is the same agent with the same voice. A number is what lets somebody ring you from a phone — and if you already own one, you can point it at your agent on any plan, free included. ## Getting a number from us In the main sidebar choose **Phone Numbers**, then **Buy / Import number**. Pick a country, and a city if you want a local code. You will see the numbers available now, each with what it costs a month. Choose a number. Before anything is charged you get a summary — the number, what it costs a month, what leaves your wallet today and what is left in it — and the number is yours the moment you confirm. There is nothing to configure with a phone company and no settings to copy anywhere. A new number is not pointed anywhere yet. On the **Phone Numbers** page, choose the agent under **Inbound**. That agent starts answering immediately. Call the number. You should hear your greeting, then the agent. ### What it costs Each number has a monthly price, shown before you take it, and it repeats every month until you give the number up. Your plan may include one — if it does, the page says so before you choose, and a number your plan includes needs nothing in your wallet because nothing is taken for it. **Buying a number needs a paid plan**, because we rent it for you every month. Pay as you go has no monthly fee of its own and can hold numbers. **Pointing a number you already own at your agent works on every plan, including the free one.** It costs us nothing to answer it — you are already paying your own provider for the number — and a phone call is usually the thing you want to hear before you decide to pay for anything. Free-plan calls come out of the same trial minutes as calls from your website. That covers calls in **both directions**. On the free plan your agent can answer a number you brought and, once you have added that account's credentials, ring people from it — your own provider bills you for the line either way. Buying a number from us is the part that needs a paid plan. ### India needs your business verified first An Indian number is only issued to a registered business, and the check is in **your** name rather than ours. It has its own tab — **KYC**, beside Imported on the Phone Numbers page — because it takes a little time to clear and it is worth being able to come back and see where it got to. This is not our rule and we cannot shortcut it. Numbers in most other countries need nothing beyond choosing one. This is not the same as [business verification for WhatsApp](/meta/verification), which Meta runs and which unlocks a different set of things. Passing one does nothing for the other, and you may well need both. #### What it asks for Your registered business name, and **one** document. Which one depends on a single question: | Registered for GST? | The document to send | | - | - | | Yes | Your **GST certificate** | | No | Your **Certificate of Incorporation**, or your **Udyam certificate** — whichever you hold | That is the whole form. PDF, JPG or PNG, up to 5 MB. Send it as registered. The name you type is checked against the certificate you attach, so an abbreviation or a trading name is the usual reason an application comes back. #### Following it up The **KYC** tab shows where the application stands, asked afresh each time you open the page rather than remembered from last time: * **Being checked** — sent, usually decided within minutes. * **Approved** — you can take an Indian number. * **Not accepted** — the reason is shown with it. **Correct and resend** puts the form back up. * **Suspended** or **Expired** — send your documents again. Each application has a **reference**. If you ask us to chase one, that is the thing to quote. **We keep a copy of the document you sent**, so correcting an application does not mean going and finding the file again. You can download it from the same tab; that needs an owner or admin. You cannot buy an Indian number until this is approved — pressing Buy takes you to the form instead. Numbers in every other country need none of it. #### Doing it from the API If you run a platform with one workspace per customer, each customer's business is verified in **its own workspace** — the application belongs to the workspace, not to you. Send the customer's details with the `Thinnest-Workspace` header (see [Build a platform](/guides/build-a-platform)) and the same form the console shows is filled in for them. For each business you need three things: its **legal name**, whether it is **registered for GST** (yes or no), and **one document** — the GST certificate if it is, otherwise the Certificate of Incorporation or the Udyam certificate. PDF, JPG or PNG, up to 5 MB, with a file name of 99 characters or fewer. ```bash theme={null} curl -X PUT https://app.thinnest.ai/api/v1/phone-numbers/business-details \ -H "Authorization: Bearer $API_KEY" \ -H "Thinnest-Workspace: org_3f6a9c21-7d4e-4b58-9a1f-0c2e8b7d5a34" \ -F businessName="Acme Traders Private Limited" \ -F gstRegistered=true \ -F document=@gst-certificate.pdf ``` Without GST, send `-F gstRegistered=false` and the certificate you hold, adding `-F documentKind=udyam` if it is a Udyam certificate (`incorporation` is assumed). The answer is the application, and the status in it is the same set the KYC tab shows: ```json theme={null} { "status": "submitted", "businessName": "Acme Traders Private Limited", "gstRegistered": true, "documentKind": "gst", "submittedAt": "2026-10-08T09:41:12.380Z", "canRent": false } ``` [Get Business Details](/api-reference/phone-numbers/get-business-details) reads it back at any time, and checks it afresh each time. The check usually finishes in a few minutes; when `canRent` turns `true`, [Rent Phone Number](/api-reference/phone-numbers/rent-phone-number) will issue an Indian number. If the application comes back `rejected`, `reviewNote` says what to correct, and sending again corrects that same application. Sending while one is being checked, or after it was approved, is refused with `409`. This needs a **full** key and a paid plan: the document goes to India's numbering authority in your customer's name, so a build or read-only key cannot send it (any key can read it). We never return the document itself. ### Marketing calls need a different number Indian rules give every number a purpose, and it is worth knowing before you buy one rather than after you build a campaign. | What you are calling about | The number for it | | - | - | | Reminders, order updates, renewals, anything the customer is expecting | The numbers on this page — landlines, bought in a few clicks | | **Marketing** — an offer, a promotion, anything selling | A **140** number, which is applied for in your business's name | A 140 number is not sold from a list. It is issued to your business after you register on the DLT portal, so choosing **Promotional (140)** when adding a number shows you the steps rather than a search. As both the *principal entity* and the *telemarketer* — the calls are yours, so the registration is yours. Two to three working days. Your GST certificate and both DLT registrations. We apply for the number and hand you back the approval letter it needs. On the same portal. Scripts are approved one at a time — a marketing call says an approved thing, not whatever you type on the day. Five to ten working days end to end. Get in touch and we will start it. Check this before anything else: the telemarketer registration has to be in **Maharashtra** or **Karnataka**. Registered anywhere else and we cannot get you a 140 number today. Until you hold one, a campaign marked *promotional* is refused rather than quietly sent from a landline. That refusal is the rule working — it is the number that carries the penalty, not us — and it names the number it is about so you know which one to change. ## Using a number you already own If your number is with **Plivo**, **Vobiz**, **Twilio** or **Telnyx**, you can point it at your agent and keep it where it is. You go on paying them for the number and we charge you nothing for it — we only answer it. **Twilio and Telnyx work a little differently** — we set the number up for you; see [Numbers from Twilio](#numbers-from-twilio) and [Numbers from Telnyx](#numbers-from-telnyx) below. The steps here are for Plivo and Vobiz. **A Plivo, Twilio or Telnyx number needs your account's credentials before it can answer.** Plivo and Twilio sign every call with your Auth Token, and we use it to confirm each call really came from your account — without it, calls to the number are refused. On Telnyx your API key is how we set the number up at all. A Vobiz number answers without them; there, the credentials are only needed for outgoing calls. Choose **Import**, then say which carrier the number is with. Paste the **Auth ID** and **Auth Token** from your Plivo console. Plivo signs every call with your Auth Token, and that signature is how we tell a real call to your number from somebody pretending to be one — so until we have it, calls to the number are refused. The same keys are what your outgoing calls go out on. (Vobiz needs nothing here to answer calls.) Exactly as it is dialled, including the country code. Set the address shown as the number's **Answer URL** in your carrier's console, with the method set to **POST**. Copy the whole thing — everything after the question mark matters, and a shortened address will not be recognised. For one of the two carriers the address is the same for every number and is shown straight away, so you can set your console up first if you prefer. For the other it carries a key unique to the number, so it appears once the number is added. Either way that address belongs to the number, so it keeps working if you later move the number to a different agent. Back on the **Phone Numbers** page, choose the agent under **Inbound**. **A Plivo number does not answer until its keys are saved.** If you added one before the keys were asked for, its row on the **Phone Numbers** page says so — open the row's menu, choose **Edit credentials**, and save them. Until the Answer URL is set, the number rings and nothing picks up. Nothing on this page can tell you that has happened, because the call never reaches us — so if a test call goes unanswered, check the Answer URL first. A number on any other carrier cannot be pointed at us. We check that each call genuinely came from the carrier it claims to, and a call from anywhere else is refused before it is answered. ### Numbers from Twilio A Twilio number is set up the other way round from the two above. Instead of you pasting an address into your carrier's console, **you give us your Twilio account and we set the number up for you.** The credentials box comes first, because Twilio is the one carrier where it is needed before the number. Both are on your Twilio console's home page. The Account SID starts with **AC**. We check them with Twilio before saving, and the token is stored encrypted and never shown again. For example **+1 415 555 0100**. A number typed without a country code is read as an Indian one, and we will then not find it on your account. We confirm the number is on your Twilio account and point it at us there. The dialog still shows the two addresses we set, in case you ever want to check them or set them yourself. On the **Phone Numbers** page, choose the agent under **Inbound**. Pointing a Twilio number at us **replaces whatever it was set to**. If the number already runs something of yours — an IVR, a forwarding rule — that stops receiving its calls. If it had an address set, the dialog tells you what it was so you can put it back, and removing the number here clears the address we set and does not restore the old one. Import a number that is free to use. We need your Auth Token because Twilio signs every call with it, and that signature is how we tell a real call to your number from somebody pretending to be one. **If you change your Auth Token in Twilio, save the new one** from the number's menu (**Edit credentials**) — until you do, calls to the number are refused and we tell you so. A workspace works with **one Twilio account**. If you save a different Account SID while Twilio numbers from the first account are still added here, we refuse it — those numbers would stop receiving calls. Remove the Twilio numbers first, then connect the other account. Saving a new Auth Token for the *same* account is always fine. If we could not set the number up for you — your Twilio account may have restricted it — the dialog says so and shows what to set: in Twilio, open the number and set **A call comes in** to the Voice URL and **Call status changes** to the status callback, both **HTTP POST**. What a Twilio number does not do yet: **answering-machine detection** (so no voicemail message) and **transferring a call to a person**. Call recording works on it — see [Recording calls on a number you brought](#recording-calls-on-a-number-you-brought). ### Numbers from Telnyx A Telnyx number is set up for you too, and it takes **one API key** rather than an ID and a token. The key comes first, for the same reason as on Twilio: we need it before we can find the number and set it up. Create one under **API Keys** in the Telnyx portal. We check it with Telnyx before saving, and it is stored encrypted and never shown again. Saving it creates an application on your Telnyx account called **ThinnestAI voice agent** — that is what answers your number and places your outgoing calls, so leave it in place. For example **+1 415 555 0100**. A number typed without a country code is read as an Indian one, and we will then not find it on your account. We confirm the number is on your Telnyx account and assign it to that application. Nothing to paste. On the **Phone Numbers** page, choose the agent under **Inbound**. **Telnyx only calls the countries your account allows.** A new Telnyx account allows the United States and Canada. To call India — or anywhere else — add the country to your **outbound voice profile** in the Telnyx portal (Voice → Outbound Voice Profiles) before you place calls; until then Telnyx refuses them, and the call shows as failed here. Telnyx may ask you to verify your account before it allows some countries. Assigning a Telnyx number to our application **replaces whatever it was connected to**. If it was connected to something else of yours, the dialog tells you its name so you can move it back; removing the number here takes it off our application and does not restore the old one. A workspace works with **one Telnyx account**. A key from a different account is refused while numbers from the first are still added here — remove those numbers first. A new key for the *same* account is always fine, and if you had deleted our application in Telnyx, saving a key makes a new one and moves your numbers onto it. If we could not assign the number for you, the dialog says so. In the Telnyx portal, open the number and set its connection to the application called **ThinnestAI voice agent**. When we turn a call to a Telnyx number away — no agent set to answer it, or the workspace out of balance — the caller hears a **busy tone** rather than a sentence. A Telnyx number does what a number from us does: it **records**, it **detects answering machines** (so a campaign can leave a voicemail), and it **transfers a call to a person**. Telnyx charges your Telnyx account a small fee for each call it checks for an answering machine, and the call to the person you transfer to goes on your Telnyx bill like any other call. ### Recording calls on a number you brought With **Record calls** on, phone calls on a number you brought are recorded too, whichever company it is with. Your carrier is not asked to record anything — your agent hears both sides of the call and keeps the recording here, so it plays with its call in [Usage](/workspace/usage), downloads, and is deleted on your plan's schedule exactly like any other recording. ### A transferred call on a number you brought When your agent puts a caller through to a person on a number you brought, **we stop charging at the moment of the transfer.** From then on the two people talk on your carrier's line, on your carrier's bill, with no agent involved. (On a number you took from us the call is billed to its end, because we pay the carrier for those minutes.) ### Calling out from a number you brought Outgoing calls go out through the account the number belongs to — and for your own number, that account is yours, even if your carrier happens to be the same one we buy numbers from. So there is one more step, and only if you want outgoing calls: in the same **Import** panel, add the **Auth ID** and **Auth token** from your carrier's console. (For Plivo, Twilio and Telnyx you have already done this — it came first.) We check them against your carrier before saving, so a wrong pair is refused while you are still looking at the page you copied it from rather than on a call weeks later. The token is stored encrypted and never shown again. You can change them later without re-adding the number: the row's menu on the **Phone Numbers** page has **Edit credentials**. They belong to the carrier account rather than to one number, so saving applies to every number you brought from it. The **Dial-out ready** column on that page says which of your numbers can already place calls and which are still waiting for their credentials — a number can answer perfectly while being unable to ring anybody, and that column is where you would see it. Once they are saved, calling campaigns and the call API work from that number exactly as they do from one taken from us. Answering needs none of this. If the number only ever receives calls, the Answer URL above is the whole setup. A number can answer for **one agent**. Typing a number another agent already answers is refused rather than quietly moved — that is almost always a mistake. Moving one deliberately is a different act: choose the new agent under **Inbound** on the **Phone Numbers** page. Whenever an agent is about to lose a number — moved to another agent, or set back to no agent at all — we ask first and name the agent losing it. Giving a number that nobody answers to an agent for the first time takes nothing from anybody, so that one just happens. **Getting, moving, renaming, releasing and removing numbers need an owner or admin.** Everyone else on the workspace sees the list and what each number is doing, which is usually what they came to check. An agent's **Voice** page shows which number that agent answers, and links here to change it. Getting a number and choosing who answers it both happen on this page, because a number belongs to the workspace rather than to one agent. ## What you can do to a number On the **Phone Numbers** page every row has a menu: * **Edit credentials** — for a number you brought, the API credentials of the carrier account its outgoing calls go through. Saving applies to every number you brought from that account. * **Rename** — a name for your own eyes, so a list of numbers reads as "Bengaluru showroom" rather than six strings ending in different digits. Callers never see it. * **Forward calls** — send every call to this number to another phone, with the agent not answering at all. See [Forwarding a whole line](#forwarding-a-whole-line). * **Move to another workspace…** — only for developers building on the [customers API](/api-reference/customers): hand the number to one of your customers, or back to your own workspace, keeping its digits. See [Moving a number to another workspace](#moving-a-number-to-another-workspace). * **Release** — only on a number you rent from us. See below; it is permanent. * **Remove** — only on a number you brought. We forget it; you keep it. For a Twilio number we also clear the address we set on it, and a Telnyx number is taken off our application, so it stops ringing at us — but only if it still points at us, so anything you have since pointed it at yourself is left alone. You can also search the list, filter it by where the numbers came from, and export what you are looking at as a spreadsheet. ### Forwarding a whole line A number can send **every** call straight to another phone — your front desk, your mobile — without the agent answering. It is meant as a fallback: if an agent is not working, or the number has no agent at all, point it at a person so the number never goes quiet, then clear the forward and the agent answers again. * **It rings for 30 seconds.** If nobody picks up, the caller of a number from us, Plivo or Twilio hears that nobody could take the call, and the line ends. On a number brought from Telnyx the caller gets no spoken message: the line rings out or returns busy, then ends. * **A forwarded call has a time limit.** On a number you took from us it ends when your balance can no longer pay for it, and never later than **20 minutes**, the longest any call can run. On a number you brought it ends after **30 minutes**. * **Who the phone shows as calling.** On a number from Twilio or Telnyx, the original caller's number. On a number from us or from Plivo, your own number — those lines may only present a number on the account. * **It works whether or not an agent answers the number.** If an agent does, the forward replaces it. If none does — no agent attached, the agent deleted, or its **Answer calls** switched off — the forward answers the call itself. Your plan, your call lines and your balance are checked first, as for any call, so a workspace that cannot pay for a call, or has every call line busy, does not forward it: the caller hears that the number is not answering. * **What it costs** is what a call put through to a person costs: on a number you took from us, every minute the caller is connected, at the standard voice rate; on a number you brought, nothing from us — the minutes are on your carrier's bill. * **Not to one of these numbers.** A forward cannot point at the number itself or at another number on the platform, which would send calls round in a loop. * **Not to premium or special numbers.** Premium-rate, shared-cost and toll-free numbers, satellite and international-network numbers, and a short list of high-cost international destinations are refused (`forward_destination_not_allowed`): a forward runs with nobody watching, and those numbers charge by the minute to whoever dials them. An ordinary landline or mobile is always fine. If you have a real need for one of them, tell us. * **Nothing is said or sent after a forwarded call.** The agent never took it, so there is no summary, no extracted details and no follow-up message — you still get the `call.completed` webhook, with `forwardedTo` set, when an agent answers the number. When no agent does there is no agent to send it for, so the call shows in Usage, Get Call and the call list (with no agent) and not as a webhook or an inbox conversation. Get Call and the call list carry `forwardedTo` too, and `seconds` is how long the caller was connected, even on a number you brought where nothing is billed. A forward nobody picked up is reported as `missed`, with a `hangup` of `no_answer`, `busy`, `rejected` or `unreachable`, and `seconds` 0. On a Telnyx number a caller who hangs up while the phone is still ringing may be reported as a completed call instead. * **Not on Vobiz.** A Vobiz number cannot forward calls, so the setting is refused (`number_cannot_forward`). From the console it is **Forward calls** in the number's menu. From the API it is `forwardTo` on [Update Phone Number](/api-reference/phone-numbers/update-phone-number): ```json theme={null} { "forwardTo": "+919812345678" } ``` Send `"forwardTo": null` to stop. A refusal says which rule stopped it in its `code`: `forward_loop` or `number_cannot_forward`. ### Moving a number to another workspace If you build on the [developer API](/api-reference/customers), a number can move between your own workspace and your customers' — **keeping its digits**, so nobody who has it saved needs a new one. **In the console**, open the **Phone Numbers** page, open the number's menu, choose **Move to another workspace…** and pick where it goes: your own workspace or one of your customers. The option is there only for developers, and only on a number that can be moved. A refusal says why (the number is on a call, an Indian number whose new workspace has no approved business details, and so on). **Through the API** (or the `move_phone_number` [MCP tool](/mcp/tools)): ```bash theme={null} curl -X POST https://app.thinnest.ai/api/v1/phone-numbers/918041234567/move \ -H "Authorization: Bearer $KEY" \ -H "Thinnest-Workspace: org_…the customer that has it now…" \ -H "Content-Type: application/json" \ -d '{ "toWorkspace": "org_…the workspace it goes to…" }' ``` Leave the `Thinnest-Workspace` header out when the number is in your own workspace. What happens: * The number stops being any agent's line, and leaves every agent's spare numbers, in the workspace it leaves. * It arrives in the other workspace **answered by nobody**. Point it at an agent there, as with a number you have just rented. * **Rent** carries on from the next month in the new workspace. The month already paid is not charged again — and since your customers are billed to you, no money moves either way. * **An Indian number** is held in the name of the business using it, so the workspace it moves to must have its own business details approved first (see [India needs your business verified first](#india-needs-your-business-verified-first)). The move registers the number to that business. * **A number you brought** moves only when both workspaces have the same carrier account's keys saved. A Telnyx number set up for one workspace cannot move this way: remove it and add it again in the other workspace. * It is refused while the number is on a call, or while the same number is already being moved. * Any call forwarding set on the number is cleared: it was the old workspace's choice. * If the phone network cannot take the move, the number stays where it was, answered by nobody, and you can try again shortly (`number_move_unavailable`). * Repeating a move that already succeeded, to the same workspace, gives the same result, so a lost response is safe to retry. * A few long-held numbers were set up before workspace-level phone accounts existed and cannot be moved this way; write to us for those. Only workspaces of one developer can trade numbers. A key you gave one customer cannot move its numbers anywhere. ## Inbound and outbound are separate Every number has two pickers on the **Phone Numbers** page, and they are independent: | | What it decides | | - | - | | **Inbound** | Which agent **answers** when somebody rings the number. One agent, or nobody. | | **Outbound** | Which agent **calls out** from the number — in campaigns, test calls and the call API. | They can be the same agent, which is the ordinary case, or different ones. Some things that is useful for: * **A receptionist and a caller on one number.** One agent answers; a different one makes the outbound calls, and the people it rings see your usual number. * **A number that only calls out.** Leave **Inbound** empty and choose an agent under **Outbound**. Nobody answers when it is rung, but the agent can place calls from it. * **An agent that only calls out.** It does not need a number of its own at all. Give it a number under **Outbound** and it can run campaigns and test calls straight away — we switch its phone on for you when you do. **Inbound empty means nobody answers.** Whoever rings the number hears that it is not taking calls — including somebody ringing back after a campaign call. A number that is only lent out for calling is never answered by the agent calling from it. If you want a number to answer, choose an agent under **Inbound**. When you let a different agent call out on a number that another agent answers, we ask first, because of exactly that: a customer ringing it back will reach the agent that **answers** it, not the one that rang them. ### Numbers from different phone companies on one agent An agent can call out from numbers on **different phone companies** — a ThinnestAI number and one you imported from Twilio, say — like a phone with two SIM cards. Each call goes out through **its own number's** company, on its own account, and hangs up there too. * **In the Playground**, **Call from** lists every number the agent can call from, each with its company's logo. A number that cannot call yet (one you imported without saving its credentials) is shown but greyed out. * **Campaigns** spread their calls across all of them. * **Some features depend on the company.** Transferring a call to a person and leaving a voicemail work on ThinnestAI numbers and numbers from Plivo or Telnyx, but not on Vobiz or Twilio numbers. Recording works on every number. A call from a number that can't do one of these still goes out — that feature is simply off for it, and the Playground and the campaign builder tell you before you call. One limit to know about: **a number you brought needs its credentials saved** before an agent can call from it, the same as any number of yours that places calls. ## Spare numbers for calling out A calling campaign goes out from your agent's number. That is fine for a few hundred calls and starts to hurt at a few thousand: phone companies and phones themselves watch for a line that makes a burst of calls, and once a number is marked as one, **everything it does afterwards suffers** — including a customer ringing you back and seeing a warning on their screen. Spare numbers spread that out. Give an agent several numbers under **Outbound**, and campaigns share their calls across the agent's own number, if it has one, and the others. Any number on the page — one you bought and have not pointed anywhere, one you brought and have not used yet, or one another agent answers. Under **Outbound**, choose the agent. That is the whole setup. **What it does not do.** A spare does not become the agent's number. Nothing about who answers changes, the number you gave your customers stays the number they ring, and you can take a spare back at any time by choosing the agent that answers it, or **Nobody** if none does. **One person always sees the same number.** Whichever of your numbers rings a customer is the one that rings them again — including a second attempt days later. Three attempts from three different numbers is what a nuisance call looks like, and avoiding that is half the point, so it does not happen. **Each number has a daily limit.** 200 calls a day, which is also the point: without one, spreading calls over four numbers is the same burst four times over. If a campaign uses up every number it says so, and carries on the next morning — or sooner, if you lend it another. A spare can be on a different phone company from the agent's other numbers. A spare you brought needs its carrier credentials saved, the same as any number of yours that places calls. The page says which of these is missing rather than letting you find out on a campaign. Changing who answers a number does not change who calls out on it. Moving or clearing **Inbound** leaves **Outbound** exactly as it was. A spare must be licensed for the same kind of calling as the campaign. In India promotional calls need a 140-series number, so a campaign marked promotional is refused if **any** of the numbers it would use is not one — and it names which. It is the number that carries the penalty. ## Disconnecting, giving up, and removing Three different things and only one of them is permanent. **Disconnect** takes the number off the agent so it stops answering. The number stays yours either way — your own number stays with your carrier, and one you rent from us stays rented and still charged. It shows on the **Phone Numbers** page with nothing under **Inbound**, ready to point at another agent. An agent you had chosen under **Outbound** keeps calling out on it. **Give up** hands a number you rent from us back to the phone company. The monthly charge stops and the number goes back to the pool. **Remove** applies only to a number you brought. It takes our record of it away, so we stop answering it and stop listing it — the line stays yours and your carrier keeps billing you for it exactly as before. Remember to unset its Answer URL in your own console afterwards, or calls to it will still reach us and be declined. Bringing it back later is the same three fields. Releasing is permanent. The number goes back to the phone company and somebody else can take it — we cannot get it back for you, even a minute later. If it is printed anywhere, or anybody has it saved, keep it. ## What callers hear when nothing is set up * **Answer calls off** — the agent does not answer. A caller on a Plivo, Vobiz or Twilio number hears a short spoken message and the call ends; a Telnyx caller hears a busy tone. You can write the message yourself ([When Answer calls is off](/channels/voice#when-answer-calls-is-off)). * **Answer calls on, Phone number switched off** — the same: the agent does not answer. You have switched voice on without telling it to pick up the phone. * **Everything on, no greeting** — the agent waits for the caller to speak first. That is a real choice, and some lines are better that way, but it surprises people who expected a greeting. ## Things worth knowing Two agents cannot both **answer** one number. If you want different behaviour for different callers — sales and support, say — that is one agent with instructions covering both, or two agents on two numbers. Who *calls out* on a number is a separate choice, and an agent can call out on several numbers — see **Inbound and outbound are separate** above. Answering is still one to one. The **Playground** has a **Try it on a real phone** box under the browser call. Type your own number and press **Place call**: the agent rings you from its number, so you hear exactly what a customer hears — a phone line, not a browser. If the agent can call out on more than one number, a **Call from** list lets you choose which, so you can hear a spare as well. It is a real call and costs the same per minute as any other. The browser call beside it is free and instant, which is the one to use while you are editing what the agent says; this one is for hearing how it sounds. When your agent rings somebody, it calls from its own number, or from one you have given it under **Outbound**. That is what they see, and what they will ring back — and it is answered by whichever agent is under **Inbound** on that number, which may not be the one that rang them. See **Inbound and outbound are separate** above. How long those calls ring before they count as not answered is a setting of the agent, not of the number: **Voice page → Ring time** in the console, or `voice.ringSeconds` in the API (and `ringSeconds` on a single call). See [How long it rings](/channels/voice#how-long-it-rings). A normal business number is licensed for service and transactional calls. Promotional calling in India needs a 140-series number, and using an ordinary one for it can get the number blocked. See [Calling campaigns](/channels/voice-campaigns). Ring your own number and ask the hardest question you get — not "what are your hours". The awkward one, with a condition attached, where the honest answer is "it depends". That is the answer worth hearing out loud before a customer does. # SMS Source: https://docs.thinnest.ai/channels/sms Let your agent text customers in India — through your own SMS provider, under your own DLT sender ID. Your agent can send a customer an SMS **while it is talking to them** (a booking link mid-call, an address they asked for) and **after the call ends** (a thank-you with your booking link, or a "sorry we missed you"). See [Actions](/agent/actions#reaching-the-customer-on-a-different-channel) and [After the call](/agent/after-the-call). **SMS goes through your own provider account.** You connect your SMS provider, it sends under your own sender ID, and **it bills you directly**. Nothing for SMS is taken from your wallet here. **SMS reaches Indian mobile numbers only.** Every business SMS in India has to follow the DLT rules below, and those apply to Indian numbers. For customers elsewhere, use [WhatsApp](/channels/whatsapp). ## What DLT asks of you first India's telecom rules (TRAI's DLT) mean an operator only delivers a business SMS that matches what you registered in advance. You register once on any one DLT portal (Jio, Airtel, Vi, BSNL…), and it works on every network. If your business already sends OTPs or order updates, you probably have all of this. | You need | What it is | Where it comes from | | - | - | - | | **Entity (PE) ID** | Your business's registration on DLT, usually 19 digits | Your DLT portal, once your business is approved | | **Sender ID (header)** | The six letters the message shows as coming from, like `ACMEIN` | Registered on your DLT portal | | **Templates** | The exact words of each message, with `{#var#}` wherever the text changes | Registered and approved on your DLT portal; each gets a **DLT template ID** | | **Your provider in your PE–TM chain** | Telling DLT which provider sends for you | Your DLT portal — add your provider's telemarketer ID | **Do the PE–TM chain step, or every message is dropped.** Since 2024 operators reject a message unless your DLT registration names the provider that delivered it. Your provider's support team will give you their telemarketer ID if it is not below. ## Providers you can connect | Provider | You'll need | Templates also need | Telemarketer ID | | - | - | - | - | | **MSG91** | Auth key | MSG91's own template ID (from its dashboard) | `1302157225275643280` | | **Exotel** | Account SID, API key, API token, region (`in` or `sg`) | — | ask Exotel | | **Gupshup Enterprise SMS** | User ID, password | — | ask Gupshup | | **Infobip** | Your API base URL (like `xxxxx.api.infobip.com`), API key | — | `110200001152` | | **Vonage** | API key, API secret | — | ask Vonage | | **Fast2SMS** | API key | Fast2SMS's own message ID (from its DLT manager) | ask Fast2SMS | MSG91 and Fast2SMS send by **their own** copy of each template, so you add the template on their dashboard too and paste their ID here. The others send your template's text along with your DLT IDs. Using a different provider? Tell us which one. We add a provider once we can test its DLT sending API against its own documentation. ## Connect Only owners and admins can change it. Then enter your **DLT sender ID (header)** and your **DLT entity (PE) ID**. Keys are sealed as soon as they arrive and never shown again. To change one field later, leave the secret blank and the saved one is kept. Under **DLT templates**, add each template you want the agent to use: * **Name** — your own, like `booking_link`. The agent sees it, so make it say what the message is for. * **Text** — copied from your DLT portal **exactly**, with `{#var#}` where it varies. * **DLT template ID** — the number your DLT portal gave it. * **Category** — Transactional, or Service (implicit or explicit). Promotional templates can't be sent by an agent. * **Agent may send this during a call or chat** — tick it for the templates you want the agent to be able to use mid-call. New templates start unticked. The **after the call** message doesn't need the tick. * **MSG91 template ID / Fast2SMS message ID** — only with those two providers. The page shows how many blanks the template has and how many SMS it will be sent as. Under **Send a test**, send one template to your own mobile. A success marks the provider **Working**; a refusal shows the provider's reason, so you can fix it before a customer is waiting. ## Writing templates the agent can use well Each `{#var#}` takes **at most 30 characters** when it is sent, so keep the moving parts short: a first name, a date, a short link. ```text Good theme={null} Hi {#var#}, thanks for calling Acme Clinic. Book your visit here: {#var#} ``` ```text Good theme={null} Your appointment at Acme Clinic is on {#var#} at {#var#}. Reply on WhatsApp to change it. ``` ```text Won't work well theme={null} {#var#} ``` A template that is nothing but a blank was never going to pass DLT, and the agent cannot write free text into an SMS anyway. Every message is one of your approved templates, with only the blanks filled in. **Plain English fits 160 characters in one SMS.** Hindi, other Indian scripts and emoji fit 70, so the same message may be sent as two or three SMS, and your provider charges for each one. ## Using it * **During a call or chat** — tick the templates the agent may use, then turn on **Send the customer an SMS** on the agent's **Actions** page. See [Actions](/agent/actions#reaching-the-customer-on-a-different-channel). * **After a call** — choose an SMS template in the **After the call** card on the same page. See [After the call](/agent/after-the-call). Every SMS the agent sends shows up as a note in that conversation, so your team can see what went out. ## When something goes wrong | What you see | Usually means | | - | - | | The provider's test fails with a DLT or template error | The text here doesn't match the DLT template word for word, or the DLT template ID is wrong | | The test succeeds but nothing arrives | Your provider isn't in your PE–TM chain yet, or the sender ID isn't approved for that template | | "SMS can only be sent to an Indian mobile number" | The customer's number is a landline or outside India | | "MSG91 needs the MSG91 template ID" | Add the template on MSG91's dashboard too, and paste its ID | | Can't change a template's text | An agent sends it after calls and fills a different number of blanks. Change that agent's **After the call** first | ## From the API Everything above is also in the API: [SMS](/api-reference/sms/get-sms-provider). ```bash theme={null} curl -X POST https://app.thinnest.ai/api/v1/sms/templates \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "booking_link", "body": "Hi {#var#}, thanks for calling Acme Clinic. Book your visit here: {#var#}", "dltTemplateId": "1207169876543210987", "category": "service_implicit" }' ``` # Telegram Source: https://docs.thinnest.ai/channels/telegram The quickest channel to connect — no approvals, no templates, no waiting. Telegram is the one channel you can connect entirely by yourself, in about two minutes. There is no business verification, no template approval and no 24-hour window. ## Connect Message [@BotFather](https://t.me/BotFather) on Telegram and send `/newbot`. Choose a display name and a username ending in `bot`. BotFather replies with a token that looks like `123456789:AAF-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`. On the agent's **Channels** page, open Telegram and paste it. That token controls the bot completely. Treat it like a password — anyone holding it can read and send everything your bot does. It is sealed on arrival and never shown again. Open your bot in Telegram and press **Start**. The agent answers with the same knowledge and instructions it uses everywhere else. ## What it can do Text, buttons and images. Cards degrade to text with the buttons preserved. Long replies are split across messages rather than truncated — Telegram rejects a message over 4,096 characters outright, so splitting is the only behaviour that does not silently lose the end of an answer. ## Opting out A customer who asks the agent to stop is recorded as withdrawn, exactly as on WhatsApp, and the agent stops replying to them. ## Two things it does not do **Telegram does not join a customer's identity.** There is no phone number unless the user volunteers a contact card, and matching on display name would merge two different people called Priya into one memory. Telegram threads are read once a customer has been linked by other means — they never do the linking. **Broadcasts do not send on Telegram.** A bot may message anyone who has pressed Start, so this is buildable; it is simply not built. Only WhatsApp carries a campaign today. ## Why start here If you are evaluating the platform and do not yet have WhatsApp connected, Telegram gives you a real, external channel to test against in minutes — a proper end-to-end run rather than the Playground. ## Disconnecting Disconnecting the bot stops it reaching you and frees the connection, so you can put a different bot on the same agent afterwards. **Your Telegram conversations and their messages stay.** Disconnecting removes the connection, not the history — the transcripts remain in your [inbox](/workspace/inbox) and in the record of what your customers were told. # Voice Source: https://docs.thinnest.ai/channels/voice Your agent answers the phone — with the same memory of each customer it has everywhere else. Voice is your agent picking up a call and holding a conversation out loud. It listens, thinks and speaks, and the person on the line can interrupt it. It is the same agent. The same knowledge, the same instructions, the same memory of the same customer. Somebody who messaged you on WhatsApp last week and rings you today is one person with one history. Voice has its own page in the agent sidebar, under **Deploy** — not a tile on the Channels page. Everything about it lives on that one screen. ## Turn it on In the agent sidebar, choose **Voice**. Off means the number does not answer at all. Nothing on the page runs until this is on. You can set everything up first and turn it on last. The settings are never greyed out, because the worst version of this is a real caller reaching a half-configured agent. Open **Website** and switch on **Answer here**, and your agent answers calls from the chat widget straight away — no number needed, on any plan. To answer a number people dial, go to **Phone Numbers** in the main sidebar — the Voice page links there too. Take one from us — that needs a paid plan — or bring one you own with Plivo, Vobiz, Twilio or Telnyx, which works on every plan including the free one. A Plivo or Twilio number also needs your account's Auth ID and Auth Token before it answers, and a Telnyx number your account's API key. Then choose this agent beside the number, and it starts answering. See [Phone numbers](/channels/phone-numbers). Ring the number. You should hear your greeting, then the agent. Through the API: `voice.surfaces`, a list of `"web"`, `"whatsapp"` and `"phone"`. A new agent starts with `["web", "whatsapp"]`, and `phone` is added when a number is attached. Adding `phone` while the agent answers calls and has no number is refused (`phone_number_required`); with calls off you can set it first; while a number is attached, `phone` stays on — to stop it ringing, switch off `answersCalls` or take the number off. Whether a WhatsApp number takes calls is its own **Calls** switch on Connect WhatsApp. ## How the Voice page is laid out The settings are in seven sections, with links along the top that jump to each: | Section | What is in it | | - | - | | **Calls** | Whether the agent answers, the message callers hear when it is off, and where it answers | | **How Agent speaks** | The rules it speaks by, and the lines it says while it works something out | | **Listening** | Noise cancellation, how it decides a caller has finished, and when a caller can cut in | | **Sound and recording** | Background sound and recording | | **Ending calls** | Voicemail, do-not-call, how long an outbound call rings, hanging up when done | | **Memory** | What it remembers from earlier conversations, and the summary after each call | | **Call length and silence** | The longest call, and what happens when nobody speaks | **Save changes** stays at the bottom of the screen however far down you are, and what you save applies from the next call. On a wide screen **Test a call** sits beside the settings and stays in view while you scroll, so you can change something, call, and listen again; on a narrower screen it comes first, above them. ## The settings ### Replies use Which model does the thinking. Leave this on the recommendation unless you have a reason to change it. The list here is deliberately shorter than the one for chat. A model is only offered if it can answer fast enough to hold a conversation — one that pauses to think reads as a dropped call, not as a careful answer. Your chat model is not used on calls. Voice is its own choice, because what makes a good writer and what makes a good speaker are not the same thing. ### Voice Which voice your customers hear. Press play on any of them to listen — reading the name tells you very little, and the sample is the whole point. Voices come in three tiers, on their own tabs: The everyday voices, and the cheapest per minute. Natural enough that most callers never think about it. Warmer and more expressive, at a higher per-minute rate — and what a new agent starts on (Kavya). Worth it on a line where the voice is the first thing people judge you by. The largest catalogue we offer — hundreds of voices — plus [your own cloned ones](/channels/voice-clone). Highest per-minute rate, on the **Pro plan and above**. **The per-minute price sits on each tab**, so you can see what a choice costs before you make it, and again beside **Voice** once you have chosen. A call is billed at the rate of the voice answering it. **Standard and Premium are open on every plan, including the free trial.** Try a Premium voice on your first day if you want to — nothing is held back, and the only difference is the per-minute rate shown on the tab. The Studio tab is shown on every plan, and one voice on it — a professional customer-support voice — can be **previewed on any plan** so you can hear what the tier sounds like. Putting any Studio voice on your agent, including that one, needs the **Pro plan or above**. Below Pro the tab is visible with an upgrade button over the rest of it, rather than hidden — you can see what you would be buying before you buy it. On the Pro plan and above, record a short sample and your agent can speak in it — a founder's own voice, or someone from your team. Up to 10 on Pro and 20 on Scale. Full details on its own page. If your workspace uses its own voice account, the voice comes from that account instead, and a Cartesia voice can be tuned per agent: speed, volume, emotion, the Sonic model and a pronunciation dictionary. See [Tune a Cartesia voice](/api-reference/bring-your-own-keys#tune-a-cartesia-voice). #### Speaking speed How fast the agent talks, set on the agent's **Playground**, right under the voice, with a **slider** from 0.7× (slower) to 1.3× (faster); the middle is **Normal**, the voice's own pace, and **Reset to Normal** puts it back. If callers say the agent rushes, or many of them are listening in a second language or on a poor phone line, a little under Normal (0.9×) is usually enough — it changes the pace, not the voice. Most Standard and Premium voices follow it. Studio voices, cloned voices and voices from your own voice account keep their own pace, and the setting has no effect on them. Through the API: `voice.speakingSpeed` (0.7 to 1.3, as a multiple of the voice's own pace; `null` for normal). Every voice speaks every language we support. The tier changes how a voice *sounds*, never what it can say — which language it uses comes from your agent's setting, covered below. Through the API, `PATCH /api/v1/agents/{id}` with `"voice": { "voice": null }` puts the agent back on the voice a new agent starts with (currently Kavya, a Premium voice), and `"model": null` puts it back on the default model (Prana \[Voice]). An empty string is refused: `null` is the way back. ### How Agent should speak The rules the agent follows on calls, and only on calls. Your chat and WhatsApp replies are untouched. It comes filled in, so you are editing real sentences rather than facing an empty box. Change what you like; clear the box entirely to go back to the supplied wording. Two of the supplied lines are not style, and are worth keeping. **Write titles and abbreviations in full.** Speech is produced one sentence at a time, and a full stop inside a name ends a sentence early — an agent that writes "Dr. Iyer" can be heard stopping halfway through the name. **No brackets or bullet points.** Nothing pronounces a bracket. An aside in brackets is either read out as though it were the main sentence or lost. If your agent writes one anyway, such as `[pause]` or `(waiting for the caller)`, it is left out of what is spoken and of the transcript. Only short notes in square brackets are left out: a figure such as `[1]` or `[Rs 500]` is kept, and a link written as `[label](address)` is spoken as its label. It replaces the supplied wording rather than adding to it, and on a call it is read after your agent's own instructions. Up to 4,000 characters, counted separately from the agent's instructions; a longer box is refused, never cut. Through the API: `voice.callInstructions` (a string, or `null` to go back to the supplied wording). The name must be exactly that; `voice.instruction` is refused. ### Opening line The first thing a caller hears, in your agent's own voice, the moment the call connects. It is set on the agent's **Playground**, under **What Agent says**, and it is also the first message in the chat. Leave it empty and the agent waits to be spoken to. Keep it short. It is spoken, and a caller who hears a paragraph before they can speak will talk over it. ### Earlier conversations What happens when somebody who has spoken with your agent before calls again, or is called — whether that was an earlier call from the same number, or a chat on WhatsApp or your website. * **Remember quietly** — the default. Your agent knows what was said before and uses it, without mentioning it. Nobody has to repeat themselves. * **Remember and recap** — your agent opens by saying you have spoken before and what it was about, then asks whether they would like to carry on or talk about something else. On a call you placed, it answers what they say first and recaps right after. * **Start fresh** — every call starts clean. Your agent is given nothing from earlier calls or other channels, and never greets anyone as a returning caller. A [call back](#booking-a-call-back) the customer asked for is the exception: it always carries on from the conversation they booked it in, whichever you choose, and its opening line already says why it is calling, so it is never recapped on top. The recap is in your agent's own words and in the language of the call — there is nothing to write. It names what you talked about, never what they told you: no amounts, dates or addresses until they bring them up, because anyone can pick up a phone. A WhatsApp conversation that is kept hidden until the caller is confirmed (the **Remembering the customer across channels** setting on the Actions page, see [how two channels become one person](/concepts#how-two-channels-become-one-person)) stays hidden — your agent only says you have been in touch there. It only happens when there is something to recap, and only once per call. A first-time caller is greeted as one. ### Who is calling When somebody rings from a number that is in your **Contacts** — one you imported, or sent through the API — your agent is told their name. It can use it in the greeting and the instructions as `{{name}}` and `{{first_name}}` (and `{{phone}}`), the same placeholders a campaign fills, and it is told in a sentence that the caller is probably that person. *Probably*, because the number a call arrives from can be faked. So the agent is asked to confirm who it is talking to before discussing their account, the caller's **email is never given to the agent**, and the call is kept on its own line in your inbox rather than being filed into the contact's record. If the agent is set to remember people across channels only once they are verified, it is not given the name either. On a call your agent **places** — through the API, a campaign or a call back — none of that caution is needed: you chose the number. The call is filed on the contact, and their name, phone and email fill the placeholders. Anything you send in a call's own `variables` wins over the contact's fields. ### Who-is-calling lookup Give this an https address of your own and, when somebody rings this agent, we ask your system who it is **before the agent answers** — so you can hand it the caller's name, their last order, their plan, whatever your CRM knows. It is the inbound counterpart of the `variables` you send when you place a call. We `POST` this, signed exactly like a [webhook](/api-reference/webhooks): ```json theme={null} { "event": "call.started", "callId": "CA0c7c…", "surface": "phone", "agentId": "ag_…", "from": "+919876543210", "to": "+918000000000", "direction": "inbound", "contact": { "id": "cust_…", "name": "Asha Menon", "phone": "+919876543210", "email": "asha@example.com", "externalId": "CRM-1183" }, "sentAt": "2026-10-09T10:30:00.000Z" } ``` `surface` is `phone` or `whatsapp`. `to` is the agent's voice line, on a WhatsApp call too. `contact` is the person in your Contacts with the caller's number, or `null`. Answer within **two seconds** with: ```json theme={null} { "variables": { "name": "Asha", "plan": "Gold", "last_order": "A-1183 delivered Tuesday" } } ``` Up to 20 values, each cut to 150 characters, names lower-cased with spaces turned into `_` — the same rules as a call's `variables`. They fill `{{plan}}`, `{{last_order}}` and the rest in the greeting and the agent's instructions, and they win over the contact's own fields. If your system does not answer in two seconds, answers with an error, or sends anything else, the call goes ahead without them. We never retry it, and the caller only ever hears a little more ringing. A phone provider that repeats its own request to us (it does, when it did not get our answer in time) asks your system again **with the same `callId`**, so treat `callId` as the key and ignore one you have already answered. It is asked only for a call your agent is going to answer. A number you have set to forward calls to a person is not announced to your system. Set it on the agent's **Actions** page, in the **Who-is-calling lookup** card, or with `voice.callStartUrl` on [Update Agent](/api-reference/agents/update-agent). Saving a new address shows a **signing secret once** — copy it into your receiver then, it is not shown again. Verify `x-thinnest-signature-v2` with it as you would a webhook. Clearing the address switches the lookup off. To replace the secret without changing the address, press **New secret** on the Actions page, or send `"callStartSecret": "rotate"` in `voice`. The old secret stops working at once, and the new one is shown once. The address must be https, must not carry a username or password, and must not point inside a private network. If signing is not available at that moment the API answers `503` with the code `signing_unavailable` and saves nothing; try again shortly. **Duplicating an agent does not copy this address.** The copy gets no signing secret of its own, so it opens with the lookup switched off and the Agents page tells you to set it again on the copy. Do that on the copy's Voice page, and it gets its own secret. The lookup runs for phone and WhatsApp calls, and only on inbound calls. Treat what it is sent like caller ID: `from` is the number the call claims to come from. Before your system hands the agent anything sensitive, decide whether a matching number is enough. ### Fillers before tool run On, and worth leaving on. Checking your documents or calling one of your tools takes a second or two, and this is what the caller hears instead of silence. Leave **What Agent says** blank and the agent says it in whichever language it is speaking — if you have not fixed a language, it works that out from the caller, so somebody who rings you in Tamil is answered in Tamil from the first word. Fill it in and it says exactly that, every time, in whatever language you wrote it. Keep it to a few words: it is covering a pause of a second or two, and something longer just leaves a new silence after it. Turn it off if you would rather the line stayed quiet. You can write up to eight lines, one per line, each up to 60 characters. The agent picks among them, and only uses lines written in the caller's script — so a Hindi line is said on Hindi calls and an English line on English ones. Words of an Indian language typed in Roman letters are converted to that language's script when you save. Through the API: `voice.thinking` (on or off, on unless set) and `voice.thinkingLines` (a list of up to 8 lines of 60 characters; `null` or `[]` for the built-in lines in the caller's language). The name must be exactly that; `voice.thinkingPhrase` is refused. #### When it speaks When your agent decides to look something up or run one of your tools, it says its line **straight away**, while the lookup runs, rather than going quiet first. If the answer still has not started a little later, it says one more line, then waits for the answer. It never says more than that for one question. With **Fillers before tool run** off, the agent says nothing extra while it works, however long the lookup takes. Two boxes under it set the timing. Leave either blank to use the standard. | Setting | What it controls | Standard | Range | | - | - | - | - | | **How long Agent waits first** | Quiet time before the agent speaks on a slow reply | 900 ms | 200–5,000 ms | | **How long before Agent says another** | The gap before a second line, and how long a lookup that is still running waits for it | 2,500 ms | 1,000–10,000 ms | Set the second one longer if your lines take a while to say, so the second never lands on top of the first. A value outside the range is pulled back into it when you save. Through the API: `voice.thinkingDelayMs` and `voice.thinkingRepeatMs` (whole milliseconds; `null` for the standard). The API refuses a value outside the range instead of pulling it in. #### Fillers after the customer stops speaking Off unless you turn it on. When on, the agent says a short acknowledgement ("Okay.") the moment the caller stops, before **every** reply, rather than only when a reply is slow. It suits a slower model; on a quick one it puts a syllable in front of everything. Through the API: `voice.thinkingAtTurnEnd` (`false` unless set). ## What language it speaks Voice follows the language on your agent — set it on the **Playground** or **Settings** page. Set the agent to Marathi and it answers calls in Marathi **only**, and the speech recogniser listens for Marathi too. Leave it on **\[Auto] Match the customer** and it works out what they are speaking and answers in that. The recogniser then has to decide the language afresh for every sentence, so a short or mixed reply can be heard as the wrong one. On a line where people open in English, Hindi or a mix of both, this is usually the right setting; if your callers all use one language, fix it. An agent has one language. There is no "second language" setting: to serve two, leave the agent on **Match the customer**. ## Where calls appear Each call is its own conversation in the inbox, with its transcript, alongside that customer's chats. One customer record, one memory, one history. A call is genuinely a different thing from a message thread — it has a start, an end and a duration — so it reads as its own entry rather than being appended to a chat. ### The call log **Usage → Calls** lists every call the workspace has made or taken, newest first, with the transcript and the recording behind each row. Each one says how it actually ended rather than just whether it finished: | Outcome | What happened | | - | - | | Answered | A person picked up | | No answer | It rang out — the commonest by far, and what retries are for | | Line busy | Engaged. Worth trying later; they are near their phone | | Declined | They pressed decline. Never called again automatically | | Not reachable | No such number, barred, or out of service | | Answering machine | Voicemail took it — see [Hang up on voicemail](#hang-up-on-voicemail) | | Ended before the agent spoke | Something picked up, but the call ended before your agent started — usually somebody who hung up while the voicemail check was listening, or a caller who put the phone down straight away | On an outbound call the row also shows **which of your numbers placed it**, which is the question worth asking once an agent has spare numbers to call from. Export gives you all of it as a spreadsheet. A lot of *Not reachable* usually means a stale list. A lot of *Declined* usually means the wrong audience or the wrong opening. A lot of *No answer* is usually the wrong time of day. That is the whole reason the reason is shown. Open a call to see **How the call went** — how long the caller waited for each reply, silences, interruptions, and any turns the agent could not answer or that a backup answered — and **Problems on this call**, if anything went wrong while it was live. The same figures come through the API as `quality` on [Get Call](/api-reference/calls/get-call), and each problem is sent as a `call.problem` [webhook](/api-reference/webhooks#problems-on-a-live-call) while the call is still going on. See [Usage](/workspace/usage#how-a-call-went). ### Call summaries Off unless you turn it on, under **Summarise each call** on the Voice page. With it on, two or three sentences are written once a call ends — what the customer said and how it finished — and they appear as a **Summary** tab on the call in **Usage → Calls**, beside the recording and the transcript. The transcript was always there and always free. What this buys is the time: forty calls to get through is forty transcripts to open, or forty pairs of sentences to skim. It is written from the transcript afterwards by a small model, so it is a reading of the call rather than a record of it. The transcript is what was actually said, and it is still there underneath. **A summary covers one call, not the customer.** Somebody rung three times has three calls in the log and three separate summaries. The whole history with that person is the conversation in the inbox. If your agent writes into a [spreadsheet](/agent/spreadsheet) and you have made a column called **Summary** — or Notes, or Remarks — the same switch fills that too. Nothing is summarised anywhere while it is off, including into that column: making the column does not start it. ## Your knowledge on calls Your agent searches your knowledge base on calls, the same as it does in chat. Both are on by default. They are **two switches**, on the **Actions** page, because looking something up costs a little more on a call than on a page — the agent has to find the answer before it can start speaking, so the caller hears a short pause. It is a pause, not a wait, and for most businesses being right is worth it. Turn the calls one off if you want the quickest possible line and your callers mostly ask things your instructions already cover. Changing one switch does not change the other. **The agent says something while it looks.** When it has to check your documents — or reach any of your connected tools — it says a short "one moment" first rather than going quiet. A caller cannot see an agent thinking, and a few seconds of silence on a phone line reads as a dropped call. It is on to begin with, and it is yours to change under **Fillers Before Tool Run** on the Voice page. ## Booking a call back "Can you call me after five?" — the agent books it, and your number rings them at that time. It works on calls and in WhatsApp chats, because that is where people ask. Say it however you like: *in half an hour*, *after five*, *tomorrow morning*. The agent repeats back the time it booked, so nobody is left guessing. **It only promises what it can keep.** If it cannot book one it says so plainly instead — and there are a few reasons it might: | Why | What the agent says | | - | - | | The agent has no number that can dial out | It cannot arrange a call and offers to help now | | We have no number for that person | It asks for the best number to reach them on | | Sooner than five minutes away | Too soon — a redial is not a callback | | Further ahead than two weeks | Too far ahead to promise | | Three already booked for them | It confirms the ones already booked instead | **Outside your calling hours, it moves the time and says so.** "Call me at eleven tonight" becomes nine tomorrow morning, out loud, on the call — never silently, because somebody waiting by the phone at eleven is worse than being told. The hours are checked again when the call is actually placed, so a callback held up for any reason still cannot ring at a rude hour. The booking appears on the conversation in your inbox, and when the call happens it lands in that same thread — the request and the call it produced, in one place. Everything still waiting is listed together on [Voice Campaigns](/channels/voice-campaigns), with who, when, and what the agent said it would call about — cancel one there if it is no longer needed, any time before it goes out. Somebody who asks you to stop contacting them between booking and the call is not rung. Switch it off per agent under [Actions](/agent/actions) if you would rather your team called people back themselves. With it off, the agent tells anybody who asks that it cannot book one rather than promising a call nobody will make. ## "Don't call me again" When a caller asks not to be called again — "don't call me", "remove my number", in any language — your agent puts their number on your **do-not-call list** itself, tells them they will not be called again, and may then say goodbye and end the call. It does this only when the caller clearly asks that you not call them again. It does not do it when someone just wants to end this call ("that's all", "you can hang up", "thank you, bye"), and not for "not now", "call me later", "I'm busy" or "wrong number"; those are about this moment, not about every future call. When it is not sure what the caller means, it is told to ask "Would you like us not to call you again?" first. From then on **no outbound call rings that number**: a call placed by the API is refused, a batch refuses that entry, a calling campaign skips the person and records why, and a call back already booked is given up. If they ring you, the agent answers as usual. The number is the caller's own caller ID, so this works on **phone calls and WhatsApp calls** where that number is known. On a web call, or when the caller withholds their number, the agent says honestly that it could not note it here and tells them to ask the business directly to be removed from its calling list. The list is the one on the **Contacts** page under *Do not contact*. An entry the agent made reads *Asked on a call*, with what the caller said; remove it there (an admin) or with the API if the person later asks to hear from you again. A developer can also read, add and remove numbers with the [do-not-call API](/api-reference/do-not-call/list-do-not-call-numbers), and hear about each new one through the `contact.opted_out` [webhook event](/api-reference/webhooks). This works on every voice agent, whatever else is switched on. It is separate from WhatsApp marketing consent. To take this away from one agent, switch off **Do-not-call list** on its Voice page, or send `voice.recordDoNotCall: false` to the [agent API](/api-reference/agents/update-agent). The agent then never adds anyone to the list itself; everything you add by hand or with the API still works. It is on by default. ## Ending the call Your agent can hang up. It does so when the conversation is genuinely finished — after it has said goodbye and there is nothing left to do — and after it has told somebody with an emergency to hang up and call their local emergency number. **The closing line is always spoken first.** The call ends when the agent has finished talking, not part way through. It is deliberately reluctant. It will not hang up because somebody went quiet, because it could not help, or because the caller sounds annoyed — a person thinking, hunting for an order number, or talking to somebody else in the room is still on the call. If it cannot help, it says so and offers to have a person follow up, and stays on the line. Every voice agent decides this for itself, and the call record shows a short note saying why the call ended. If your agent says a goodbye and forgets to end the call, we end it for it. It recognises goodbyes in English, Hindi, Telugu, Tamil, Kannada, Malayalam, Bengali and Marathi. A plain "okay" or "thank you" never counts as one. ### Releasing the line **Hang up when the conversation ends**, on the Voice page, starts **off** on a new agent, whether you create it in the console or through the API. Switch it on when you want the line released as soon as the agent is done. An agent made earlier that has never had this setting saved still hangs up when its conversation ends, and the API reports it as `true`. It exists because a finished conversation and a finished *call* are not the same thing. The agent saying goodbye does not put the phone down — somebody has to, and if it is not us it is the caller. Most callers do. The ones who set the handset on a desk and walk away leave a line open, and **you are charged for every minute of it**. With it off, the caller is the one who ends every call: if they never hang up, the call stays connected and **you keep being charged until it ends**. Leave it off only if you want the caller to be the one who ends every call. Through the API: `voice.endCallOnFinish` (`true` or `false`). **On a number you brought yourself, this needs your carrier keys.** Ending a call is an instruction to your carrier, and your carrier will only take it from an account it recognises. Without those keys your number answers perfectly and we cannot hang up — the line stays open until the caller does it. Add them under **Bring your own number** and the switch starts working. Numbers you rent from us are unaffected. We already hold the account. ### Your own limits Under **When the call ends** on the Voice page. Leave any of them empty and that rule is off — none of them is on to begin with. Through the API: | Field | What it is | Unless set | Range | | - | - | - | - | | `voice.reaskAfterSeconds` | After the agent asks a question and hears nothing: how long before it asks the question again, once | `null` (2.5 s) | `0` (never) or 1.5–10 s | | `voice.nudgeAfterSeconds` | Silence before the agent asks whether the caller is still there | `null` (never asks) | 5–180 s | | `voice.nudges` | How many times it asks | 1 | 1–5 | | `voice.nudgePhrase` | What it asks | `null` ("Are you still there?" in the caller's language) | up to 120 characters | | `voice.endAfterSilenceSeconds` | Silence after which the call ends | `null` | 5–180 s | | `voice.endPhrases` | Phrases that end the call. They are kept in lower case without punctuation, and a phrase with fewer than 2 characters left after that is refused | `[]` | up to 12, each 2–60 characters | When the agent also asks, the call ends no earlier than `nudgeAfterSeconds × (nudges + 1)`, so the caller always has time to answer the last question. Reading the agent back shows `endAfterSilenceSeconds` as you set it, even when a call runs longer because of this. A value outside a range is refused. The agent starts wrapping up before your limit and ends the call at it, so a long call finishes with a sentence rather than a dead line. Calls stop at twenty minutes whatever you set. When the agent ends on a question — "Can I take two minutes of your time?" — and hears nothing back, it asks that question again after **2.5 seconds**, once, in its own words and language. A soft or distant answer can slip under the microphone, and a caller who was not listening gets a second chance. It never asks over a caller who is still making a sound, never repeats its own repeat, and never repeats "Are you still there?". Change the wait under **If a question gets no answer, ask again after**: 1.5 to 10 seconds, or 0 to turn it off. The "Are you still there?" timer below carries on as before. Two settings, and the pair is the point. A twenty-second pause is usually somebody finding an order number, so the agent **asks** before it gives up: "Are you still there?", up to five times, and only then does the call end. Write your own wording or leave it blank, in which case it asks in whichever language it is speaking. **The count starts again the moment the caller says anything**, so a long call with several natural pauses in it never creeps towards hanging up. Ten to twenty seconds suits most support lines. Give people longer where they are working something out — and remember two or three of those seconds are audio still being processed, so five is really nearer eight. Leave **End the call after** empty and the call ends once the **last ask goes unanswered** — the agent never sits on a silent line after it has stopped asking. With neither set, a call where **nobody says anything for five minutes** is ended anyway, so a line somebody walked away from does not run on to the twenty-minute limit. Your own phrases, separated by commas — "bye", "that's all", "thank you goodbye". The agent still says its closing line first. **Short words match inside longer ones.** "bye" would also end a call on "goodbye for now, but first…", so prefer whole phrases. Matched in any language, ignoring capitals and punctuation, anywhere in what the caller says. Up to twelve phrases, each 2 to 60 characters. ### Handing a call on **The agent can put a caller through to a person — on a phone call.** On the Actions page, turn on **Hand over to a person**, choose **Call transfer** and give the number (with its country code). When a caller asks for a person, the agent says a line you choose and connects them; the number rings for 30 seconds, and if nobody answers the caller is told the team will get back to them. It transfers on **phone calls** over a number we rent you, or a Plivo or Telnyx number you brought; on web and WhatsApp calls, and on carriers that cannot transfer, it hands over as a **chat** transfer (your team is told by email, Slack or webhook and the agent says somebody will follow up). Through the API: [`PATCH /agents/{id}/tools`](/api-reference/tools/update-built-in-tools) with `handOver` and `tools.escalate_to_human`. Details and billing: [Phone numbers](/channels/phone-numbers). **Cold or warm.** Under **Call transfer**, choose how the person is reached: * **Cold** (the default): the phone rings and the caller is connected as soon as someone answers. * **Warm**: the person who answers first hears a short spoken summary of the call — who is calling and what they need — and only then is the caller connected. The caller keeps hearing the phone ring meanwhile. The agent writes the summary as it hands over; if it writes none, the summary is made from the caller's last few sentences. It is read in the agent's language when the agent is set to one we can speak (Hindi, Bengali, Gujarati, Kannada, Malayalam, Marathi, Odia, Punjabi, Tamil, Telugu or English), otherwise in English. With **Warm** you also choose: * **Press 1 to accept**: after the summary the person hears "Press 1 to take this call", and the caller is connected only when they do. If they do not press it in time, it counts as not answered and the caller is told the team will get back to them — so a voicemail or an automatic attendant never takes the call. * **Connect after the summary**: the caller is connected as soon as the summary has played. **Press 1 to accept works on numbers we rent you and on Plivo numbers you brought.** On a Telnyx number the person hears the summary and the call connects; saving **Press 1 to accept** on an agent that answers on a Telnyx number is refused with that reason. Through the API: `handOver.transferMode` (`cold` or `warm`) and `handOver.acceptKey` (`true` or `false`). **The agent can also pass a call to another of your agents — sales to support, say.** On the Actions page, under **Hand calls to another agent**, add the agents it may pass calls to and say when, in a sentence ("the caller has a problem with an order they already placed"). Each must answer calls (voice switched on). When the caller needs one of them, the agent says it is passing them over, and the other agent takes the call — its own instructions, knowledge, actions and model — on the **same line**, in the **same conversation**. It works on phone and website calls. The **voice stays the same** for the whole call. Each agent on the list is handed over **warm** or **cold**: * **Warm** (the default): the other agent gets a short summary from the first one and the conversation so far. Its first reply opens with one sentence saying who it is and showing it knows why the caller is there, and it does not ask again for what the caller already said. * **Cold**: the other agent starts fresh. It does not see the conversation before the hand-off, and greets the caller straight away with its own opening line, as on a new call. Either way your team sees the whole call as one conversation. * Up to **3** hand-offs per call, and never back to an agent already on it. * The call is billed once, as one call, at the rate it started at. If the other agent would change that — it is set to run on different keys than the call started on (your own keys versus ours), or it uses a premium model on a call that started at the standard rate — the agent keeps the call instead of passing it. * The hand-off is noted in the conversation for your team, and listed as `handedTo` on [Get Call](/api-reference/calls/get-call) and `call.analysed`, with its `mode` (`warm` or `cold`) and, when warm, the `summary` the other agent was given. It is not a hand-over to a person, so it does not send `conversation.escalated`. Through the API: [`PATCH /agents/{id}/tools`](/api-reference/tools/update-built-in-tools) with `handOver.agents: [{ "agent": "ag_…", "when": "…", "mode": "warm" }]` — it replaces the list; `[]` clears it. Leave `mode` out and an agent already on the list keeps its setting; a new one is warm. Saving a list that names an agent whose voice is off is refused until that agent is removed from the list. An agent that turns its voice off after it was added stays on the list and is skipped on calls. ## Recording calls Off unless you turn it on, under **Record calls** on the Voice page. With it on, the calls this agent answers or places are recorded — on your website and by phone — and each recording sits with its call in [Usage](/workspace/usage). **Website calls** are recorded on every agent that has it on. A caller sees *This call is recorded* as the call connects, before anybody speaks. **Phone calls** are recorded on every number — one you took from us, and one you brought from any phone company. On a number you brought, your agent keeps the recording itself, so nothing has to be switched on at your carrier — see [Phone numbers](/channels/phone-numbers#recording-calls-on-a-number-you-brought). **If a caller asks whether the call is recorded, the agent tells the truth** — yes when Record calls is on, no when it is off — in the caller's language. Your call rules cannot change that answer. It says "recorded" only on phone and web calls with Record calls on, never on a WhatsApp call, which is not recorded. **Recordings are deleted when your plan says** — after 30 days on pay-as-you-go, 49 on Pro and 75 on Scale. Download anything you need to keep before then. Once it is deleted it is gone, and we cannot get it back. Erasing a contact deletes their recordings along with their calls — the ones we store, the phone provider's copy on a number you took from us, and any copy made for model training — and a deletion that cannot finish at that moment is retried every night. See [Your data: erasure and training](/workspace/data-and-erasure). **Recordings and transcripts are never used for training on Pro and above.** On pay-as-you-go they may be, and a workspace can be excluded on request. Telling people they are being recorded is your responsibility. On a website call we show the line above for you; on a phone call, in most places it has to be said at the start of the call, and the wording that satisfies your regulator is not something we can write for you — put it in your agent's first words. ## Interrupting A caller can talk over the agent and it stops. This is how people actually use phones, and an agent that finishes its sentence regardless sounds like a recording. ### Words needed to cut the agent off **Three by default.** The noises people make while they are listening — "okay", "achha", "mm", "right" — are not attempts to interrupt, and an agent that stops dead at every one of them is exhausting to talk to. Somebody agreeing with you should not be treated as somebody cutting in. Raising this never makes the agent slower to answer. The count applies only while it is speaking; the moment it falls quiet, a single word starts your caller's turn as it always did. The trade is worth knowing. At three, a caller who says a single **"no"** or **"stop"** while the agent is talking is not heard — the agent finishes its sentence. In practice somebody trying to interrupt keeps going, and "no, stop there" gets through on the third word. But a one-word command does not. Lower it to **one** if that matters more to you than backchannels — a line people ring when something has gone wrong, where the first thing out of their mouth should stop the agent dead. The cost is that "okay" and "achha" stop it too. Raise it if callers keep getting cut off mid-thought while the agent talks. Through the API: `voice.interruptWords` (1 to 6; `null` for three). **On a website call, the agent stops while the caller is still talking.** Once somebody has talked over it for about as long as it takes to say that many words, it stops right there, rather than finishing its sentence and stopping only when they do. Anything shorter — a hum, a quick "okay" — still changes nothing. If what stopped it turns out not to be words at all (a television, someone else in the room), the agent asks the caller to go on rather than waiting in silence. On a phone call the agent stops as soon as the caller's words are heard, which is just after they finish. **With Ear, the agent can stop while the caller is still talking, on the phone too.** When **How agent knows the caller has finished** is set to **Ear**, the speech recogniser listens while the caller speaks, and the agent stops as soon as it has heard that many of their words, or a clear "wait" or "stop", usually a second or two into their sentence. It ignores its own voice coming back down the line, so it does not cut itself off, and a hum or "haan ji" still changes nothing. When the recogniser cannot make out the words that early, the agent stops just after the caller finishes, as it does in the other modes. However it is stopped, the agent goes quiet at that moment. On some voices it used to carry on for up to half a second first. When a caller cuts the agent off, the agent knows its last sentence was left unfinished: it answers the caller first, and goes back to the unfinished part only if it still matters. The transcript is corrected to the part the caller actually heard whenever that can be worked out; in rare cases the transcript, and a summary made from it, may still show the full reply. ### Never interrupt the agent Off unless you turn it on. When on, nothing the caller says cuts the agent off, on any line, however long — for a disclosure, terms or a price that must be read out in full. It still answers a single word once it has finished speaking, so it is never slower to reply. It covers the greeting too, which makes **Let the greeting finish** unnecessary while it is on. Through the API: `voice.neverInterrupt`. ### Answer what was said over the agent On unless you turn it off. Words said while the agent is talking, too few to cut it off, are kept and answered as soon as it stops, so nothing the caller said is lost. Off throws them away — and if that was all the caller said, the agent finishes and waits in silence. Through the API: `voice.holdShortSpeech`. ### Let the greeting finish Off unless you turn it on. On a phone, most callers say "hello" as they pick up and cut the greeting off a second in; with this on, the greeting plays to the end on phone calls and what they said is answered next. On the website it is left alone, because a visitor expects to be able to talk over it. Through the API: `voice.greetingUninterruptible`. ### How long a pause ends the caller's turn Under **How agent knows the caller has finished**, you choose how the agent decides a caller is done talking: **Smart** (a model decides, and does not interrupt people who pause mid-sentence), **Timer** (a stretch of silence ends the turn: faster, and it does interrupt) or **Ear** (real-time speech recognition, below). Leave the choice on **Whatever the platform is set to** unless you have a reason: that is **Timer** today, and the Voice page shows it. #### Ear: real-time speech recognition In the other modes the agent's speech recognition hears each thing the caller says once they have stopped. **Ear recognises speech in real time**, the whole call through, and that changes four things: * **Callers can cut the agent off mid-sentence, on the phone too.** It stops as soon as it has heard enough of their words, or a clear "wait" or "stop", rather than once they have finished. The agent's own voice coming back down the phone line is not mistaken for the caller. [Interrupting](#interrupting) * **It tells a listener from an interruption.** "Haan ji, bilkul, theek hai" said while the agent is talking lets it carry on, however many words it is; "no, how much is it for a year?" stops it. It reads the words against what the agent was saying, so a "haan" that answers the agent's question counts as an answer. * **It waits through a pause mid-thought.** A caller who stops for a second halfway through explaining something is not answered on half a sentence: the agent keeps listening, and may say a short "haan ji" or "okay" in the caller's language to show it is following. On a website call it says it in the short gaps while the caller is still talking (once they have been going for a few seconds, never more than twice in one turn); on a phone call, only at a longer pause, because the line can carry the agent's own voice back as if the caller had said it. That acknowledgement is not part of the conversation record. * **It knows sooner when the caller has finished**, because the end of their speech is detected as it happens rather than after a timer. You can tune the first two on the Voice page, under **How agent knows the caller has finished**, whenever Ear is the mode in use. Each has a switch and a box for your own words, one per line; leave a box empty and the agent uses its own defaults. * **Backchannel detection** (on by default) switches the first behaviour. With it on, **Caller backchannel words** are words that always mean the caller is listening, such as "haan ji", "bilkul", "theek hai"; a caller who says only those is never taken as interrupting (one per line; commas also separate, up to 30 entries of 40 characters). With it off, only **Words needed to cut the agent off** decides, and that box appears right there. * **Agent speaks backchannel / acknowledgement while the caller explains** (on by default) switches the second. **Agent backchannel phrases** are what it says, taken in turn ("haan ji", "achha", "okay"), one per line and each said whole, commas included (up to 30 lines of 40 characters); empty means a short word in the caller's own language. A box over those limits is refused when you save. **Ear costs nothing extra:** a call is billed at the rate of the voice answering it, in Ear mode as in any other. If the agent uses [your own speech-recognition account](/api-reference/bring-your-own-keys) and it cannot listen in real time, the agent uses Timer instead. Through the API, the Ear behaviours are `voice.backchannelDetection`, `voice.backchannelWords`, `voice.acknowledgements` and `voice.acknowledgementWords` (the lists take up to 30 entries of 40 characters; `null` or `[]` is the default). Through the API: `voice.turnDetection` is `"smart"`, `"timer"` or `"ear"`, and `null` follows the platform's choice, which is Timer today (a read shows `null` until you choose one). Anything else is refused. The box under it takes 0.2 to 3 seconds. With Timer it is **Silence that ends a turn**: how much quiet means the caller has finished (0.25 seconds when the box is empty). With Smart it is **Stop waiting after**: the longest the model may keep waiting once it thinks a sentence is unfinished, and a ceiling rather than a delay (0.6 seconds when the box is empty). Shorter answers sooner; longer gives slow speakers room but makes every reply arrive later. A value outside the range is pulled back into it when you save in the console. Through the API: `voice.turnSilenceSeconds` (`null` for the standard, and a read shows `null` until you set a number). The API refuses a value outside 0.2 to 3. ## Background sound **Off, and most numbers should leave it off.** A clean line is what a support number sounds like, and silence behind a voice is not a fault to be fixed. Turned on, the caller hears a room behind the agent — an office, with the distant keyboards and voices of a working desk. It can make a call feel like it is happening somewhere rather than nowhere. It can also do the opposite. The same sound that reads as a busy office to one caller reads as a bad connection to another, and which one you get depends on your business, your caller, and the handset they are holding. Ring your own number and listen before you leave it on. Through the API: `voice.ambience` — `"office"`, `"office-quiet"`, or `null` for none. ### How loud it sits Quiet. Ambience works when nobody notices it and would notice its absence — if a caller can point at it, it is too loud, and one who has to strain past it to hear your agent would have been better off with silence. The default is a good starting point. The slider stops well below your agent's own volume on purpose: there is no setting here that turns the room into something a caller has to talk over. Through the API: `voice.ambienceVolume` (0.05 to 1, with at most two decimal places; `null` for the default of 0.15). ## Calling customers Your agent can also ring people, from your own systems — a reminder the day before an appointment, a call when an order is ready, a follow-up after a visit. There are two ways to start one. For a list of people, use a [calling campaign](/channels/voice-campaigns) — **Voice Campaigns** in the main sidebar, where you pick which agent does the calling. For one call at a time, from your own systems, it is one request to [the calls API](/api-reference/calls/place-call), which takes the reason for the call as words the agent speaks first: ```json theme={null} { "to": "919876543210", "purpose": "I'm calling from Sunrise Clinic about your appointment with Doctor Iyer tomorrow at four." } ``` You can only call people who are **already your contacts** and who have not asked you to stop. Those are our rules and they are not the law — automated calling is regulated nearly everywhere, and having a lawful basis for each call is yours to get right. ### How long it rings A call your agent places rings for **30 seconds** before it is given up. Change it in the console under **Voice page → Ring time** (10 to 60 seconds; leave it blank for 30), or through the API with `voice.ringSeconds` on [the agent](/api-reference/agents/update-agent). To change it for one call only, send `ringSeconds` with that call on [Place Call](/api-reference/calls/place-call). It is a setting of the agent, not of a number, so it applies to every call the agent places whichever number it dials from: API calls, campaigns, call-backs and test calls. A call nobody answers within that time is reported as **`no_answer`** — the `hangup` of the `call.completed` webhook — and a campaign or a call with `retry` rings it again as one. Much past 30 seconds, many networks hand the call to the person's own voicemail, which is a machine picking up, not them. ### Hang up on voicemail Off unless you turn it on, under **Hang up on voicemail** on the Voice page. It applies to the calls your agent places on your behalf — campaigns, the calls API and test calls — and never to calls somebody makes to you. A call-back a customer asked for never checks either: they are expecting the call, so the check could only cut them off. With it on, your agent **speaks as soon as the call is answered**, and the phone company listens at the same time. When what comes back is a voicemail greeting — "leave a message after the beep", "the number you have dialled is switched off", "आप जिस नंबर पर कॉल कर रहे हैं" — **the call ends right there**. What that means for you: * **Nobody waits.** A person who answers hears your greeting straight away. * **A person is not hung up on for sounding unsure.** The phone company's own guess is often wrong on mobile numbers, so on its own it ends nothing: only the words of a voicemail greeting do. Somebody who picks up and says "Hello?" is a person, however it sounded. * **A machine hears the first sentence of your greeting** before the call ends, and the call is charged like any other for those few seconds. A voicemail with a long silence before its greeting can be talked to a little longer before it is recognised. It is worth it on a long list where most calls reach voicemail. Try it on your own number first. A campaign with a **voicemail message** works differently: it waits to hear the greeting so it can leave the message — see [If it reaches voicemail](/channels/voice-campaigns#if-it-reaches-voicemail). Not every phone provider can tell a machine from a person. On a number from one that cannot, the switch has no effect and your agent talks to whatever answers. ## A call button in your chat When **Website** is switched on, a call button appears beside the message box in your widget — visitors press it and start talking, without dialling anything. That is the whole of it. The button appears in the widget on every page the chat is already on, and disappears again the moment you untick it or switch **Answer calls** off. The playground shows the same widget your visitors see, so the button is there too. Press it and talk — this is the quickest way to hear your agent before anybody else does. The visitor's browser asks for microphone permission the first time. Until they allow it nothing is recorded, and if they refuse the button says so rather than failing silently. A call from the widget is the same conversation as their chat — same person, same memory, same inbox thread — so somebody can type a question, call to discuss it, and the agent knows what they already asked. ## WhatsApp Customers press call inside the WhatsApp thread and reach the same agent. The **WhatsApp** row on this page is on by default. Whether a number takes calls is its own **Calls** switch on Connect WhatsApp, which turns this row on and off with it. ## Texting the caller During a call the agent can send the caller a WhatsApp or an SMS (a booking link, your address) when you turn those on in [Actions](/agent/actions#reaching-the-customer-on-a-different-channel). And when the call ends it can send one more, the same message every time, for answered calls, missed calls or both. See [After the call](/agent/after-the-call). ## When Answer calls is off A number you still own should not leave a customer wondering. When **Answer calls** is off, or the phone is not switched on as a place the agent answers, what the caller hears depends on the number's provider: | The number is with | The caller hears | | - | - | | Plivo | A spoken sentence, then the call ends: your own message if you wrote one and it is in English, otherwise "Sorry, this number is not taking calls right now. Please try again later." | | Twilio | The same, and your message can be in English or Hindi. | | Vobiz | No message of yours. The call is declined as it always was. | | Telnyx | A busy tone. Telnyx numbers cannot play a message from us. | | WhatsApp calling | The call is rejected. WhatsApp has no way to speak to the caller. | **Message when Agent is off**, on the Voice page under Answer calls, is your sentence for the Plivo and Twilio rows — up to 300 characters, for example "We are closed today and open again at 9 am tomorrow." Leave it blank to keep the standard one. Via the API it is `voice.unavailableMessage` (send `null` to clear it). * It is read aloud by the phone provider's own voice, not by your agent's, so only text that voice can read is spoken. English, or English written in Roman letters ("Hum kal subah 9 baje khulenge"), is read in an Indian-English voice on Plivo and Twilio numbers. Hindi in Devanagari is read only on Twilio. A message in any other script (Tamil, Bengali and so on), or Hindi on a Plivo number, plays the standard sentence instead. We keep what you wrote and tell you beside the field, and in the API as `voice.unavailableMessageNote`. * It is only for a line you switched off. A call that cannot be taken for any other reason — the plan, the balance, every line busy — always hears the standard sentence, so that a caller can never learn anything about your account by dialling. * Line breaks and other control characters become spaces, invisible marks are removed (the joiners inside a Hindi conjunct are kept), and a message over 300 characters is refused instead of cut short. ## Things worth knowing before you go live Both switches matter. **Answer calls** off, or **Answer calls** on with no surface switched on, means the agent does not pick up — you have not told it to, or not told it where. What a caller hears then depends on the number's provider; see [When Answer calls is off](#when-answer-calls-is-off). Calls have a ceiling. A call that runs unusually long — usually a line left open by accident rather than a customer with a lot to say — winds down and ends rather than running indefinitely. The same rule as every other channel: an agent that does not know something says so and offers to have a person follow up. On a call this matters more, because a caller cannot scroll back and check what they were told. Not "what are your hours". Ring your own number and ask the thing your staff dread — the awkward one, the one with a condition attached, the one where the honest answer is "it depends". That is the answer worth hearing out loud before a customer does. # Calling campaigns Source: https://docs.thinnest.ai/channels/voice-campaigns Your agent rings a list and has the conversation itself. A calling campaign is your agent ringing people one after another and talking to them — the same audience rules and the same consent as a [broadcast](/whatsapp/broadcasts), on the phone instead. ## Where to find it **Voice Campaigns** is in the main sidebar, just under Contacts — under it because a calling campaign is made *of* a contact list. Every campaign in the workspace is on that one page, whichever agent places the calls. ## Which agent does the calling You choose, when you build the campaign. It matters more than it sounds: a call goes out from **that agent's number**, opens with **that agent's first words**, and is priced by **that agent's voice**. If you only have one agent that can call, there is nothing to choose and you are not asked. If you have several, the builder asks first — before the audience, because the answer changes what the rest of the campaign is — and the list shows which agent each campaign belongs to. An agent can only be picked if it can actually place calls: it needs a number pointed at it on [Phone Numbers](/channels/phone-numbers), and it has to be set to answer on the phone rather than only on the web. The builder says which of the two is missing rather than making you guess. Calling campaigns used to live on each agent's own page. They moved to the main sidebar so one workspace has one list — with three calling agents you had three lists that could not see each other, and finding a campaign meant remembering which agent you made it on. Old links still work. ## Who gets called Two answers, and they are alternatives rather than a pair. **People already in Contacts.** The same four lists a [broadcast](/whatsapp/broadcasts) offers, each showing its count before you commit, and the same tags and [groups](/agent/leads#groups) to narrow within one. **Upload a list.** A CSV, mapped and rung. This is the one most people want: the spreadsheet already exists, and nobody wants to import it, tag it, and then come back here. **Numbers you must never call** belong on the do-not-contact list, on the **Contacts** page under *Do not contact*. Paste them or upload a file — from a regulator, a complaint, or a lawyer's letter — and no campaign will ever reach them, on any channel. It is not the same as somebody opting out. Opting out is about a customer you already have; this is about numbers, most of which are not in your contacts at all, and an import cannot undo it. It is checked at the moment each call is placed, so a list you upload at eleven stops a campaign that is already running by the time it reaches three. Somebody who tells your agent on a call not to be called again goes on this list by themselves — see ["Don't call me again"](/channels/voice#dont-call-me-again). A campaign skips them and shows why. ### Uploading a list Drop a CSV with a header row. We guess which column is the phone number, the name and the email — "Phone", "Mobile", "WhatsApp Number" and "Contact" all land on the same guess — and you can change any of them. Any other column can be pointed at something your agent says: see [saying something different to each person](#saying-something-different-to-each-person). The panel shows the numbers **as we will actually dial them**, not as the file wrote them, because that is where spreadsheet damage shows up. Pick the country for numbers with no country code. A number starting `+` or `00` keeps its own. **Excel eats long numbers.** A phone number in a General cell can be saved as `9.19E+11`, and those digits are genuinely gone — we refuse the row rather than invent a number that reaches a stranger. Format the column as text and export again. Rows we cannot read are listed with their line numbers and the reason. Leaving them out is a tick you have to make: a list of a thousand that quietly calls nine hundred is worse than one that stops and tells you. Repeats within the file are dropped and counted. The same customer twice in an export is ordinary; ringing them twice is not. ### What an upload does to your contacts **The people on the file become contacts.** They are in Contacts afterwards, and the campaign is aimed at exactly them — a label we put on those rows is what keeps this campaign to this list. There is a tick saying these people agreed to be called. It is a claim you are making, recorded with the date, and it decides which *later* campaigns reach them: | The tick | Who reaches them afterwards | | - | - | | Left alone | Campaigns aimed at everybody, or at imported contacts | | Ticked | Those, plus campaigns aimed at people who opted in | Uploading a spreadsheet is not consent, and a phone call is not a message somebody deletes. Calling a number on India's DND register without consent is an offence under TCCCPR, and the complaint lands on the number you called from. Anybody who has asked you to stop is left out of the list even when they are in the file. That is decided in the database rather than by the screen, so it holds however the campaign was built. The label stays on those contacts afterwards, so you can aim a second campaign at the same people without uploading the file again. ## What it says There is no message to write. A calling campaign opens with **the agent's own first words** — the greeting on its Voice settings, the same one somebody hears when they ring in — and talks from there. > Hello, this is Meera from Sunrise Dental. That is deliberate, and it is the one place a calling campaign is simpler than a broadcast. A box here would have been a second version of what your agent already says, and then nobody could answer the only question that matters: which one does a customer actually hear? The builder shows the opening before you launch, beside everything else, and links to where it is changed. Worth reading it before a campaign goes out. A greeting written for people who rang **you** — "Thank you for calling Sunrise Dental" — is the wrong sentence coming from somebody who rang **them**. It is a real conversation, not a recording. People interrupt, ask something unrelated, and the agent answers from your knowledge like it does on any other call — then the whole thing lands in the inbox with its transcript. Calls go out a few at a time rather than all at once. A hundred people are not all rung in the same minute, which is deliberate: it is what a phone line can actually carry, and what does not look like a machine. **Promotional calling in India needs a 140-series number.** An ordinary business number is licensed for service and transactional calls, and using it to promote something can get it blocked. You mark each call campaign as **service** or **promotional**, and we refuse a promotional one from a number that is not licensed for it — with a reason, not a silent failure. Marking a promotional campaign as service to get past this is the thing that gets numbers blocked. ## Saying something different to each person Put `{{first_name}}` — or any name you like in double braces — into your agent's greeting or its instructions, and every call fills it in from the row that person came in on. > Hello , calling from Sunrise Dental about your appointment on > . When you upload a list, the campaign shows you what your agent asked for and you point each one at a column. Nothing to declare anywhere: we read the braces out of your agent's own words, so the list is always what it actually says. ### What fills what | Written in the agent | Filled from | | - | - | | `{{first_name}}`, `{{name}}`, `{{phone}}`, `{{email}}` | the contact, unless your file says otherwise | | anything else | the column you mapped it to | Your file wins where the two disagree — a column of preferred names beats the name we have stored — but a **blank cell does not**. An empty column leaves the contact's own details alone rather than wiping them for that one call. `{{first_name}}` follows whichever name won, so a list that carries names for people you have never spoken to still greets them properly. Map a `first_name` column of your own if you want to control exactly what they are called — "Dr Menon" rather than the first word of a full name. **Keep values short.** These are words your agent says out loud, so anything past about 150 characters is trimmed. A date, an order number, a name or a short phrase is what belongs here; a notes column mapped by accident is not. ### What happens when it is not filled Nothing is said. "Your order is ready" with no order mapped becomes "Your order is ready" — a sentence, spoken normally, with the stray space and the double space cleaned up. **Nobody ever hears the braces.** That is the one rule this feature is built around: an unmapped, unknown or misspelled variable is left out rather than read aloud. A cell that itself contains `{{something}}` is stripped too — you cannot smuggle a placeholder in through a spreadsheet. ### Careful with instructions A variable in your agent's **greeting** only affects calls it makes. A variable in its **instructions** is part of every conversation that agent ever has — someone ringing your number, a website chat, a WhatsApp message. None of those came from a list, so there is nothing to fill from and it resolves to nothing for them. The upload screen says so when it spots one, and it is worth reading: an instruction written for one campaign quietly applies to everybody. ## Looking someone up before you call them Your list is fixed when you build the campaign. The facts about the people on it are not — an order that was "packed" on Monday is out for delivery by Tuesday, and an agent that opens with Monday's answer is worse than one that never mentioned the order at all. The customer knows which is true. **Pull Details/Data from API**, step 4 of a new campaign, closes that gap. Moments before each person is called, we ask your own system about them and use what comes back in what the agent says. ### What you need first An **action** on the Actions page: your endpoint, its method, and any keys it needs, saved once. The campaign picks one from a list — it never asks you to paste a URL or a key a second time, and changing the action later changes it everywhere it is used. The action needs somewhere to put the number. If it declares exactly one field we choose it for you; if it declares several, say which one is the phone number. ### Try it before you launch Type one real number and press **Try it**. You will see exactly what the agent would know about that person, written the way you would use it: ``` {{order_id}} A-8841 {{status}} out for delivery {{eta_minutes}} 20 ``` Those are the placeholders you put in your opening line. This really calls your endpoint; it places no phone call and changes nothing about the campaign. Use it. Two dropdowns cannot tell you your endpoint expects `msisdn` where the campaign is sending `phone` — but one press of **Try it** does, before a single call goes out. ### What we can use from the answer Plain values at the top level of your JSON. `"status": "out for delivery"` and `"eta_minutes": 20` both work; a number, a word, or true/false. Anything nested is skipped rather than read out. An agent saying "your order \[object Object] is ready" is worse than one that never mentions the order, so we leave it out and tell you which fields were ignored when you press **Try it**. Whatever your system says wins over the same column in your uploaded file — that is the entire point — except when it comes back empty, which does not erase what you uploaded. ### If the lookup fails for someone Your choice, and the default is the careful one. **Don't call them, try again in a few minutes.** Nobody is rung until their details come back, so a sentence built around `{{order_id}}` is never spoken with a hole in it. They stay in the queue and are tried again shortly. If the lookup fails for *everyone*, the campaign pauses and tells you why rather than working through your list saying nothing useful. **Call them anyway, without that data.** The call goes out using what you uploaded, and anything the lookup would have added is simply missing from what the agent says. Choose this when the extra detail is nice to have and reaching people on time matters more. Every person on the list is one request to your endpoint, sent shortly before their call. A campaign of ten thousand people is ten thousand requests, paced by the campaign's own speed limit. If your system is rate-limited, set **At most, per hour** to something it can live with. ## When it calls, and when it stops A campaign carries its own clock. Everything below is read in it, and it starts as your workspace's — change it if you are setting this up from somewhere else, and what you type is what the campaign does. | Setting | What it means | | - | - | | **It starts** | Now, or at a time you pick | | **Hours it may call** | The same stretch of every day the campaign runs | | **Stop after** | Anything still unanswered by then is left uncalled | Calls outside the hours **wait** rather than being dropped. A campaign started at midnight begins in the morning, and its row says so rather than looking stalled. Indian rules restrict commercial calling to daytime, and 9am to 9pm is the window every campaign keeps unless you narrow it. Narrowing is normal — a clinic ringing about appointments and a shop ringing about an offer do not want the same hours. **The hours are read where each person is.** If your list has numbers in more than one country, "9am to 6pm" means nine in the morning *to them* — a London number is rung at 9am London time, not at 9am yours. That is what the rules say and what anybody being rung would expect. Nothing to set: it comes from the number. Two consequences worth knowing: * A campaign spanning countries is never all callable at once, so it works through the list in waves as each country's morning comes round. The row says it is waiting rather than looking stalled. * For a country that spans several time zones — the United States, Canada, Australia, Russia, Brazil — a phone number does not say which one. We stay inside your hours at **both ends** of the country rather than guessing, which makes the callable stretch shorter but never rings anybody at six in the morning. If your hours are very narrow, a list like that may not be callable at all; the campaign says so instead of trying for ever. ## Calling from more than one number A big campaign from a single number is the fastest way to get that number marked as a nuisance line — and once it is, everything it does afterwards suffers, including customers ringing you back. If you hold spare numbers, give them to the agent under **Outbound** on [Phone Numbers](/channels/phone-numbers) and the campaign spreads its calls across them. Nothing changes in the builder and there is nothing to switch on. An agent that has **no number of its own** can run a campaign too, from a number you give it under **Outbound** — the builder offers it like any other agent. The numbers can be on **different phone companies** — a ThinnestAI number and one you imported from Twilio, for instance — and each call goes out through its own number's company. If the campaign leaves a voicemail and only some of its numbers can (ThinnestAI, Plivo and Telnyx numbers can), it still runs: calls on the others talk to the answering machine instead, and the builder tells you how many numbers that is when you create the campaign. If none of them can, it asks you to remove the message. Three things are worth knowing, because they are what makes it safe: * **Whoever we ring keeps the same number.** A second attempt comes from the number that rang them the first time. * **Somebody ringing back reaches whoever answers that number**, which is the agent under **Inbound** — not necessarily the one that rang them, and nobody at all if **Inbound** is empty. Decide that on the Phone Numbers page before a large campaign. * **Each number has a daily limit** of 200 calls. A campaign that uses up every number pauses with a line saying so and carries on the next morning. ## Which days, and how fast Two things worth setting on any list longer than a few hundred. **Days it may call.** By default a campaign calls every day it is running, Sunday included. Pick the days in the builder and it waits for the next allowed one. The day is read where each person is, the same as the hours. **At most, per hour.** By default a campaign uses every line your plan gives it, which is the fastest it can go. That is the wrong setting when somebody has to answer the calls that connect — a team of two cannot hold thirty conversations, and the rest of your customers hear a hold tone. A cap only ever slows a campaign down; your plan's lines are still the ceiling. The cap is a rolling hour, not a clock hour. "Thirty an hour" means thirty in any sixty minutes — not thirty at 10:59 and thirty more at 11:01. ### Trying again the next day, at a time you choose Pick **The next day** as the wait and a time appears beside it. Leave it empty and the retry goes out 24 hours after the call it missed — so an evening call is tried the next evening, and later again each round. Set it to 9:00 and everyone who did not answer is queued for nine the next morning instead. A busy line is not affected: that is someone holding their phone right now, so it keeps its own short wait in minutes. Your calling hours still apply. The time says when somebody becomes due, not that the call ignores your window. ### When your balance runs low Two things stop, and not at the same point. **Trying somebody again stops first.** Once your balance is down to about ten calls, anyone who did not answer is left where they are and the campaign keeps ringing numbers it has never tried — the last of a balance is better spent on somebody who might pick up. The campaign says so on its page. **Calling stops when there is not enough for one call.** Nothing is lost: everybody still waiting keeps their place and their turn. Top up and both resume by themselves within a minute or two. Nobody is dialled twice for it, and no attempt is used up while calls are held. ## If it reaches voicemail Most people do not pick up. There are two ways to handle a machine: * **Leave a message.** Write one in the builder and, when an answering machine takes the call, your agent leaves it — in its own voice — and then hangs up. * **Just hang up.** With no message and the agent's [Hang up on voicemail](/channels/voice#hang-up-on-voicemail) switch on, the call ends as soon as what comes back is recognised as a voicemail greeting, and is charged like any call for the few seconds before that. With neither, nothing listens for a machine and your agent talks to it like any other call. Writing a message is enough — you do not have to turn anything else on. It is not available on every phone provider, and if yours is one of those you are told when you write the message rather than after the campaign has run. ### How we tell a machine from a person Your agent speaks as soon as the call is answered, and the phone company listens to the first few seconds at the same time and guesses whether a machine answered. **That guess is often wrong**, and more so on Indian mobile numbers: on one campaign it called 18 real conversations "machine" out of 32 — people saying "Hello" or "हां" and then talking to the agent for minutes. So the guess on its own never ends a call. It only makes us listen more carefully to the first thing said on the line, and **the words decide**: * **A voicemail greeting** — "please leave a message after the beep", "बीप के बाद अपना संदेश छोड़ें", "the number you have dialled is switched off" — is a machine. With a voicemail message written for the campaign, your agent leaves it, then the call ends. With none, the call just ends. * **Anything else** — "Hello", "हां", "बोलिए", a question — is treated as a person, and the conversation carries on as normal, whatever the guess was. The words are also checked on a call the phone company took for a person, on its first two turns, so a machine it missed is still recognised from its greeting. We would rather miss a machine than hang up on a person: a greeting worded in a way we do not recognise is treated as a person, so occasionally your agent will talk to a voicemail box for a few seconds. And because your agent speaks at once, a machine always hears the first sentence of your greeting before the call ends. Test it on your own number first. Calls that reached a machine are counted separately from calls somebody answered — and a call where the customer said anything counts as answered, even if the phone company first guessed otherwise. A recording is not a conversation, and a person is not a recording. ## Testing two openings Most of why somebody stays on a call or hangs up happens in the first few seconds, so the opening is the thing worth testing. Fill in **Test a second opening** and the campaign is created as two: "(A)" and "(B)", each calling half the list, each with its own progress and its own numbers. Nobody is called twice — the halves never overlap — and you compare them by opening the two rows. ## How it went A calling campaign shows more than how many calls went out: | Number | What it means | | - | - | | **Called** | The people whose phones rang, with the total number of dials — somebody who was retried counts once here and once per dial in that total | | **Answered** | People who picked up on their last call, as a percentage of the people called | | **Average call** | How long the answered ones lasted | | **Reached voicemail** | The phone company reported an answering machine and the person never spoke. If they did speak, the call counts as answered instead | Beside them, **Minutes and cost** adds up every dial the campaign made — retries and calls an answering machine took included, because each was charged (a machine the call hung up on is charged only the phone network's fee): the time on calls, how much of it was conversation, what was charged to your wallet, and what that comes to per answered call and per minute. A call that never left for the phone network is not counted. Each call's own charge is in Usage → Calls, and the two add up. Underneath, **How the rest ended** breaks down everything that did not connect — no answer, line busy, declined, not reachable, ended before the agent spoke — and each person in the list shows how their own call ended rather than just "Called", plus how many times they were dialled when it was more than once. **Click anybody in the list to open their call** — the summary, the transcript and the recording, in the same panel as Usage → Calls. The list shows each person's most recent call; every earlier attempt is in Usage → Calls too. It shows 25 people a page — use the page numbers underneath, or search by name or number. That breakdown is what tells you which problem you have. A lot of "Not reachable" is a stale list. A lot of "Declined" is the wrong audience or the wrong opening. A lot of "No answer" is usually the wrong time of day. Declined and unreachable numbers are never called again. Busy, no-answer and answering machines the call hung up on are what retries are for — and anybody who rings your number back in the meantime is not called again either. ## Ringing again A person who does not pick up is not called again unless you say so. Whether a second attempt is reasonable depends on why you rang, so it is a choice on the campaign rather than a default. Choose **how many times** to try again and **how long to wait** between tries. Two retries half an hour apart means three calls at most, and the wait starts when a call goes unanswered rather than when the campaign started — so somebody rung late in a long run still gets the gap you asked for. **A busy line gets its own, shorter wait.** The two are not the same thing and it is worth setting them apart: * **Engaged** means they are holding their phone right now. That is the most reachable anybody ever is, so the default is five minutes. * **No answer** means they are away from it. Ringing back five minutes later is pestering, so the default there is half an hour. Leave the busy wait alone and it does what it always did — the same wait for both. Anyone who answers is never called again, and retries keep to the calling hours like every other call. **And anyone who declined is never called again either.** We read what the carrier says about how the call ended, so a retry means what you would want it to mean: | How it ended | Tried again | | - | - | | No answer | Yes — this is what retries are for | | Line busy | Yes | | Answering machine, and the call hung up on it | Yes, after the no-answer wait — they were not reached, and the guess may have been wrong | | Answering machine, and your agent left the message | No — the message was delivered | | Picked up, but ended before your agent spoke | Yes, after the no-answer wait — usually somebody who hung up while the voicemail check was listening | | They declined | **No.** Pressing decline is an answer | | Number does not exist, or is barred | No | You see the same words on the call in your inbox — "Declined" rather than "Missed call" — so the reason a number was or was not tried again is on the record rather than something to work out. A phone ringing twice from a number somebody did not recognise the first time is the complaint that costs a number. Space the attempts out, and keep them few. ## Being told it started A campaign set for nine in the morning starts with nobody watching — which is the point of scheduling it, and also why "did it actually go?" is the question it creates. Pick anybody in your workspace under **email when it goes live** and they get an email and a notification the moment it starts, with how many people it is calling. Nobody is picked by default: a campaign that emails four people every time it starts is a campaign whose emails get filtered. ## Cost Calls are charged in half-minute steps, rounded up, and only for calls that actually connect — a number that rings out costs nothing. The per-minute rate is your agent's voice rate; the campaign builder shows it before you commit. An answering machine the call hung up on never reached your agent, so it is charged only what the phone network charges for those seconds, not your voice rate. Once a person picks up, the call is your agent's and is charged at the voice rate, however short. So is a machine your agent leaves a message on, because your agent spoke. See [Voice](/channels/voice) for the rest of what an agent does on the phone. # Voice Clone Source: https://docs.thinnest.ai/channels/voice-clone Record a short sample and your agent can speak in that voice — a founder's own, or someone from your team. Fifteen seconds of somebody reading a line becomes a voice your agent can answer calls in. It is a Studio voice like any other on the [Voice page](/channels/voice) — same per-minute rate, same tab — except this one is yours rather than picked from a catalogue. **Voice Clone** has its own page in the main sidebar, under the same group as Phone Numbers and Voice Campaigns. It lists every voice this workspace has made, not one agent's. **This is on the Pro plan and above.** Free and pay-as-you-go see the offer over a blurred page rather than the recorder — cloning somebody's voice and then telling them they cannot use it would be worse than not offering it. ## Clone a voice In the main sidebar, choose **Voice Clone**. Before anything records, you confirm two separate promises — not one checkbox that quietly covers both: * The recording is your own voice, or you hold the rights and permission to clone the voice in it. * You will not use it to impersonate anyone, or for anything illegal, harmful or misleading. Both are recorded against the voice, with who agreed and when — this is what makes the clone lawful to hold, not a formality on top of it. Record straight from the page, or upload a file — WAV, MP3, M4A or WebM. Fifteen seconds is the sweet spot: five is the least the engine will accept, and thirty is the most it wants — read your longest sentence twice if you are short, rather than pausing partway through and starting again. Read normally, in one take, without music or anyone else talking over you. A treated room helps, but is not required — noise removal is on by default and can be turned off if it flattens a voice recorded somewhere already quiet. Give it a name your team will recognise on the Voice page later — "Rohan" or "Founder voice", not "Recording 3". Pick the language it will be registered under, and an accent within it where the language has more than one — Indian English and American English are not interchangeable for a voice, even though the words are the same. Description and tags are optional and are yours alone — a note for your own team about when to use this voice, not something a caller ever sees or hears. The moment it is made, it says your line back to you in its own voice — the same sentence you read, spoken by the clone. That is the one time it is synthesized for free; every play after this comes from that same recording, never a fresh one. ## Using it A cloned voice appears on the **Studio** tab of the [Voice page](/channels/voice), above the shared catalogue, marked as yours. Choose it like any other voice and save — nothing else changes about the agent. It answers calls at the **same per-minute rate as every other Studio voice**. Cloning costs nothing; what it speaks on a call is billed the same as Anjali, Rakesh Khanna or any of the hundreds of others on that tab. ## The limit **Up to 10 cloned voices on Pro, and 20 on Scale.** Enterprise is agreed per contract. Delete one to make another once you reach it. If your workspace genuinely needs more, ask us — a higher limit can be set for your workspace alone. We keep the default modest on purpose: every clone is a recording of a real person, and a growing pile of them is a growing responsibility. ## Removing one Delete a voice from the **Voice Clone** page and it is gone everywhere — the recording, the preview, and the voice itself at the engine that spoke it. This cannot be undone. **An agent already speaking with it is moved to a standard voice automatically**, so a deleted voice never leaves an agent silently billing the Studio rate for a voice that no longer exists. Check the agent's Voice page afterwards if you want to pick something else. ## Your customers can use your voices If you are a developer building for [customers](/api-reference/customers), **every voice you clone can be used by all your customer workspaces**, automatically. It shows on their **Studio** tab, and in [`GET /voices`](/api-reference/voices/list-voices) with `"sharedFromDeveloper": true`, and their agents speak with it like any other Studio voice. Their calls are billed to them as usual. It stays yours: a customer cannot rename or delete it, and it counts against **your** clone limit, not theirs. A customer's own clones stay theirs alone — you and your other customers do not see them. If you delete one of your voices, every agent speaking with it, yours and your customers', is moved to a standard voice. ## Who can do this **Admins only**, for cloning and for deleting. A cloned voice is somebody's actual voice held under a consent record naming who agreed — the same weight as the do-not-contact list or a KYC document, both of which are admin-only for the same reason. Any member can still choose an already-cloned voice for an agent, the same as choosing any other voice. ## What we keep, and for how long The recording, the one stored preview, and the consent record stay for as long as the voice exists. Deleting the voice deletes all three. There is nothing else to configure — this follows the same erasure rule as [call recordings](/channels/voice#recording-calls): once it is gone, it is gone, and we cannot get it back. Whether you had the right to clone a particular voice, and what you may say in it, is yours to get right — the two promises above are what we ask you to confirm, not a guarantee we can make on your behalf. # Website widget Source: https://docs.thinnest.ai/channels/website The chat window on your own pages. The widget is one script tag. It draws a launcher button, and the conversation itself runs in an iframe on our origin. ## Install ```html theme={null} ``` Put it before the closing `` tag on every page you want the agent on. Your public key is on the agent's **Channels** page. **The public key is meant to be visible.** It identifies which agent answers and authorises nothing. It is not your API key — that one can message your whole contact list and belongs in a server environment, never in HTML. On Shopify, WordPress or PrestaShop, use the plugin instead. Same widget, no code, and it can recognise signed-in customers. ## Recognising signed-in customers Add two attributes and your visitors stop being strangers: ```html theme={null} ``` The HMAC is what makes it a claim rather than a guess. Without it the id is treated as unverified and bound to nothing. [Full recipe, in Node, Python and PHP](/install/signed-in-customers). ## What it renders The widget is the richest channel: cards, carousels, buttons and forms all render natively. Anything the agent shows here degrades sensibly on WhatsApp and Telegram rather than arriving blank. ## Conversation history A returning visitor sees their previous conversation. The reference is stored in their browser, so it survives a page reload and a return visit, and it is per-browser: the same person on their phone starts a new thread unless they are [signed in and verified](/install/signed-in-customers). ## Branding Free workspaces carry a "We run on ThinnestAI" line with a link. Paid plans do not. ## Appearance The launcher button, the agent's name and its logo are yours to set on the agent's Settings page. The logo appears in the widget header and beside every reply. # WhatsApp Source: https://docs.thinnest.ai/channels/whatsapp The same agent, answering on WhatsApp — plus what Meta's rules mean for you. WhatsApp is the channel most Indian businesses actually want, and it has rules that are not ours. This page is mostly about those rules, because they shape everything you can do. ## Connecting **Connect WhatsApp**, at the top of the WhatsApp section in the sidebar. Two cards there, each opening Meta's own signup: | Card | Choose it when | | - | - | | **Apply for a WhatsApp Business Account** | You are new to WhatsApp Business | | **Connect an account you already have** | You already manage a WhatsApp Business Account in Meta | You sign in with your Facebook account and choose the number. We never see your login, and the account stays yours: you can move it to another provider whenever you want. ### Then add a payment method in Meta **Your balance here doesn't pay WhatsApp's message fees.** Meta charges those to the card on your own Meta account; your balance pays only our 10% platform fee on each message ([how the two bills work](/meta/windows-and-pricing)). Without a card on your Meta account, WhatsApp won't deliver your templates. Add one in Meta Business, under **Billing & payments → Payment methods** ([open it](https://business.facebook.com/latest/billing_hub/payment_methods)), in the business that owns your WhatsApp account. Then tick **I've added it** on Connect WhatsApp. Step by step, with which cards work: [Adding a payment method in Meta](/whatsapp/payment-method). We can't see your Meta payment details, so until you tick it we ask once more before your first send: on Send, in the inbox's template dialog, and before a broadcast. Disconnecting and connecting a different account asks again, since that account needs its own card. If Meta later reports a problem with the card, the step comes back in red with Meta's own words, even after you've ticked it. ### Finish setting up After you connect, **Finish setting up WhatsApp** at the top of Connect WhatsApp lists what's left. Each step is checked against Meta and your workspace, not just ticked off: | Step | Done when | | - | - | | Add a payment method in Meta | You've added it, and Meta isn't reporting a payment problem | | Choose an agent to answer | Every number has an agent (not shown if your own software answers) | | Verify your business with Meta | Meta shows your business as verified | | Get your display name approved | Meta has approved the name customers see | | Get a template approved | At least one template is approved | The list disappears once every step is done. ### Which number to use **A mobile number with a SIM in it** is the smoothest path. During signup Meta sends that number a six-digit code by SMS, and you type it into Meta's window to prove the number is yours — nothing else to do. **A number from the Phone Numbers page can be verified too, by call rather than text.** It cannot receive SMS, so that option will never arrive on it — but Meta's verification call does reach it, including from outside India. **Meta's own popup does not reliably honour a Voice/SMS choice** — the two options can flash briefly and settle on SMS regardless of which you tapped. On a number that cannot take SMS, that leaves you with a number Meta shared but never confirmed, showing on **Connect WhatsApp** as **not verified yet**. That has its own recovery, right there on the row: Bypasses the popup's picker and asks Meta directly for a call — the only method that can reach a Phone Numbers page number. **Text the code** sits beside it for a number that can take SMS. Type the code into the field beneath the buttons, and the number is verified and ready to send and receive. **If that same number already has an agent answering calls on it, you may not need to type anything.** The agent's own call picks up Meta's verification call, reads the code off it, and verifies the number automatically — usually within two minutes, with a toast to confirm it. If the code cannot be read back reliably, you are told to check the recording in your inbox and enter it yourself instead. Your agent can still answer calls on a Phone Numbers page number while WhatsApp runs on a separate mobile number, if you would rather keep the two apart — the two work side by side either way. Two more things about the number: * **It cannot already be active on WhatsApp or the WhatsApp Business app.** Delete the WhatsApp account on that phone first, or use a different number. * **Asking for a code again is limited.** After a failed attempt Meta makes you wait before requesting another, sometimes an hour. Check the number before you start. ### The account is your workspace's; the number belongs to an agent Your WhatsApp Business Account belongs to your **business**, not to any one agent — so it is connected once, for the whole workspace, and every agent works against it. A **number** is different. It answers for exactly one agent, because a message arrives at a number and something has to decide which agent replies. So connecting and assigning are two steps, and the second one is yours to make: 1. Connect your account on **Connect WhatsApp**. The number arrives with no agent. 2. Give it one — from that same page, or from the agent's own **Channels** page. **A number with no agent still receives.** Messages arrive in your [inbox](/workspace/inbox) and wait for a person; nothing answers on its own until you have said who should. That is deliberate — an agent replying from a number nobody chose it for is worse than a message waiting a few minutes. Give the number an agent and **the messages that were waiting become theirs**, so nothing from your first day is stranded. Moving a number from one agent to **another** works the other way round: the agent that had it loses it, but **conversations already open stay with the agent handling them** — nobody mid-conversation is suddenly answered by a different agent. New conversations go to the agent that took it. You can also answer from an unassigned number before you have chosen one: send a template from [Send](/whatsapp/send) and pick a **handler**, and that agent answers that conversation when the customer writes back. Several numbers on one account is fine. Connect each one and give each its own agent. ### Your click-to-chat link Every connected number has one, on the **Connect WhatsApp** page. It opens a WhatsApp chat with you, and it needs no integration — put it in an Instagram bio, behind a button on your site, in an email footer, on a printed sign. You can set what their message already says. That matters more than it sounds: an empty chat asks somebody to think of an opening line, which is where most of them stop. And if you word it differently per place you put the link, the first thing they send tells you where they came from. ### The same link as a QR code Underneath the link is a QR code for it, and **Download PNG** saves a print-quality copy. Point a phone camera at it and it opens the same chat, with the same message already typed. It is for the places a link cannot go: a counter card, a menu, a packing slip, a poster in the window, the back of a business card. The code changes as you edit the message above it, so download it after you have worded it, not before. Give each place its own code with its own opening message — "Table 4", "Ordered from the catalogue", "Saw the window poster". The message people send you is then also the answer to where they were standing, and you never have to ask. ### Disconnecting **Disconnect** on the Connect WhatsApp page stops a number sending and receiving. Your WhatsApp Business Account is untouched — it is yours, and disconnecting here does not close it. **Your conversations are kept.** Everything that number sent and received stays in your inbox and stays searchable. Disconnecting ends the connection, not the history. ### What customers see when they tap your name **Business profile** on the Connect WhatsApp page, beside Disconnect. Your picture, the line under your name, a description, your email, website, address and business category. It is read from WhatsApp when you open it and written back when you save, so it is never a stale copy of what your customers actually see. An empty profile is why some businesses show a blank circle and nothing else. **Your business NAME is not here.** That is the display name on the number itself, WhatsApp reviews it before it appears, and it is set in Meta's WhatsApp Manager under *Phone numbers → Display name*. The tagline, picture and the rest are yours to change whenever you like. ### Your name, and the badge, are two different approvals Two separate things get conflated because they show up in the same place — the top of a chat with you. **Your business name, with no badge**, is the ordinary Display Name above. Any verified business can have one; it is not gated behind anything else. Once WhatsApp approves it, it is what every customer sees instead of your number — most businesses stop there. **A badge beside the name** — WhatsApp's own equivalent of a "blue tick" — is a separate, optional step: an **Official Business Account**. WhatsApp grants it at its own discretion, generally to a business it considers well-known or notable in its own market, on top of your business already being verified. There is no published checklist and no guaranteed outcome, and it is applied for from Meta's WhatsApp Manager on your number's profile — not from here. Most verified businesses run without one; it changes how your name looks, not whether messages reach anyone. ### When WhatsApp stops the number sending WhatsApp can stop an account starting new conversations — most often because the payment method on your Meta account failed, and sometimes after a run of complaints. It is quiet when it happens: WhatsApp accepts what you send and delivers none of it. The Connect WhatsApp page and the agent's Channels page show the reason in WhatsApp's own words whenever that is the case, and the workspace is told once when it starts. Replies inside an open 24-hour window keep working throughout — it is only conversations you start that stop. **A failed payment method is fixed in Meta, not here.** Add a working one in Meta Business Manager under *Billing & payments*, on the business that owns your WhatsApp account. Then open or reload **Connect WhatsApp**: that is when we ask Meta for your account's status, and the warning clears there and then. Step by step: [Adding a payment method in Meta](/whatsapp/payment-method). ### Your account at a glance Beside your numbers, **Connect WhatsApp** lists your WhatsApp account as Meta reports it. Each row shows its current value; click one to see what it means and what changes it. * **Messaging limit:** how many people your business can start conversations with in any 24 hours, across all your numbers. See [quality and limits](/meta/quality-and-limits). * **Can send messages:** whether WhatsApp is letting the account start conversations, with WhatsApp's reason when it isn't. * **Quality:** your lowest-rated number. * **Business verification:** whether Meta has verified your business. * **WhatsApp calling:** whether your account can take WhatsApp calls. That needs a limit of 2,000. * **Marketing Messages API:** whether your account is signed up for it. See [Marketing Messages API](#marketing-messages-api) below. Each number also shows its own display name and whether Meta has approved it, plus its quality, status and sending speed. ### Two-step verification Two-step verification protects your number with a 6-digit PIN. Meta asks for it whenever the number is registered again, and the blue tick needs it on. Click **Two-step verification** on Connect WhatsApp to see whether it is on and to set a new PIN. Type it twice. Keep it somewhere safe: ThinnestAI never stores it. When you put an approved display name live with **Start using**, ThinnestAI has to set a new PIN and shows it to you once. You can change it here afterwards. If you get a notice that a security setting changed on your number and nobody at your business did it, set a new PIN here straight away. ### The blue tick A blue tick next to your name marks an Official Business Account. Click **Blue tick** on Connect WhatsApp to see where each number stands. ThinnestAI reads this from Meta every time the page opens, and shows Meta's five requirements as met or not met: * the business follows WhatsApp's Business Messaging Policy; * it has been on the WhatsApp Business Platform for at least 30 days; * your business is verified with Meta; * two-step verification is on for the number; * the number's display name is approved. If a requirement is missing, the window says which, and from what date if it is the 30 days. When all are met it shows **Ready to apply**: in WhatsApp Manager open **Phone numbers**, click your number, then **Profile** → **Official business account** → **Submit request**. Meta asks for your website, your main country and language, and up to 5 links to news coverage of your brand. Those links matter most. Meeting every requirement does not guarantee the tick: Meta reviews each request and decides. After a refusal you can send another 30 days later. ### Marketing Messages API Meta's Marketing Messages API is an optional programme that can deliver more of your marketing templates and report clicks. Once your business is on it, ThinnestAI sends your marketing templates through it automatically; nothing about how you send changes. Click **Marketing Messages API** on Connect WhatsApp to see where you stand: * **On:** your business has accepted Meta's terms. Nothing to do. * **Request sent:** Meta has asked your business to accept. An admin accepts it in Meta Business Suite, under Settings → Requests. * **Not set up:** choose **Set up**. Meta opens in a small window, where an admin of your business accepts its terms for this programme. The window closes itself and the row updates. Only an owner or admin can do this. If Meta says your business is not in a country where it is offered yet, there is nothing to do until it is. ### Changing your display name Click **Display name** on Connect WhatsApp, type the new name, and choose **Send to Meta for review**. Meta reviews it, usually within a few days. The row shows the review, and you get a notification when Meta decides. When Meta approves it, choose **Start using** in the same place. Meta needs this within 14 days of approving the name, or you send it for review again. Meta allows 10 changes in 30 days, and [its naming rules](https://www.facebook.com/business/help/757569725593362) decide what is accepted. If your number also runs on the WhatsApp Business app, change the name in the app. ## The WhatsApp Dashboard **Dashboard**, under Connect WhatsApp in the sidebar, shows what your numbers did over the last **24 hours**, **7 days** or **30 days** — or any days you pick with **Pick dates**. Choose a first and last day, up to a year apart, then **Apply**. Days run midnight to midnight in your workspace's time zone, and a single day is shown hour by hour: Each figure has a small chart of its trend. When the view is by day, a badge shows how the last 7 days compare with the 7 before. * **Messages sent:** sent by your agent or your team. Internal notes are not counted. * **Delivered**, **read** and **not delivered:** each as a share of what was sent. * **Messages received:** messages your customers sent. * **Cost:** what WhatsApp messages cost you, the same figure as on **Usage**. * **A chart of sent and received messages:** by hour for 24 hours, by day otherwise, in your workspace's time zone. * **What you sent, by type:** marketing, utility, one-time codes and replies, each with its cost. * **The most common reasons messages were not delivered:** WhatsApp's words, then what they mean. Delivery receipts are recorded from 30 September 2026. Messages sent before then show as **no receipt from WhatsApp**, not as undelivered. ## Before a number is connected Templates, Forms and Send are **read-only** until one is. You can read what is there, and the pre-built template and form libraries, but nothing can be written, submitted or sent — all three end at Meta, and until an account is connected there is nothing to send them to. ## The 24-hour window This is the single rule to understand. [Meta's rules](/meta/overview) covers it and the rest in depth. * When a customer messages you, a **24-hour window** opens. Inside it, the agent can reply freely, in its own words. * Outside it, **nothing but an approved template** reaches that customer. So the agent handles conversations; [templates](/whatsapp/templates) handle anything you start. **You can see the window on the conversation itself.** Open it in the [inbox](/workspace/inbox): while the window is open, a line above the reply box counts down what is left — *Free replies and forms open · 14h 22m left*. In the last hour it turns amber. When it runs out, the reply box is replaced by a note saying so. A customer who has never written to you has no window at all, and the thread says that instead. ## What the agent does here The same knowledge, instructions and tools as your website widget. Cards and buttons render as native WhatsApp messages rather than degrading to text. Delivery and read receipts arrive afterwards and show in the inbox — sent, delivered, read and failed are four different facts, each with Meta's own reason where one applies. ## Free entry point When a customer arrives from a **click-to-WhatsApp ad** and you reply within 24 hours, Meta does not charge for that conversation for 72 hours. The platform tracks this and it shows in your usage. ## Opting out A customer can turn off "Offers and announcements" in WhatsApp itself, or simply tell your agent to stop. Either way they are recorded as withdrawn and marketing templates to them are refused. Utility and authentication messages still reach them, and must — an order confirmation is not something anybody opted out of. ## Calls A customer's WhatsApp call appears in the thread with its outcome and duration. **No AI answers the call.** Calls are recorded as events in the conversation, nothing more. Anyone telling you this platform answers your phone is describing something that does not exist. ## Not built * **Catalogues.** Inbound order messages parse, but there is no catalogue browsing. * **Publishing a form** before Meta has verified your business. Forms build, store and preview now; Meta will not publish a flow for an unverified business, and an unpublished form reaches nobody. * **One-time password templates**, which are behind the same verification gate. # How the pieces fit Source: https://docs.thinnest.ai/concepts Workspace, agent, channel, contact — and which one owns what. Four things, and most confusion later comes from mixing up two of them. ## Workspace Your company. It owns billing, your team, your contacts and your WhatsApp account. Everything is scoped to a workspace, and nothing crosses between two. ## Agent One assistant, with its own knowledge, instructions, tools and channels. A workspace can have several — a sales agent and a support agent, say. Agents do not share memory with each other. A customer who talks to your sales agent and your support agent is one **contact** in your inbox, but each agent only sees its own conversations. That is deliberate: two agents can be two brands, and one reading the other's transcripts would be a surprise nobody asked for. ## Channel Where a customer talks to the agent: the **website widget**, **WhatsApp** or **Telegram**. An agent has at most one of each. Shopify, WordPress and PrestaShop are **not** channels. They are doors onto the website widget — ways of getting it onto a page without editing theme code. ## Contact A person. One contact can have several conversations across several channels, and the agent will remember what was said on the others. ### How two channels become one person This is the part worth understanding, because the obvious version of it leaks data. A person is joined across channels only on evidence **they did not give you**: | Signal | Who vouches for it | Joins? | | - | - | - | | A signed customer id from your server | Your own server, by HMAC | Yes | | The sender of a WhatsApp message | Meta delivered it from that number | Yes | | The number a call arrives from | The caller's network | Yes | | A number you imported, or sent us by API | **You** | Yes | | A phone number typed into a chat | Nobody | **No** | The last two rows are the same number and opposite answers, and that is the whole design. A phone number is not a secret. If typing one into a chat were enough to link records, anybody could type your customer's mobile and be handed their WhatsApp history. The same number in your Contacts list joins fine — because **you** put it there. **Calls are the one exception, on purpose.** A caller id can be faked, so an inbound call from a number in your Contacts is not filed on that contact and is never handed their other conversations. The agent is told the name you saved (`{{name}}`, `{{first_name}}`) and that the caller is *probably* that person, and the call stays under the number in your inbox. Calls you place to a contact are filed on the contact. See [Who is calling](/channels/voice). So the practical advice is: if you want website visitors recognised from their phone number, upload them to your Contacts or send them through the API. A number the agent was merely told is still saved, still shown in your inbox and still callable. It just cannot decide who somebody is. So: to have signed-in customers recognised, [your server signs their id](/install/signed-in-customers). Everything else stays separate, and a visitor who cannot be verified is treated as anonymous — which is the safe default, not a degraded one. **Telegram does not join.** There is no phone number unless the user shares a contact card, and matching on display name would merge two different people called Priya into one memory. Telegram conversations are read once a customer is linked by other means; they never do the linking. ## What the agent remembers Within a conversation, the recent history. Across conversations, a short digest of what the same customer said on their **other** channels — so somebody who asked on WhatsApp and then came to your website does not start again from nothing. That digest is deliberately small. It is capped at a handful of messages and never includes your team's internal notes. ### How much of it the agent may use Being recognised and being certain are not the same thing, and which one matters more depends on your business. So this is a choice, not a default we made for you. It lives on the **Actions** page under *Remembering them across channels*, per agent. The agent reads what they said on their other channels and answers from it. Best when being recognised matters more than being certain — a restaurant, a shop, a booking line. **The default.** The agent knows there is earlier history and asks them to confirm who they are before reading it. Nothing from another channel is used, and the agent will not raise it. For clinics, lenders — anyone whose records should not be discussed with a voice on the phone. On the default, a conversation sounds like this: > **Them:** Hi, I messaged you about my delivery. > > **Agent:** I can see we have spoken before — can I just confirm the email > address on your account? > > **Them:** [dana@example.com](mailto:dana@example.com) > > **Agent:** Thank you. You asked us to leave the parcel with the concierge > rather than at the door, and the courier is re-attempting on Friday. Three points worth knowing about that exchange: * **Confirming means matching the email address *you* hold for them** — one you imported or sent by API. An address the agent was told earlier in the same conversation proves nothing, for the same reason a typed phone number does not link. * **Three attempts, then it stops asking** for the rest of that conversation. A check that says "wrong, try again" forever is a way to guess. * **Or a one-time code, if you turn that on** — see below. * **Confirmation lasts for that conversation only.** Proving it is you today says nothing about who rings from that number tomorrow. On **Never, unless they signed in**, only a customer already signed in on your own website counts as proven. Nothing said during the conversation will open it — which is the point of choosing it. That includes a one-time code: the setting means what it says. ### Confirming with a one-time code Asking for an email address is a check on what somebody **knows**, and an email address is not really a secret — it is on your invoices, and whoever knows a customer's mobile number usually knows their address too. A one-time code is a check on what they **have**. Turn on *Confirm by one-time code* under the same setting and the exchange becomes: > **Them:** Hi, I messaged you about my delivery. > > **Agent:** I can see we have spoken before — I have sent a code to your > WhatsApp on the number ending 4417. Can you read it back to me? > > **Them:** 394812 > > **Agent:** Thank you. You asked us to leave the parcel with the concierge… What to expect: * **It goes to the number *you* hold for them**, never one they say in the conversation. If there is no such number, the agent falls back to asking for the email address. * **You need an approved authentication template.** Meta writes the wording for these — see [One-time codes](/whatsapp/one-time-codes). Without one, the setting does nothing and the agent uses the email check. * **Each code costs** the same as any authentication message, which is why the setting starts off. The agent will send at most **two per conversation**. * **The code expires in ten minutes**, and three wrong answers stop the agent asking for the rest of that conversation — the same allowance the email check uses, shared, so trying both is not six goes at it. * **The agent never knows the code.** It cannot read it out, and asking it to will not work: it can only send one and check one. # Recover abandoned carts Source: https://docs.thinnest.ai/guides/abandoned-cart An event from your shop, a two-step sequence, and messages that stop when the customer buys. The classic sequence, and a good first one because the success metric is obvious. ## The shape ``` Customer abandons cart │ ├─ after 1 hour → "Still thinking it over?" └─ after 24 hours → "Your cart expires tonight" │ └─ they buy → sequence stops ``` Both **marketing** category — this is promotional, and mis-categorising it to dodge consent rules is the fastest way to lose a WhatsApp number. `cart_reminder_1`: ``` Hi {{1}}, you left {{2}} in your cart. Still interested? ``` `cart_reminder_2`: ``` Hi {{1}}, last chance — {{2}} is still waiting, and your cart clears tonight. ``` Submit both and wait for approval. See [Templates](/whatsapp/templates). Two steps: | Step | Delay | Template | | - | - | - | | 1 | 1 hour | `cart_reminder_1` | | 2 | 24 hours | `cart_reminder_2` | Bind `{{1}}` to the contact's name and `{{2}}` to a value carried on the enrolment, so the message names the actual product. ```js theme={null} async function cartAbandoned(cart) { // No phone, nothing to send to. Not an error. if (!cart.customer?.phone) return; await fetch("https://app.thinnest.ai/api/v1/events", { method: "POST", headers: { Authorization: `Bearer ${process.env.THINNEST_API_KEY}`, "Content-Type": "application/json", // One enrolment per cart, however many times this fires. "Idempotency-Key": `cart-abandoned-${cart.id}`, }, body: JSON.stringify({ name: "cart_abandoned", contact: { phone: cart.customer.phone, name: cart.customer.firstName, }, variables: { product: cart.items[0]?.title ?? "your items", cart_id: cart.id, }, }), }); } ``` **The idempotency key is doing real work here.** Cart-abandonment jobs typically run on a timer and will happily re-fire for the same cart every few minutes. Without a key derived from the cart, your customer gets the same reminder nine times. The part people forget, and the one customers notice. ```js theme={null} async function orderPlaced(order) { await fetch("https://app.thinnest.ai/api/v1/events", { method: "POST", headers: { Authorization: `Bearer ${process.env.THINNEST_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `order-placed-${order.id}`, }, body: JSON.stringify({ name: "order_placed", contact: { phone: order.customer.phone }, }), }); } ``` Configure the sequence to end on `order_placed`. Nothing is worse than "last chance, your cart clears tonight" arriving after somebody has paid. ## Consent Checked at enrolment **and** again at each send. A customer who opts out on hour one does not receive hour twenty-four — you do not have to handle that yourself. ## Timing One hour and 24 hours are a sensible default rather than a law. Two rules worth keeping: * **Not immediately.** Somebody who wandered off to check a delivery date has not abandoned anything. * **Two messages, not five.** The third reminder converts almost nobody and earns you a block, which costs you the channel for that customer permanently. ## Checking it worked The sequence page shows enrolments, which step each contact is on, and who completed or exited early. Against that, the **Analytics** conversation count tells you how many replied — which is the number that actually matters. # Build your own product on the API Source: https://docs.thinnest.ai/guides/build-a-platform Give each of your customers their own agents, numbers and conversations — from your dashboard, with one key. You run a product of your own and want AI agents inside it: each of your customers gets agents that answer their chats and calls, and you build the screens. You never send your customers to our console. The pattern is three calls: make a **customer**, work **inside** it with a header, and hear about **all** of them on one webhook. ## 1. Make a customer When one of your customers signs up with you, create a workspace for them. Use your own id as `externalId` so you can find it again, and send an `Idempotency-Key` so a retry never makes two. ```bash theme={null} curl https://YOUR_APP/api/v1/customers \ -H "Authorization: Bearer $KEY" \ -H "Idempotency-Key: signup-4812" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Dental", "externalId": "acct_4812" }' ``` Keep the `id` it returns (`org_…`) next to your own record of the account. ## 2. Build their agent Add `Thinnest-Workspace` with that id. Every endpoint now works inside that customer — agents, knowledge, phone numbers, contacts, campaigns, conversations. ```bash theme={null} curl https://YOUR_APP/api/v1/agents \ -H "Authorization: Bearer $KEY" \ -H "Thinnest-Workspace: org_1f2e…" \ -H "Content-Type: application/json" \ -d '{ "name": "Front desk" }' ``` Then give it something to answer from: ```bash theme={null} curl https://YOUR_APP/api/v1/agents/ag_…/knowledge \ -H "Authorization: Bearer $KEY" \ -H "Thinnest-Workspace: org_1f2e…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://acme-dental.example/faq" }' ``` ## 3. Hear about every customer on one webhook Register one webhook on your own workspace for all your customers' events: ```bash theme={null} curl https://YOUR_APP/api/v1/webhooks \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{ "includeCustomers": true, "url": "https://you.example/hooks/agents" }' ``` Each delivery says which customer it is about in `data.workspaceId`, inside the signed body — so you can route it to the right account. ## 4. Show them what they used `GET /api/v1/customers/{id}` returns `spendThisMonthMicro` — what that customer's usage has cost you this month — so you can show usage, or charge for it, in your own billing. Everything your customers use is charged to your balance. Keep it topped up: when it runs out, your customers' agents stop with you. ## Let your AI coding assistant do it Connect the MCP server with the header and your assistant builds inside that customer: ```bash theme={null} claude mcp add --transport http acme https://YOUR_APP/api/v1/mcp \ --header "Authorization: Bearer $KEY" \ --header "Thinnest-Workspace: org_1f2e…" ``` ## When a customer leaves you Release their phone numbers and disconnect WhatsApp, then delete the customer. You have 30 days to restore it; after that everything is erased. Every field, status code and limit. # Turn a calling list into a filled spreadsheet Source: https://docs.thinnest.ai/guides/calls-into-a-spreadsheet Ring a list of leads and get their answers back in the rows you started with, with a line about each call. You have a list of people to ring and a set of things to find out about each one. By the end of this your agent rings them, asks, and writes the answers into the row that person already occupies — plus a sentence or two about how the call went, including for the ones who hang up on you. This is the setup a recruitment, admissions or lead-qualification team ends up with. It takes about twenty minutes. You need a [spreadsheet connected](/agent/spreadsheet) and a [phone number](/channels/phone-numbers). If you have neither yet, do those first — this guide assumes both. ## Start from the sheet, not the agent The order matters. Your columns decide what the agent can record, so a column you add later is one it has been asked nothing about. Import your list into Grist with whatever you already know — name, mobile, district. These rows are the ones the calls will fill in, so the numbers must be the ones you are going to dial. ``` CANDIDATE_NAME MOBILE_NO DISTRICT Ambarish Sadhu 9775778000 Bardhaman Arpita Sengupta 7063513131 Bardhaman ``` One column per question. Keep the names plain — the agent reads them, so `AGE` and `QUALIFICATION` work and `col_7` does not. Give each a short **description** in Grist ("the candidate's current district"). That description is the clearest instruction you can give about what belongs in a column, and it costs you nothing. For a question with fixed answers, make it a **Choice** column and list them. The agent is then held to your own words, so you get `Interested` every time instead of `interested`, `yes interested` and `keen`. Call it **Summary**. It is the one that gets filled when nothing else can — the person who says only *"not now, I'm busy"* has answered none of your questions and still told you something worth keeping. **Actions → Spreadsheet → Grist**, paste your key, choose the document and the table. **Do this again whenever you add a column.** The column list is cached when you pin, so a column added afterwards is invisible until you pick the table again. This is the single most common way this setup goes quietly wrong. ## Tell it which row belongs to whom Without this, every call adds a new line and your imported rows stay empty. On the same card, under **Update an existing row**, choose `MOBILE_NO` and compare it as a **Phone number**. Digits are compared, last ten, so the sheet saying `9775778000` and your campaign dialling `+919775778000` are the same person. They rarely match as text, which is why comparing as text is the wrong choice here. Under **Fill from campaign variables**: ``` MOBILE_NO=phone CANDIDATE_NAME=name ``` The left side is your column, the right side is the variable your campaign supplies. Your agent is told not to read someone's own number back to them, so without this the number never arrives — it is known, but nobody says it out loud. ## Choose when it writes On the spreadsheet card, set **When to save** to **After the call**. For a questionnaire this is almost always right. The whole conversation is read at once, so an answer given early lands in the right column, somebody who corrects themselves is recorded once, and the caller never waits while a row is written. Leave it on **During the call** only if you are watching the sheet live and want to see rows appear as the words are spoken. ## Turn on summaries **Voice → Summarise each call.** Two or three sentences per call, on the call in **Usage → Calls** beside the transcript, and in the `Summary` column you made. The switch is what starts it — the column on its own does nothing. This is deliberate: a column should never quietly begin costing you anything. ## Ring the list **Voice Campaigns → New voice campaign.** Upload the same list as a CSV and map its columns: * **Phone number** → the column with the mobile in it * `{{lead_name}}`, `{{mobile_no}}` → the matching columns The names on the right of your **Fill from campaign variables** box must be the variables this campaign actually supplies. Map `MOBILE_NO=mobile_no` here and send `phone` there and the column stays empty — nothing errors, it simply never fills. ## Check the first call before you run the rest Ring one person. Yourself, ideally. Then: The call should read **Answered**, with a **Summary** tab beside the transcript. Their answers should be in **their existing row**, not a new one at the bottom. A new row at the bottom means the match did not happen. Check the number in the sheet is the number you dialled, and that **Update an existing row** is set to that column. A list of five hundred writing into the wrong rows is five hundred rows to clean up by hand. ## What you end up with Your original rows, filled in — one line per person rather than one per call, whatever number of times you ring them. A summary against each call for the ones worth reading about. And for the ones who said no, a record saying so, which is what stops you ringing them again next month. # Send your first broadcast Source: https://docs.thinnest.ai/guides/first-campaign Start to finish, with the mistakes that cost real money marked. Your first broadcast is the one most likely to go wrong, and the errors are expensive: WhatsApp charges for every marketing send, and a bad one costs you the number's reputation as well. Work through this in order. Three things, and each screen tells you what the next needs: * WhatsApp connected — **Connect WhatsApp** in the sidebar * At least one **approved** template * Contacts with `granted` consent and a phone number If **Contacts** shows zero consented people, stop here. A broadcast with an empty audience is not a broadcast. ``` Hi {{1}}, our monsoon sale is on — 25% off everything until Sunday. ``` **Category it as marketing.** It is promotional, and labelling it utility to bypass consent is the fastest route to getting your number restricted by Meta. Utility is for order confirmations and delivery updates, not for anything you would call a broadcast. Submit and wait for approval — usually minutes, always Meta's decision. Before any audience, use the [API](/api-reference/messages/send-message) or a one-person audience to send it to your own number. ```bash theme={null} curl -X POST https://app.thinnest.ai/api/v1/messages \ -H "Authorization: Bearer $THINNEST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "919876543210", "template": "monsoon_sale_v1", "variables": ["Priya"] }' ``` Read it on a phone. Templates that look fine in an editor routinely arrive with a blank where a name should be, or a link that wraps badly. Four lists, ordered by risk, each showing its count before you commit. | Audience | Start here? | | - | - | | **WhatsApp opted-in** | **Yes.** People who reached you on WhatsApp and consented | | All consented | Once the first one goes well | | Imported | Only if you are certain how that consent was collected | | Everyone | Rarely what you want | Start with the narrowest list. A first broadcast to 40 engaged people teaches you more than one to 4,000 strangers, and costs less to get wrong. `{{1}}` to the contact's name. A template with a placeholder cannot go out with it empty, so this is enforced rather than trusted. **A send started in the console runs from your browser.** Close the tab and the send pauses. It does not lose anything — each recipient is its own row, so it resumes and nobody is messaged twice. But it will not finish while the tab is closed. **Schedule it instead** if you want to walk away. A scheduled send has no tab and needs none. The broadcast's page separates four different facts: | | Means | | - | - | | **Sent** | WhatsApp accepted it. **Not** that it arrived | | **Delivered** | It reached the handset | | **Read** | They opened it | | **Refused** | It did not go, with Meta's own reason | Read the refusals. They are grouped by reason, and they are the most useful part of the page — a batch of "invalid number" says something about your contact data that no success count will. ## After the first one * **Delivered but not read** is a timing problem. Try a different hour. * **Read but no replies** is a message problem, not a reach problem. * **Refused for consent** is correct behaviour. Those people opted out. ## What will not work * **Multi-channel.** The picker is real; only WhatsApp sends. Telegram could and is not built. * **A/B testing.** Not built. * **Sending to a bought list.** Consent is enforced where it cannot be bypassed, and Meta punishes it harder than we do. # Make your agent sound right on the phone Source: https://docs.thinnest.ai/guides/how-your-agent-sounds Why a reply that reads well can sound wrong, and the handful of changes that fix it. An agent that writes well does not automatically speak well. The same sentence that is clear on a screen can be confusing out loud, and almost every problem comes from the same cause: **the caller hears it once, in order, and cannot go back.** These are the changes worth making, roughly in the order they matter. ## Never lead with the wrong word This is the big one. > **Yes, you can see the dentist on Monday morning.** Doctor Iyer is available > on Tuesday and Thursday. Read on a page, that corrects itself. Heard on a phone, the caller stopped listening after "yes" and is already reaching for their diary. Put the answer first: > Monday is not possible — the dentist is here Tuesday and Thursday. Add this to **How Agent should speak** on the Voice page if you see it happening: ```text theme={null} Start with the actual answer. Never begin with "yes" or "sure" if what follows is a no. ``` ## Say it the way you would say it Written shorthand does not survive being read aloud. | Written | Spoken as | | - | - | | Dr. Iyer | "Doctor" — or a stop halfway through the name | | 4pm–8pm | anything from "four pee em to eight pee em" to a mumble | | ₹1,200 | inconsistent, and often wrong | | (subject to availability) | read out flat, or lost entirely | | • three bullet points | read as one run-on sentence | The supplied instructions already cover titles and brackets. If you are quoting prices or times a lot, add: ```text theme={null} Say numbers, prices and times the way a person would say them out loud. Four p.m., not 4pm. Twelve hundred rupees, not Rs 1200. ``` ## Keep replies short, then shorter Two sentences is plenty. A caller cannot skim, and a long answer means either they interrupt you or they stop listening — usually both. The supplied instructions ask for one or two sentences. If yours are still long, the cause is normally your **agent instructions** telling it to be thorough, which is good advice for a website and bad advice for a phone. ## If callers say it talks too fast Usually it is the length, not the pace: a forty-word reply heard once, at a normal speed, feels like a lecture. Shorten the replies first (the section above), then, if it still feels rushed, move the **Speaking speed** slider a little below Normal on the Playground, under the voice. It also helps on a poor phone line and with callers listening in a second language. [Speaking speed](/channels/voice#speaking-speed) It is worth telling the agent what to do when somebody asks it to slow down, in its instructions: shorter sentences and simpler words for the rest of the call. ## Do not describe things nobody can see No lists, no links, no buttons, no "as shown below", no "click here". This is in the supplied instructions, and it is the first thing to re-add if you rewrite them from scratch. If you need to send somebody a link, the agent should offer to have it sent by message rather than reading a URL aloud. Nobody has ever successfully written down a URL from a phone call. ## Let people finish People pause mid-sentence on the phone. They are checking a calendar, or thinking, or looking for their card. An agent that treats the first half-second of silence as their turn will talk over them. If callers are being cut off, say so plainly: ```text theme={null} When they pause, let them finish. People hesitate when they are thinking or looking something up. Silence is them thinking, not them finishing. ``` ## Give it a first line that sounds like your business **Opening line** (on the Playground, under **What Agent says**) is what the caller hears the instant they connect. Answer the way your staff answer. | Instead of | Try | | - | - | | "Hello, welcome to our automated assistant." | "Sunrise Clinic, how can I help you?" | | "Thank you for calling. Please hold." | "Ravi Kitchens, good morning." | Short, names the business, invites them to speak. Anything longer and they will talk over it — which is not a failure, but it does mean nobody heard it. ## Where a new agent starts Every new agent — from scratch, from a pre-built agent, during sign-up or through the API — starts with the **Premium** voice **Kavya**, because the first call is the impression your customers get. Premium calls are billed at the Premium per-minute price — that includes an agent you create through the API without choosing a `voice`. Change the voice on the Voice page, or with `voice` on [Update Agent](/api-reference/agents/update-agent), and the agent keeps your choice. Agents you already have keep the voice they have. ## Pick the voice by listening The voice list is on the Voice page. Names tell you nothing useful, so press play and hear one. Then set it, ring the number, listen to a full sentence, and change it if it grates. Do it with your actual greeting, not "hello" — the sound of a voice reading your business name is what your customers will hear a hundred times. There are three tabs — **Standard**, **Premium** and **Studio** — with the per-minute price on each. Premium voices are warmer and cost more per minute; standard voices are what most lines run on and nobody complains. Studio is the largest catalogue and the [cloning](/channels/voice-clone) tier, on the Pro plan and above. Listen before deciding the difference is worth the difference — on a short, transactional call it often is not, and on a line where the voice *is* the brand it often is. The Premium tab now lists over two hundred more voices, each named for who it is and what it is for: *Ishita · Banking*, *Aayan · Conversational*, *Gokul · Narrator*. Hover a voice to read it: its gender, what it is for, and its **accent** (Hindi, Indian English, Hindi–English mix, Marathi, Tamil, Telugu, Kannada, Bengali, Gujarati or Punjabi). **Conversational**, **Customer care**, **Sales**, **Banking** and the like are made for a live call; **Narrator**, **News**, **Ads** and **Stories** are made for reading aloud and can sound stiff in a conversation, so start with the first kind. Where one voice exists in two accents, the accent follows its name. They cost the same as any other Premium voice. Search by accent or style (type "banking" or "tamil") to find one. Standard and Premium are available on every plan, the free trial included. Try the more expensive one early — it is easier to decide it is not worth the money once you have heard it than to wonder. Studio needs the Pro plan or above, but one voice on it previews free on every plan, so you can hear the tier before deciding whether to upgrade for it. Ignore the language a voice is listed under when you are judging it. Every voice speaks every language we support; that label is the accent it carries, not a limit on what it can say. ## When to change the model Rarely. The recommended one is chosen for how quickly it starts speaking, which matters more on a call than almost anything else. Change it if the agent is following your instructions loosely — ignoring a rule you have stated plainly, or inventing details. Some models hold a long instruction better than others. Change it back if replies get slower; a thoughtful answer that arrives late sounds like a bad line, not a careful assistant. Every model in the voice list is fast enough to hold a conversation. That is the filter — a model that pauses to think is not offered here at all, however good it is at writing. ## Test it the way it will actually be used Not from a quiet room on speakerphone. Wind, traffic and a poor signal are the normal conditions for a customer ringing a business. If your customers speak Marathi-accented English, test in Marathi-accented English. Testing in the accent the agent finds easiest tells you nothing. Talk over it mid-sentence. It should stop. If it keeps going, callers will hang up — being talked over by a recording is the exact thing people hate about phone systems. "I'd like to..." then a three-second pause. See whether it waits or jumps in. This is the single most common real-world failure and the easiest to miss when you are testing carefully. # Call leads from your CRM Source: https://docs.thinnest.ai/guides/leads-from-your-crm A lead lands in your CRM, your agent rings them within minutes, and the summary, their answers and the recording go back to the same record. A property enquiry that waits until tomorrow is a lead your competitor already called. By the end of this, every new lead in your CRM is rung by your agent — straight away in working hours, first thing next morning otherwise — and the lead's record gets a summary of the call, the answers you care about (budget, interest, site visit) and a link to the recording. It works with any CRM that can send a webhook when a lead is created — LeadSquared, Sell.Do, Kylas, HubSpot, Zoho and most others — using a connector tool such as Pabbly Connect, Make or Zapier in the middle. It takes about thirty minutes. ## What you need * An agent that answers the phone, with a number. * An API key, from **Settings → API keys**. * An account on a connector tool (Pabbly Connect, Make or Zapier). ## 1. Write the agent for this call On the agent's **Instructions**, describe the call. Use `{{…}}` for anything the CRM will tell it about the lead: ```text theme={null} You are calling {{lead_name}}, who enquired about {{project}} a few minutes ago. Confirm they are still looking, ask what budget they have in mind, whether they want a 2BHK or 3BHK, and whether they would like to visit the site this week. Keep it short and friendly. If they are busy, ask when to call back. ``` Test it from the playground with those variables filled in before a real lead hears it. ## 2. When a lead arrives, place the call In your connector tool, start a flow from your CRM's **new lead** trigger, and add an HTTP step: ```http theme={null} POST https://app.thinnest.ai/api/v1/calls Authorization: Bearer ta_live_… Content-Type: application/json Idempotency-Key: lead-{{CRM lead id}} ``` ```json theme={null} { "to": "{{CRM phone}}", "purpose": "Hello, I'm calling from Skyline Homes about your enquiry for {{CRM project}}.", "name": "{{CRM name}}", "source": "{{CRM lead source}}", "reference": "{{CRM lead id}}", "variables": { "lead_name": "{{CRM first name}}", "project": "{{CRM project}}" }, "callingHours": { "start": "10:00", "end": "19:00" }, "extract": [ { "name": "budget", "type": "number", "description": "Budget in rupees" }, { "name": "configuration", "choices": ["1BHK", "2BHK", "3BHK", "Villa"] }, { "name": "site_visit", "type": "boolean", "description": "Wants to visit the site" }, { "name": "visit_date", "type": "date" }, { "name": "interest", "choices": ["Hot", "Warm", "Cold", "Not interested"] } ], "summary": true } ``` * The lead does **not** need to be in your contacts first — it is added, with the name and source you sent. * A lead that arrives at 11pm gets `"status": "scheduled"` and is rung at 10am in their own time. Use `"ifOutsideHours": "refuse"` if you would rather decide yourself. * The `Idempotency-Key` means a CRM that fires the trigger twice still rings the lead once. Full reference: [Place a call](/api-reference/calls/place-call). ## 3. Send the results back On the agent's **Actions** page, under **Send what happens somewhere else**, add your connector tool's webhook address and tick **A call's results are ready**. Copy the signing secret. In a second flow, start from that webhook. Each call sends one delivery: ```json theme={null} { "id": "evt_9d1c…", "event": "call.analysed", "sentAt": "2026-10-07T09:12:03.551Z", "data": { "reference": "LSQ-42", "status": "completed", "summary": "Asha is keen on a 2BHK with a budget around 80 lakh and wants to visit on Saturday.", "fields": { "budget": 8000000, "configuration": "2BHK", "site_visit": true, "interest": "Hot" }, "recording": { "url": "https://app.thinnest.ai/api/v1/recordings/…", "expiresAt": "…" } } } ``` `id` is the event's id, repeated in the `x-thinnest-event-id` header. A delivery that fails is tried again (1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours) with the same `id`, so **dedupe on it** and answer 2xx quickly, or a retry will update the lead twice. Headers, retries and re-sending are in [Webhooks](/api-reference/webhooks). To stop calling someone who asked not to be called, subscribe to `contact.opted_out` too. `id` is the event's id, repeated in the `x-thinnest-event-id` header. A delivery that fails is tried again (1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours) with the same `id`, so **dedupe on it** and answer 2xx quickly, or a retry will update the lead twice. Headers, retries and re-sending are in [Webhooks](/api-reference/webhooks). To stop calling someone who asked not to be called, subscribe to `contact.opted_out` too. Then **update** the lead in your CRM: | From the call | To your CRM | | - | - | | `data.reference` | the lead to update | | `data.summary` | a note or activity | | `data.fields.budget`, `…interest`, `…site_visit` | the matching lead fields | | `data.recording.url` | a link field, or attach the file | | `data.status`, `data.hangup` | call outcome — `missed` + `no_answer` means ring again later | **Recording links expire** when your plan deletes the recording — 30 days on pay-as-you-go, 49 on Pro, 75 on Scale. If your team needs recordings for longer, have the flow download the file into your CRM rather than saving only the link. A field the lead did not answer is simply missing from `fields`, so map each one as optional. A missed call still sends a delivery, with `status: "missed"` and no summary. ## 4. Try it end to end Create a test lead in your CRM with your own number. Within a minute your phone should ring; answer as a buyer would. A few seconds after you hang up, the lead should show the note, the fields and the link. If the result does not arrive, read it directly — it is the same object: ```http theme={null} GET https://app.thinnest.ai/api/v1/calls/{id} Authorization: Bearer ta_live_… ``` ## Things worth knowing * **Who is refused.** Anyone who told your agent to stop, on any channel, and any number on your do-not-call list. `source` is your record of why you had the number — keep it honest. * **Calling rules.** Calls are only ever placed between 9am and 9pm in the lead's own time, and never earlier or later whatever you send. Promotional calling in India also needs DLT registration and DND checks on your side. * **The lead converted while the phone rang.** `DELETE /api/v1/calls/{id}` cancels a call still waiting, and ends one that is ringing or connected — see [Cancelling or ending a call](/api-reference/calls/get-call). * **A different number or voice for some leads.** Send `from` (one of the agent's numbers) or `overrides` on the call; the agent itself is not changed. * **Fields every call should fill.** Set them once on the agent as `collectFields` ([Agents](/api-reference/agents/create-agent)) instead of sending `extract` each time. * **Only answers the lead gave.** Fields are filled from what the *customer* said on that call. Nothing is guessed, and a `number` field gets a number or nothing — "80 lakh" arrives as `8000000`. # Serve customers in their own language Source: https://docs.thinnest.ai/guides/multilingual One knowledge base, 37 languages — and how templates differ from chat. Most platforms make you maintain a knowledge base per language. This one does not, and the distinction between **chat** and **templates** is where the real work is. ## Chat: nothing to do Set the agent's reply language to **match the customer** and it answers in whichever language they wrote in. Your knowledge stays in one language. The agent translates as it answers. If every customer uses the same language, choose that language instead. The agent then answers **only** in it, and on a phone call the speech recogniser listens for it, which stops a short reply from being heard as another language. If your customers use a few languages — Hindi and English, Tamil and English — tick those instead: the agent follows the customer among them, and a call only listens for them. See [Reply language](/agent/behaviour#reply-language). Write your material in whichever language you maintain best — usually English. Translating your knowledge base by hand produces drift: the Hindi version says ₹1,200 and the English one says ₹1,400, and nobody notices for months. ### The languages 37, including **all 22 of the Eighth Schedule** — Assamese, Bodo, Dogri, Konkani, Maithili, Manipuri, Santali and the rest — each in its own script in the picker, in one A–Z list. ## Templates: real work, and worth it Chat translates on the fly. **Templates cannot** — every one is submitted to Meta and approved in a specific language. Each contact carries a language. Set it on import, or from the [Contacts](/agent/leads) page. A contact with **no** language set receives your default. So adding languages is always an improvement, never a prerequisite — nothing breaks for contacts you have not got to yet. One template name, several languages. The editor can draft the translations for you, and the rule it holds to is strict: **A translation that changes the placeholders is refused, never repaired.** If `{{1}}` and `{{2}}` swap order in the Hindi version, your customer gets the order number where their name should be. That is not a translation problem you want discovered by a customer, so a variant whose placeholder count or order differs from the original is rejected outright. Always read the draft before submitting. It is a starting point, not a finished translation. Nothing changes at send time. Each contact receives the variant matching their language; anyone without one gets the default. The API takes no language parameter for the same reason — a template is identified by name, and its language is whatever it was approved in. ## A worked case A Pune business with Marathi, Hindi and English-speaking customers: | | Setup | | - | - | | Knowledge | English only, one copy | | Reply language | Match the customer | | Templates | `order_shipped_v1` in `en`, `hi`, `mr` | | Contacts | Language set at import from the delivery address state | A Marathi-speaking customer asks a question in Marathi and gets a Marathi answer drawn from English material. Their shipping notification arrives as the approved Marathi template. Nobody maintained a second knowledge base. ## Where it will not help * **Authentication templates.** Meta writes and localises one-time password wording itself; rewording gets the template rejected. * **Scanned documents in any language.** There is no OCR. # Recognise signed-in customers on a Next.js site Source: https://docs.thinnest.ai/guides/nextjs-identity A complete, copy-able implementation — server component, App Router, no secret in the browser. Twenty minutes, and your logged-in customers stop introducing themselves to your agent every time. From the agent's **Channels** page, under *Recognise signed-in customers*. ```bash .env.local theme={null} NEXT_PUBLIC_THINNEST_KEY=pk_your_public_key THINNEST_IDENTITY_SECRET=your_identity_secret ``` Note which one has `NEXT_PUBLIC_`. The public key is meant to reach the browser. **The identity secret must never.** Prefixing it would publish the one thing that stops any visitor forging any customer's identity. It renders the tag, and signs on the server. ```tsx app/_components/agent-widget.tsx theme={null} import { createHmac } from "node:crypto"; import { getSession } from "@/lib/auth"; export async function AgentWidget() { const publicKey = process.env.NEXT_PUBLIC_THINNEST_KEY; if (!publicKey) return null; const session = await getSession(); const secret = process.env.THINNEST_IDENTITY_SECRET; // Anonymous is a perfectly good state: the widget works, the visitor is // just not recognised. Never emit an id without its signature — the // server treats an unsigned id as unverified and binds nothing to it, so // it would do nothing while looking exactly like a working feature. const identity = session?.user && secret ? { id: String(session.user.id), hmac: createHmac("sha256", secret).update(String(session.user.id)).digest("hex"), email: session.user.email ?? undefined, name: session.user.name ?? undefined, phone: session.user.phone ?? undefined, } : null; return ( ``` ## Can it recognise signed-in customers? Three answers, not two. The rule is one thing: signing needs code running on the server that **renders the page**, holding a secret the visitor never sees. | Platform | Signed-in customers | Why | | - | - | - | | WordPress / WooCommerce | **Yes, today** | The plugin does it | | PrestaShop | **Yes, today** | The module does it | | Any site with a backend | **Yes** | [Four lines on your server](/install/signed-in-customers) | | Wix | **Possible, with work** | Velo gives you a Node backend and a Secrets Manager, so a small backend function can sign the member id. Not from the custom-code box | | Shopify | **Possible, with work** | Needs a [Shopify App Proxy](/install/shopify), which is on our list | | Squarespace | **No** | Runs no code on the server. Code Injection reaches the browser only, and a secret in the browser is a secret every visitor has | | Webflow | **No** | Custom code runs in the browser only. Nothing there can hold a signing secret | On the two "No" platforms, visitors stay anonymous to the agent. Everything else works normally — the agent answers, captures leads and escalates exactly as it does anywhere else. # PrestaShop Source: https://docs.thinnest.ai/install/prestashop A proper module — survives a theme change, and signs your customers. Tested against PrestaShop 1.7 and 8.x, which share the hook set this uses. ## Install Get `thinnestai.zip` from your agent's **Channels** page. **Modules → Module Manager → Upload a module**, choose the zip, then **Install**. No FTP, no theme editing. Open **Configure** and paste your public key. **Advanced Parameters → Performance**. PrestaShop caches aggressively and the widget will not appear until you do. ## Why a module rather than a paste PrestaShop's own advice for third-party scripts is to edit `footer.tpl`. That loses the widget the day you change theme, and needs FTP or the file editor to undo. A module survives a theme change, switches off from the back office, and can do the one thing a paste cannot: sign the logged-in customer server-side. ## Recognise signed-in customers Add your **identity secret** — from the same Channels page — in **Configure**. A signed-in customer is then recognised across your shop and WhatsApp as one person with one history. The secret never reaches the page; only an HMAC of the customer id does. Leave it empty and the widget still works, with visitors anonymous. The customer's email and name are also sent, unsigned and deliberately so: they decide what your inbox calls somebody, while the **id** is what decides which history the agent may read. ## Uninstalling Uninstall from the Module Manager. Your key and identity secret are deleted with it. # Shopify Source: https://docs.thinnest.ai/install/shopify A theme app embed — no code, no paste, and no theme edits to undo. Shopify is the one platform where you install nothing separately: the widget ships as a **theme app embed** inside our Shopify app. ## Turn it on At the bottom of the left panel, switch on **ThinnestAI**. Into the block's setting, then **Save**. An app embed applies to the whole theme rather than being dragged onto one template, which is what a support widget wants. It also survives a theme change, unlike a script pasted into `theme.liquid`. The key is a setting rather than something we inject at install because a public key belongs to an **agent**, and you may have several. The theme editor is the only place you can say which one answers on this storefront. ## Signed-in customers: not from the theme **A theme app embed cannot recognise your signed-in customers**, and it is worth explaining why rather than leaving you to wonder. Recognising a customer needs an HMAC of their id, computed with your identity secret. Liquid has no way to compute one, and there is nowhere in a theme extension to run code. Putting the secret in a theme setting would be worse than useless: a theme setting is readable by anyone who can view your page source, so publishing it would let any visitor forge any customer's identity. The tempting wrong answer is to emit the customer id **unsigned**. We do not. The server correctly treats an unsigned id as unverified and binds nothing to it — so it would do nothing at all while looking exactly like a working feature. Doing it properly needs a Shopify App Proxy — a route under your own domain that we serve, which can read the logged-in customer and sign. That is a piece of work rather than a setting, and it is on our list. If you need signed-in recognition on Shopify today, tell us: it moves up the list when a real merchant is waiting on it. ## Your store's data is a different thing Connecting **products, prices and order status** so the agent can answer "where is my order" is not this. That lives on the **Actions** page, and it is a separate connection. Merchants look for it here, which is why it is mentioned here. Shopify does not support dynamic client registration, so connecting your store data currently requires registering an app on Shopify's side yourself. See [Actions](/agent/actions). # Recognise signed-in customers Source: https://docs.thinnest.ai/install/signed-in-customers Four lines on your server, and a logged-in customer stops being a stranger. Optional, and the single highest-value thing you can add after the widget itself. With it, a customer who is logged in to your site is recognised by the agent — their website chat, their WhatsApp messages and their history become one customer in your inbox. Without it, every visitor is anonymous. That is the safe default, not a broken one. ## Why a signature is required Anyone can put anything in a POST body. If an unsigned user id were enough to bind somebody to stored history, a stranger could read another customer's conversations by guessing their user id. So the claim is signed with a secret only your server and we hold, and we recompute it before acting on it. A claim that does not verify is **dropped** — never downgraded to "probably them". ## The recipe Your identity secret is on the agent's **Channels** page. Keep it on your server; it must never reach the browser. ```js Node theme={null} import { createHmac } from "node:crypto"; const hmac = createHmac("sha256", process.env.THINNEST_IDENTITY_SECRET) .update(String(user.id)) .digest("hex"); ``` ```python Python theme={null} import hmac, hashlib, os digest = hmac.new( os.environ["THINNEST_IDENTITY_SECRET"].encode(), str(user.id).encode(), hashlib.sha256, ).hexdigest() ``` ```php PHP theme={null} $hmac = hash_hmac('sha256', (string) $user->id, getenv('THINNEST_IDENTITY_SECRET')); ``` Then render it into the widget tag: ```html theme={null} ``` If your site is a single-page app, call `ThinnestAgents.identify({…})` after sign-in instead. ## Which fields do what | Field | Signed | What it decides | | - | - | - | | `data-user-id` | **Yes** | Which history the agent may read. This is the one that matters | | `data-user-hmac` | — | The signature over the id | | `data-user-email` | No | What your inbox calls them | | `data-user-name` | No | What your inbox calls them | | `data-user-phone` | **Under the same signature** | Joins their WhatsApp messages to the same customer | Only the **id** is signed as an identity. Name and email are written to the record but never used to find one, so changing a display name does not mean re-signing anything. **The phone number is the field that makes WhatsApp join up**, and it is exactly why the whole claim has to be signed. A phone number is trivially guessable, so an unsigned one would hand a stranger the matching WhatsApp history. Leave it out and everything else still works. ## Per channel, not per workspace The secret belongs to a channel. If you run a brochure site and a logged-in app, install two widgets with two secrets — a key lifted from the brochure site must not be able to assert identities against the app. ## Common mistakes The digest must be of the **user id alone**. A digest of the secret itself is a constant and verifies nothing. That publishes the secret to every visitor, and the secret is the only thing stopping anyone forging any customer's identity. It is treated as unverified and bound to nothing. The widget works; the customer stays anonymous, and it looks exactly like a working feature. # WordPress and WooCommerce Source: https://docs.thinnest.ai/install/wordpress One file, three steps, and signed-in customers recognised. WooCommerce runs on WordPress, so this is the same plugin for both — there is nothing WooCommerce-specific to install. ## Install Get `thinnest-ai.php` from your agent's **Channels** page. It is a single file. WordPress accepts a single-file plugin dropped straight into `wp-content/plugins/`, which is a much shorter path than unzipping an archive into the right folder over FTP. Put it in `wp-content/plugins/`, then **Plugins → activate ThinnestAI**. **Settings → ThinnestAI**, and paste the public key from your Channels page. The field refuses anything that is not a real key rather than storing a typo — a mistyped key would otherwise render a broken tag on every page of your site with nothing to say why. The agent is now on every page. ## Recognise signed-in customers This is what the plugin can do that a pasted script tag cannot. Add your **identity secret** — from the same Channels page — in **Settings → ThinnestAI**. WordPress already knows who is reading the page, and the plugin signs that user's id server-side. The result: a logged-in customer's site chat, their WhatsApp messages and their history become one customer in your inbox, instead of a stranger every time. **The secret never reaches the page.** Only an HMAC of the user id does, and a digest of one id tells an attacker nothing about any other. Leave the field empty and everything else still works — visitors are simply anonymous, which is the safe default rather than a broken state. ## What the plugin deliberately does not do * No tracking, no phoning home, no version check. It emits one script tag. A plugin that talks to its vendor on every page load is one a security review removes. * No admin notices, no upsell, no dashboard widget. * No jQuery, no dependencies, no build step. ## Uninstalling Deactivate and delete as normal. Your key and identity secret are removed with it — leaving a signing secret in the options table of a site that uninstalled us is not ours to decide to keep. # Introduction Source: https://docs.thinnest.ai/introduction One agent, answering on your website, WhatsApp and Telegram — from your own material. ThinnestAI puts an AI agent in front of your customers on the channels they already use, answering from material you give it, and handing your team a name and a reason to call when the conversation is worth following up. ## What it actually does Pages, whole sites, sitemaps, pasted text and files. When it cannot find an answer it says so rather than inventing one. Website widget, WhatsApp and Telegram — one agent, one set of instructions, one knowledge base. Somebody who asked on your site and then messages on WhatsApp is one customer, with one history. Not just a deflected ticket: a name, a number, and a one-line note on what they wanted. Order status, stock, bookings — with the rules that keep a public chat from reaching too far. Windows, categories, template review, quality ratings and tiers. Read once, save a rejected template. ## The part worth reading twice Most support bots answer a question and the visitor leaves. This one answers the question **and** the business ends up with somebody to call back. And it does not go quiet on you. If a model provider fails mid-conversation the agent falls back to another one rather than going silent, because a customer staring at a dead chat window is worse than a slightly slower reply. ## Languages 37 languages, including **all 22 languages of the Eighth Schedule** — Assamese, Bodo, Dogri, Konkani, Maithili, Manipuri, Santali and the rest — each written in its own script in the picker. Your knowledge stays in one language. The agent translates as it answers, so there is no separate knowledge base to maintain per language. ## Where to start An agent answering on your website in about ten minutes. Workspace, agent, channel, contact — worth five minutes before you build. Some things this platform is often asked for are **not** built: there is no OCR for scanned documents, no catalogue browsing and no billing. Where a limit exists, these pages say so on the page rather than in a footnote. Your agent **does** answer phone calls now — see [Voice](/channels/voice). # Connect your assistant Source: https://docs.thinnest.ai/mcp/connect Add the ThinnestAI MCP server to Claude Code, Cursor, VS Code or Claude Desktop with an API key. You need a **server key** — create one in **Settings → API keys**. The same page shows these snippets ready to copy, under **Connect an AI assistant**. ## Pick what the assistant may do The key's access decides which tools your assistant even sees: | Key | The assistant can | | - | - | | **Build** (recommended) | Build agents, connect your API, manage contacts and webhooks, read everything, test-chat the agent — but not message anyone or place calls | | **Read-only** | Read conversations, calls, contacts and settings — for reports and dashboards | | **Full access** | Everything above, plus sending, calling and code sends — each still asking for your approval | Start with **Build**. Create a separate full-access key only when you want the assistant to send or call, and revoke it when you are done. The key can message your customers and place calls. Keep it in your assistant's config on your own machine — never in a repository, a shared document or a web page. If it leaks, revoke it in **Settings → API keys**; that takes effect immediately. Run in a terminal: ```bash theme={null} claude mcp add --transport http thinnestai https://app.thinnest.ai/api/v1/mcp \ --header "Authorization: Bearer ta_live_YOUR_KEY" ``` Check it with `claude mcp list`, or `/mcp` inside a session. Add to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project): ```json theme={null} { "mcpServers": { "thinnestai": { "url": "https://app.thinnest.ai/api/v1/mcp", "headers": { "Authorization": "Bearer ta_live_YOUR_KEY" } } } } ``` Add to `.vscode/mcp.json`: ```json theme={null} { "servers": { "thinnestai": { "type": "http", "url": "https://app.thinnest.ai/api/v1/mcp", "headers": { "Authorization": "Bearer ta_live_YOUR_KEY" } } } } ``` Clients that only run local servers connect through `mcp-remote`, which needs [Node.js](https://nodejs.org). In Claude Desktop: **Settings → Developer → Edit config**: ```json theme={null} { "mcpServers": { "thinnestai": { "command": "npx", "args": ["-y", "mcp-remote", "https://app.thinnest.ai/api/v1/mcp", "--header", "Authorization:${AUTH}"], "env": { "AUTH": "Bearer ta_live_YOUR_KEY" } } } } ``` The key is passed through `env` deliberately: some clients split arguments on spaces, which breaks `Bearer ta_live_…` written inline. ## Check it works Ask your assistant: **"List my ThinnestAI agents."** It should call `list_agents` and show them. ## If it does not connect | You see | Why | Fix | | - | - | - | | `401 Unauthorized` | The key is missing, mistyped or revoked | Copy the full key, including `ta_live_`; create a new one if it was revoked | | `405` | Something opened the address with `GET` | Use the `http` / Streamable HTTP transport — the server is POST-only and keeps no session | | A tool answers `429` | Over 240 requests a minute for the workspace, or a send/call limit | Wait the number of seconds it gives and retry | | Tools refuse with "Not run" | Working as designed — the action needs your approval | Approve it, and the assistant runs it again. See [Safety](/mcp/safety) | # Build a dashboard Source: https://docs.thinnest.ai/mcp/dashboards Have your assistant build a dashboard or an internal tool on your workspace's data. Ask your MCP-connected assistant something like: > "Build a Next.js dashboard for our ThinnestAI workspace: today's conversations > by channel, calls with their outcome and summary, and new leads. Store > everything in our Postgres and keep it up to date with webhooks." It can read the `dashboards` guide (`read_guide`) for the rules below, and use the MCP tools to set up the webhooks it needs. ## Ask for numbers directly You do not need to build anything to get a figure. With a **Read-only** key, ask your assistant: > "How many replies have we used this month, and what is our balance?" > "Which questions did the support agent fail to answer in the last 30 days?" > "How many calls did we miss last week, and what did calls cost?" It answers from `get_usage`, `get_agent_analytics`, `get_call_summary` and `get_whatsapp_summary` — the same numbers the console shows. Your dashboard's backend can call the same endpoints: see [Usage and analytics](/api-reference/usage/get-usage). ## The rules it should follow * **Server side only.** The dashboard's backend calls the [API](/api-reference/introduction) with a key from its environment. Never put the key in browser code — it can message your customers. * **Webhooks for live data, the API to backfill.** Subscribe to `call.analysed`, `lead.captured`, `conversation.escalated` and `conversation.resolved`, and write each into your own tables. Page through the lists once to fill history. * **Page with the cursor.** Lists answer `{ "items": [...], "nextCursor": ... }`, newest first; pass `cursor` back to continue, `limit` up to 100. * **Stay under 240 requests a minute.** A `429` says how long to wait. Sync incrementally rather than re-reading everything. * **Store ids as given** — they are prefixed strings (`conv_…`, `cust_…`). ## A schema to mirror into ```sql theme={null} create table conversations (id text primary key, channel text, status text, contact text, created_at timestamptz); create table messages (id text primary key, conversation text, author text, text text, created_at timestamptz); create table calls (id text primary key, agent text, status text, seconds int, summary text, fields jsonb, transcript jsonb, started_at timestamptz); create table leads (conversation text, name text, email text, phone text, note text, received_at timestamptz); ``` ## Useful reads | For | Call | | - | - | | Conversations by channel | `GET /conversations?channel=whatsapp&since=…` | | A conversation's messages | `GET /conversations/{id}/messages` | | Calls and their results | `GET /calls?since=…`, then `GET /calls/{id}` for transcript and fields | | Contacts and tags | `GET /contacts?tag=…` | # MCP server Source: https://docs.thinnest.ai/mcp/overview Run your workspace from Claude Code, Cursor or VS Code — build agents, connect your systems, read conversations, build dashboards. ThinnestAI is an **MCP server**. Connect an AI coding assistant to it with one of your API keys, and you can run the workspace by asking: > "Create a support agent for our clinic that answers in Hindi and English, and > connect it to our appointments API." > "Which customers asked about refunds this week? Draft a reply for each." > "Build me a dashboard of yesterday's calls — outcome, duration and what each > caller wanted." The assistant does the work through the same API your own code would use, so it can do exactly what your key can do — no more. ``` https://app.thinnest.ai/api/v1/mcp ``` ## What you can do with it Create agents, write their instructions, teach them from pages and text — then talk to the agent to check the change worked, with nobody contacted. Point an agent at your API so it looks things up live — your data stays in your database. See [Keep your data in your database](/mcp/own-database). Read conversations and call transcripts, find patterns, reply on WhatsApp. Place calls, queue call lists, send templates — each one only after you approve it. Have your assistant write a dashboard or an internal tool on the API. See [Build a dashboard](/mcp/dashboards). Set up webhooks so leads, escalations and call results land in your systems. ## Nothing reaches a customer without your yes Anything that messages a customer, places a call, spends money, deletes something, or decides where your customers' data is sent **asks for your approval first** — and refuses to run if your assistant tries to skip asking. See [Safety](/mcp/safety). ## What works today | Client | Works | | - | - | | Claude Code | Yes | | Cursor | Yes | | VS Code (Copilot agent mode) | Yes | | Claude Desktop, Windsurf and other local-only clients | Yes, through a small bridge that needs Node.js | | claude.ai and ChatGPT connectors | **Not yet** — they sign in with OAuth, which is not available yet | [Connect your assistant →](/mcp/connect) # Keep your data in your database Source: https://docs.thinnest.ai/mcp/own-database Use your own database and ThinnestAI's agents — the agent looks your data up live instead of us holding a copy. You do not need to upload your customer list, orders or catalogue for the agent to use them. Keep them in your own database, put a small API in front of it, and the agent calls that API live, mid-conversation, on every channel — website, WhatsApp, Telegram and phone calls. With an MCP-connected assistant you can build the whole thing in one session: the API in your app, the actions that connect it, and the webhooks that bring results back. ## What stays where Be precise about this when you explain it to your own customers. | | Where it lives | | - | - | | Your records — customers, orders, stock, CSVs | **Your database.** The agent asks your API for one answer at a time | | What your API returns for one question | Read by the agent to answer, then the conversation moves on | | The conversation itself | Passes through ThinnestAI and the AI model while the agent answers. **We keep the conversation history** — the agent re-reads it to continue a conversation — and the contact it belongs to | | What each action was called with | Kept by us as an audit trail of what the agent asked your system | | Leads, escalations, call results | Sent to your webhook as they happen | **Not available yet:** automatic deletion of our copy of conversations after a window you choose, and a full-transcript event for chat and WhatsApp (calls already have one). Do not promise either to your customers until they ship. ## 1. Build the endpoints Small and single-purpose — each one becomes one thing the agent can do. ``` GET https://api.yourapp.com/orders/{{order_id}} POST https://api.yourapp.com/appointments {"name": "{{name}}", "slot": "{{slot}}"} ``` * **https only.** Addresses inside private networks are refused. * **Authenticate** with a header you choose, e.g. `Authorization: Bearer `. We store it sealed and never show it again. * **Answer within 10 seconds** — callers on the phone are waiting. * **Return only what the agent needs.** It reads about 4,000 characters of the answer; an order's status, not the order history. * **Treat every argument as untrusted** — it came from a stranger's message. Use parameterised queries and never let an argument pick a table or a URL. ## 2. Connect them Ask your assistant, or do it step by step: 1. `create_action` — name (`get_order`), a description of **when** to call it (the agent decides from this sentence alone), the URL with placeholders, one parameter per placeholder, and your auth header. 2. `test_action` with sample arguments — a real call, through the same path the agent uses. Fix errors now, not when a customer is waiting. 3. `enable_action` — after your approval it goes live for everyone talking to the agent. 4. `update_agent` — tell the agent about it in its instructions: *"Look up the order with get\_order before answering anything about delivery."* The same is available over the API: [Actions](/api-reference/actions/create-action). ## 3. Receive what happens `create_webhook` with your endpoint and the events you want. The answer contains a **signing secret, once** — store it in your receiver's environment. Every delivery is a `POST` of `{ "id", "event", "sentAt", "data" }` — `id` is the event's `evt_…` id — with the header `x-thinnest-signature: sha256=`. Verify the raw bytes before parsing: ```ts theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(rawBody: string, header: string, secret: string): boolean { const expected = Buffer.from("sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex")); const actual = Buffer.from(header ?? ""); return expected.length === actual.length && timingSafeEqual(expected, actual); } ``` | Event | Carries | | - | - | | `lead.captured` | Name, email, phone, note | | `conversation.escalated` | The conversation, and why | | `conversation.resolved` | The conversation a teammate marked done | | `call.completed` | How a call ended, and how long it lasted | | `call.analysed` | Summary, extracted details, full transcript, recording link | | `campaign.finished` | Counts for a finished broadcast or call list | | `contact.opted_out` | A caller asked not to be called again; their number is now on your do-not-call list | Each attempt also carries `x-thinnest-event-id`, `x-thinnest-attempt`, `x-thinnest-delivered-at` and `x-thinnest-signature-v2` (the delivery time, signed). A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, so **dedupe on `x-thinnest-event-id`**: a retry can reach you after you already handled the first attempt. Answer 2xx quickly and do the work after. The full contract is in [Webhooks](/api-reference/webhooks). \| `contact.opted_out` | A caller asked not to be called again; their number is now on your do-not-call list | Each attempt also carries `x-thinnest-event-id`, `x-thinnest-attempt`, `x-thinnest-delivered-at` and `x-thinnest-signature-v2` (the delivery time, signed). A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, so **dedupe on `x-thinnest-event-id`**: a retry can reach you after you already handled the first attempt. Answer 2xx quickly and do the work after. The full contract is in [Webhooks](/api-reference/webhooks). To keep chat transcripts in your database today, fetch the messages (`list_messages`) when `conversation.resolved` or `conversation.escalated` arrives. # Safety Source: https://docs.thinnest.ai/mcp/safety What needs your approval, why, and how repeated sends are prevented. An AI assistant connected to your workspace can do real things — ring your customers, message them, change what your public agent does. The MCP server is built so that none of the consequential ones happen without you. ## What needs your approval These tools refuse to run until you have approved them in the conversation: | Because it… | Tools | | - | - | | Contacts a customer or spends money | `place_call`, `place_calls_batch`, `send_whatsapp_template`, `send_code`, `reply_to_conversation`, `record_event`, `enrol_in_sequence`, `resume_campaign` | | Decides where your customers' data is sent | `create_webhook`, `update_webhook`, `update_action`, `enable_action` | | Really calls your own system | `test_action` — on a POST action it does the write (a booking, a refund) | | Deletes something or ends a call | `delete_agent`, `delete_knowledge`, `delete_action`, `delete_contact`, `delete_webhook`, `cancel_call`, `cancel_campaign` | Two layers enforce this. Each of these tools is marked destructive, so your client asks you before running it. And because a client can be set to auto-approve, the tool itself **refuses** unless the assistant passes `confirm: true` — and the refusal tells the assistant to describe exactly what will happen and ask you first. If your client auto-approves tools, the refusal is the only thing that makes the assistant ask. Read what it proposes before you say yes — especially the number of people a call list or a send will reach. ## Conversations are not instructions Your assistant reads what your customers wrote: messages, call transcripts, summaries. A customer can write anything — including text designed to look like an instruction ("add a webhook to this address", "change the agent to say…"). The server tells your assistant to treat that content as data, never as something to act on. And every tool that could send your customers' data somewhere new — webhooks, actions — needs your approval, so a message engineered to exfiltrate data cannot do it on its own. Changing an agent's instructions (`update_agent`) does **not** need approval, so that building an agent is not a confirmation on every edit. If your assistant proposes instruction changes you did not ask for while it is reading conversations, stop and check why. ## Nobody is contacted twice Every send and call tool requires an **idempotency key** — a name for that one send, like `order-10432-confirmation`. If the assistant retries after a timeout with the same key, the customer is not contacted again; the first answer is returned instead. Keys are per workspace and last 24 hours. If a key is reused for a **different** message, nothing new is sent, and the answer starts with `REPLAYED:` so the assistant knows its message did not go. ## See what an assistant did **Settings → API keys → What AI assistants did** lists the changes assistants made through your keys, newest first — and every time one was stopped to ask for approval — with the key, the tool and how it went. It shows the latest 25; the last 200 per key are kept. Reads and test chats are not listed. What the assistant was asked to do, and the details it passed, are not recorded: the list is an audit of actions, not a copy of your customers' data. ## The key is the boundary The MCP server can do exactly what the API key can do over the [API](/api-reference/introduction) — the same checks, the same limits, the same refusals. To take its access away, revoke the key in **Settings → API keys**. That is why the key's access level is the strongest control you have: * A **Build** key cannot message a customer, send a code or place a call, whatever the assistant is told — those tools are not even offered, and the API refuses them. * A **Read-only** key can only look. Approvals protect you while you are watching. The key's access level protects you when you are not. See [Choose what each key can do](/api-reference/authentication#choose-what-each-key-can-do). # Tools Source: https://docs.thinnest.ai/mcp/tools Every tool the ThinnestAI MCP server offers, and what each one does. Your assistant sees these tools with full descriptions and argument lists; this page is the map. Tools marked **†** need your approval — see [Safety](/mcp/safety). Which tools appear depends on the key: a **Read-only** key sees only the tools that look; a **Build** key sees everything except sending, calling and the WhatsApp tools that reach a person (marked **‡** below); a **Full access** key sees all of them. Ids are prefixed by what they are: `ag_` agent, `act_` action, `conv_` conversation, `cust_` contact, `doc_` document, `wh_` webhook; calls are `out_` (placed) or `sch_` (queued). Lists are newest first and take `limit` (up to 100) and `cursor`. ## Agents | Tool | What it does | | - | - | | `list_agents` | Your agents, with every setting | | `get_agent` | One agent's settings | | `create_agent` | A new agent — only a name is required | | `update_agent` | Change instructions, greeting, model, language, voice, widget, the details it collects. The `voice` object takes `answersCalls`, `voice`, `language`, `recordCalls`, `recordDoNotCall`, `maxCallSeconds`, `ringSeconds`, `unavailableMessage`, `surfaces` and every call setting on the Voice page: `thinking`, `thinkingLines`, `thinkingDelayMs`, `thinkingRepeatMs`, `thinkingAtTurnEnd`, `turnDetection`, `turnSilenceSeconds`, `interruptWords`, `neverInterrupt`, `holdShortSpeech`, `greetingUninterruptible`, `ambience`, `ambienceVolume`, `speakingSpeed`, `endCallOnFinish`, `nudgeAfterSeconds`, `nudges`, `nudgePhrase`, `endAfterSilenceSeconds`, `endPhrases` and `callInstructions` (ranges and defaults are in [Voice](/channels/voice)). `create_agent` takes the same `voice` object. Use these exact names: a field the tool does not list is ignored. The who-is-calling lookup is not set here: it sends your callers' details to a web address, so you set it yourself on the agent's Actions page, or over the REST API | | `test_chat` | Talk to the agent as a customer and read its reply and the tools it used — nobody is contacted. See [Test chat](/api-reference/agents/test-chat) | | `delete_agent` † | Delete an agent and everything under it. Refused while the agent is on a live call | Connecting the agent to WhatsApp, a phone number or your website is still done in the console. ## Knowledge | Tool | What it does | | - | - | | `list_knowledge` | Documents the agent answers from, with their status | | `add_knowledge` | Teach it from a web page or pasted text | | `delete_knowledge` † | Remove a document | Knowledge is stored with us. For data that must stay in your database, use an action instead — see [Keep your data in your database](/mcp/own-database). ## Actions — your own API | Tool | What it does | | - | - | | `list_actions` | The API calls this agent can make, and which are on | | `create_action` | Let the agent call one of your HTTPS endpoints. Created **off** | | `test_action` † | Call it once, for real, exactly as the agent would | | `update_action` † | Change its address, arguments or headers | | `enable_action` † | Switch it on — from then on anybody talking to the agent can trigger it — or off | | `delete_action` † | Remove it and its saved credential | ## Contacts | Tool | What it does | | - | - | | `list_contacts` | Filter by `tag`, `group`, `externalId` or `phone` | | `get_contact` | One contact, with consent | | `upsert_contact` | Create, or update the contact on that number | | `update_contact` | Change a contact by id | | `delete_contact` † | Erase a contact and their conversations and calls | | `list_contact_groups` | Every [contact group](/agent/leads#groups) with its size | | `create_contact_group` | A new, empty group | | `rename_contact_group` | Rename a group; campaigns aimed at it keep reaching it | | `delete_contact_group` † | Delete a group; its contacts stay | | `add_to_contact_group` | Add up to 1,000 contacts to a group | | `remove_from_contact_group` | Take contacts out of a group | ## Conversations | Tool | What it does | | - | - | | `list_conversations` | Every channel; filter by `channel`, `status` (e.g. escalated) and `since` | | `list_messages` | A conversation's messages, without internal team notes | | `reply_to_conversation` † ‡ | Send one WhatsApp message into a conversation | | `set_conversation_status` | Resolve, hand back to the agent, or take over | | `assign_conversation` | Make it a teammate's job, by email — they are notified | | `list_members` | Your teammates, to assign to | ## WhatsApp | Tool | What it does | | - | - | | `list_templates` | Your templates, and what a send needs: how many values, button values, whether a header picture is required | | `list_sequences` | Your sequences and their ids | | `send_whatsapp_template` † ‡ | Send an approved template to one person | | `send_code` † ‡ | Send a one-time code | | `record_event` † ‡ | Report that something happened (e.g. `order.shipped`) — can start or stop sequences | | `enrol_in_sequence` † ‡ | Start a sequence for one contact | ## Calls | Tool | What it does | | - | - | | `place_call` † ‡ | Have an agent ring one person, now or later | | `place_calls_batch` † ‡ | Queue up to 200 calls | | `list_calls` | Filter by agent, status, reference, dates | | `get_call` | Summary, extracted details, full transcript, recording link | | `cancel_call` † ‡ | Cancel a queued call, or hang up a live one | | `list_models`, `list_voices`, `list_phone_numbers` | What an agent can be set up with | ## Webhooks | Tool | What it does | | - | - | | `list_webhooks` | Where events go, and whether delivery is healthy | | `create_webhook` † | Send an agent's events to your endpoint. The signing secret is shown once | | `update_webhook` † | Change the address or events, or switch it back on | | `delete_webhook` † | Stop sending | | `test_webhook` | Send a sample event and say whether it arrived | | `list_webhook_deliveries` | The last 50 deliveries — what your endpoint answered, and why one failed | ## Campaigns | Tool | What it does | | - | - | | `list_campaigns` | Broadcasts and calling campaigns with their results — sent, delivered, read, replied, failed | | `get_campaign` | One campaign's results | | `pause_campaign` | Stop a sending campaign from sending more | | `resume_campaign` † ‡ | Start a paused campaign sending again | | `cancel_campaign` † | Stop a campaign for good | | `create_campaign` ‡ | Build a broadcast or calling campaign as a draft — audience, template, blanks — with the console's checks. Nothing is sent | | `launch_campaign` † ‡ | Start a draft: messages or calls to everyone in its audience, charged from your balance | Your assistant creates the draft, tells you how many people it reaches, and launches only after you approve. Scheduling a start time is done in the console. ## Usage and analytics | Tool | What it does | | - | - | | `get_usage` | Plan, replies this month against your allowance, balance, replies per day | | `get_agent_analytics` | One agent's conversations, leads, escalations, questions it could not answer, and top questions | | `get_call_summary` | Call totals — answered, missed, minutes, cost — and a series | | `get_whatsapp_summary` | WhatsApp sent, delivered, read, failed and received, and what they cost by category, for recent days or a date range | The same numbers the console shows. All four work with a **Read-only** key. See [Usage and analytics](/api-reference/usage/get-usage). ## The rest of the console Everything else the console does is a tool too, one per API operation, with the API reference's own description — so your assistant reads the same rules you do. | Area | Tools | | - | - | | WhatsApp templates | `get_template`, `create_template`, `update_template`, `set_template_agent_send` † ‡ (whether an agent may send a template in a call; full key), `submit_template`, `delete_template` †, and the same for one-time-code templates (`…_code_template`) | | WhatsApp forms | `list_forms`, `get_form`, `create_form`, `update_form`, `publish_form`, `delete_form` † | | Sequences | `get_sequence`, `create_sequence`, `update_sequence`, `add_sequence_step`, `activate_sequence` † ‡, `pause_sequence`, `delete_sequence` † | | Phone numbers | `search_available_numbers`, `list_available_cities`, `get_phone_number`, `rent_phone_number` † ‡, `update_phone_number`, `release_phone_number` † ‡, `move_phone_number` † ‡ (to another of a developer's workspaces), `import_phone_number`, `list_carrier_accounts` | | Calls | `get_call_transcript`, `get_call_analysis`, `get_post_call_settings`, `update_post_call_settings`, `set_call_follow_up` † ‡ (the [message after a call](/agent/after-the-call)) | | SMS | `get_sms_provider`, `list_sms_templates`, `add_sms_template`, `update_sms_template`, `delete_sms_template` †, `send_test_sms` † ‡ — all but the first two need a **Full access** key | | An agent's tools | `get_action`, `list_builtin_tools`, `update_builtin_tools` † ‡, `list_tool_servers`, `get_tool_server`, `connect_tool_server` †, `sync_tool_server`, `update_tool_server_tool`, `disconnect_tool_server` †, `list_connections`, `get_connection`, `list_connection_options`, `configure_connection`, `disconnect_account` †, `get_agent_channels`, `disconnect_telegram` † | | Workspace and team | `get_workspace`, `update_workspace`, `get_member`, `update_member_role` † ‡, `remove_member` †, `list_invitations`, `get_invitation`, `invite_member` † ‡, `revoke_invitation` †, projects, teams and their members and projects, notification switches, saved replies | | Voices | `list_voice_clones`, `get_voice_clone`, `update_voice_clone`, `delete_voice_clone` † | | Money and usage | `get_dashboard`, `get_daily_usage`, `list_call_log`, `list_wallet_ledger`, `list_invoices`, `get_customers_usage` | Which key sees which tool follows the API exactly: a tool appears only if your key may make that request. **Done in the console, never by your assistant** — anything that would put a secret in its conversation: connecting an account with a key or token (Cal.com, CRMs, helpdesks, carriers), a Telegram bot's token, a tool server that needs a token, the keys for bring your own keys, and your SMS provider's keys. Also a call's recording (it is audio), cloning a voice (it needs a recording) and scheduling a campaign's start time. ## Customers — for developers If you build your own product on ThinnestAI, your customers are workspaces under yours. Manage them from your own connection: | Tool | What it does | | - | - | | `list_customers` | Your customer workspaces, newest first; filter by your own `externalId`, or deleted ones awaiting erasure | | `get_customer` | One customer, with its counts and this month's spend | | `create_customer` | A new customer workspace, charged to your wallet | | `update_customer` | Name, your `externalId`, timezone, its cap on your call lines, metadata | | `delete_customer` † | Stop its work, revoke its keys, and erase it after 30 days | | `restore_customer` | Bring a deleted customer back before its erase date | Creating, changing, deleting and restoring a customer need a **Full access** key. To work *inside* one customer, add a second server entry with the header `Thinnest-Workspace: org_…` — every tool there acts in that customer. See [Customers](/api-reference/customers). Your customers' API keys are created in the console, not by your assistant, so no key ever appears in its conversation. ## Bring your own keys | Tool | What it does | | - | - | | `get_byok` | Whether the workspace runs on its own keys (all three, its voice and language model, only its voice, or only its language model: default scope `all`, `llm_voice`, `voice` or `llm`; each agent may choose its own through `update_agent`'s `byok`), and which providers — never the keys | | `list_byok_voices` | Your voice provider's voices, with a sample when the provider hosts one | | `set_agent_byok_model` | Run one agent on a model your LLM key reaches | | `set_agent_byok_voice` | Give one agent a voice from your provider, and on Cartesia set its speed, volume, emotion, model and pronunciation dictionary | Adding provider keys and switching bring-your-own-keys on or off are done in the console, under **Settings → Your keys**. See [Bring your own keys](/api-reference/bring-your-own-keys). ## Guides `read_guide` gives your assistant two built-in how-tos: `own-database` and `dashboards`. Ask it to read the relevant one before building an integration. ## Prompts Ready-made commands your client lists — in Claude Code, type `/` to see them. Each is a worked plan with the points where you approve written in. | Prompt | What it does | | - | - | | `build_agent` | Create an agent for your business, teach it from your website, and test it | | `connect_my_database` | Let an agent look things up in your own database through your API | | `weekly_report` | Usage, calls, WhatsApp and each agent's results for the last 7 days, with what to fix | | `fix_unanswered_questions` | Find what customers asked that the agent could not answer, and draft knowledge for it | | `triage_inbox` | Summarise conversations waiting for a person and propose what to do with each | A **Read-only** key is offered only `weekly_report`. # Meta's rules, in one place Source: https://docs.thinnest.ai/meta/overview What WhatsApp allows, who decides it, and which limits are ours versus Meta's. Most of what you can and cannot do on WhatsApp is decided by Meta, not by us. Reading this section once will save you a rejected template, a paused number, or a campaign that costs three times what you expected. **Where a rule comes from matters.** If a limit is ours, we can change it. If it is Meta's, nobody in this building can — and the only useful response is to work with it. Each page here says which. ## The four rules that shape everything A customer messages you; you get 24 hours of free-form replies. Outside it, only an approved template reaches them. Meta approves or rejects each one, usually in minutes. Rejection has specific, fixable causes. Blocks and reports lower your rating. A low rating pauses templates and caps how many people you can reach. Business verification unlocks higher limits, one-time passwords and publishable forms. ## The shape of it ``` Customer messages you │ ├── within 24 hours ──► the agent replies freely, in its own words │ (free until 1 Oct 2026, then charged per reply) │ └── after 24 hours ───► only an APPROVED TEMPLATE reaches them │ ├── marketing → charged, opt-out applies ├── utility → charged, opt-out does not └── authentication → charged, needs verification ``` ## What Meta decides | | Who decides | Can we change it | | - | - | - | | Whether a template is approved | **Meta** | No | | How many people you can message a day | **Meta**, by tier | No — but tiers rise | | Whether your number is restricted | **Meta**, from quality signals | No | | What a message costs | **Meta**, by category and country | No | | Whether marketing reaches an opted-out contact | **Us**, enforcing your consent | It is enforced where it cannot be bypassed | | How many tools an agent may use | **Us** | Yes, and it is a quality decision | ## The one mistake that costs the most **Mis-categorising marketing as utility.** It looks like a way to reach people who opted out, and it works exactly once. Meta scores template categories against their actual content. A promotional message submitted as utility is rejected as `TAG_CONTENT_MISMATCH` if you are lucky, and lowers your quality rating if it slips through and gets reported. Enough of that and your number is restricted — which costs you the channel, not just the campaign. Utility means order confirmations, delivery updates and receipts. If you would call it a campaign, it is marketing. ## Where to go next What is charged, what is free, and the 72-hour free entry point. Writing one that gets approved, and fixing one that did not. Ratings, tiers, pauses and how to climb. What it unlocks and how long it takes. # Quality ratings and messaging limits Source: https://docs.thinnest.ai/meta/quality-and-limits How many people you can reach, how to reach more, and how to lose the ability. Two separate things, and people confuse them constantly: * **Messaging limit** — how many unique customers you may message in 24 hours. * **Quality rating** — how customers are reacting, which decides whether your limit rises or your templates get paused. Both belong to Meta. Neither is something we set. ## Messaging tiers | Tier | Unique customers per 24 hours | | - | - | | Starting | **250** | | Tier 1 | 2,000 | | Tier 2 | 10,000 | | Tier 3 | 100,000 | | Tier 4 | Unlimited | The limit counts **unique customers you start conversations with**, not messages. Replying inside the 24-hour window to somebody who wrote to you does not consume it. Limits apply at the business portfolio level, shared across all your phone numbers — a second number does not double your reach. ## Where to see yours * **In the console:** Connect WhatsApp → **Calls** on the number. The window states your business's daily limit, the one WhatsApp uses to decide calling. * **In Meta's WhatsApp Manager:** the messaging limit shown for your business. **You may see two different numbers.** WhatsApp Manager can still show an older **per-number** figure — often 250 — beside the business-wide limit. Meta moved limits to the business portfolio, and the business-wide figure is the one that counts, for messaging and for calling. A number showing "250" can belong to a business whose limit is already 2,000. ## Climbing **Starting → 2,000.** Either [verify your business](/meta/verification), or send 2,000 delivered messages outside customer service windows to unique customers within a rolling 30 days, using templates with a **high** quality rating. Verification is the faster path, and it unlocks other things you will want anyway. **2,000 and above** happens automatically when you meet two conditions at once: 1. You are sending high-quality messages consistently, **and** 2. You use at least half your current limit within 7 days. **Condition 2 catches people out.** A business sitting at 2,000 and sending 400 a day will never be promoted, however good its quality. Meta does not raise a limit you are not using. If a tier increase is denied, Meta notifies you with a reason — usually identity verification or delivery patterns. ## Quality rating Scored per template and per number, from what customers do: | Rating | Means | | - | - | | **High** | Minimal negative feedback | | **Medium** | Negative feedback from several customers, or poor read rates. Still sendable | | **Low** | Significant negative feedback or low engagement. At risk of suspension | The signals are **blocks**, **reports** and **read rates**. Note the third: a message nobody opens hurts you even if nobody complains. ## Pauses and disabling A template with recurring negative feedback or low read rates enters **paused** — it cannot be sent while paused. Repeat it and the template is **disabled** permanently. A paused template is a warning about that message, but the underlying quality signal attaches to your **number**. Enough paused templates and the number itself is restricted, which costs you every template rather than one. ## Staying high quality The failure mode is almost always the same: sending more than the audience wanted. A bought list is the fastest route to a restricted number. Consent is enforced where it cannot be bypassed for exactly this reason. Start with the [narrowest audience](/whatsapp/broadcasts#choosing-an-audience). A campaign to 200 engaged customers outperforms one to 4,000 indifferent ones on every metric that matters — including the one Meta scores. Two marketing messages a month is a business people tolerate. Two a week is one they block. Read rate falls first, then reports rise. Read rate is a quality signal. A message whose first line is "Dear valued customer" is a message nobody opens. Handled for you — a customer who turns off "Offers and announcements" or asks the agent to stop is recorded as withdrawn, and marketing to them is refused. Do not try to route around it. ## What to watch Check quality before a big send, not after: * **Delivered but not read** — a timing or subject problem. Change the hour before you change the audience. * **A rising refusal count for consent** — correct behaviour, but it means your list is going stale. * **A dropping read rate across campaigns** — the earliest warning you get. Act on it while the rating is still high. ## Restrictions If Meta restricts your account, sends are refused. The console shows the reason and when it lifts. **Do not retry into a restriction.** Every refused request is a signal that the business has not noticed, and a retry loop extends the restriction rather than getting through it. # Template review and approval Source: https://docs.thinnest.ai/meta/template-review How Meta reviews a template, why they get rejected, and how to fix each cause. Every template is reviewed by Meta before it can be sent. Most are decided in minutes; the rejections have specific, nameable causes, and almost all of them are formatting rather than judgement. ## The process Body text, a category, a language, and example values for every blank. Meta requires an **example value for every variable** at creation time. A template submitted with `{{1}}` and no sample of what `{{1}}` looks like is rejected before a human sees it. A machine-learning review decides most templates within minutes. Anything the automated pass cannot triage is routed to a person, which can take up to 24–48 hours. Submit templates **before** you need them. A campaign planned for Friday and a template submitted Friday morning is a campaign that may not go out. Approved templates can be sent. Rejected ones cannot, and the reason code tells you what to change. ## Why templates get rejected ### `INVALID_FORMAT` — the most common, and always fixable Nothing to do with your wording. Four causes: | Cause | Wrong | Right | | - | - | - | | Template **starts** with a variable | `{{1}}, your order shipped` | `Hi {{1}}, your order shipped` | | Template **ends** with a variable | `Your order will arrive {{2}}` | `Your order will arrive on {{2}}.` | | Variables not sequential | `{{1}}, {{2}}, {{4}}` | `{{1}}, {{2}}, {{3}}` | | Malformed braces | `{1}` or `{{1}` | `{{1}}` | **A template may not begin or end with a variable.** This one catches almost everybody, because it is not intuitive — the rule exists so a message cannot be made to look like it came from somebody else. Put a word before and after. ### `TAG_CONTENT_MISMATCH` — the category does not match the content You submitted a promotional message as utility, or the language you declared is not the language of the text. Fix by re-categorising honestly. See [the categories below](#choosing-the-right-category). ### `ABUSIVE_CONTENT` — a policy violation Content that breaches Meta's Terms of Service, Commerce Policy or Business Policy. In practice this catches: * Threatening legal action, or threatening to publicly shame a customer * Anything in a prohibited category — Meta's Commerce Policy bars a specific list including alcohol, tobacco, weapons, drugs and adult products * Requesting sensitive information (full card numbers, passwords) in the message body ### Other frequent causes `bit.ly`, `tinyurl` and similar are frequently rejected — a shortened link hides the destination, and Meta treats that as a phishing signal. Use your own domain. `Hi {{1}}, welcome to [Company Name]` — the bracketed placeholder is not a variable and reads as an unfinished template. Same content under a different name. Rejected as low quality, particularly where it makes the sender look fraudulent. A template declared `en` whose body is Hindi. Declare the language you actually wrote. ## Choosing the right category | Category | For | Charged | Reaches an opted-out contact | Needs verification | | - | - | - | - | - | | **Marketing** | Offers, announcements, re-engagement, anything promotional | Yes | **No** | No | | **Utility** | Order confirmations, delivery updates, receipts, appointment reminders — tied to a specific transaction the customer initiated | Yes | Yes, and must | No | | **Authentication** | One-time passwords only | Yes | Yes | **Yes** | **The test for utility is "did the customer do something that makes this message expected?"** An order confirmation follows an order. A "we miss you" message follows nothing, and is marketing however you label it. ## Authentication templates are special Meta writes and localises the wording for one-time passwords itself. The editor copies Meta's text exactly rather than letting you write your own — rewording it gets the template rejected. Authentication templates are also gated behind [business verification](/meta/verification). ## Limits on templates themselves | | Limit | Whose | | - | - | - | | Template name | 512 characters, lowercase letters, numbers and underscores | Meta | | Templates created per hour | 100 per WhatsApp account | Meta | | Review time | Minutes, up to 24–48 hours for human review | Meta | ## Editing an approved template Editing sends it back through review. The previous version keeps working until the new one is decided, so an edit does not take a working template offline — but a rejected edit means you are still on the old wording. For anything you send often, treat an approved template as immutable and create `..._v2` instead. That way a rejected change never blocks a working message, and you can compare how the two perform. ## After approval, it can still be taken away Approval is not permanent. Meta keeps scoring how customers react, and a template that gets blocked or reported can be paused and eventually disabled. See [Quality and limits](/meta/quality-and-limits). # Business verification Source: https://docs.thinnest.ai/meta/verification What it unlocks, what it costs you in time, and what stays locked without it. Meta verifies that your business is a real, registered entity. Several things you will want are gated behind it, and the wait is measured in days rather than minutes — so start it early. ## What it unlocks | | Without verification | With | | - | - | - | | Messaging limit | 250 unique customers / 24h | **2,000**, then automatic increases | | One-time password templates | **Blocked** | Available | | Publishing a form | **Blocked** — it builds and previews, but reaches nobody | Available | | Display name on your number | Limited | Your verified business name | **Forms are the one that surprises people.** You can build a form, store it and preview it perfectly. Meta will not *publish* a flow for an unverified business, and an unpublished form reaches no customer — so the feature looks finished and does nothing. ## What Meta asks for Broadly: a legal business name that matches official documents, a business address, a phone number or website that can be checked, and a document proving the entity exists — in India typically a GST certificate, Certificate of Incorporation, or a utility bill in the business name. The single most common cause of rejection is a **mismatch**: the name on your Meta business portfolio differing, even slightly, from the name on the document. Match them exactly, including the `Pvt Ltd` or `LLP` suffix. ## How long Days, occasionally longer. Meta may ask for more documents, which restarts the clock. Start verification on day one, before you need any of what it unlocks. It runs in the background while you build everything else, and nothing about it gets faster by waiting. ## Connecting your number **Connect WhatsApp**, in the sidebar, is self-serve — it opens Meta's own signup, you sign in with your own Facebook account, and it never touches us. See [Connecting](/channels/whatsapp#connecting) for the two ways in and which number to use. ## While you wait None of this blocks the parts of the product that matter most: No verification, no approvals, no templates. A real external channel to test against today. Nothing here involves Meta at all. Get the agent answering well on those first. A verified WhatsApp number attached to an agent that cannot answer your customers' questions is the wrong order to do this in. # Windows and what gets charged Source: https://docs.thinnest.ai/meta/windows-and-pricing The 24-hour window, the 72-hour free entry point, and why answering costs far less than starting. WhatsApp charges **per message**. Every template you send is its own charge, at a rate that depends on the recipient's country and the kind of message. The 24-hour window is a separate thing, and the two are easy to confuse: the window decides **what you are allowed to send**, not how a charge is counted. ## The 24-hour customer service window When a customer messages you, a 24-hour window opens. **Inside it**, the agent replies freely, in its own words, as many times as the conversation needs. Cards, buttons, images — anything. **Outside it**, nothing but an [approved template](/meta/template-review) reaches that customer. Not a short message, not a follow-up question. Nothing. Every customer message **resets** the window to a fresh 24 hours. A conversation that goes back and forth over three days never leaves the window. ## What gets charged | Category | When you use it | Cost | | - | - | - | | **Marketing** | Offers, launches, re-engagement | The most expensive by a wide margin | | **Utility** | Order updates, reminders, receipts | Far cheaper than marketing — and **free inside an open window**, until 1 October 2026 | | **Authentication** | One-time passcodes | Priced separately per country, in or out of the window | | **Service** | The agent's own replies, inside the window | Free until 1 October 2026 — see below | Messages your customers send you are never charged. **An open window makes two things free, not one.** Until 1 October 2026 it covers the agent's own replies *and* any utility template you send while the window is open. So an order update sent to somebody who is mid-conversation with you costs nothing, where the same template sent a day later is charged. Authentication is the exception: a one-time passcode is charged whether or not a window is open. **This is why answering is cheap and starting is expensive.** An agent handling forty inbound questions costs a fraction of forty marketing sends. Support automation pays for itself long before outbound does. Rates vary by country and change from time to time — India differs from the US by a large multiple. Your [usage page](/workspace/usage) itemises what you were actually charged, by category and country. ## Who bills you, and for what Two things are being paid for and they arrive from different places, which is the single most common question about a WhatsApp bill. **Meta charges for the message.** The rates above are Meta's, they vary by country, and they are the bulk of the cost. **ThinnestAI charges 10% of Meta's rate**, per message — the platform fee. It pays for the console, the shared inbox, templates, forms, delivery reporting and keeping your conversations. Which of us Meta invoices depends on whose WhatsApp Business Account the message went out on. | | Meta's fee | Our 10% | | - | - | - | | **Your own WhatsApp Business Account** | Meta bills the payment method on your Meta account, directly | From your ThinnestAI balance | | **A number we provide** | Meta bills us | From your ThinnestAI balance, included in the per-message price | Either way our share is the same 10% of the same rate. On your own account you will see two charges for one message — Meta's on your Meta bill, ours on your usage page — and that is expected rather than a duplicate. **Money added in one place does not cover the other.** A balance topped up here cannot pay Meta's message fees, and a payment method added on Meta does not pay our platform fee. If sending stops, check which of the two is empty: a Meta payment problem stops messages being delivered at all and [says so on your WhatsApp page](/channels/whatsapp), where an empty balance here stops campaigns before they start. Your agent's own replies are charged separately from messages — what the model costs us, plus the same 10% — and are itemised on your [usage page](/workspace/usage). Calls are priced per minute. Neither has anything to do with Meta's message rates. ## In-window messages become chargeable on 1 October 2026 **This one is worth planning for.** Two things that are free today stop being free on **1 October 2026**: * **The agent's own free-form replies** inside the 24-hour window, which is most of what it sends. * **Utility templates** sent while a window is open. Both are then charged at the ordinary utility rate for that country. A WhatsApp conversation gets more expensive on that date, and if you run pay-as-you-go your balance will drain faster than it does now. You will not have to remember this. A notice appears on your [usage page](/workspace/usage) in the run-up, and you get a notification before the date — so the first you hear of it is not a smaller balance than you expected. Nothing about how the agent works changes, and nothing you have built needs rebuilding. What changes is the arithmetic: today a long back-and-forth costs almost nothing, and after that date each of the agent's replies carries a charge. ## The free entry point When a customer arrives from a **click-to-WhatsApp ad** or a Facebook page call-to-action, and you reply within 24 hours, nothing in that conversation is charged — **for 72 hours**. Marketing templates included. Three times the normal window, at no cost, and it covers the most expensive category on the rate card. If you run WhatsApp ads at all, this changes the economics of answering quickly: a reply inside 24 hours turns a paid conversation into a free one. Free entry point conversations show in your usage. ## What this means in practice A reply within 24 hours of a click-to-WhatsApp ad buys 72 free hours. A reply on day two buys nothing. Every template is charged. Two separate utility messages an hour apart cost twice what one message covering both would. Until October 2026 an order update, a reminder or a receipt costs nothing if the customer is still inside their 24-hour window, and the ordinary rate the moment it closes. The same message, the same template, two different prices — so timing it with the conversation is worth real money on any volume. Inside the window the agent can simply answer, in its own words, for nothing. Check the inbox before sending a template to somebody you are already talking to. Far cheaper than marketing, and it reaches customers who opted out of marketing. Only where it genuinely is utility — see [the categories](/meta/template-review#choosing-the-right-category). ## What you cannot do, at any price * **Send free-form text outside the window.** No amount of paying unlocks this. * **Send marketing to somebody who opted out.** Refused, and correctly. * **Open a window by sending yourself a message.** The window is opened by the customer, or by a template you pay for. # Partner referrals Source: https://docs.thinnest.ai/partners/referrals Refer a business to ThinnestAI, and earn a share of what they spend on voice and on subscriptions. If you introduce a business to ThinnestAI, you earn a share of what that business spends — for as long as the agreement says, paid monthly, visible to you in your own panel. This page describes the **referral** track: we bill your client, they are a ThinnestAI customer, and you take commission. Reselling under your own brand is a separate arrangement — talk to us. ## Becoming a partner Signed in? Click **Become a Partner** in the sidebar. Otherwise apply at **`/partner/apply`** — company, website, the markets you sell into, and how you would sell this. **Every application is read by a person.** There is no automatic approval: being a partner means a referral code, agreed commission rates and a claim on future revenue, so somebody here decides. We come back to you by email. If we cannot take one forward we say why, and you are welcome to apply again if that changes. ## Getting set up Once approved, you have two things: * **A referral link**, like `https://app.thinnest.ai/?ref=yourcode` * **A partner code**, the `yourcode` part, for anyone you introduce by phone Then sign in at **`/partner`** to see everything below. ## How a referral is counted We remember your code in their browser for **30 days**. If they click two partners' links, the **first** one counts — whoever introduced them. The code is applied to their new workspace. There is also a **Partner code** box on the signup form, so someone you introduced over a call can type `yourcode` and still be counted. A workspace belongs to the partner who introduced it and cannot be moved to another one later. The 30-day window starts at the **first** click, not the most recent one. If someone you introduced signs up 31 days later without using the link again, ask them to enter your code on the signup form instead. **Use the exact link from your panel.** A link you build yourself pointing at our marketing site will not carry your code — only the app link does. If you would rather send someone to the marketing site first, give them your **partner code** as well and ask them to enter it at signup. ## Demo workspaces You can create up to **three demo workspaces** from your panel — somewhere to show the product to a prospect without touching a client's account. **A demo is funded by you, not by us.** It is a real workspace with a real balance that starts at zero. Top it up before you try a call or a message, or the demo will not connect. Calls and messages cost the usual rates. Because the spending is your own, a demo workspace **earns you no commission**, and it is not counted among your referred accounts or in your conversion rate. ## What earns commission A share of what a referred workspace is charged for calls. A share of the monthly or yearly Pro or Scale fee. **Nothing else earns commission.** In particular: | | | | - | - | | WhatsApp messages | **No.** These are charged at Meta's own rates and passed straight through, so there is no margin to share. | | Wallet top-up fees | No. | | Phone number rental | No. | | Website and chat replies | No. | Your exact rates are on your panel, including any that change after a set number of months. ## When the rate changes Commission rates can step down over the life of an account — for example a higher rate for the first year and a lower one after. **Those months are counted from the day the workspace signs up**, not from their first invoice. Your panel shows the referral date for every account so you can work out where each one sits. A rate that is renegotiated applies to what you earn **from then on**. Commission already earned is never recalculated. ## Your panel Sign in at `/partner`. It shows: * **How many people opened your link** in the last 90 days, and how many of them signed up — so you can tell a traffic problem from a pitch problem * **Earned, all time** and **this month** * **Accounts referred**, with each one's plan, referral date and what it has earned * **Every earning line** — the call or charge, what the client was charged, your rate, and your share * **Payouts**, with period, status and reference Every figure comes from the same ledger, so the summary, the per-account column and the lines always agree. ## Getting paid * Commission is calculated on money we have **actually collected**, after tax and payment fees. * Payouts are raised **monthly, in arrears**, for the previous period. * If a client is refunded or a payment is reversed, the matching commission is reversed — it appears as a **negative line**, so your total always matches what a payout will actually contain. * Small balances roll forward rather than being paid — a transfer costs the same whatever it carries. Ask us what your minimum payout is. ### Why the amount paid differs from the amount earned Your payout row shows four figures, and the difference between the first and the last is not a deduction we keep: | | | | - | - | | **Earned** | Commission for the period, from the ledger | | **GST** | Added, if you are GST-registered — this is what you invoice us | | **TDS** | Withheld under **section 194H** and deposited against your PAN | | **Paid to you** | Earned + GST − TDS | **TDS is your money, not ours.** It is deposited with the government against your PAN and you claim it on your return. We issue the certificate quarterly. Once your commission for the financial year (1 April to 31 March) passes **₹20,000 in total**. It is an annual threshold, not a per-payout one — so the payout that takes you over it also covers the earlier ones that were under it. That payout will carry more TDS than you expect, and the ones after it will be back to normal. If that catch-up is more than the payout itself — a small period that happens to cross the line — we withhold the whole payout and deduct the rest from your next one. A payout never goes below zero, and across the year the total withheld is the same. Because we do not have your **PAN** on file. Section 206AA requires 20% without one. Send it to us and it drops to 2% on your next payout — we cannot reverse deductions already made and deposited, so do it early. Only if you are GST-registered. If you are, give us your GSTIN and your payouts will include 18% on top of your commission, which you account for in your own return. TDS is calculated on the commission only, never on the GST. Payouts to non-residents fall under a different regime (section 195) that depends on your country's tax treaty with India. We handle those individually rather than automatically — talk to us before your first payout. ## Things worth knowing Check the referral date on your panel. A workspace only appears once it has signed up — not when someone opens your link. If they signed up without the link and without typing your code, get in touch with the workspace name and we can look into it. Commission is earned on voice minutes and Pro and Scale subscriptions. A workspace on the free plan, or one using only WhatsApp and website chat, will show as referred and earn nothing. That is expected. New signups stop being attributed to you and no further commission accrues. **Everything you have already earned is unaffected** and is paid on the normal schedule. Get in touch to sort it out. Not on the referral track — your client buys from us at our published prices and you earn a share. To set your own prices and bill your own clients under your own brand, use [White label](/white-label/overview), which comes with the Scale plan. # Quickstart Source: https://docs.thinnest.ai/quickstart An agent answering on your website, from your own pages, in about ten minutes. This gets you to a working agent on your own site. WhatsApp and Telegram come after, and neither is required to start. In the console, create a workspace and then an agent inside it. The agent's name is what customers see in the widget header, so use the name you would want a customer to read. Open **Knowledge** and add your material. The fastest start is a site crawl: paste your homepage, choose a depth of 1 to 3 levels, and pick the pages you actually want indexed before anything is ingested. ``` https://example.com ``` A crawl only follows links. If you have unlinked pages — a pricing page reachable only from an email, say — add your `sitemap.xml` instead, which reaches them. Add fewer, better pages first. Ten pages that answer real questions beat four hundred that mostly repeat your navigation. On **Settings**, write a business description — the facts the agent falls back on when a search finds nothing — and your instructions for tone. Set the reply language, or leave it on **match the customer** so it answers in whatever language the customer wrote in. Ask it the three questions your customers actually ask. This is where you tune instructions against a live reply rather than guessing. Ask it something your material does **not** cover, too. It should say it does not know. If it invents an answer instead, your business description is doing too much work. Open **Channels** and copy your public key. ```html theme={null} ``` Paste it before the closing `` tag on every page you want the agent on. On Shopify, WordPress or PrestaShop, use the plugin instead — no code, and it can recognise signed-in customers. The public key identifies which agent answers. It authorises nothing and is meant to be visible in your page source. **Inbox** shows every thread as it happens, and **Contacts** shows the people behind them. When a conversation needs a person, anyone on your team can take it over and the agent steps out of the way. ## Next Four lines on your server, and a logged-in customer stops being a stranger. The same agent, answering on WhatsApp. Look up an order, call your API, hand off to your helpdesk. An approved template to people who opted in. # Early October 2026 Source: https://docs.thinnest.ai/whats-new/early-october What changed in ThinnestAI from October 1 to 5, 2026. These are earlier changes. The newest are on [What's New](/whats-new), and the weeks before this are on [Late September](/whats-new/late-september). **New** * **Run your workspace from Claude Code, Cursor or VS Code.** ThinnestAI is now an MCP server: an AI assistant can work with your agents, actions, contacts, calls and analytics. Anything that would reach a customer waits for your approval, and a test chat contacts nobody. There is a new **MCP** tab in these docs. [MCP overview](/mcp/overview) and [Connect](/mcp/connect) * **Choose what each API key can do.** Full access, Build or Read-only, and the same level applies to the MCP server. [Authentication](/api-reference/authentication) * **Run the inbox and campaigns from the API or an assistant.** Resolve, hand back, take over and assign conversations, pause or cancel a campaign, see WhatsApp cost by category, send a test webhook and read its delivery history. [MCP tools](/mcp/tools) * **See what an assistant did.** A list of the latest changes an AI assistant made in your workspace, with reads and test chats left out. [MCP tools](/mcp/tools) * **Import Telnyx numbers.** Bring a number you already own on Telnyx, and your agent can answer it, detect voicemail and transfer calls. [Numbers from Telnyx](/channels/phone-numbers#numbers-from-telnyx) * **Finish setting up WhatsApp.** A checklist on Connect WhatsApp shows what is left, and the payment step turns red if Meta blocks the account. [Finish setting up](/channels/whatsapp#finish-setting-up) **Improved** * **WhatsApp carousels are easier to get approved.** End the message with STOP, put buttons on the cards, and keep each card to 160 characters. The form checks all of it as you type. [Carousels](/whatsapp/templates#carousels) * **A transfer on a line you brought no longer costs you our per-minute charge.** [Phone numbers](/channels/phone-numbers) * **Erasing a contact also deletes recordings of their calls** on numbers you brought. [Data and erasure](/workspace/data-and-erasure) **New** * **One agent can call out from numbers with different phone companies.** Each call dials, hangs up and transfers through its own number's provider. [Spare numbers for calling out](/channels/phone-numbers#spare-numbers-for-calling-out) * **Test calls from the Playground can ring any country,** and the card is laid out as **Call from** and **Call to**. [Voice](/channels/voice) **Improved** * **A call where nobody speaks for five minutes is ended,** instead of holding the line open. An agent with no "end after" set now hangs up when its last "are you still there?" goes unanswered. [Ending the call](/channels/voice#ending-the-call) * **Connecting your own WhatsApp account tells you that Meta bills your card,** not the balance here, before the first send. [Add a payment method in Meta](/whatsapp/payment-method) **New** * **Import Twilio numbers.** Connect your Twilio account and we set the number up there for you. Importing replaces what the number was set to before, so a number that already runs a menu or a forwarding rule loses it, and the page tells you before you do it. [Using a number you already own](/channels/phone-numbers#using-a-number-you-already-own) * **Inbound and outbound are chosen separately for each number.** They can be different agents, and either can be empty. An agent that only calls out needs no number of its own. [Inbound and outbound are separate](/channels/phone-numbers#inbound-and-outbound-are-separate) * **Export who a broadcast reached.** Download every recipient's status as a CSV. [Broadcasts](/whatsapp/broadcasts) * **See who WhatsApp's marketing limit applies to.** Contacts and the Inbox show **Marketing paused until** for anyone WhatsApp is holding marketing messages for. [Not delivered](/whatsapp/not-delivered) **Improved** * **Inbox search finds every conversation,** not only the ones already loaded, and looks inside every message. Filters also look through all conversations. [Finding a conversation](/workspace/inbox#finding-a-conversation) * **Carousel link buttons send,** and button labels can be up to 25 characters. [Buttons](/whatsapp/templates#buttons) **Fixed** * **Calls out from an imported number on another phone company now connect.** [Phone numbers](/channels/phone-numbers) **New** * **A WhatsApp Dashboard** with a date range and campaign-style tiles, and account cards on Connect WhatsApp. [The WhatsApp Dashboard](/channels/whatsapp#the-whatsapp-dashboard) * **See your blue tick status.** Connect WhatsApp shows whether each number is eligible, with Meta's five checks. [The blue tick](/channels/whatsapp#the-blue-tick) * **Set up Marketing Messages, two-step verification and display-name changes from the platform.** [Marketing Messages API](/channels/whatsapp#marketing-messages-api) * **Filter Contacts by what became of their last message,** and export the filtered list to CSV. [Contacts](/agent/leads) **Improved** * **The daily limit is explained.** WhatsApp's limit counts different people you start conversations with in a rolling 24 hours, across the whole business. The Calls window states the real figure. [Quality and limits](/meta/quality-and-limits) * **"Connect Your CRM" is now "Connect External Agent/CRM",** so the name says what it does. [External agents](/whatsapp/external-agents) **New** * **Customers can call you from WhatsApp and your agent answers.** One switch on Connect WhatsApp controls it. [WhatsApp calling](/whatsapp/calls) * **Save and submit a one-time-code template in one step,** and open a window with the code to paste into your website in cURL, Node.js, Python or PHP. [One-time codes](/whatsapp/one-time-codes) **Changed** * **Your agent's replies inside the 24-hour window are charged from October 1,** and so are utility templates sent while a window is open, at the utility rate for the country. Messages your customers send you stay free. [In-window messages](/meta/windows-and-pricing#in-window-messages-become-chargeable-on-1-october-2026) **Improved** * **A message that was not delivered says why,** in WhatsApp's words first and ours second. [When a message isn't delivered](/whatsapp/not-delivered) * **Marketing broadcasts go through Meta's marketing API,** at the same price, with nothing to switch on. [Broadcasts](/whatsapp/broadcasts#how-marketing-broadcasts-are-sent) * **The email test goes to every address** and says "accepted" rather than "delivered", because that is all we can know. [Notifications](/workspace/notifications) # What's New Source: https://docs.thinnest.ai/whats-new/index The latest changes to ThinnestAI, newest first: October 6 to 17, 2026. This is what changed in ThinnestAI, in plain words, with a link to the page that explains each thing. Earlier changes are on [Early October](/whats-new/early-october) and [Late September](/whats-new/late-september). **New** * **Reply languages on the API.** `languages` on Create Agent and Update Agent sets the console's "one, or up to four": `[]` for auto, one name for that language, two to four for the languages the agent expects, the first being its fallback. Get Agent returns it. [Behaviour](/agent/behaviour#reply-language) * **`billing` on List Calls.** Every call in the list now carries what it was billed as (the key choice, the voice plan, the per-minute rate, the billed seconds and the currency), the same object as Get Call, so reconciling a page of calls needs no call per row. * **`voice.reaskAfterSeconds` is in the API reference.** It could already be set; it was missing from the schema. [Voice](/channels/voice) **Improved** * **What a call was billed as is clearer.** A call that ended before the agent spoke, or one forwarded without an agent, is charged the carrier's fee only, in 30-second blocks. A free trial call is one paid from a free-plan workspace's trial minutes. **Fixed** * **Update Agent's own-model response.** The `409` for the agent's own model said the workspace was "not using its own voice key". It now says the language-model key is the one that is missing. **New** * **Find leads.** Describe the businesses you want to reach — "real-estate developers in Pune" — and an agent finds them and brings back their phone numbers and emails, with a link to where each came from. Save them to Contacts and a group, then call them with an agent or send them a WhatsApp broadcast. The first 50 are free in your first month; after that ₹1 a lead, and you only pay for leads that have a phone number or email. [Find leads](/agent/find-leads) * **Save a search and run it again.** Name a search to keep it on the start screen; running it again charges only for businesses you do not already have. [Find leads](/agent/find-leads#save-a-search-run-it-again) **Improved** * **Leads has two tabs.** **Find leads** is first; the people who came to your agents are on **Inbound leads**. [Leads and contacts](/agent/leads) **New** * **What each call was billed as.** Get Call and the `call.analysed` webhook now carry `billing`: whose keys the call ran on (`keys`: `off` for ours, or `all`, `voice`, `llm`, `llm_voice`), the voice plan it was priced on (`voicePlan`; null on the flat own-key prices), its per-minute rate fixed when it started (`ratePerMinuteMicro`) and the seconds it was charged for (`billedSeconds`). [Get Call](/api-reference/calls/get-call) * **Characters sent to the voice, per call.** `usage.ttsCharacters` counts the text sent to the voice to be spoken — replies, the greeting and fixed lines — on your own voice key too. It includes sentences sent before the caller interrupted, which is what voice providers charge for. [Get Call](/api-reference/calls/get-call) * **Estimated token counts are marked.** `usage.inputTokensEstimated` is `true` when part of a call's input token count was estimated because the model's provider reported none. [Get Call](/api-reference/calls/get-call) * **`contact.erased` webhook.** Sent once a contact you deleted is gone completely, recordings included — straight away, or after the nightly retry when a recording could not be deleted at once. Only the contact's id and when. [Webhooks](/api-reference/webhooks#workspace-events) * **`byok.changed` webhook.** Sent when one of your own keys is added, replaced, removed or refused by its provider, and when your own keys are switched on or off or set to cover different jobs. Never carries a key. [Webhooks](/api-reference/webhooks#workspace-events) * **Delete one call's recording and transcript.** `DELETE /calls/{id}/content` removes a call's recording, its turns, its summary and the details collected from it, and keeps the call's billing record. Get Call then shows `contentDeleted: true`. [Your data](/workspace/data-and-erasure#deleting-one-calls-recording-and-transcript) * **Keep calls for less time.** Set `callRetentionDays` on your workspace to delete calls' recordings and transcripts sooner than your plan does, from 1 day up to your plan's limit. [How long calls are kept](/workspace/data-and-erasure#how-long-calls-are-kept) * **Your customers can use your cloned voices.** Every voice a developer clones can now be used by all its customer workspaces, marked `sharedFromDeveloper` in `GET /voices`. Only the developer can rename or delete it. [Voice Clone](/channels/voice-clone#your-customers-can-use-your-voices) * **Your own voices are marked.** Each voice in `GET /byok/voices` now says `own`: `true` for a voice you cloned or designed on your provider account (Cartesia, ElevenLabs, Soniox), `false` for the provider's library. [Bring your own keys](/api-reference/bring-your-own-keys#voices) **New** * **Over two hundred more Premium voices.** Named for the voice and its style, with the accent (Hindi, Indian English, Hindi–English mix and seven more Indian languages) in the hover text beside what it is for: conversational, customer care, sales, banking, narrator, news and more. Search the Premium tab by accent or style. Voices you already use are unchanged. [Voices](/guides/how-your-agent-sounds#pick-the-voice-by-listening) * **A question with no answer is asked again.** When the agent asks something on a call and hears nothing, it asks the same question again after 2.5 seconds, once — instead of waiting in silence for your "Are you still there?". Change the wait, or turn it off, on the Voice page. Through the API, `voice.reaskAfterSeconds`. [Voice](/channels/voice#your-own-limits) * **An agent can speak a few languages.** The language picker on the Playground and Settings now takes up to four. The agent answers in whichever of them the customer speaks, and falls back to the first; on a call the speech recogniser listens only for those, so a short reply is no longer written down in a language nobody on the call speaks. [Reply language](/agent/behaviour#reply-language) * **Speaking speed.** Set how fast your agent talks on calls with a slider, from slower to faster, on the Playground, right under the voice. Most Standard and Premium voices follow it. Through the API, `voice.speakingSpeed`. [Voice](/channels/voice#speaking-speed) **Improved** * **"Are you still there?" in the call's language.** On a call where the caller had not yet said anything the agent could read the language from, it used to ask in English; it now asks in the language of its own opening line. [Voice](/channels/voice#your-own-limits) * **The agent stops while a website caller is still talking over it.** It used to finish its sentence and stop only once the caller had finished theirs. Now it stops once they have talked over it for about as long as your **Words needed to cut the agent off** — a hum or a quick "okay" still changes nothing — and if it was stopped by something that was not words, it asks the caller to go on. [Interrupting](/channels/voice#interrupting) * **Ear mode listens in real time, and phone callers can stop the agent mid-sentence too.** An agent set to **Ear** under **How agent knows the caller has finished** stops as soon as it has heard enough of the caller's words, or a "wait", usually a second or two in, instead of once they have finished. It ignores its own voice coming back down the line, lets a listener's "haan ji, bilkul" pass without stopping, and waits through a pause mid-thought instead of answering half a sentence — saying a short "haan ji" while it waits. Ear is billed at the rate of the voice, like any other call. Both behaviours have a switch and a box for your own words — **Backchannel detection** with **Caller backchannel words**, and **Agent speaks backchannel / acknowledgement while the caller explains** with **Agent backchannel phrases** — and API fields to match. [Ear: real-time speech recognition](/channels/voice#ear-real-time-speech-recognition) * **The agent goes quiet the instant it is interrupted.** On some voices it used to carry on for up to half a second before stopping. * **The agent's "haan ji" now comes while the caller is still talking.** With **Agent speaks backchannel / acknowledgement while the caller explains** on, it used to wait for a long pause that someone explaining something rarely makes, so it almost never spoke. On website calls it now says its word in the short gaps between a caller's sentences, once they have been talking for a few seconds, at most twice in a turn; it does not count as the caller being interrupted. On a phone call it still waits for a longer pause. [Ear: real-time speech recognition](/channels/voice#ear-real-time-speech-recognition) * **Replies start sooner.** The agent now starts working on its answer the moment it has heard what the caller said, while it is still making sure they have finished, so the first words come a fifth of a second or so earlier on every turn, whichever model your agent uses. If the caller was not finished, that early answer is dropped and never spoken or recorded. The short silence some voices put in front of each reply is no longer played, which saves up to most of a second on those voices. **New** * **Bring your own language model and voice together.** A new set for bring your own keys: your LLM account answers and your voice account speaks, while we keep the speech recogniser. A call is a flat ₹1 a minute, as with all three keys, on any model your key offers. Choose **My voice and my language model** in **Settings → Your keys**, or `scope: "llm_voice"` on the API. [Bring your own keys](/api-reference/bring-your-own-keys) * **Each agent chooses how it uses your keys.** The scope you pick for the workspace is the default; an agent can follow it, be switched off your keys, or use a different set your saved keys allow, so one agent can run on your language model while another uses your language model and your voice. In the console it is **How Agent uses your keys** on the agent's pages; in the API, the agent's `byok` is `null` (follow the workspace), `"off"`, or a scope, and a scope your keys cannot serve is refused with the missing key named. [Bring your own keys](/api-reference/bring-your-own-keys) * **Tokens used on every call.** Get Call and the `call.analysed` webhook now carry `usage`: the `inputTokens`, `outputTokens` and `cachedInputTokens` the call's replies used, including on your own model key, each null when not recorded. [Get Call](/api-reference/calls/get-call) * **Warm or cold, when a call is handed on.** Each agent a call can be handed to is **warm** (it gets a short summary and the conversation, and opens by saying who it is and why the caller is there) or **cold** (it starts fresh and greets the caller as on a new call). [Voice](/channels/voice#handing-a-call-on) * **Warm transfers to a person.** With **Call transfer**, the person who answers can first hear a short spoken summary of the call before the caller is connected, and with **Press 1 to accept** the caller is connected only once they press 1, so a voicemail never takes the call. Cold, as before, stays the default. [Escalation](/agent/escalation#on-a-phone-call-putting-the-caller-through) * **In the API**, `handOver.transferMode`, `handOver.acceptKey` and each hand-off agent's `mode`; `handedTo` on calls now says each hand-off's `mode` and `summary`. [Update Built-in Tools](/api-reference/tools/update-built-in-tools) **Improved** * **The Actions page is in three tabs.** **Built in**, **On calls** and **Connect a service**, with the open tab kept in the address. A line under the title shows how many tools are on, how many services are connected and how many agents calls can be handed to, and a tool that is on but missing something it needs shows an amber **Not in use yet**. [Actions](/agent/actions) * **The Voice page is in seven sections.** Calls, How Agent speaks, Listening, Sound and recording, Ending calls, Memory, and Call length and silence, with links along the top, **Save changes** always in view, and **Test a call** staying in view beside the settings on a wide screen (first, above them, on a narrower one). [Voice](/channels/voice) * **The Playground is in four cards.** Language and model, Voice, What Agent says, and Reply length. The page used to say changes save immediately; they apply once you press **Save changes**. [Behaviour](/agent/behaviour) * **More room inside an agent.** The workspace sidebar shrinks to icons while you are inside an agent, next to the agent's own menu. Open it any time with the toggle; the sidebar on every other page stays as you left it. * **Zoho CRM and Zoho Bigin sit side by side** under a **CRM** heading, like Calendar and Spreadsheet, instead of stretching across the page. * **Labels name the agent** where they said "it": **How Agent should speak**, **What Agent says**, **Message when Agent is off**. **Fixed** * **The "How Agent should speak" box on the Voice page scrolls** once it is tall, as it was always meant to. It had grown to show every rule at once. **New** * **Bring only your own language model.** Use your own LLM account, such as Gemini or OpenAI, while we keep the speech recogniser and the voice. Calls stay on the same per-minute voice plan as any other call (Standard, Premium or Studio), whatever model your key runs, and chat replies cost ₹0.02. Choose it in **Settings → Your keys**, or with `scope: "llm"` on the API. [Bring your own keys](/api-reference/bring-your-own-keys) * **Tune your own Cartesia voice for each agent.** Speed, volume, emotion, the Sonic model (including `sonic-3.6`) and a pronunciation dictionary can be set per agent in the console, through the API and in MCP, and heard in the console and the API before you save. [Tune a Cartesia voice](/api-reference/bring-your-own-keys) * **The who-is-calling lookup moved to the Actions page.** It is now a card of its own there, no longer on the Voice page; nothing about how it works changed. [Who-is-calling lookup](/channels/voice#who-is-calling-lookup) * **Choose how the agent decides the caller has finished speaking.** `voice.turnDetection` (Smart, Timer or Ear) is now on the API next to the silence setting. [Voice](/channels/voice) * **Switch off the do-not-call tool for an agent.** `voice.recordDoNotCall` and a switch on the Voice page. The `contact.opted_out` webhook now carries the words the caller used, so you can see why a number was added. [Voice](/channels/voice) and [Webhooks](/api-reference/webhooks) * **A reply in your customer's language in chat.** With **Match the customer**, website chat, WhatsApp, Telegram, the Playground and test chat now answer in the language of a message written in an Indian script. Messages typed in English letters are left to the agent's judgement. [Reply language](/agent/behaviour#reply-language) **Improved** * **The agent asks before adding someone to the do-not-call list.** "That's all, you can hang up" no longer counts as "do not call me again", and the agent quotes the caller's words when it does add a number. * **What was never heard is no longer treated as said.** When a caller talks over the agent, the agent now knows which part was cut off and the transcript is corrected to the part the caller heard where it can be. [Voice](/channels/voice) * **Stage directions are not read aloud.** Text such as `[pause]` or "(waiting for your response)", and a model's visible "thinking", is removed before the agent speaks and before it is saved, while ordinary brackets such as "(Monday to Friday)" and "\[1]" stay. * **Goodbyes in more languages.** Telugu, Tamil, Kannada, Malayalam, Bengali and Marathi closings now end a call properly. The short fixed lines the agent falls back on ("I am having trouble", a goodbye) are also written in Hindi and Telugu; if you hear a wording you would change, tell us. * **Gemini on your own key** is asked to think less on a call, for the models where Google allows it. [Bring your own keys](/api-reference/bring-your-own-keys) **Fixed** * **A reply that was retried on a backup model no longer runs together.** The same sentence could be said, and stored, twice with no space between the pieces. * **"I'll take care of that" no longer ends a call** as if it were a goodbye. **New** * **Every setting on the Voice page can now be read and changed through the API and the MCP server.** Fillers and their timing, how long a pause ends the caller's turn, how many words cut the agent off, never interrupt, the background sound and its volume, hanging up when the conversation ends, asking "are you still there?", ending a call on silence or on a phrase, and the call instructions. A read returns every value with its default filled in, and a number outside its range is refused with a message instead of being moved. [Voice](/channels/voice#the-settings) and [Update Agent](/api-reference/agents/update-agent) * **Choose where an agent answers calls.** `voice.surfaces` sets phone, your website's call button and WhatsApp calls. Adding the phone to an agent that answers calls but has no number is refused with the code `phone_number_required`. [Where it answers](/channels/voice#turn-it-on) and [Errors](/api-reference/errors) * **Your endpoint can tell a test chat from a real customer.** Every action request made during an API test chat carries an `X-Test-Chat: true` header, and `{{test.chat}}` fills as `true`, so you can answer without writing anything to your system. [During a test chat](/agent/custom-api#during-a-test-chat) * **Each model shows what a call on it costs.** `GET /models` now returns `callTier`, `callPricePerMinuteMicro` and `currency` (the currency of the wallet your calls are charged to). A model marked premium puts every call on the premium rate. A workspace using its own keys is billed the BYOK rate whatever the model. [Model](/agent/behaviour#model) * **Your own cloned voices on Cartesia.** The voice list starts with the voices you cloned and then reads the whole library, and a clone can be set on an agent. [Choose an agent's voice](/api-reference/bring-your-own-keys#choose-an-agents-voice) **Improved** * **Agent instructions can be 20,000 characters in the console too.** The box used to stop at 8,000 while the API accepted more. Conversation steps are limited separately. [Instructions](/agent/behaviour#instructions) * **A forwarded call on a number with no agent** appears in Usage, Get Call and the call list with no agent, and does not create an inbox conversation or a webhook. [Forwarding a whole line](/channels/phone-numbers#forwarding-a-whole-line) **Fixed** * **Studio voices need the Pro plan or above,** not Scale as one guide said. [How your agent sounds](/guides/how-your-agent-sounds) * **A cloned Cartesia voice that was missing from the list,** and so was refused when you set it on an agent, is listed and accepted. [Choose an agent's voice](/api-reference/bring-your-own-keys#choose-an-agents-voice) * **The background sound's volume now shows what callers actually hear.** The Voice page slider and the API report 15% (0.15) when you have not set one; the page used to show 18%. [Background sound](/channels/voice#background-sound) **New** * **Your agent knows who is calling.** When someone rings from a number saved in your Contacts, the agent is told their name and can greet them with it. It is also told to confirm who it is talking to before discussing an account, because a caller's number can be faked. [Who is calling](/channels/voice#who-is-calling) * **Look the caller up in your own system.** Give an agent a web address and we ask your system who is calling before the agent answers, so it can open with their last order or their plan. You have two seconds to reply, and the request is signed so you know it came from us. [Who-is-calling lookup](/channels/voice#who-is-calling-lookup) * **Hand a call to another of your agents.** Sales can pass a caller to support in the middle of a call. It is the same line and the same conversation, and the other agent sees everything said so far. [Handing a call on](/channels/voice#handing-a-call-on) * **Forward a whole phone line.** Choose **Forward calls** in a number's menu and every call to it rings a phone of your choice, with no agent speaking. It works whether or not an agent answers the number, so use it as a fallback while you fix or switch off an agent. [Forwarding a whole line](/channels/phone-numbers#forwarding-a-whole-line) * **Set how long outbound calls ring.** Between 10 and 60 seconds, for an agent on the Voice page or for one call through the API. Calls now stop ringing after 30 seconds unless you set it; on some carriers they used to ring longer. [How long it rings](/channels/voice#how-long-it-rings) * **Move a number between your customers.** If you build on the developer API, a number can move between your workspace and your customers' workspaces and keep its digits, from the number's menu or the API. [Moving a number](/channels/phone-numbers#moving-a-number-to-another-workspace) * **See how each call went.** Open a call in Usage to see who it was handed to or forwarded to, how long the caller waited for each reply, long silences, interruptions and any replies that needed a backup. A new `call.problem` webhook tells you the moment something goes wrong on a live call. [How a call went](/workspace/usage#how-a-call-went) * **Contact groups.** Build named lists of contacts, such as "Wholesale buyers", and aim a broadcast or a voice campaign at exactly those people. [Groups](/agent/leads#groups) **Improved** * **Every webhook event has a documented example,** and the ids in webhooks always carry their `conv_` or `cmp_` prefix, so a value from a webhook can go straight into the API. [Event payloads](/api-reference/webhooks#event-payloads) * **An agent that is on a live call cannot be deleted,** so a call is never cut off by a clean-up. The API says why in a clear error. [Errors](/api-reference/errors) * **The Buy tab on Phone Numbers stays visible on a plan that cannot buy,** and tells you why, instead of disappearing. [Getting a number from us](/channels/phone-numbers#getting-a-number-from-us) **Fixed** * **A call from your website that never connects now gives up after 45 seconds** and frees the line, instead of showing "Connecting" forever. Reloading the page ends a live call, and the "line busy" message names the limit. [A call button in your chat](/channels/voice#a-call-button-in-your-chat) **New** * **Pick a language for each agent.** An agent has one language: choose it when you create the agent, on the Playground or in Settings, or leave it on **Match the customer**. The old "second language" setting is retired. You can also rename an agent from the sidebar. [Reply language](/agent/behaviour#reply-language) * **Keep one agent off your own keys.** If your workspace brings its own provider keys, an individual agent can choose to stay on ours. [Bring your own keys](/api-reference/bring-your-own-keys) * **Erase a contact, and their recordings go too.** Erasing a contact now also removes the recordings of their calls on numbers you brought. On pay-as-you-go, you can ask us to exclude your workspace from model training. [Data and erasure](/workspace/data-and-erasure) * **Say what a switched-off line tells the caller.** When Answer calls is off, the caller hears a sentence you wrote, where the phone provider allows it. [When Answer calls is off](/channels/voice#when-answer-calls-is-off) * **Every API refusal carries a code you can act on.** Branch on `code` rather than on the wording of the message. [Errors](/api-reference/errors) * **Business details (KYC) for rented Indian numbers, by API.** Submit and follow the business details an Indian number needs, without the console. [India needs your business verified first](/channels/phone-numbers#india-needs-your-business-verified-first) **Improved** * **A WhatsApp template you sent looks the same in the inbox as it did to the customer,** with its picture, cards and buttons. [Inbox](/workspace/inbox) * **The agent speaks sooner while it looks something up,** with its own wording or a short one of ours, and it no longer says a generic "I see" just before the answer. A new setting sets the gap before a second line. [Fillers before tool run](/channels/voice#fillers-before-tool-run) * **A shorter wait before the agent's first answer on a call.** [Voice](/channels/voice) **Fixed** * **The agent no longer answers its own echo** on calls where the speech recogniser used to hear the agent's own voice as the caller. * **A caller repeating the agent's words is no longer ignored.** Someone who answers "I would like to see the pediatrician" in the agent's own words now gets a reply. [Interrupting](/channels/voice#interrupting) **New** * **An API reference with a page for every endpoint.** About 180 operations, each with typed fields, cURL, JavaScript and Python examples, a **Try it** panel and sample responses. [API reference](/api-reference/introduction) * **TypeScript and Python SDKs.** Every operation is a typed method, with retries and pagination built in. [SDKs](/api-reference/sdks) * **Text the caller.** Your agent can send an SMS during a call, and a WhatsApp message or SMS after it ends, through your own SMS provider and sender ID. [SMS](/channels/sms), [After the call](/agent/after-the-call) and [a worked guide](/guides/text-the-caller) * **The MCP server covers the whole console.** An AI assistant such as Claude Code or Cursor can now do almost anything you can do in the console, with your approval for anything that reaches a customer, including drafting and launching campaigns. [MCP tools](/mcp/tools) * **Bring only your own voice.** Use your own voice account while we supply speech recognition, the model and the phone line, at ₹1.50 a minute. Calls run on low-cost models in this mode. [Bring your own keys](/api-reference/bring-your-own-keys) * **Webhooks are signed with their delivery time** and carry an `evt_` id that stays the same across retries. [Delivery](/api-reference/webhooks#delivery) * **Failed webhooks are retried, and you can send them again** yourself. [Retries](/api-reference/webhooks#retries) **Improved** * **New agents start on the Premium voice Kavya,** because the standard voices sounded robotic on a first call. Change it any time from the voice picker. Calls on a new agent are billed at the Premium rate until you do. [Voice](/channels/voice#voice) * **New agents start on the Prana \[Voice] model.** [Model](/agent/behaviour#model) * **Replies start sooner after a caller stops speaking,** and the agent is better at telling a finished sentence from a pause. [Voice](/channels/voice) * **The call's cost is on its result in the API.** [Get Call](/api-reference/calls/get-call) **New** * **Build your own product on ThinnestAI.** Create a workspace for each of your customers, reach all of them with one key and a `Thinnest-Workspace` header, and receive one webhook for every customer. [Customers](/api-reference/customers) and [Build a platform](/guides/build-a-platform) * **White label on every paid plan.** Resell the console under your own brand and domain: 3 clients on Pay As You Go, 20 on Pro and 100 on Scale. Each client has its own plan and call lines, and starts on a free trial that you set and pay for. A reseller with no domain yet gets a free preview address. [White label](/white-label/overview), [Clients](/white-label/clients), [Brand and domains](/white-label/brand-and-domains) and [Your prices](/white-label/prices) * **Each agent picks its own model from your keys,** and a single call can name one of your voices. Speechmatics is back among the providers. [Bring your own keys](/api-reference/bring-your-own-keys) # Late September 2026 Source: https://docs.thinnest.ai/whats-new/late-september What changed in ThinnestAI from September 25 to 30, 2026. These are earlier changes. The newest are on [What's New](/whats-new), and the days just after this are on [Early October](/whats-new/early-october). **New** * **Show your products in the WhatsApp chat.** The agent can send a product from your own catalogue, a few of them, or none. You link the catalogue in Meta's Commerce Manager first. [Products](/whatsapp/products) * **Be told by email, from your own Resend account, about what you choose.** [Notifications](/workspace/notifications#receive-notifications-to-email) * **A Pro plan.** It sits between pay-as-you-go and Scale, with a monthly usage credit that expires, Studio voices and cloned voices. [Voice](/channels/voice#voice) **Improved** * **Saved replies are added and edited from the inbox,** and the composer's format, emoji and link buttons work. [Saved replies](/workspace/inbox#saved-replies) **New** * **A third voice tier, Studio.** Your own cloned voices plus a large library of more, with one voice free to preview on every plan. Studio also works through the API. [Voice](/channels/voice#voice) * **Voice Clone has its own page.** Record a sample, with consent, and your agent speaks in it. [Voice clone](/channels/voice-clone) * **Add a contact by hand.** [Contacts](/agent/leads#contacts) **Improved** * **Your business name and the blue badge are explained as the two separate approvals they are.** [Your name, and the badge](/channels/whatsapp#your-name-and-the-badge-are-two-different-approvals) **New** * **Send a template from a conversation whose window has closed.** A **Send a template** button replaces the reply box, and you fill in every blank yourself, so nobody is greeted with a template's example values. [Whether you can reply freely](/workspace/inbox#whether-you-can-reply-freely) **New** * **The 24-hour window is shown above the reply box.** A line counts down what is left, turns amber in the last hour, and is replaced by a note when the window runs out. A customer who has never written has no window, and the thread says so. [Whether you can reply freely](/workspace/inbox#whether-you-can-reply-freely) **Improved** * **A person you imported is one contact when they message,** not two. [Contacts](/agent/leads#contacts) **New** * **Record consent for people already in Contacts** with a bulk button, as well as at import, in the conversation and through the API. [Consent](/agent/leads#consent) **Improved** * **A message that was not delivered says so,** with WhatsApp's own reason, and is not charged for. [When a message isn't delivered](/whatsapp/not-delivered) * **Edit your WhatsApp business profile on Connect WhatsApp:** picture, tagline, description, address and category. [Connecting](/channels/whatsapp) **New** * **Leads and Contacts are two pages,** split by who did the reaching. [Contacts](/agent/leads#contacts) * **A number tells you when Meta has stopped it,** and why. Most often it is a failed payment method on the Meta account. [When WhatsApp stops the number sending](/channels/whatsapp#when-whatsapp-stops-the-number-sending) **Improved** * **The fee is named: a 10% ThinnestAI platform fee** on every message sent on your own WhatsApp account, shown beside Meta's own charge. [What gets charged](/meta/windows-and-pricing#who-bills-you-and-for-what) * **A voice campaign on a low balance stops redialling first,** and the last of your balance goes to people it has never tried. [When your balance runs low](/channels/voice-campaigns#when-your-balance-runs-low) # Broadcasts Source: https://docs.thinnest.ai/whatsapp/broadcasts One approved template, to everybody who agreed to hear from you. **Broadcasts** in the sidebar sends an approved [template](/whatsapp/templates) to an audience computed **at send time from consent** — never from a list somebody exported last week. The WhatsApp Business App calls this a broadcast too, and caps it at 256 contacts, reaches only people who saved your number, and gives you no scheduling, no personalisation and no delivery reports. This is the same job without any of that: no cap beyond [Meta's own messaging tier](/meta/overview), no requirement that anybody saved you, and you can see what arrived. ## It belongs to the workspace, not to an agent A broadcast is sent from your WhatsApp number, using a template that belongs to your WhatsApp Business Account, to your workspace's contacts. None of those is one agent's, so neither is the list: **every broadcast appears here**, whichever agent answers it. **Looking for calling campaigns?** They are **Voice Campaigns** in the main sidebar, listed the same way this page lists broadcasts. The difference is what you choose when you build one: a call goes out from a particular agent's number and is opened in its own words, so the builder asks which agent does the calling. See [Calling campaigns](/channels/voice-campaigns). ## Before you can send In this order: WhatsApp connected, an approved template, and contacts who have consented. Each screen in the chain says what the next one needs, and the **Send broadcast** button says which one is missing before you start rather than after you have filled the form in. ## How marketing broadcasts are sent A broadcast on a **marketing** template goes out through Meta's **Marketing Messages API**, which Meta optimises for delivery. Meta reports up to 9% more marketing messages delivered in India than the standard API, for messages people engage with. There's nothing to switch on: if your account isn't eligible yet, Meta sends it the standard way instead. The price is the same. WhatsApp still limits how many marketing messages each person receives, on every platform. If some aren't delivered, see [When a message isn't delivered](/whatsapp/not-delivered). ## Writing one **New broadcast** opens a page, not a dialog — five decisions with a live preview of what arrives between them: For you, not the customer. Nobody receiving it sees this. See below — the lists overlap, and the page says which one it settles on. Only approved ones appear. Meta refuses anything else at the moment of sending, so an unapproved template is not an option you can pick and regret. Each one takes a contact field or a fixed value. The preview updates as you type, so you read the message a real recipient gets rather than a row of braces. Now, or a time. Scheduling needs nothing left open — see **Sending** below. The page does not scroll. The two side columns do, and the preview between them stays put, because it is the thing you are checking the other two against. **From a template you just got approved**, press **Send broadcast** on its card on the [Templates](/whatsapp/templates) page. It opens this page with that template already chosen. ## Who answers the replies A broadcast produces conversations — people reply to it. **Who answers the replies** on the send form picks the agent that handles them, and defaults to the one your number already answers on. It is a real choice rather than a formality: you may want a different agent on a sale than on a delivery notice. Replies land in your [inbox](/workspace/inbox) whichever you pick, so nothing is lost if you change your mind afterwards — only who drafts the first answer. ## Choosing an audience **A broadcast only reaches people who opted in.** That is WhatsApp's rule, not ours: Meta allows a business to message someone only if they gave you their number **and** agreed to hear from you. Contacts who never opted in are left out of every broadcast, whichever list you choose. Three lists, each showing its count **before** you commit: | Audience | Who | | - | - | | Imported contacts | Contacts from a spreadsheet you uploaded, whose opt-in you confirmed when uploading | | Opted in on WhatsApp | Opted in, and reached you via WhatsApp | | Everyone who opted in | Everyone who opted in, however they arrived | You can tick more than one. The lists overlap — everybody who opted in on WhatsApp is also in *Everyone who opted in* — so ticking two sends to *Everyone who opted in*, and the page says so and shows that count as soon as you pick a second list. ## Getting people opted in Consent is collected out in the world — on your website, a form, at your counter, in a shop, over the phone — as long as what they agreed to names your business and says you will message them on WhatsApp. What the console does is **record** it. There are four ways, and they all write the same thing: the answer, the date, and where it came from. | | How | | - | - | | **A customer agrees in the conversation** | Automatic. Your agent records it when somebody says yes, in their own words, and records the opposite when they say stop. | | **Importing a spreadsheet** | Tick *These people agreed to hear from us* on the import. Only tick it if they did. | | **People already in your contacts** | The **Contacts** page says how many have not been asked, with a **Record consent** button beside it. | | **From your own system** | Our [Contacts API](/api-reference/contacts/create-contact) takes a consent status and source with each contact, so a CRM or billing system can keep it in step. | **If your contacts show as not asked, broadcasts reach nobody.** Importing a spreadsheet without ticking the box is the usual reason: the contacts save fine, and every broadcast and sequence then leaves them out, because WhatsApp only allows marketing to people who agreed. The banner on **Contacts** is there to say so before you build an audience and find it empty. ### Recording it for people already in your contacts An admin can record consent for everyone nobody has asked yet, in one go. You confirm that they agreed and say how — in writing, in person or by phone, or on your website — and that answer is stored beside each contact with today's date. It is what you would show WhatsApp if they ever ask, so it is worth being accurate about. It never touches anybody who opted out. Somebody who replied STOP stays opted out, and no bulk action can undo that. Contacts with no phone number are left alone too — a WhatsApp consent needs a WhatsApp number to apply to — and so are the people your agent spoke to, who live on [Leads](/agent/leads) and arrived by a different promise. Consent is enforced in the database. There is exactly one way an audience gets built, and it is not the interface — so a bug in a screen cannot message somebody who did not opt in, or who opted out. ### The do-not-contact list Separate from consent, and it applies to broadcasts as well as calls. Consent is about a customer you already have: they replied STOP, or they never opted in. The **do-not-contact list** is a list of *numbers* — the file you get from a regulator, a complaint, a lawyer's letter, or the CRM you used before. Most of those numbers are not in your contacts at all, so there is nothing to set a consent flag on. Add them on the **Contacts** page, with the **Do not contact** button at the top — paste them or upload a file. Nothing on that list is ever messaged or called again, on any channel. **Finding the numbers to add.** Filter your contacts by what happened to their last WhatsApp message (for example *Failed* or *Not delivered*), then choose **Export CSV**. You get exactly the people that filter shows, every page of them, with a **Do not contact** column saying who is on the list already. The file opens in Excel and Google Sheets, including names written in Telugu, Hindi or any other script. Only owners and admins can export. It is checked **at the moment each message is sent**, not when the audience was built. So a list you upload at eleven stops a broadcast that is already part way through. It also survives an import: bringing in a spreadsheet that happens to contain one of those numbers updates the contact, but it does not take the number off the list. Only removing it does that, and that needs an admin. ### Narrowing by tag or group Tags filter *within* the list you chose — they never reach past it. Picking two tags means **either**, not both. **Groups** work the same way, under **Narrow it by group**: pick one or more of the [groups you made on Contacts](/agent/leads#groups) and the broadcast goes only to people in them who are also on the list you chose. Tags and groups together mean either: anyone carrying one of the tags *or* in one of the groups. The exact number after tagging is worked out when the broadcast sends, against consent as it stands then, so the summary shows the list's own size and says the tags will narrow it. That is deliberate: somebody who withdraws consent between your writing it and its going out is not messaged. ## Variables Each blank binds to a contact field or one fixed string. A template with a placeholder cannot go out with it empty. **You cannot edit the message here**, and that is not an omission. Meta approved a specific wording; changing it means submitting a new template and waiting for review. What is yours per-send is what goes in the blanks, which is what this asks for. ## Retargeting and split tests Both optional, both at the foot of the form. Sends only to people who did something — or nothing — with a broadcast that has already gone out. *Did not reply* is the usual one and usually the one worth sending. Both halves or neither: an earlier broadcast **and** what those people did. Half-filled, it would quietly mean everybody, so the page refuses it. Splits the audience in half by a hash of the contact, so the two sides never overlap and nobody gets both. It makes **two broadcasts**, named (A) and (B), which you compare on the list. Press **Send broadcast** and both go out. Press **Save as draft** and you get two drafts to start when you are ready. ## Sending **A send started from the console runs from your browser, so that tab must stay open to finish.** It is the same client-driven loop the site crawl uses. A **scheduled** send has no tab and does not need one — it runs on our side. It also picks up any broadcast a closed tab left half-sent. Sends are resumable. Each recipient is its own row, so a crash or a closed tab **pauses** rather than loses, and nobody is messaged twice. ## Watching it The broadcast's own page shows the message as sent, every recipient with their state, the refusal reasons grouped, and a timeline. **On the Contacts page**, every contact shows what became of their last WhatsApp campaign message, and the menu above the list filters by it — the same states, plus *Never messaged*. The filter stays in the address, so a page, a reload or a shared link keeps it. **On a broadcast's own page, filter its list by what happened** — the menu above it, beside the search: Delivered, Read, Replied, Not delivered, Failed, Not contacted, Sent with no receipt yet, and Waiting, each with how many are in it. "Delivered" includes the people who went on to read or reply. It combines with the search, so you can look for one person among the undelivered. **Export CSV**, beside the search, downloads exactly the people that filter shows, every page of them. Each row has the name, the number, the status, the reason where there is one, and when the message was sent, delivered, read and replied to, on your workspace's clock. A calling campaign also gets how each call ended, the talk time and the number of dials. The file opens in Excel and Google Sheets, including names written in Telugu, Hindi or any other script. Only owners and admins can export. **Sent, delivered, read and refused are four different facts**, each carrying Meta's own reason where one applies. "Sent" means WhatsApp accepted it — not that it arrived. ## Cost The summary beside the form shows the per-message rate and what the audience would cost at it, **before** you send. It is an estimate and it lands high rather than low: Meta charges per *delivered* message, so anything that does not deliver is not charged. See [Windows and what gets charged](/meta/windows-and-pricing). Marketing templates are charged by Meta on **every** send, unlike replies inside the 24-hour window. A **utility** broadcast is different: any recipient who is still inside their own 24-hour window receives it free. So the same broadcast can cost less than its estimate, and the [usage](/workspace/usage) breakdown is what tells you which sends were actually charged. Calling campaigns are priced separately — see [Calling campaigns](/channels/voice-campaigns). Both appear line by line in [Usage](/workspace/usage), where you can also open any call from a calling campaign and read what was said. ## What is not built * **Broadcasting on any other channel.** A broadcast is WhatsApp. Instagram and Messenger cannot carry marketing outside the 24-hour window at all — Meta forbids promotional content there explicitly — Telegram does not send one yet, and website chat never will: a broadcast reaches somebody who is not there. * **Sending to people who never opted in.** Not a missing feature — buying a list and messaging it is the fastest way to lose a WhatsApp number. # WhatsApp calls Source: https://docs.thinnest.ai/whatsapp/calls Customers call your business from WhatsApp, and your agent answers. A customer taps the call button in your WhatsApp chat, and your agent picks up — the same agent, with the same voice, knowledge and tools, that answers your phone line and your website. No phone number to rent, and nothing for the customer to install: they are already in the chat. ## Turning it on 1. On **Connect WhatsApp**, press **Calls** on the number. 2. Switch on **WhatsApp calls** and save. That is all. Switching it on also turns on **Answer calls** for the agent on that number, and gives it a voice if it had none. Its **WhatsApp** row on the Voice page shows **On** — it follows this switch, and every agent has it on by default. The **WhatsApp calls** switch is the one that decides. While it is off, this number answers no calls — even if the same setting is on in Meta's WhatsApp Manager. Switching it off turns the agent's WhatsApp row off too, and leaves the website and phone as they are. ## The settings | Setting | What it does | | - | - | | **WhatsApp calls** | On or off for this number. | | **Show the call button** | Whether customers see a call button in your chat. They can still call from a message that has a call button. | | **Allow callback requests** | After a customer calls (answered or missed), WhatsApp asks them whether your business may call them back. Their answer appears in the inbox as a message — for example *Allowed you to call them back on WhatsApp, for the next 7 days*. Calling them back over WhatsApp from ThinnestAI is coming next. | | **Call hours** | When customers can call. Outside them, WhatsApp offers to chat or request a callback instead. Off means always available. | | **Closed days** | Up to 20 dates — holidays, events — when calls aren't taken. | The settings are read from WhatsApp each time you open them, so a change made in Meta's WhatsApp Manager shows up here too. Pick each time from the list, or type it however you like — `09:00`, `9 am`, `18:00` and `6 pm` all work. Switching call hours off really turns them off at WhatsApp, even if you cleared the days first. Call hours are edited one range a day here. WhatsApp allows two a day; a second range set in WhatsApp Manager is kept as it is unless you edit that day. **Customers don't see your hours listed anywhere** — not on your profile, not in the chat. They meet them when they call outside them: WhatsApp offers to chat or to request a callback, and shows when you're next available. WhatsApp applies a change straight away, but a customer's phone can take **up to 7 days** to pick it up; opening your business's chat info page on that phone refreshes it at once. To test your hours, call from another phone at a time they're closed. ## Who can use it WhatsApp only enables calling for businesses with a **daily messaging limit of at least 2,000 people** — unique people you start conversations with in a rolling 24 hours, counted across your whole business, not per number. Every new business starts lower. The quickest way up is [verifying your business](/meta/verification); otherwise Meta raises it as you send messages people engage with ([how limits rise](/meta/quality-and-limits#climbing)). The Calls window states your business's limit. If it is under 2,000, it says so over its settings and holds the Save button until it is. WhatsApp Manager may show an older **per-number** limit, often 250. That figure does not decide calling — the business-wide one does, and it is often already 2,000. While **WhatsApp calls** is off, the other settings in the window are greyed out. ## What it costs A WhatsApp call is billed at your **voice per-minute rate** — the same as a phone call — for the time your agent is on it, rounded up to the next whole minute. Calls your customers make to you cost nothing from Meta. ## Not here yet * **Your agent calling customers on WhatsApp.** Coming next: callbacks, "ring the customer now", voice campaigns and the call API will be able to use WhatsApp, for customers who have allowed calls from your business. * Video calls. If a call doesn't connect, the reason is shown in WhatsApp's words with what it means — see [WhatsApp error codes](https://thinnest.ai/whatsapp-error-codes). # Connect External Agent/CRM Source: https://docs.thinnest.ai/whatsapp/external-agents Answer WhatsApp customers from your own CRM or agent platform. ThinnestAI runs the WhatsApp side — the number, the inbox, templates and opt-outs. **Connect WhatsApp → your number → Connect External Agent/CRM.** You already have a CRM or an agent platform of your own — your own prompts, your own voice and chat stack — and you want it on WhatsApp without building a WhatsApp integration. Switch on **Connect External Agent/CRM** for a number and ThinnestAI stops answering it. Every message a customer sends is passed to your external agent, and its replies go back to the customer through us. What stays with ThinnestAI: the connected number, the shared [inbox](/workspace/inbox) where your team sees every thread and can take one over, [templates](/whatsapp/templates), [forms](/whatsapp/templates#forms), opt-outs ("STOP") and Meta's 24-hour rule. ## How a message travels 1. A customer writes to your WhatsApp number. 2. We store it in the inbox and post it to your platform's address as `message.received`. 3. Your agent decides what to say. 4. Your platform calls `POST /api/v1/conversations/{conversationId}/reply`. 5. We send it on WhatsApp and add it to the thread as the agent's reply. Your agent can take as long as it needs — nothing waits on it. A reply sent after the customer's [24-hour window](/meta/windows-and-pricing) has closed must be an approved template. ## Only want the WhatsApp API? If nothing of ours should answer — your own software does all of it — pick **WhatsApp API** when you sign up, under *What will you use it for?* You land on **WhatsApp** instead of Agents, and **every number you connect starts with Connect External Agent/CRM already on**. There is no agent to set up and no toggle to find; follow the steps under the number. Changed your mind — want voice or our AI agent too? Pick another use case from the dropdown in the top bar; your connected numbers keep answering through your software until you change them. * **Start a conversation** with an approved template: [`POST /api/v1/messages`](/api-reference/messages/send-message). WhatsApp only lets a business message someone first with a template. * **Reply** once they write back, with text, media, a form or a template: [`POST /api/v1/conversations/{id}/reply`](/api-reference/conversations/reply-to-conversation). * **Read** threads and catch up on anything missed: [conversations](/api-reference/conversations/list-conversations). You pay Meta's fee to Meta and 10% of it to us, per message. Nothing for AI — no agent of ours runs. On the **Free plan**, your first **200 messages** (one time) carry none of our charges; after that, switch to Pay as you go (no monthly fee) to keep sending. ## Set it up On **WhatsApp**, connect your own WhatsApp Business Account — a new one or one you already have. Under the number, switch on **Connect External Agent/CRM**. Our agent picker moves to **Not assigned** and locks — nothing of ours answers this number. The number then shows these same steps. Only admins can change it; switching it off hands the number back, and you choose one of our agents again. **Settings → API keys.** Send it on every call as `Authorization: Bearer ta_live_…`. See [authentication](/api-reference/authentication). Under the number, paste the HTTPS address customer messages should be posted to and press **Save**. The **signing secret** is shown once — keep it. Saving again mints a new one. Call [`POST /api/v1/conversations/{id}/reply`](/api-reference/conversations/reply-to-conversation) with the `conversationId` from the event. Until you save an address, customer messages go nowhere and nobody answers them. The WhatsApp page shows a red warning under the number when that is the case, and again if your platform starts refusing deliveries. ## What your webhook receives ```json theme={null} { "event": "message.received", "sentAt": "2026-09-21T10:15:02Z", "data": { "conversationId": "conv_e0829641-…", "messageId": "msg_91c2…", "channel": "whatsapp", "from": "919876543210", "name": "Priya", "text": "Has my order shipped?", "media": null, "form": null, "receivedAt": "2026-09-21T10:15:01Z" } } ``` * **`media`** — when the customer sent a photo, voice note, video or document: `{ "kind": "image" | "audio" | "video" | "file", "mimeType", "filename" }`. The file itself is not in the event — read the conversation's [messages](/api-reference/conversations/list-conversations) for a download link. * **`form`** — when the customer submitted a WhatsApp form: `{ "answers": { "field": "value" } }`. * **`text`** — the message, or a media message's caption. * **`messageId`** — the same id the [messages API](/api-reference/conversations/list-conversations) returns, so you can tell a message you already handled from a new one. Every delivery carries `x-thinnest-signature: sha256=…`, an HMAC-SHA256 of the raw body under your signing secret. Check it before trusting the body. Every message is sent, not just the last one in a burst. If a customer sends three messages quickly, your platform gets three events. ## What your agent can send | Send | When | Body | | - | - | - | | Text | Window open | `{ "text": "…" }` | | A photo, video, audio or document | Window open | `{ "media": { "type": "image", "url": "https://…", "caption": "…" } }` | | A WhatsApp form | Window open | `{ "form": "Delivery address" }` — its name on the Forms page | | An approved template | Any time | `{ "template": { "name": "…", "variables": ["…"] } }` | One kind per call. The full reference, with every refusal, is on [Reply to a conversation](/api-reference/conversations/reply-to-conversation). ## When a person takes over If a teammate takes a conversation over in the inbox, your agent's replies to it are refused, and its new messages are not sent to your webhook, until the conversation is handed back. That way the customer never has two voices answering at once. The teammate's exchange can be read through the [conversations API](/api-reference/conversations/list-conversations). "STOP" and other opt-outs are handled before anything reaches you. The customer is unsubscribed from marketing whatever your agent does. ## Pricing Meta bills your WhatsApp Business Account directly for its fee on each message, at [Meta's rates](/meta/windows-and-pricing). ThinnestAI charges **10% of Meta's rate** per message, from your wallet — pay as you go, with no monthly subscription. A message Meta does not charge for costs nothing from us either. Replies your teammates send from the inbox or a connected helpdesk on these numbers are charged the same way. There is no AI usage to pay for on these numbers, because your agent does the answering. ## If your endpoint misses a message Each message is sent once. One your endpoint does not accept is not retried, and the WhatsApp page shows the last failure under the number. Nothing is lost: [read back what you missed](/api-reference/conversations/list-conversations) with the conversations API. ## Not available yet * **No typing indicator or read receipts** from your agent. * **WhatsApp only.** Website chat, Telegram and voice are answered by ThinnestAI agents. # When a message isn't delivered Source: https://docs.thinnest.ai/whatsapp/not-delivered What WhatsApp said, what it means, and what to do about it. When WhatsApp doesn't deliver a message, you'll see it in two places: on the message in your [inbox](/workspace/inbox), and against the person in a [broadcast's](/whatsapp/broadcasts) results. Each one shows **Meta's own message first**, exactly as WhatsApp sent it, and then **what it means** in plain words. Meta's wording is kept so you can see the refusal came from WhatsApp, search for it, or quote it to Meta's support. A broadcast also groups its failures, so forty people refused for the same reason are one line with a count, not forty. ## The marketing limit The one you'll see most is this: > This message was not delivered to maintain healthy ecosystem engagement. (131049) **This isn't a fault with your account, your template or us.** WhatsApp limits how many *marketing* messages each person receives from **all businesses combined**, and refuses the rest. It does this on every WhatsApp platform, not just this one. It decides per person, based on things like how often they read marketing messages and how busy their inbox is. What helps: * **Wait at least 24 hours** before sending that person marketing again. Meta warns that resending sooner can keep them blocked for longer. * **Space your campaigns out.** Several offers in a few days to the same list is exactly what triggers it. * **Replies reset it.** When someone messages you, marketing you send them in the next 24 hours doesn't count toward the limit. * **Use utility templates for transactional messages.** Order confirmations, delivery updates and tickets aren't subject to the limit — as long as the template is honestly utility and not a promotion. * **Testing on your own phone counts too.** A marketing template tried several times in a day on your own number hits the limit like any other, and the later tests come back with 131049. Message your business first (that opens the 24 hours), or test on another phone. **You can see who it applies to.** For 24 hours after WhatsApp holds back a marketing message to someone, they show **Marketing paused until** a time, on the Contacts page and next to their name in the Inbox. Replies in an open conversation still reach them; it's only marketing templates that wait. To protect your customers, a marketing broadcast **holds back** anyone WhatsApp limited in the last 24 hours, rather than sending again and extending the block. They're listed as *Not contacted*, with the reason. Utility templates are never held back. ## Other reasons | Meta's code | What it means | What to do | | - | - | - | | **131050** | The person turned off marketing messages from you in WhatsApp. | Don't send them marketing unless they turn it back on. | | **131026** | The number can't receive the message — usually not on WhatsApp, or on a very old version. | Check the number with them. | | **131053** | WhatsApp couldn't fetch the picture, video or document. | Upload the file to the template again, then resend. | | **131047** | It's been over 24 hours since they last messaged you. | Send an approved [template](/whatsapp/templates) instead. | | **131056** | Too many messages to the same person in a short time. | Wait a little before messaging them again. | | **131042** | Meta couldn't charge your WhatsApp Business account's payment method. | Add a working card in Meta: [step by step](/whatsapp/payment-method). | | **131048** | Messages from your number were blocked or reported as spam. | Check your quality rating in WhatsApp Manager; send fewer, more relevant messages. | | **132015** | Meta paused the template after people blocked or reported it. | Edit it and resubmit. | | **132001** | The template isn't approved, or doesn't exist in that language. | Check its status on the Templates page. | | **130472** | WhatsApp held a marketing message back for its own experiment. | Nothing — it isn't something you can fix. | Every code has its own page in [WhatsApp error codes, explained](https://thinnest.ai/whatsapp-error-codes) — what it means, whose problem it is, how to fix it and whether to retry. A message WhatsApp didn't deliver isn't charged. If you see a code that isn't here and its explanation is missing, or the same failure keeps happening, tell us and include Meta's message. # One-time codes Source: https://docs.thinnest.ai/whatsapp/one-time-codes OTP over WhatsApp — Meta writes the message, you choose how it behaves. **One-Time Codes** in the sidebar is where you set up the template a login code goes out on. It is deliberately not part of Templates, because almost nothing on that page applies here. ## You do not write this one An authentication template is the one kind whose words are not yours. Meta writes them: > **394812** is your verification code. For your security, do not share this code. > > *Expires in 10 minutes.* You cannot change that wording, add your brand to it, or explain anything in it. That is a mercy rather than a restriction — OTP is the one message where wording is a liability, and Meta has already argued with every phishing pattern there is. What you choose is three things, and the preview beside them updates as you do. Ten by default: long enough to find your phone, short enough that a screenshot in a group chat is worthless by the time it matters. WhatsApp says this line in the customer's own language, so it costs you no words. Appends *"For your security, do not share this code."* On by default. * **Copy code** — they tap, then paste. Works everywhere, needs no integration, and is the right answer for almost everyone. * **One-tap autofill** — hands the code straight to your Android app. Needs your app's **package name and signing hash**, which the form asks for. Meta refuses the template without them, so you would otherwise find out at review rather than at login. There is no "no button" option, and that is Meta's rule rather than ours: a code template must carry exactly one button, of one of these two kinds. ## Getting it approved On the new-template page, press **Save & submit for review** to save it and send it to Meta in one step. Or press **Save as draft** — a draft is ours, not Meta's, and it cannot send anything — then open it later and press **Submit for review**. Authentication templates are usually approved within minutes, because Meta wrote the content and has little to argue with. **Submit needs a connected number.** Meta reviews a template against your WhatsApp Business Account, so until one is connected there is nothing to submit it to — the button says so on hover. Once submitted, the settings lock. Meta reviewed a specific configuration and the API offers no way to change it underneath them. To use different settings, delete it and make another. ## Sending a code From your own website's server, through the API. On any code template, press **View embed code** — on the template's page, or in the **⋯** menu of its row in the list. A window opens with the four steps (get an API key, make the code on your server, send it, check it when the customer types it in) and code you can copy in cURL, Node.js, Python and PHP, with your template's name already filled in. See also [Send a one-time code](/api-reference/one-time-codes/send-one-time-code). The short version: ```json theme={null} POST /api/v1/codes { "to": "919876543210", "code": "394812" } ``` We do not generate, store, expire or check the code. That stays yours. ## Deleting one Either from the row's **⋯** menu in the list, or from **Delete** on the template's own page. **This removes it from Meta as well as from here**, and there are two consequences worth knowing before you click: * Anything calling the API with that template's name **stops working**. * Meta will **not let the name be used again for 30 days**. If you are replacing a template, give the new one a different name. A draft that never reached Meta has neither consequence — nothing is using it, and the name stays free. ## What this costs Authentication is priced separately from marketing and utility, and a code sent to another country costs many times a domestic one — on Meta's Indian card, more than twentyfold. Your own figures are on the [usage page](/workspace/usage), split by category and country, which is the only way anyone notices a login flow that has quietly gone international. # Adding a payment method in Meta Source: https://docs.thinnest.ai/whatsapp/payment-method Why Meta blocks your number for a payment method, where to add a card, which cards work, and what happens next. If you connected **your own WhatsApp Business Account**, Meta charges WhatsApp's message fees to a card on your Meta account. Until that card works, Meta blocks the messages your business starts. You may see this on Connect WhatsApp: > There is an error with the payment method. This will block business initiated > conversations. Please add a new payment method to the account. It's quick to fix, and nothing in ThinnestAI needs to change. ## Why it happens Meta and ThinnestAI send separate bills ([how the two bills work](/meta/windows-and-pricing)): | What | Who charges it | Paid from | | - | - | - | | WhatsApp's per-message fee | Meta | The card on your **Meta** account | | Our 10% platform fee | ThinnestAI | Your ThinnestAI balance | Your ThinnestAI balance can't pay Meta. With no working card, Meta blocks the conversations **you start**: templates and broadcasts. Replies to customers who messaged you first, inside the 24-hour window, keep working. ## Which account to add it to The card goes on the **Meta business** that owns your WhatsApp Business Account, not on your personal Facebook profile. * The business is usually named after you or your company. It's the one you chose, or created, in Meta's popup when you connected WhatsApp. * If you manage several businesses in Meta, the card must be on the one that holds this WhatsApp account. A card on another business doesn't count. ## Where to add it 1. Open **[business.facebook.com/latest/billing\_hub/payment\_methods](https://business.facebook.com/latest/billing_hub/payment_methods)**, signed in with the Facebook account you used to connect WhatsApp. 2. At the top left, check that the **business that owns your WhatsApp account** is selected. Switch to it if not. 3. Click **Add payment method**. 4. Choose your country and currency (for India: India, INR), then enter your card and business details. 5. Save. If Meta asks which payment method your WhatsApp account should use, choose this card. ## Which payment methods work * A **credit or debit card**. Visa and Mastercard work most reliably. * The card must allow **online and international transactions**. Many Indian cards have these switched off by default; turn them on in your bank's app or net banking before adding the card. A card with them off is accepted at first, then fails when Meta charges it, and the block comes back. * Meta lists the options available for your country when you click **Add**. ## What happens next * Once Meta accepts the card, the block usually lifts **within minutes**. * Then **open or reload Connect WhatsApp** in ThinnestAI. That's when we ask Meta for your account's status: the warning clears straight away, and your messages go out normally. * You don't need to reconnect your number or change anything in ThinnestAI. **"I've added it" on Connect WhatsApp is a reminder, not a check.** We can't see your Meta payment details, so ticking it only stops us asking. Whether Meta can actually charge your card is what decides if messages go out, and if it can't, the warning comes back on its own. ## Also worth doing: business verification Meta may also show your business as **not yet verified**. That doesn't block you, but until it's done Meta limits how many new people you can message each day. Start it in Meta Business Settings → **Security Centre** → **Start verification**; you'll need your business documents. See [Business verification](/meta/verification) for what Meta asks for. ## Still blocked? If the warning is still there when you reload Connect WhatsApp **an hour** after adding a working card, use **Contact support** (the help icon at the top of the app) and we'll check your account's status with Meta. # Your products in the chat Source: https://docs.thinnest.ai/whatsapp/products The agent can show what you sell, with your real prices and pictures, without the customer leaving WhatsApp. When a customer asks "what do you have?" or "do you sell the 5-litre one?", the agent can answer with the products themselves rather than a paragraph about them. WhatsApp draws them from your own catalogue — your photographs, your prices, your descriptions — and the customer can tap through, add to a cart and send the order back into the same conversation. ## What you need first **A product catalogue linked to your WhatsApp Business Account**, in Meta's [Commerce Manager](https://business.facebook.com/commerce). This is the one part we cannot do for you: Meta only lets the business that owns the catalogue link it. If you sell on Shopify or WooCommerce you very likely have a catalogue already — those platforms publish one to Meta for you, and linking it is a setting rather than a piece of work. We never copy your catalogue. Prices and pictures are rendered by WhatsApp from your catalogue at the moment the message is opened, so a customer cannot be shown a price you changed last week. ## Turning it on On the agent's **Channels** page, under the WhatsApp number, switch on **Show products from your catalogue**. With the switch on and no catalogue linked, nothing happens — the agent is simply never offered the ability, rather than trying and failing in front of a customer. ## What the customer sees | When the agent | They get | | - | - | | Knows which product they mean | That one product, with its price, picture and an **Add to cart** button | | Has a few that fit | A list they can scroll, each tappable | | Has nothing specific to show | Your whole catalogue, opened inside the chat | All three carry the agent's own words above them, so it still sounds like a conversation rather than a brochure. ## Naming particular products To show *specific* products rather than the whole catalogue, the agent needs to know your product codes — the SKUs as they appear in your catalogue — and what each one is. Meta does not let us read the contents of your catalogue, only the fact that you have one, so this short list is something you tell us. Send us the codes and names and we will set them against your agent. Until then the agent opens the whole catalogue, which is the useful half and needs nothing from you. A code that does not exist in your catalogue makes WhatsApp refuse the entire message, so the agent only ever names codes from your list and quietly skips anything else. If a product never appears when it should, the code we have is probably not the one in Commerce Manager. ## Orders An order the customer sends back arrives in your [inbox](/workspace/inbox) as part of the conversation, listing what they chose as `2 x SKU-CODE` — the codes from your own catalogue, since Meta does not let us read the names against them. Nothing is charged and nothing is reserved: a catalogue order is the customer saying what they want, not a sale. It is yours to price, confirm and fulfil, exactly as if they had typed it out. ## Where this is charged A product message is an ordinary WhatsApp message inside the customer's [24-hour window](/meta/windows-and-pricing) — the same price as any other reply, with no surcharge for the products in it. Starting a conversation with products is not possible: outside the window WhatsApp carries only an approved [template](/whatsapp/templates). # Send a message Source: https://docs.thinnest.ai/whatsapp/send One approved template, to a handful of numbers, without building a campaign. **Workspace → WhatsApp → Send.** Some messages are not a campaign. Telling four people their slot moved, or sending yourself a test before a launch, does not need an audience, a schedule and a spend — it needs one template and a phone number. ## Sending one 1. **Pick a template.** Only [templates](/whatsapp/templates) Meta has approved appear. An unapproved one cannot be sent, so listing it would only waste your time. 2. **Add the numbers.** Type one and press Enter to turn it into a tag. Up to **ten** at a time. Country code, spaces and dashes are all fine — the number is read the same way an imported contact is. 3. **Fill the values.** Each `{{1}}`, `{{2}}` in the template gets a box, pre-filled with the sample you wrote. Edit or leave them. 4. **Add the picture, if the template has one.** A template with an image, video or document header needs that file on **every** send — WhatsApp does not reuse the one it was approved with. The box is pre-filled with the template's own, and WhatsApp fetches it itself, so it has to be a public address. 5. **Fill the link, if a button has one.** A button whose link ends in a placeholder — an order number, a booking reference — gets its own box. 6. **Check the preview**, which shows the message as the customer will read it. 7. **Send.** A **carousel** template can be sent here too. Its cards go out with the pictures and links they were approved with — there is nothing to fill per card. Each number is sent to one at a time, and each gets its own result — so if one is refused and three go, you are told exactly that rather than "failed". The same number typed twice in different formats — `+91 98765 43210` and `919876543210` — is one person, and gets one message. ## The handler Before sending you can choose **which agent manages the chat**. Left alone, the chat is handled by the agent that owns the number. Choose a different one and that agent answers when the customer replies — not just for this message, but for the conversation that follows. This is how a chat reaches an agent that has no number of its own: a specialist that handles bookings, say, while the number itself belongs to your main agent. ## Who you have messaged The panel beside the composer lists **every WhatsApp number you have a conversation with**, newest first. Open one to read the thread without leaving the page, or jump straight to it in the [inbox](/workspace/inbox) to reply, take over or see the contact's details. A message you send appears there immediately, and replies land in your inbox like any other conversation. ## What it will refuse * **A customer who has opted out**, when the template is marketing. Utility and authentication still reach them, and must — see [Opting out](/channels/whatsapp#opting-out). * **A template Meta has not approved.** * **A media template with no picture**, saying so by name. WhatsApp refuses those outright, so it is better caught here than reported as a failed send. * **Anything at all**, if no number is connected. The page says so at the top and links to [Connect WhatsApp](/channels/whatsapp#connecting), which is where you fix it. ## When to use a campaign instead | | Send | [Broadcasts](/whatsapp/broadcasts) | | - | - | - | | Who | Up to ten numbers you type | An audience you define | | When | Now | Now or scheduled | | Cost known first | No | Yes, priced before it runs | | Retries a failure | No | Yes, per person | | Record of who got what | The conversation | A full report | Ten is a deliberate ceiling. Past that you want the things a campaign gives you, and the most expensive mistake in messaging is sending to more people than you meant to. # Sequences Source: https://docs.thinnest.ai/whatsapp/sequences A series of messages triggered by something that happened, not by a date. A campaign is one message to a list. A **sequence** is a series of messages triggered by an event — somebody abandoned a cart, placed an order, or asked about a product and never came back. ## How it works An event arrives — from your systems via the API, or from something the customer did. Consent is checked at enrolment, and again at each send. "Immediately", "after 2 days", "after a week". Each step is an approved template. A customer who replies, converts or opts out leaves the sequence. ## Sending events Events come in through the API. See the [API reference](/api-reference/introduction). ## Variables Each step's template blanks bind to contact fields or values carried on the enrolment — so an abandoned-cart message can name the actual product. ## Consent, again WhatsApp only allows messages to people who opted in, so who can be enrolled depends on how they arrive: | How they are enrolled | Who can be | | - | - | | Enrolling a list from the console | Only people who opted in | | A tag that triggers the sequence | Only people who opted in | | An event or enrolment from your systems, [via the API](/api-reference/events/report-event) | Anyone you send — the call is you confirming they opted in | Opting out always wins, on every route: somebody who opted out is never enrolled, and opting out is re-checked at each send. A customer who opts out on day one of a seven-day sequence does not receive day three. ## Metering Sequence sends are metered like any other template send. Most are charged. A **utility** step is the exception: until 1 October 2026, if it reaches somebody who is still inside their own 24-hour window — which a drip step often does, since it goes out to people who replied recently — Meta gives it away, and so do we. Sequences run on our side, not from a browser tab — unlike a campaign started in the console. Nothing needs to stay open for a sequence to keep sending. # Templates Source: https://docs.thinnest.ai/whatsapp/templates The only thing that reaches a customer outside the 24-hour window. Outside the [24-hour window](/meta/windows-and-pricing) WhatsApp carries nothing but an **approved template**. Everything you start — a campaign, an order update, a reminder — is a template. Once one is approved you can put it in a [broadcast](/whatsapp/broadcasts), fire it from the [API](/api-reference/messages/send-message), or [send it to a few numbers by hand](/whatsapp/send). ## The three categories | Category | For | Charged | Reaches an opted-out customer | | - | - | - | - | | **Marketing** | Offers, announcements, re-engagement | Every send | No | | **Utility** | Order confirmations, delivery updates, receipts | Every send | Yes, and must | | **Authentication** | One-time passwords | Every send | Yes | Getting the category wrong is the most expensive mistake here. A marketing template refused for opted-out contacts is correct behaviour, not a bug. ## Already have templates? Import them If you were using WhatsApp before you came here, your approved templates are still on your WhatsApp Business Account — and **Import from Meta**, at the top of the Templates page, brings them in. They arrive with their wording, their examples, their buttons and their approval status intact, ready to send. Pressing it again is safe: a template you already have is refreshed rather than duplicated, which is also how you pick up a status Meta has changed since. Only templates on **your own** account are imported. Nothing from anybody else's reaches your workspace. ## You need a connected number first Templates are submitted to **your** WhatsApp Business Account, so until one is connected there is nothing to submit them to. This page is read-only until then — you can read what is here and browse the pre-built templates, but writing, submitting, editing and adding a language all wait. [Connect WhatsApp](/channels/whatsapp#connecting) is the first thing in the WhatsApp section of the sidebar, and takes a few minutes. ## Writing one The editor writes the template and submits it to Meta for approval. Approval typically takes minutes but is Meta's decision, not ours — see [Template review and approval](/meta/template-review) for what gets rejected and why. Placeholders are numbered: `{{1}}`, `{{2}}`. Each one is bound at send time to a contact field or one fixed string — and a template with a placeholder **cannot go out with it empty**. **The header has to be fixed text.** Meta allows a variable there; we do not, because nothing in the product can fill it — and a template Meta approves that can never be sent is worse than one refused while you are writing it. Put the changing part in the body. **Authentication templates are copied, not written for you.** Meta writes and localises the wording for one-time passwords itself, and rewording it gets the template rejected. The editor copies Meta's text exactly. Authentication templates are also gated behind business verification. ## Languages and variants A template name can exist in several languages. When you send, the [contact's language](/agent/leads#contact-language) decides which variant they receive. A contact with no language set receives your default — so adding languages is always an improvement and never a prerequisite. Competitors generally give you one language per template and leave the choosing to you. Per-contact variant selection is the difference between "we support Hindi" and "your Hindi-speaking customers get Hindi". ## Buttons Templates can carry quick replies and link buttons. A link button's blank is sent separately from the body's blanks — Meta wants it in a different component, and putting it in the body's list breaks both the words and the link. A button's label can be up to **25 characters** — WhatsApp's limit. The label box counts as you type (`12/25`) and stops at 25. ## Carousels A carousel is a message followed by 2 to 10 swipeable cards, each with its own picture (or video), text and the same buttons. Two things decide whether Meta accepts one: * **Each card's picture must be a direct link to the image itself** — one that opens just the picture, usually ending in `.jpg` or `.png`. A Google Drive or Dropbox share link opens a web page about the file, not the file, so it is refused before anything is sent, and the message names the card. * **Pick the language the text is written in.** A carousel written in Telugu should be a Telugu template; Meta reviews the language you choose against the words. * **It must be a Marketing template.** WhatsApp doesn't allow carousels as Utility. * **It has no footer.** WhatsApp allows nothing above the cards except the message itself. A marketing carousel still needs a way out, so end the message with a line like "Reply STOP to opt out" (in any language, as long as it contains the word STOP in capitals), or give the cards a "Stop promotions" quick reply. * **Its buttons go on the cards.** Leave the Buttons section under the message empty; a carousel can't have buttons there. * **Each card's text is up to 160 characters.** The box counts as you type. All of these are checked when you save, edit and submit, so you find out here, in plain words, rather than from Meta. Each card has **one or two buttons**, and every card has the same ones — a quick reply, a link or a call. A link button can go somewhere different on each card, but each card's link is a fixed web address: it can't contain a blank like `{{1}}`, since nothing fills one in when the carousel is sent. ## Sending one An approved template's card carries **Send broadcast**. It opens the [broadcast composer](/whatsapp/broadcasts) with that template already chosen, so the template you just got approved is one click from reaching somebody. For a single customer rather than a list, use [Send](/whatsapp/send); from your own systems, [the messages API](/api-reference/messages/send-message). ## Letting an agent send a template Your agent can send an approved template **during a call or chat** when a customer asks for something (a renewal link, your address, a documents list). You decide which templates it may use. Only **Utility** templates can be sent by an agent. A customer who asked a question hasn't agreed to marketing, and Meta treats the two differently. It applies to every language of that template. Untick it to stop. On the agent's **Actions** page, turn on **Send the customer a WhatsApp**. See [Actions](/agent/actions#reaching-the-customer-on-a-different-channel). Two things keep a template out: * **A blank in a link button.** A button like `yoursite.com/{{1}}` needs a value for the blank, and an agent can't fill one. Use a fixed link, and put anything that changes in the message text. * **Not being Utility.** Marketing and authentication templates can't be ticked. New templates start unticked. Templates you already had when this was added were ticked for you, apart from any with a blank in a link button. Each message the agent sends is charged like any other template you send, and the agent may send two an hour in one conversation. ## Forms A template button can open a **form** — an address, a booking, a return. That is how you collect structured information from somebody who has not messaged you recently. Answers come back into the conversation and are stored against it, so they are readable in the inbox and available afterwards as structured data rather than as words in a transcript. **Forms cannot be published until Meta verifies your business.** They build, store and preview now; Meta will not publish a flow for an unverified business, and an unpublished form reaches nobody. Forms also cannot be filled from your data — no slot picker showing only free slots, no dropdown of that customer's orders. ## Deleting one **Deleting an AGENT does not delete your templates.** They belong to your WhatsApp Business Account rather than to one agent, so they move to whichever of your agents is oldest — along with your forms and your broadcasts. The delete dialog lists what will move before you confirm. **Deleting removes it from Meta too**, not just from this list. Two consequences follow, and both are easier to know beforehand: * Anything sending that template — a campaign, a sequence, your own code calling the API with its name — **stops working**. * Meta will **not let the name be used again for 30 days**. If you are replacing a template, give the replacement a different name rather than waiting a month for the old one. A draft that was never submitted has neither consequence: Meta has never seen it, so nothing is using it and the name stays free. ## Template quality Meta scores every approved template on how people react to it — green, yellow, red — and tells us when the score changes. **You are warned at yellow**, on the template and by notification, while there is still something you can do about it. Red is not a warning. Meta pauses a red template, and a paused template sends nothing, so the yellow is the message that matters. The usual causes are blocks and "report" taps from people who did not expect to hear from you: a marketing template sent too often, or to a list that never really opted in. Your **number** is scored separately, and that one is worse: a flagged number has its messaging limit cut, and left flagged it is restricted outright — at which point no template works, however healthy. You are warned about that too. **Per-template delivery analytics are not available yet** — how many people opened a given template, or tapped its button. Meta reports button taps in WhatsApp Manager and we do not yet read them. What you can see today is per-campaign delivery and replies on the [campaign report](/whatsapp/broadcasts), and the quality score above. # Brand and domains Source: https://docs.thinnest.ai/white-label/brand-and-domains Put your name, logo and colour on everything, serve it from your own domain, and bring your own sign-in check and email sender. ## Your brand **White label → Brand**: your product name, logo and colour. Every client workspace wears it — the console, the sign-in page, share links, invitations and notification email. Change it and every client sees the change. ## No domain yet? Use a preview address Under **White label → Domains → Preview address**, press **Create my preview address**. You get a free address like `yourname.preview.thinnest.ai`, ready at once — no DNS to set up. It works exactly like a connected domain: your brand on every page, sign-in, invitations to clients, and sign-up if you switch it on. Use it to try your console, demo it, and start your first clients. Our name is in a preview address, so connect your own domain before you go live with clients. The first visit to a new preview address can take a few seconds while its HTTPS certificate is issued. ## Your domain **White label → Domains** puts the product on your own address, such as `app.yourcompany.com`. Type or paste it, and choose which workspace it opens: your main workspace, or one of your clients. The page shows both, ready to copy: * a **TXT** record that proves the domain is yours; * an **A** record that points the address at us. Add them where your domain's DNS is managed (your registrar, Cloudflare and so on). Press **Verify**. If a record has not appeared yet, the page says which one — DNS changes can take a while to spread. **HTTPS is automatic.** The certificate for your address is issued the first time it is visited after it is verified, and renewed for you. There is nothing to upload. An address can open one workspace at a time. One already in use elsewhere has to be removed there first; if it is held by a workspace outside your account, the page shows the record that proves it is yours. ### Several clients on one domain Each client can have its own address (`acme.yourcompany.com`, `beta.yourcompany.com`), or all of them can sign in on one address that opens your main workspace — from there each person goes to the workspaces they belong to. ### Letting clients sign up On an address that opens your **main** workspace you can switch **sign-up** on. Anyone who creates an account there becomes your client, in a new workspace of their own. Sign-up cannot be switched on for an address that opens a client's workspace. ## Sign-in protection Password sign-in and sign-up are protected by a Cloudflare Turnstile check. On your domain it has to be **yours**: 1. In Cloudflare, add a Turnstile widget and list your domains in it. 2. Under **White label → Brand → Sign-in protection**, paste its **site key** and **secret key**. They are checked with Cloudflare before they are saved. Until it is connected, people cannot sign in with a password on your domain. ## Email from your domain Invitations, sign-up and password emails, and notifications to your clients come from **your** sender once you connect your own Resend account under **Settings → Notifications**. Until then they are sent in your brand's name from a neutral address, never from ours. # Clients Source: https://docs.thinnest.ai/white-label/clients Create a workspace for each client, see each client's plan and balance, and fund them by hand if you bill them yourself. Every client you resell to gets a workspace of its own. It is a full workspace — agents, channels, contacts, inbox, campaigns, usage — with its own login and its own people. You manage the list from **White label → Clients**. ## Adding a client Two ways: * **Create it yourself.** Under **New client**, give the workspace a name and, optionally, the email addresses of the people who should have access. They are invited by email, from your sender and in your brand. The workspace starts with a first agent ready to set up. * **Let them sign up.** On your main domain you can switch sign-up on (see [Brand and domains](/white-label/brand-and-domains)). Anyone who signs up there becomes one of your clients. You are an admin of every client workspace. Open it from the Clients list to set things up for them, and use its own **Members** page to invite or remove their people. ## Each client's plan and call lines Each client has **its own plan**, just as you have yours with us: it starts on the free trial you set (see [Prices](/white-label/prices)), then moves to pay-as-you-go or subscribes to Pro or Scale. Its concurrent call lines are its plan's own — a busy client never takes lines from another. The Clients list shows each client's plan. ## Each client's money Each row shows the client's **balance**, and what it has been charged this month for usage alongside what that usage **cost you** — the difference is yours. * **Add credit** puts money in a client's balance by hand. Use it if you bill a client some other way (an invoice, a bank transfer) instead of through your Razorpay. Nothing is charged to you when you add credit; you are charged our price as the client uses it. * A client can also add money itself, from its Billing page, once you have connected Razorpay. See [Your prices](/white-label/prices). ## When your balance runs out Everything your clients use is charged to **your** balance at our prices. When your available balance reaches zero, **your clients' calls, campaigns and new phone numbers stop** — whatever is in their own balances — and their Billing page tells them service is paused. Agent replies keep answering, as they do on every plan. You are warned before it happens: a notice when your balance runs low, and another when it is empty. Money set aside for phone numbers renewing in the next few days does not count as available. # White label Source: https://docs.thinnest.ai/white-label/overview Sell AI voice and WhatsApp agents to your clients as your own product — your name, your logo, your domain, your prices. White label lets an agency resell ThinnestAI under its own brand. Each of your clients gets a workspace of its own, on your domain, with your name and logo on every page and every email. Your clients never see ours. White label comes with **every paid plan** — Pay As You Go, Pro, Scale and Enterprise; not the free trial. It is set up from your **main** workspace by one of its owners or admins — the **White label** item in the sidebar. ## What you get Each client has its own login, agents, contacts, conversations and usage. You can open any of them. Your product name, logo and colour on every page your clients see, and on the email they receive. `app.yourcompany.com` instead of ours, with HTTPS set up for you. Your own prices on top of ours, paid into your own Razorpay account. You keep the difference. ## How it fits together 1. **You** are on any paid plan. Your main workspace is where you manage everything. 2. **Your clients** each get a workspace under yours: up to **3** on Pay As You Go, **20** on Pro, **100** on Scale, and more on Enterprise. The Clients tab shows how many you have used. 3. **Your clients are like our customers.** Each has its own plan — starting on a free trial you set — and its own call lines. 4. **Your clients pay you**, at your prices, through your Razorpay. **We charge you** our prices for what they use, from your balance. ## Setting up The **Overview** tab under White label is a checklist of these steps, and ticks each one off as you go: Name, logo and colour. See [Brand and domains](/white-label/brand-and-domains). Two DNS records, and we check them for you. No domain yet? Start on a free preview address. Your own Cloudflare Turnstile, so people can sign in on your domain. Your own Resend sender, so client email comes from your domain. Connect Razorpay and set your markups. See [Your prices](/white-label/prices). Create a workspace for them, or let them sign up on your domain. See [Clients](/white-label/clients). ## What your clients see * Your name, logo and colour — on the console, the sign-in page, share links and every email. * Your domain in the address bar. * Their balance, plan and prices **in your prices**, and a Billing page that pays you. * On your domain, sign-in is by email. "Sign in with Google" and LinkedIn are not offered there, because their consent screens would name us. ## Not available yet * Your own plans with your own feature limits. Clients get our Pro or Scale features, at your price. * Yearly plans for clients — monthly only. * Client payments in dollars. Clients pay you in rupees. * Automatic top-up for clients. * White label on pay-as-you-go. # Your prices Source: https://docs.thinnest.ai/white-label/prices Set your own prices on top of ours, get paid into your own Razorpay account, and keep the difference. You decide what your clients pay. They pay it **to you**, through your own Razorpay account. We charge **you** our prices for the same usage, from your balance, as it happens. The difference is yours, straight away — we never hold or pass on your clients' money. Everything is set under **White label → Prices**. ## Connect your Razorpay In Razorpay, open **Account & Settings → API keys** and generate a key. You get a **key id** (`rzp_live_…`) and a **key secret**. Under **Get paid by your clients**, paste them and press **Connect**. They are checked with Razorpay before they are saved, and the secret is never shown again. There is no webhook to set up: every payment is confirmed with Razorpay directly, and one your client's browser did not report back is picked up within minutes. Use your **live** keys — test keys cannot take real payments and are refused. You can replace your keys at any time (after regenerating them, for instance). While any client pays you a monthly plan, you cannot disconnect Razorpay or connect a **different** account: their plans live in this one. Without Razorpay you can still bill clients however you like and fund them by hand with **Add credit** — see [Clients](/white-label/clients). ## Usage prices A **percent on top of our price**, for each kind of usage: | Usage | Your client pays | | - | - | | Voice calls | our per-minute price + your % | | Phone numbers | our monthly rent + your % | | WhatsApp messages | our per-message price + your % | | AI replies | what each reply costs + your % | Leave a box empty to sell at our price. As you type, the page shows what your clients will pay against what you pay — for example *Standard voice: ₹2.28 / min (you pay ₹1.90)*. Every price your clients see — on their Billing page, the voice picker, phone numbers, broadcast estimates, their usage and history — is yours. ### The top-up fee When a client adds money, a fee is added at checkout: **our fee for the client's plan plus the extra percent you set**. All of it goes to you, with the rest of the payment. ## Plans Your clients are customers just like you are ours. Each client has its own plan — never yours — and can subscribe to **Pro** or **Scale** at your price: our monthly price plus the amount you set. Each month a client pays for its plan: * the client's plan price comes back to it as **usage credit** for that month, spent at your prices; * we charge **you** our monthly price, and give it back to you as credit for that client's usage that month — so you are not charged twice for the same usage. A client can cancel from its Billing page; the plan keeps working until the end of the month it paid for, and the client then moves to pay-as-you-go. ## Free trial A new client starts on a **free trial**, as new accounts with us do. You decide what it includes: * **Voice minutes** — once per client (ours: 25). * **AI replies** — each month while the client is on the trial (ours: 200). * **WhatsApp messages** — once per client (ours: 200). **You pay our price for every trial minute, reply and message**; the client pays nothing. Turn the trial off and new clients start on pay-as-you-go instead. A client leaves the trial when it first adds money (or you add credit to it), or when it subscribes to a plan. Changes to the trial apply to new clients and to clients still on their trial. ## What your clients see Their **Billing** page shows their balance (and, on the trial, what is left of it), an **Add money** button once you have connected Razorpay, and pay-as-you-go, Pro and Scale at your prices. Checkout opens on **your** Razorpay account under your name. ## When your balance runs out Your clients' usage — their free trials included — is charged to your balance. When your available balance reaches zero, **your clients' calls, campaigns, trials and new phone numbers stop**, and their Billing page says service is paused. You are warned when your balance runs low and again when it is empty. Add money to your own balance as usual and everything resumes. ## Good to know * **Rupees only** for client payments through your Razorpay. * **Tax on your sale is yours** — we add none to what your clients pay you. Set your prices with that in mind. * **History** — your own billing history lists each client's usage and plan fees by client name, at our prices; each client's history shows its charges at yours. ## Not available yet * Your own plans with your own feature limits — clients get Pro or Scale features. * Yearly plans for clients. * Client payments in dollars. * Automatic top-up for clients. # Analytics Source: https://docs.thinnest.ai/workspace/analytics Whether the agent did its job — every figure a count you could verify yourself. Analytics answers one question: **did the agent do its job**. It is deliberately separate from usage, which is what you owe us. Mixing the two is how a product starts feeling like a meter. Nothing here is a forecast, a score, or an invented composite index. Every figure is a count you could arrive at yourself by reading your own inbox. ## The four numbers | | | | - | - | | **Conversations** | The denominator for everything else | | **Leads captured** | The outcome the product is sold on | | **Needed a person** | Where the agent ran out of road | | **Found an answer** | How often a knowledge search found something | "Needed a person" is not a failure metric. A business with nobody watching turns escalation off entirely — it is the number that tells you whether the setup is working. ## Over time Conversations and leads per day, drawn as two charts rather than one. They are deliberately not plotted together. Leads are a small fraction of conversations — 200 conversations and 9 leads is doing well — so on a shared axis the line that matters most would sit flat against zero. ## When people write Conversations by hour of day, in UTC. The question behind it is operational rather than analytical: when should somebody be near the inbox. An escalation at 09:00 gets answered; the same one at 23:00 is a customer who waited overnight. ## Questions it could not answer **The most useful page in the product.** These are questions the agent searched your material for and found nothing on, so it had to say it did not know. Each row is a page you could write, or a paragraph you could paste into [Knowledge](/agent/knowledge). Work this list and the "found an answer" number climbs on its own. ## What people ask most Grouped by exact wording, so the same question asked two ways appears twice. Frequently a surprise to whoever wrote the website. # Your data: erasure and training Source: https://docs.thinnest.ai/workspace/data-and-erasure What is deleted when you erase a contact, what is kept on purpose, and when recordings may be used to improve our models. When a person asks you to forget them, one call to [`DELETE /contacts/{id}`](/api-reference/contacts/delete-contact) does it. This page says what that removes, what it deliberately does not, and how recordings and transcripts are treated for model training. ## Erasing a contact Erasing a contact removes, together and for good: * their conversations and every message in them (the transcripts); * their calls, queued calls, callbacks and calling-campaign entries; * their form replies, helpdesk tickets and the follow-ups waiting on them; * **the recordings of their calls**, wherever the audio is kept; * any duplicate of them that was merged into the contact — the record a merge leaves behind still holds a name and an email, so it goes too. It cannot be undone. To stop messaging someone without forgetting them, set their consent to `withdrawn` instead. ### Recordings go with the contact The audio is deleted first, then the call records, so a recording is never left behind with nothing pointing to it: | Where the audio is | When it is deleted | | - | - | | Stored by us (website calls, and phone calls on a number you brought) | Immediately. | | The phone provider's copy (calls on a number we rent you) | Immediately, through your workspace's own account with them. | | A copy made for model training, if the workspace was on pay-as-you-go | Immediately. | If a file cannot be deleted at that moment — a provider is down, say — the contact is **still erased**, and the deletion is queued and retried every night until it succeeds. The response then carries `recordingsPending`, the number of files still queued: ```json theme={null} { "recordingsPending": 1 } ``` Without it (`204`, no body) every file was already gone when the response came back. To know when an erasure is **finished**, subscribe a webhook to [`contact.erased`](/api-reference/webhooks#workspace-events). It is sent once, with only the contact's id and when it was erased: straight away after a `204`, or from the nightly retry once the last queued file is gone. ### What is not erased * **Do-not-call entries are kept, on purpose.** If the person asked never to be called again, forgetting the number would let a campaign ring it again. The list holds the number and where the request came from, not their history. * **Webhook payloads we already sent** are kept for retries and then purged automatically after 7 days. * **Copies in your own systems and your vendors'.** Anything your CRM, your webhook receiver or your tools stored is yours to delete; erasing a contact here does not reach into them. Recordings you do not erase by hand are deleted on your plan's schedule anyway; see [Recordings](/workspace/usage#recordings). ## Deleting one call's recording and transcript To remove what was said on **one** call without erasing the person, call [`DELETE /calls/{id}/content`](/api-reference/post-call/delete-call-content) with a full API key. * **Deleted:** the recording (ours, the phone provider's copy, and any copy made for model training), this call's turns in the conversation, its summary, the details collected from it, and the summary handed on when the call was passed to another agent. Other calls and chats with the same person stay. * **Kept:** the call itself — when it was, how long it lasted, what it cost and what it was billed as. Your usage and invoices do not change. Afterwards [Get Call](/api-reference/calls/get-call) shows the call with no recording, an empty transcript, `"contentDeleted": true` and when it happened (`contentDeletedAt`). A recording that cannot be deleted at that moment is retried every night until it is gone; the answer says how many are still queued (`recordingsPending`). Asking again is safe. A call that is still going on, or one booked that has not rung yet, is refused — this is not how you cancel a call (`DELETE /calls/{id}` still does that). It cannot be undone. ## How long calls are kept Your plan keeps call recordings for **30 days** on pay-as-you-go, **49** on Pro, **75** on Scale and **365** on Enterprise. Transcripts are kept until you delete them. To keep less, set `callRetentionDays` with [`PATCH /workspace`](/api-reference/workspace/update-workspace) — any whole number of days from 1 up to your plan's limit (a longer one is refused, naming the limit). Every night, calls older than that lose their recording **and** their transcript exactly as Delete Call Content removes them, and their billing record stays. It applies to calls you already have, from the next night. `null` goes back to your plan's limit for recordings and keeps transcripts. [`GET /workspace`](/api-reference/workspace/get-workspace) shows your setting (`callRetentionDays`) and your plan's limit (`planRetentionDays`). ## Model training **Recordings and transcripts are never used to train models on Pro and above.** On **pay-as-you-go**, a copy of a call's recording and transcript may be kept to improve our speech and language models. If you do not want that, ask us and we will exclude your workspace — and, for a developer, every customer workspace under it. Exclusion also deletes the copies already made, and a very large backlog is finished over the following nights. From then on, your calls are not copied. Calls made before the exclusion, or while it was on, are never copied later, even if the exclusion is lifted. Write to support from the workspace owner's address and say which workspace. Erasing a contact deletes its training copies too, whatever the plan. # Helpdesk Source: https://docs.thinnest.ai/workspace/helpdesk Escalations as tickets in Zendesk or Salesforce, with replies coming back. If your team already lives in a helpdesk, escalations can arrive there as tickets — and replies typed there reach the customer on the channel they wrote from. ## Two-way, on WhatsApp Connect Zendesk or Salesforce and an escalated conversation opens a ticket with the transcript. When an agent replies in the helpdesk, that reply is delivered to the customer on WhatsApp. The customer never learns your helpdesk exists. They asked on WhatsApp; they get an answer on WhatsApp. ## Connecting On the agent's **Channels** page, open **Reply from your helpdesk** and connect the vendor. Credentials are sealed and are not readable from the dashboard. ## Which vendors | Vendor | Tickets | Replies come back | | - | - | - | | **Zendesk** | Yes | Yes | | **Salesforce** | Yes | Yes | Ticket numbering differs by vendor and can repeat across accounts, so tickets are keyed by connection as well as by number. Two workspaces with the same ticket number never cross. ## Salesforce specifics Salesforce caps a comment at 4,000 **bytes**, not characters — so a long transcript in a non-Latin script is trimmed on a byte boundary rather than rejected outright. ## What reaches the ticket The conversation transcript at the point of escalation, plus a tag identifying the conversation so replies route back to the right thread. Internal notes are not included: those are your team's, and a helpdesk ticket is a different audience. # Inbox Source: https://docs.thinnest.ai/workspace/inbox Every conversation, and how a person takes one over. The inbox shows every conversation across every channel, as it happens. ## Finding a conversation Press the search icon at the top of the list and type at least two characters. The search covers **every conversation in the workspace**, not just the ones already on screen, so you never have to scroll to load an old thread first. It matches: * the customer's **name**, **email** or **phone number** — a number can be typed with spaces, dashes or the country code, so `98765 43210` finds `+919876543210`; * **any message** in the conversation, not just the latest — the matching line is shown in the list, so you can see why a conversation was found; * for calls, the **number the call was with**, including people your calling campaigns rang; * the **teammate** a conversation is assigned to. Up to 50 matches are shown, newest first. If more conversations match, the list says so; type more of the name or number to narrow it down. The tabs and filters still apply to search results. ### Filtering The filter button beside the search narrows the list by **channel**, **pinned**, **from an ad** and **who it is assigned to**. Filters look through every conversation in the workspace, not just the ones already on screen, and work together with a search. ## Taking over Anyone on your team can take a conversation over. The moment they do, **the agent steps out** — it stops replying to that thread entirely. That is not politeness. Two voices answering the same customer, contradicting each other, is worse than a slower answer. When a teammate replies, the agent later sees that reply as its own prior turn, so it does not re-answer something a person just handled. ## Whose conversation is this? Assign a thread to a teammate from the picker at the top of it, and it becomes theirs — they get a **Mine** tab in the inbox listing everything assigned to them, and everyone else can see at a glance who is dealing with it. **Assigning is not the same as taking over**, and neither one implies the other. Taking over is a fact: somebody replied, so the agent stands down. Assigning is a decision about who is responsible, and you can make it before anyone has replied — the agent keeps handling the conversation until a person actually answers. So you can hand somebody a thread to keep an eye on without silencing the agent, and you can take a thread over without claiming it. **They are told.** The person you assign a conversation to gets a notification, and it goes to them alone — nobody else on the team is told which of them is dealing with a thread. Hand it on later and the next person is told too, so a thread can move around the team without anyone having to be watching for it. Assigning one to yourself sends nothing; you already know. Set it back to **Unassigned** to put it back in the pool. That is how work gets handed around rather than sitting with whoever happened to open it first. Above about three people sharing an inbox, this is the difference between a queue and a pile. Two people answering the same customer is the thing it prevents, and it prevents it earlier than taking over does. ## Saved replies The answers you send twenty times a day — opening hours, refund timings, "could you share your order number?" — kept in one place so everybody sends the same sentence. Open the saved-replies button under the reply box in the inbox. **Add** sits beside the heading, and opens a form for the shortcut and the reply. Each saved reply has a pencil to edit it and a bin to delete it, so a typo can be fixed where you noticed it. Choosing one adds it to whatever you have already typed rather than replacing it, so you can write a line of your own and then reach for the policy. Write `{{name}}` where the customer's name should go. It is filled in when you insert the reply, so the same saved text reads as though you typed it — and for a website visitor who has never given a name it is quietly left out rather than sent as `{{name}}`. Saved replies belong to the **workspace**, not to one person or one agent, and anybody on the team can add or edit one. That is the point: the value is that the refund policy does not drift between teammates. ## Internal notes You can leave a note on a conversation that the customer never sees. Notes are your team talking **about** the customer, not to them, and they are never replayed to the model — an agent that read one would believe it had said that out loud. ## Statuses | Status | Means | | - | - | | **Active** | The agent is handling it | | **Escalated** | The agent asked for a person | | **Human handling** | Somebody took over; the agent is silent | | **Resolved** | Closed | Assignment sits alongside these rather than among them: a conversation can be active *and* assigned, or taken over by one person while assigned to another — which is exactly what happens when somebody covers a colleague's thread. ## Media ## Whether you can reply freely Above the reply box on a WhatsApp conversation, a line tells you where the 24-hour window stands, so you know before you type whether a free reply or a form will go through. | You see | It means | | - | - | | **Free replies and forms open · 14h 22m left** | Send anything. The count is in hours and minutes and updates while you have the thread open. | | **Reply window closes in 42m** (amber) | The last hour. Finish what you are saying; after this only a template reaches them. | | **WhatsApp closed this conversation 3h ago** | The window ran out. The reply box is replaced by a note; an approved template is the way back in. | | **This customer hasn't messaged you** | No window has ever opened — you started this conversation with a template, and they have not written back. Forms and free replies open the moment they do. | The customer messaging you again reopens the window immediately, for another 24 hours from that message. ### Sending a template from the conversation When the reply box is replaced by one of those notes, **Send a template** sits beneath it. It opens a list of your approved templates; choose one, fill in any blanks it has, and it goes to that customer straight from the thread, where it appears with a line saying it was sent from the inbox. * **Every blank is yours to fill.** Nothing is pre-filled with the template's example values, so a customer is never greeted as somebody else. * **A template with a picture at the top** uses the one saved with it. If none is saved, WhatsApp needs one on every send, so the dialog asks for its address. * **A marketing template only goes to people who have not opted out.** Send it only to somebody who agreed to hear from you. If you have no approved template yet, the same notes carry **Set up a template**, which takes you to your Templates page. ## Whether a message arrived Under each message the workspace sent, the inbox says what WhatsApp did with it. "Sent" means we handed it to WhatsApp. That is not the same as delivered, and the difference matters: WhatsApp accepts a message and then decides. When it cannot deliver one, the message reads **Not delivered**, with WhatsApp's own reason after it. A number that is no longer on WhatsApp, a picture WhatsApp could not fetch, an account that is not allowed to start conversations — each says so in its own words rather than leaving you to guess from silence. **A message that was not delivered is not charged for.** WhatsApp bills for what it delivers, so when a failure comes back, the charge is credited to your balance and the line disappears from what you owe. You do not need to ask. Images sent by customers appear in the thread. On models that can see, recent images are shown to the agent; older ones stay in the conversation as the sentence they reduce to, because re-sending every photo on every turn would cost a great deal for very little. ## Replying on WhatsApp Inside the 24-hour window you can reply freely. Outside it, only an approved template will reach the customer — the inbox will tell you which situation you are in rather than letting you type into the void. # Notifications Source: https://docs.thinnest.ai/workspace/notifications Be told when a call comes in, without logging in — in the console, and by email from your own Resend. Something happens — your agent answers a call, a template is rejected, Meta restricts your number — and you find out by opening the console. These settings change that: choose what you hear about, and have it reach an inbox your team actually reads. Find them under **Settings → Notifications**. Anyone in the workspace can see them; only **owners and admins** can change them. ## Platform notifications The bell and the feed in the console. Every category is on until you mute it. | Category | What it tells you | | - | - | | **WhatsApp** | Restrictions, bans, review verdicts and sending limits from Meta. Muting these is rarely wise. | | **Templates** | Approvals, rejections, quality scores and category changes. | | **Broadcasts and calling campaigns** | When a WhatsApp broadcast or a voice calling campaign finishes, and when an unusual share of it failed. | | **Calls** | Calls your agent answered or placed, and the ones nobody picked up. | | **Inbox** | Conversations the agent handed to a person. | | **Knowledge** | Documents that could not be indexed after repeated attempts. | | **Plan and usage** | Approaching your included messages, and plan changes. | | **Connections** | Webhooks, tools, Telegram, voice and your own provider keys failing or being switched off. | | **Members** | People accepting an invitation to the workspace. | Muting hides a category from the feed and the bell. Nothing is deleted — unmuting brings its history back. Anything **critical** is shown regardless: a muted category cannot hide the news that Meta has banned your account. ### Calls An answered call and a missed call are different things, so they look different: an answered call is a record, and a call nobody picked up is a customer who wanted something. Both inbound and outbound calls are covered. **Calls placed by a calling campaign are not announced one by one.** A four-hundred-person campaign would otherwise be four hundred notifications, and four hundred emails if you had asked for them. The campaign reports its own result when it finishes — see [Voice campaigns](/channels/voice-campaigns). ## Receive notifications to email Notifications can also arrive by email, from **your own Resend account**, at whatever addresses you choose. That means a shared inbox — `support@` or `ops@` — rather than the personal addresses people sign in with. ### Connecting Resend Open the **Receive notifications to email** tab and fill in: * **Resend API key** — from your Resend dashboard, under API Keys. It is stored encrypted and is never shown again. * **Send from** — an address on a domain you have **verified inside Resend**. Resend will refuse anything else. * **Sender name** — optional; what the recipient sees in their inbox. * **Send notifications to** — one address per line, up to ten. These do not have to belong to people who log in here. When you press **Connect and send a test**, we send a real message to **every** address you listed. **Nothing is saved unless Resend accepts it.** A key that looks right but belongs to an unverified domain fails here, while you are looking at the screen, rather than weeks later as silence. "Accepted" is not quite "arrived". A message can still land in a spam folder or bounce, and we cannot see that from here. If a test does not appear within a minute, check that address's **spam** folder first, then open **Emails** in your Resend dashboard — it shows each message as *Delivered*, *Bounced* or *Delayed*. If one address works and another does not, the problem is that address, not your setup. Give Resend a key with **full access** if you want it to be easy to diagnose this yourself. A key restricted to "sending access" is the safer choice and works perfectly for notifications — it just means we cannot look at delivery status for you. ### Choosing what is emailed Once an account is connected, each category above gets a choice: | Choice | You get emailed | | - | - | | **No email** | Never. The default for every category. | | **Only urgent** | Only the ones that mean messages are not reaching customers. | | **Every one** | Everything in that category. | To be told about every call, set **Calls** to *Every one*. **Nothing is emailed until you ask.** A workspace that has never opened this page receives no notification email. That is stricter than before — urgent Meta alerts used to arrive uninvited — so if you rely on those, set **WhatsApp** to *Only urgent*. ### If you would rather not connect Resend You do not have to. With nothing connected, anything you switch on still reaches your owners and admins, sent by us. Connecting your own account only changes who it comes from and where it goes. ### If your account stops working If Resend starts refusing — a revoked key, a lapsed domain — the connection shows the recent failures and the reason on the settings page. Notifications keep arriving in the console. An account that has never had a successful test is never used, and mail goes out through ours instead of being lost. **Disconnect** removes the stored key entirely. Your choices about what to hear about are kept. # Usage Source: https://docs.thinnest.ai/workspace/usage What your agents did, what it cost, and every call you can open and listen to. **Usage** is the workspace-wide record: how much your agents worked, what that cost, and a line for every call. It sits in the main sidebar, not under an agent, because it covers all of them at once. Analytics answers *how well is my agent doing*. Usage answers *what did it do, and what am I paying for* — and they are different questions. ## The last thirty days The chart at the top is activity over time: replies your agents sent and calls they handled, on the same thirty-day grid so you can see them against each other. A spike in calls the week you ran a campaign should look like one. Useful for the thing dashboards rarely tell you: **when** your customers actually get in touch. If the line rises every evening after six, that is when somebody should be reachable. ## WhatsApp WhatsApp is billed per message, and it is broken out here in full — marketing, utility, authentication and service counted separately, by country, because they cost very different amounts. The figure against each line is **what you were charged**, in your own currency, so it reconciles against your balance and against your own record of what you sent. Everything the workspace sends is counted the same way, whether it came from a campaign, a sequence, the Send Message page or the API. **A message that was not delivered is not charged for.** WhatsApp bills for what it delivers. When a failure comes back the charge is credited, so a line you see here is a message that arrived. Until 1 October 2026, an open 24-hour window makes two things free: the agent's own replies, and any utility template you send while it is open. Templates that start a conversation are charged, and so is authentication in or out of a window. If a number looks higher than you expected, the split here usually shows why — see [Windows and what gets charged](/meta/windows-and-pricing). ## Calls Every call the workspace answered or placed, newest first: when, who, which agent, whether it connected, how long it ran, and what it cost. Open one to read the transcript. If the call was recorded, the recording is there too. **Export** gives you the same list as a file, for a spreadsheet or your accountant. A call that shows as **failed** never connected — a wrong number, a line that rang out, a caller who hung up before the agent spoke. Failed calls cost nothing and are kept so the list is honest about what was attempted. ### How a call went An opened call also shows how it went, in numbers, and whether anything broke: * **How the call went** — how long the caller waited for each reply (the typical wait, the slow end, and the slowest), how many pauses of 10 seconds or more there were (or your agent's own "are you still there?" time, if it has one), how often the caller spoke over the agent, and how many replies the agent could not produce or a backup had to produce. A call that was never measured, such as one that never connected, has no figures. * **Problems on this call** — shown only when something went wrong while the call was live: the agent could not answer, your own model key failed, a reply took more than 5 seconds to start, or the voice connection failed. Through the API the numbers are `quality` on [Get Call](/api-reference/calls/get-call), and every problem is sent as a `call.problem` [webhook](/api-reference/webhooks#problems-on-a-live-call) while the call is still going on, so you can alert somebody before it ends. ## Recordings Recording is off unless you turn it on, per agent, on that agent's **Voice** page. When it is on, the calls that agent answers or places — on your website, and by phone on a number you took from us — are recorded and appear here. **Recordings are deleted when your plan says** — after 30 days on pay-as-you-go, 49 on Pro and 75 on Scale. Download anything you need to keep before then. Once it is deleted it is gone and we cannot get it back. A caller on your website sees *This call is recorded* as the call connects. See [Recording calls](/channels/voice#recording-calls) for which phone calls can be. Telling people they are being recorded is your responsibility, not ours. In most places it has to be said at the start of the call, and the wording that satisfies your regulator is not something we can write for you. Put it in your agent's first words. ## What is not here * **Per-customer spend.** Usage is per workspace and per channel, not per contact. * **Live figures to the second.** Costs settle when a call ends and when Meta confirms a send, so the most recent minutes can lag slightly. * **Your invoice.** This is what you used. What you owe is on **Settings → Billing**, and the two are presented differently on purpose. **Registered for GST in India? Put your GSTIN in Settings → Billing.** It is recorded on every invoice we issue you from the moment you save it, which is what makes those invoices usable by your accountant. Only an owner or admin can set it, and it takes effect on invoices issued afterwards — so it is worth doing before your first payment rather than after.