# Texa Desk developers
Source: https://developers.texadesk.com/
Texa Desk is one system between a business and its customers: a helpdesk on WhatsApp, email, phone,
SMS, live chat and social, with **Texa AI**, an agent that answers, looks things up and acts in
your systems, and hands over to your team when it should. Everything you can do on screen you can
also do from code.
These docs cover every way in: the APIs, the webhooks, the widgets for your website and app, the
SDKs, and how Texa connects to the systems you already run.
## Start here
- QuickstartA key, your first conversation with Texa AI and its reply, in five minutes.
- Texa AI in your website and appOne script tag or an app component. Visitors get answers; signed-in customers get things done.
- Live chat on your websiteThe Texa Desk chat bubble, answered by your team and Texa AI.
- Let Texa AI act in your systemsYour API, your server, MCP, a ready-made connector or the Texa Relay.
- API referenceEvery route of the Texa API and the Texa Desk APIs, with examples.
- SDKsPython, JavaScript and TypeScript, React Native, Go, Swift and PHP.
## What you can build
| You want to | Use | Start with |
|---|---|---|
| Put Texa AI in your own app, website or backend | The **Texa API** at `https://api.texadesk.com/v1` | [Quickstart](/quickstart) |
| A chat in your website or app that knows the signed-in customer | `widget.js`, `@texadesk/react-native` | [Your website and app](/guides/website-and-app) |
| Answer customers on your own WhatsApp integration | The Texa API | [A WhatsApp bot](/guides/whatsapp-bot) |
| Let customers talk to Texa AI by voice in your app | Voice sessions | [Voice in your app](/guides/voice) |
| Let Texa AI check a payment, an order or an account | Actions and connectors | [Actions](/guides/actions) |
| Read and change conversations, contacts and tickets in Texa Desk | The **Texa Desk API** at `https://desk.texadesk.com/api/v1` | [Texa Desk API](/api-reference/desk) |
| Bring a channel Texa Desk doesn't have | An API inbox and the Client API | [Your own channel](/desk/api-channel) |
| Your own bot, or a panel inside each conversation | Agent bots, dashboard apps | [Agent bots](/desk/agent-bots) |
| Show customers their requests in your own app | The customer portal API | [Customer portal](/desk/customer-portal) |
| Hear about everything as it happens | Webhooks, from the Texa API and from Texa Desk | [Webhooks](/guides/webhooks) |
## The two APIs
Texa has two APIs, because it has two jobs.
- **The Texa API** (`https://api.texadesk.com/v1`) is Texa AI as a service. Your server opens a conversation,
sends each customer message and gets Texa AI's reply, with its actions and handovers, as events.
Keys are `texa_test_…` (the sandbox) and `texa_live_…`. [More](/api-reference/texa-api)
- **The Texa Desk API** (`https://desk.texadesk.com/api/v1`) is the helpdesk: inboxes, conversations, messages,
contacts, teams, tickets, reports and settings. It acts as a person in your team, with their access
token. [More](/api-reference/desk)
They meet in one place: a conversation Texa AI hands over appears in Texa Desk for your team, and
what your team writes there comes back to your app.
## Where things are
| | Address |
|---|---|
| Texa API | `https://api.texadesk.com/v1` |
| Texa API's OpenAPI document | [https://api.texadesk.com/v1/openapi.json](https://api.texadesk.com/v1/openapi.json) |
| Texa Desk (the app and its API) | `https://desk.texadesk.com` |
| Texa Desk's OpenAPI document | [/openapi/texa-desk.json](/openapi/texa-desk.json) |
| Webhooks into Texa (connectors' replies) | `https://hooks.texadesk.com` |
| Voice sessions | `wss://voice.texadesk.com` |
| Status | [https://api.texadesk.com/status](https://api.texadesk.com/status) |
| For AI coding tools | [/llms.txt](/llms.txt) and [/llms-full.txt](/llms-full.txt) |
## Help
- **Your keys and settings** are in Texa Desk, under **Texa AI → Developers**.
- **Something not working?** Every error carries a request id in the `X-Request-Id` header. Send it
with your question to [info@texadesk.com](mailto:info@texadesk.com) or through the chat at
[texadesk.com](https://texadesk.com).
- **Using Texa Desk itself?** The [help centre](https://texadesk.com/help) is written for your team.
---
# Quickstart
Source: https://developers.texadesk.com/quickstart
This takes about five minutes. You need a Texa Desk account (sign up at
[texadesk.com](https://texadesk.com)) where you are an administrator, and a terminal.
## 1. Make a sandbox key
In Texa Desk, open **Texa AI → Developers → API keys** and choose **New key**:
- **Environment:** Sandbox. A sandbox key (`texa_test_…`) reaches the same agent, knowledge and
approved procedures as live, but nothing reaches a real customer and actions are answered by
[fixtures](/guides/sandbox) you set.
- **Name:** something you'll recognise in the request log, such as "Laptop".
The key is shown **once**. Copy it into your shell:
```bash
export TEXA_API_KEY=texa_test_...
```
Keep keys on your server. Never put one in an app or a web page: anyone with it can act as your
business. (For apps and pages there are [widget sessions](/guides/website-and-app).)
## 2. Check the key
```bash
curl https://api.texadesk.com/v1/me -H "Authorization: Bearer $TEXA_API_KEY"
```
The answer names your account and says `"environment": "test"`.
## 3. Open a conversation
```bash cURL
curl https://api.texadesk.com/v1/conversations \
-H "Authorization: Bearer $TEXA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-user-1842" \
-d '{"channel": "web", "customer_ref": "user-1842", "first_name": "Tolu"}'
```
```python Python
# pip install texa
from texa import Client
texa = Client() # reads TEXA_API_KEY
convo = texa.open_conversation(channel="web", customer_ref="user-1842",
first_name="Tolu", idempotency_key="quickstart-user-1842")
print(convo["conversation_id"])
```
```js JavaScript
// npm install @texadesk/sdk
import { TexaClient } from "@texadesk/sdk";
const texa = new TexaClient({ key: process.env.TEXA_API_KEY! });
const convo = await texa.openConversation(
{ channel: "web", customer_ref: "user-1842", first_name: "Tolu" }, "quickstart-user-1842");
console.log(convo.conversation_id);
```
```json
{"conversation_id": "cnv_8a7f0c...", "state": "active", "events": []}
```
`customer_ref` is **your** id for the customer. Texa AI remembers a customer across conversations by
it, so use the same one every time.
## 4. Send the customer's message
```bash cURL
curl https://api.texadesk.com/v1/conversations/cnv_8a7f0c.../messages \
-H "Authorization: Bearer $TEXA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message_id": "msg-1", "text": "Hello, what are your opening hours?"}'
```
```python Python
out = texa.send_message(convo["conversation_id"], "Hello, what are your opening hours?",
message_id="msg-1")
for e in out["events"]:
if e["type"] == "reply":
print(e["text"])
```
```js JavaScript
const out = await texa.sendMessage(convo.conversation_id,
{ text: "Hello, what are your opening hours?", message_id: "msg-1" });
for (const e of out.events) if (e.type === "reply") console.log(e.text);
```
```json
{
"conversation_id": "cnv_8a7f0c...",
"state": "active",
"events": [
{"type": "reply", "reply_id": "rpl_...", "format": "chat",
"text": "We're open Monday to Friday, 8:00 to 18:00, and Saturday 9:00 to 14:00."}
]
}
```
Texa AI answers from the knowledge your team approved in Texa Desk (**Texa AI → Knowledge**). If it
has no knowledge yet, it says it doesn't know rather than making something up.
`message_id` is your own id for the message. Send the same one again (after a timeout, say) and you
get the first answer back, marked `"replayed": true`: nothing runs twice.
## 5. Read the events
Every answer carries `events`, in order. Show each `reply` to the customer. The others tell you what
else happened:
| Event | What to do |
|---|---|
| `reply` | Show `text` to the customer |
| `action_request` | Texa AI needs something from your systems. See [actions](/guides/actions) |
| `handover` | People take over now; Texa AI stops replying. See [handing over](/guides/handover) |
| `close` | The conversation is over |
## 6. Look at it in Texa Desk
Open **Texa AI → Developers → Request log** to see each request you made, its status and its
request id. With handover to Texa Desk switched on (the default), conversations that need a person
appear in your **App conversations** inbox.
## Next
---
# Products and how they fit
Source: https://developers.texadesk.com/products
## The parts
| Product | What it is | What you build with it |
|---|---|---|
| **Texa Desk** | The helpdesk your team works in, on the web at `https://desk.texadesk.com` and in the iPhone and Android app. Inboxes for WhatsApp, email, phone, SMS, live chat, Facebook, Instagram, Telegram and more; contacts, teams, labels, tickets, the help centre, campaigns and reports | The [Texa Desk API](/api-reference/desk), its [webhooks](/guides/desk-webhooks), [agent bots](/desk/agent-bots), [dashboard apps](/desk/dashboard-apps), [your own channel](/desk/api-channel) |
| **Texa AI** | The agent that answers customers on every channel, follows the procedures your team approved, runs actions in your systems and hands over to people. Set up in Texa Desk under **Texa AI** | Everything below reaches it |
| **The Texa API** | Texa AI as a service for your own software, at `https://api.texadesk.com/v1` | [Conversations, events, actions, voice](/api-reference/texa-api) |
| **The website widget and app components** | Texa AI as a chat in your website (`widget.js`) and app (`@texadesk/react-native`, Flutter soon), for visitors and signed-in customers | [Your website and app](/guides/website-and-app) |
| **Live chat** | Texa Desk's own chat bubble for your website, answered by your team and Texa AI | [Live chat](/desk/live-chat), the [WordPress plugin](/desk/wordpress) |
| **Connectors** | Texa AI's way into your systems: ready-made (Paystack, Flutterwave, Shopify…), your API, your server, MCP | [Actions](/guides/actions), [connectors](/guides/connectors) |
| **The Texa Relay** | A small program that runs inside a bank's network and answers Texa AI's lookups without data or credentials leaving it | [The Texa Relay](/guides/relay) |
| **The customer portal** | Your help centre's "My requests": customers follow and answer their tickets, on the web or in your app | [Customer portal](/desk/customer-portal) |
| **SDKs** | Python, JavaScript and TypeScript, React Native, Go, Swift and PHP | [SDKs](/sdks) |
## How a message flows
```text
customer ─▶ WhatsApp / email / phone / live chat ─▶ Texa Desk inbox
│
Texa AI answers ─┤─▶ actions in your systems
│ (connectors, your server, the Relay)
needs a person? ─────┴─▶ your team, in Texa Desk
your app ─▶ Texa API (or the widget) ─▶ Texa AI ─▶ events back to your app and webhooks
└── handover ─▶ "App conversations" inbox in Texa Desk
```
- **Customers who reach you on a channel** (WhatsApp, email, a call) land in a Texa Desk inbox.
Texa AI answers there if it's on for that inbox. You extend this with the Texa Desk API and webhooks.
- **Customers who use your own app or website** reach Texa AI through the Texa API or the widget.
Your app shows the replies. When a person is needed, the conversation appears in Texa Desk.
- **Either way, actions** go to your systems the same way, and every conversation is in one place
for your team.
## Which one do I use?
- *"I want Texa AI to answer inside my app."* The [widget or app component](/guides/website-and-app)
if you want our chat; the [Texa API](/quickstart) if you're drawing your own.
- *"I want to read every conversation into my data warehouse."* [Texa Desk webhooks](/guides/desk-webhooks)
and the [Application API](/api-reference/desk).
- *"I want Texa AI to check a customer's transfer."* An [action](/guides/actions), through a
[connector](/guides/connectors) or your own server.
- *"Our data can't leave our network."* The [Texa Relay](/guides/relay), and a
[dedicated instance](/hosting/dedicated) if the whole system must be in your country.
- *"We already use Zendesk."* [Texa AI inside your helpdesk](/desk/other-helpdesks).
---
# Concepts
Source: https://developers.texadesk.com/concepts
## Account
A business on Texa Desk. Everything (inboxes, people, Texa AI's settings, keys, credits) belongs to
one account. The Texa Desk API puts it in the path (`/api/v1/accounts/{account_id}`); the Texa API
knows it from the key.
## Sandbox and live
Every account has two environments for Texa AI:
| | Sandbox | Live |
|---|---|---|
| Key | `texa_test_…` | `texa_live_…` |
| Customers | none: you play them | real |
| Actions | answered by [fixtures](/guides/sandbox), or your staging connectors | your systems |
| Webhooks | sandbox endpoints (`http` allowed) | live endpoints (`https` only) |
| Credits | counted separately as `sandbox_requests` | drawn from your prepaid balance |
The same procedures and knowledge serve both, so what you test is what customers get.
## Inbox and channel
An **inbox** is where one channel's conversations arrive in Texa Desk: a WhatsApp number, an email
address, a website's live chat, a phone line, an API inbox. Your team members belong to inboxes, and
Texa AI is switched on per inbox. Conversations from the Texa API arrive in the **App conversations**
inbox when they are handed over.
## Conversation
One exchange with a customer. There are two kinds of id, because there are two systems:
- **In Texa Desk**, a conversation has a number shown to your team (its *display id*, `#171`), used
in the Texa Desk API's paths.
- **In the Texa API**, a conversation is `cnv_…`. When a Texa API conversation is handed over to
Texa Desk, it gets a Desk conversation too.
A Texa API conversation is `active`, with **people** (handed over), or `closed`.
## Customer and contact
- A **contact** is a person in Texa Desk, with a name, phone, email and custom attributes.
- A **customer reference** (`customer_ref`) is *your* id for a person in the Texa API, such as
`user-1842`. Texa AI remembers a customer across conversations by it.
[Merge two references](/api-reference/texa-api/customers/two-references-one-person-merge-them) or
[forget a customer](/guides/customers) at any time.
## Texa AI, procedures and knowledge
- **Knowledge** is what Texa AI answers from: documents, web pages and help centre articles your
team added and approved in **Texa AI → Knowledge**.
- **Procedures** are your written instructions for one kind of problem ("a failed transfer"), in
plain words, compiled, checked and approved by a named person. Texa AI follows them step by step.
- **The agent** is Texa AI's name, voice and manner for your business (`GET /v1/agent`).
- **The kill switch** pauses Texa AI for the whole account at once (`GET /v1/kill-switch` tells
you whether it's paused).
## Action
Something Texa AI asks your systems to do: look up a transfer, cancel an order, issue a refund.
An action has a name, typed inputs and outputs, and rules: whether it changes something, whether
the customer must say yes, whether they must be verified first. Texa AI uses an action only in a step
of an approved procedure. [Actions](/guides/actions)
## Event
Everything that happens in a Texa API conversation is an event: a `reply`, an `action_request`, a
`handover`, a `close`. Events come back in the answer to your request, are all on
`GET /v1/events` in order, and are pushed to your [webhooks](/guides/webhooks).
## Handover
Texa AI passing a conversation to people, with a summary and the facts it established. From then
on it does not reply until people hand the conversation back. [Handing over](/guides/handover)
## Ticket
A customer's request that needs tracking beyond one conversation: a reference (`AUD-1001`), a type,
a state, a promise ("resolved within 14 days"), and every conversation about it. [Tickets](/desk/tickets)
## Credits
Texa Desk is prepaid. Texa AI's replies, actions, voice minutes and API requests draw down your
account's credit balance, the same whether they come through Texa Desk or the API. When it runs
out, the Texa API answers `402`. Top up in Texa Desk under **Texa AI → Credits and billing**.
[Usage and credits](/api-basics/usage)
---
# Authentication
Source: https://developers.texadesk.com/authentication
Texa has a few kinds of credential because they live in different places: your server, a web page,
an app, a webhook. Use the one meant for where your code runs.
## At a glance
| Credential | Looks like | Where you get it | Where it may live | Reaches |
|---|---|---|---|---|
| Texa API key, sandbox | `texa_test_…` | Texa AI → Developers → API keys | your server only | your sandbox |
| Texa API key, live | `texa_live_…` | the same | your server only | your live account |
| Site key | `pk_live_…`, `pk_test_…` | Texa AI → Developers → Website and app | a web page or app | visitor chats, only from your listed websites |
| Widget session token | `wst_…` | your server, `POST /v1/widget-sessions` | the page or app, for one customer | that customer's conversations, for up to a few hours |
| Voice session token | one-time | your server, `POST /v1/conversations/{id}/voice-sessions` | the app, once | one voice session |
| Texa Desk access token | a long random string | Texa Desk → Profile settings → Access token | your server | the Texa Desk API, as that person |
| Agent bot token | a long random string | when you create an agent bot | your bot's server | the conversations of the bot's inboxes |
| Live chat HMAC key | per website inbox | the inbox → Configuration → Identity validation | your server | signs `identifier_hash` for live chat |
| Webhook signing secret | per endpoint | when you add the endpoint | your server | checks that a request came from Texa |
| Platform app token | a long random string | a dedicated instance's super admin | your server | the [Platform API](/api-reference/platform), dedicated instances only |
## Texa API keys
Send the key as a bearer token on every request:
```http
Authorization: Bearer texa_live_...
```
- A key is shown **once**, when you make it. Texa keeps only a hash of it.
- **Revoke** a key in Texa AI → Developers → API keys. It stops working at once.
- Make **one key per system** (your backend, your WhatsApp bridge, a test laptop): the request log
shows which key did what, and revoking one leaves the others working.
- A key never belongs in an app or a web page. For those, your server makes a
[widget session](/guides/website-and-app#2-signed-in-customers-your-server-says-who-they-are)
or a [voice session](/guides/voice).
## Site keys and widget sessions
A **site key** is safe to put in a web page: it only opens *visitor* chats, only from the websites
you list under **Websites your widget runs on** (the sandbox site key works anywhere), and at most
20 an hour per address. A visitor is never treated as verified.
For a signed-in customer, **your server** calls `POST /v1/widget-sessions` with its own key and the
customer's reference, and hands the page the `token`. The token reaches only that customer's
conversations, and expires (`ttl_s`, one hour by default).
## Texa Desk access tokens
Every person in Texa Desk has an access token under **Profile settings → Access token**. Send it in
the `api_access_token` header:
```bash
curl https://desk.texadesk.com/api/v1/accounts/1/conversations \
-H "api_access_token: $TEXA_DESK_TOKEN"
```
The token acts as that person, with their role's permissions and only the inboxes and teams they
see. For an integration, add a dedicated person (an administrator or a custom role with only what
the integration needs) rather than using someone's own token, and reset it from their profile if it
leaks.
**Agent bots** have their own token, sent the same way, which works only on the conversations of the
inboxes the bot is connected to. [Agent bots](/desk/agent-bots)
## The Client API
The [Client API](/api-reference/client) builds a customer-facing chat on an API inbox. It needs no
secret: an inbox identifier (from the inbox's settings) and a contact identifier (returned when you
create the contact). Treat the contact identifier as that customer's own secret: whoever has it can
read their conversations. Turn on **identity validation** on the inbox so only your server can
create contacts for known users.
## Checking what Texa sends you
Everything Texa sends your server is signed with HMAC-SHA256: Texa API webhooks, action requests to
your server, Texa Desk webhooks. Check the signature over the raw body and reject old timestamps.
```python Python
import hashlib, hmac, time
def from_texa(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = "sha256=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
```
```js Node
import crypto from "node:crypto";
export function fromTexa(secret, timestamp, rawBody, signature) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = "sha256=" + crypto.createHmac("sha256", secret)
.update(`${timestamp}.`).update(rawBody).digest("hex");
return signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```
```php PHP
function from_texa(string $secret, string $timestamp, string $rawBody, string $signature): bool {
if (abs(time() - (int) $timestamp) > 300) return false;
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}
```
| Sent by | Timestamp header | Signature header |
|---|---|---|
| Texa API webhooks and action requests | `X-Texa-Timestamp` | `X-Texa-Signature` |
| Texa Desk webhooks | `X-Chatwoot-Timestamp` | `X-Chatwoot-Signature` |
Texa Desk's webhook headers keep the names of the open-source helpdesk it is built on, so existing
integrations keep working. Every SDK has a webhook check built in.
---
# Errors
Source: https://developers.texadesk.com/api-basics/errors
Every error from the Texa API has the same shape, and the request's id is in the `X-Request-Id`
header. Quote it to Texa support and we can find the request at once.
```json
{"error": {"type": "invalid_request_error", "code": "invalid",
"message": "a message has text", "field": "text"}}
```
| Field | Meaning |
|---|---|
| `type` | the family of error, below |
| `code` | a stable machine-readable reason, such as `invalid`, `not_found`, `conversation_closed`, `rate_limited`, `insufficient_credits` |
| `message` | a sentence for a developer. Don't show it to customers |
| `field` | for a request that isn't valid, the part that's wrong |
## Statuses
| Status | Meaning | Retry? |
|---|---|---|
| 400, 422 | The request is not valid. `field` names the part | No: fix the request |
| 401 | The key is missing, unknown or revoked | No |
| 402 | Your prepaid credits are used up. Top up in Texa Desk under **Texa AI → Credits and billing** | After topping up |
| 403 | The key can't do this: a sandbox route with a live key, a suspended account | No |
| 404 | No such route, or nothing with that id in your account | No |
| 409 | The conversation's state doesn't allow it (closed, or with a person), or an `Idempotency-Key` was reused with a different body | No: read `code` |
| 429 | Too many requests. Wait `Retry-After` seconds | Yes, after the wait |
| 500, 502, 503 | Our side | Yes, with the same `Idempotency-Key` or `message_id` |
## Codes worth handling
| `code` | When | What to do |
|---|---|---|
| `conversation_closed` | You sent a message to a closed conversation | Open a new conversation for the customer and send it there ([example](/guides/whatsapp-bot)) |
| `rate_limited` | Over your requests per minute | Wait `Retry-After` seconds |
| `idempotency_key_reused` | The same `Idempotency-Key` with a different body | Use a new key for a new request |
| `idempotency_in_progress` | A retry arrived while the first request with that key is still running | Wait a moment and retry |
| `invalid_api_key` | The key is missing, unknown or revoked | Check the key, or make a new one |
| `insufficient_credits` | The prepaid balance is used up | Top up in Texa Desk |
| `test_key_required` | A sandbox route (fixtures) with a live key | Use a `texa_test_…` key |
| `account_suspended` | The account is suspended | Talk to Texa support |
| `too_large` | An upload over the size limit | Send a smaller file |
| `not_found` | Nothing with that id in this account and environment | Check you're using the right key: sandbox and live don't share conversations |
New codes may be added. Treat an unknown code by its status.
## Texa Desk API errors
The Texa Desk API answers in its own shape: `{"error": "..."}` or `{"message": "...", "errors": [...]}`,
with `401` for a missing or wrong `api_access_token`, `404` for something outside the person's
account or view, and `422` for a refused change, with a sentence you can show as it is.
## Failures Texa AI meets
An error in *your* systems (an action that fails or times out) is not an API error. Texa AI tells
the customer honestly that it couldn't check and offers a person, and the action's result is in the
events. See [actions](/guides/actions).
---
# Rate limits
Source: https://developers.texadesk.com/api-basics/rate-limits
Each account has a number of Texa API requests per minute for each environment. By default:
| Environment | Requests per minute |
|---|---|
| Live | 300 |
| Sandbox | 60 |
Every answer says where you stand:
| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | requests allowed in the current minute |
| `X-RateLimit-Remaining` | requests left in it |
| `X-RateLimit-Reset` | when the minute ends (Unix seconds) |
Over the limit, the answer is **429** with `Retry-After` in seconds. Wait that long, then send the
same request again (with the same `Idempotency-Key`, if it had one).
- `GET /v1/me` shows your current limit as `limits.requests_per_minute`.
- The widget's polling for new lines doesn't count against your limit.
- Need more? Write to [info@texadesk.com](mailto:info@texadesk.com) with your expected traffic.
Limits are raised per account.
## Texa Desk API
The Texa Desk API limits requests per person and per address to protect the helpdesk your team is
working in. Space out bulk jobs (a few requests a second), and back off on 429.
---
# Retries and idempotency
Source: https://developers.texadesk.com/api-basics/idempotency
Networks fail. Texa is built so you can always send a request again without anything happening twice.
## Idempotency-Key
Any `POST` to the Texa API can carry an `Idempotency-Key` header: a string you choose, unique for
the thing you're doing.
```bash
curl https://api.texadesk.com/v1/conversations \
-H "Authorization: Bearer $TEXA_API_KEY" \
-H "Idempotency-Key: open-user-1842-2026-10-19" \
-H "Content-Type: application/json" \
-d '{"channel": "web", "customer_ref": "user-1842"}'
```
- The first request runs.
- A retry with the **same key and the same body** gets the same answer back, with the header
`Idempotent-Replayed: true`. Nothing runs again.
- The same key with a **different body** is a `409`: you've reused a key by mistake.
- Keys are kept for **24 hours**.
Good keys say what they're for: `open-wa-2348031234567`, `refund-order-88-attempt-1`.
## message_id
Messages also take your own `message_id`, which works for as long as the conversation lasts. Send
the same `message_id` again and you get the first answer, marked `"replayed": true`. If your
channel gives each message an id (WhatsApp does), use it.
## When to retry
| Answer | Retry? |
|---|---|
| A timeout or a dropped connection | Yes, with the same key or `message_id` |
| `429` | Yes, after `Retry-After` seconds |
| `5xx` | Yes, after a pause that doubles each time (1 s, 2 s, 4 s…), a few times |
| `4xx` other than 429 | No: the request itself needs fixing |
No SDK retries behind your back: you decide when, and they make it safe by sending the same key.
## What Texa sends you
The other direction works the same way. Action requests carry an `idempotency_key` for your payment
or ledger API, and webhooks carry `X-Texa-Event-Id`. Store what you've handled, and ignore a repeat.
---
# Lists and the event feed
Source: https://developers.texadesk.com/api-basics/pagination
## Cursors
Texa API lists page with a cursor, never an offset. An answer gives you `next`; pass it as `after`
to get the page after it. `has_more` says whether there is one.
```bash
curl "https://api.texadesk.com/v1/events?limit=100" -H "Authorization: Bearer $TEXA_API_KEY"
```
```json
{"events": [{"id": "evt_431", "type": "conversation.reply", "...": "..."}],
"next": "evt_530", "has_more": true}
```
```bash
curl "https://api.texadesk.com/v1/events?after=evt_530&limit=100" -H "Authorization: Bearer $TEXA_API_KEY"
```
`limit` is at most 1,000 for events. The SDKs page for you:
```python Python
for event in texa.events(after=saved_cursor):
handle(event)
saved_cursor = event["id"]
```
```js JavaScript
let after = savedCursor;
for (;;) {
const page = await texa.eventsPage(after, 100);
for (const e of page.events) handle(e);
after = page.next;
if (!page.has_more) break;
}
```
## The event feed
`GET /v1/events` holds every event of every conversation in your account, in order: replies,
action requests, handovers, closes, calls, media, quality scores. It is the **complete record**.
Webhooks push the same events, but if your server was down, read the feed from your last cursor
instead of waiting for retries.
- `types` takes a comma-separated list (`conversation.reply,conversation.handover`) for only those.
- Keep the last `id` you handled; it's your cursor.
- Events are the same in the feed and in webhooks, except that webhooks add `environment`.
## Texa Desk API lists
The Texa Desk API pages by number: `page=2`, with `meta` giving counts. Each endpoint in the
[reference](/api-reference/desk) says its page size.
---
# Usage and credits
Source: https://developers.texadesk.com/api-basics/usage
Texa Desk is prepaid, like a phone's airtime: your account holds a balance of credits, and what
Texa AI does draws it down. The API counts exactly as Texa Desk does, so a reply costs the same
whether it went out on WhatsApp or through your app.
## What is counted
| Meter | Counts |
|---|---|
| `conversations` | conversations opened with Texa AI in the loop |
| `messages_in` | customer messages Texa AI read |
| `ai_replies` | Texa AI's messages sent to customers |
| `ai_drafts` | Texa AI's drafts shown to people (suggest reply, shadow mode) |
| `handovers` | conversations Texa AI handed to people |
| `ai_resolutions` | conversations Texa AI closed as resolved without a person |
| `actions` | action requests run in your systems |
| `media_items`, `media_seconds` | voice notes, photos, documents and videos understood; seconds of audio and video transcribed |
| `voice_minutes` | minutes of Texa AI voice, on the phone and in your app |
| `sms_segments_out`, `sms_in` | SMS sent and received |
| `whatsapp_templates` | WhatsApp template messages sent |
| `campaign_sends` | campaign messages and calls |
| `research_responses` | completed surveys and interviews |
| `ask_questions` | questions put to Ask AI, including through [MCP](/guides/mcp) |
| `api_requests` | Texa API requests with a live key |
| `sandbox_requests` | Texa API requests with a test key: **free, up to 2,000 an account a month** |
Prices per meter are on [texadesk.com/pricing](https://texadesk.com/pricing) and in Texa Desk under
**Texa AI → Credits and billing**.
The sandbox is free so you can build without paying: test keys make up to 2,000 requests an account
a month at no charge. Over that, they answer `402` until the next month (ask Texa if you need more
while you build).
## This month's usage
```bash
curl https://api.texadesk.com/v1/usage -H "Authorization: Bearer $TEXA_API_KEY"
```
```json
{"month": "2026-10", "meters": [
{"meter": "conversations", "description": "conversations opened with the AI in the loop", "quantity": 412},
{"meter": "ai_replies", "description": "AI messages sent to customers", "quantity": 1830}
]}
```
Pass `month=2026-09` for an earlier month.
## When credits run out
The Texa API answers **402** (`insufficient_credits`) to anything that would be counted. Top up in
Texa Desk under **Texa AI → Credits and billing**, and requests work again at once.
- **At zero**, Texa AI is paused for the account, so conversations on your channels go to your team
on the customer's next message. A top-up resumes it by itself (a pause a person set stays).
- **Warnings** go by email to your administrators at 20% and 5% of the credits last added, and at
zero.
- **Credits expire** 12 months after the account last added credits; a notice goes 30 days before,
and any top-up keeps the whole balance.
---
# Texa AI in your website and app
Source: https://developers.texadesk.com/guides/website-and-app
Put Texa AI in your own website and app. To a visitor who hasn't signed in it answers questions
from your knowledge. Once they sign in, the same chat knows who they are and works in their
account through your systems: it looks things up, makes changes with their yes, sends them
files, takes them to the right page and fills in forms. Your team sees every chat in Texa Desk
and takes over when it's needed.
You never share your code with Texa. You give it four things, all set up in Texa Desk under
Texa AI:
1. One script tag (or our app component) and a site key: visitors can chat at once.
2. One route on your server that says who is signed in.
3. What Texa AI may do in your systems: your API, your own server, MCP, or the Texa Relay.
4. The pages Texa AI may open, and any steps your page can do for the customer.
## 1. Visitors: one script tag
In Texa Desk, **Developers → Website and app**, make a site key, and add your websites under
**API keys → Websites your widget runs on** (live keys answer only there; the sandbox answers
anywhere). Then add this to every page:
```html
```
A site key is safe in a page: it only opens visitor chats, from your websites, 20 an hour per
address. A visitor is never treated as verified, and an action you keep for signed-in customers
is refused for them: Texa AI asks them to sign in.
## 2. Signed-in customers: your server says who they are
`data-session-url` is a route on **your** server. When someone is signed in, it makes a session
with your secret key and returns it; when nobody is, it answers 401 and the widget stays a
visitor's. Your key never reaches the page.
```js
// Node (Express) with @texadesk/sdk
app.post("/texa/session", requireSignIn, async (req, res) => {
const s = await texa.widgetSession(`user-${req.user.id}`, {
first_name: req.user.firstName, name: req.user.fullName,
email: req.user.email, phone: req.user.phone,
customer_token: req.user.apiToken, // optional: see step 3
});
res.json(s);
});
```
```python
# Python (FastAPI) with texa
@app.post("/texa/session")
def texa_session(user = Depends(signed_in_or_401)):
return texa.widget_session(f"user-{user.id}", first_name=user.first_name,
name=user.full_name, email=user.email)
```
```php
// PHP (Laravel) with texa/texa-php
Route::post('/texa/session', fn () => auth()->check()
? $texa->widgetSession('user-' . auth()->id(), ['name' => auth()->user()->name])
: response()->json(['error' => 'not signed in'], 401));
```
When a visitor signs in on a new page, the widget notices and the same chat carries on as them.
In a single-page app, call `TexaWidget.signedIn()` after your sign-in, and
`TexaWidget.signedOut()` after sign-out.
**Signed in counts as verified.** With **Sign-in counts as verified** on (Developers → Website
and app), Texa AI treats a customer your server signed in as verified, so they aren't asked for
a one-time code. Leave it off if you want a code anyway (a bank moving money, say).
## 3. Acting as the customer
Pass the customer's own token for your API as `customer_token` when you make their session.
Then, in **Actions → Connectors → Your own system**, choose the sign-in **The signed-in
customer's own token**: Texa sends it, in the header you name, on that customer's actions
only. Your API decides what that person may see and change, exactly as for your own app.
The token is sealed, never logged, never shown to Texa AI, and gone when the session ends. If
it has ended, the action fails and Texa AI asks the customer to sign in again.
If your own server runs actions instead (the `conversation.action_request` webhook), each
request says who asks: `data.customer = {"ref": "user-1842", "signed_in": true}`.
## 4. What Texa AI may do
Connect your system in **Actions**:
- **Your API's OpenAPI file:** every operation becomes a draft action to review and save.
- **No OpenAPI file?** Give the address of your API's documentation page: Texa AI drafts the
actions from it, and you review and save each one.
- **Your MCP server**, **your own server** (a signed webhook), or **the Texa Relay** inside your
network.
For each action, under **In your website or app chat**:
- **Who can use it:** anyone, or signed-in customers only.
- **Show the customer:** the file (a receipt, a statement: give the result that holds its
link and its name), or a list as a spreadsheet. The customer gets it as a document in the chat.
Texa AI uses an action only in a step of a procedure you approved, and asks the customer's yes
before anything that changes something.
## 5. Pages and steps on your page
**Pages Texa AI can open** (Developers → Website and app): name the pages customers ask for,
such as *My documents* or *Payments*, or read them from your sitemap. When a customer asks where
something is, Texa AI takes them there. By default the page loads; to use your own router:
```js
window.TexaWidget.onOpenPage((url, page) => router.push(url));
```
Texa AI opens a page only from a step of an approved procedure: on the same page, **Add the
procedure** saves a draft of your procedures with the step "Show the customer a page", for you
to approve under **Procedures**.
**Screen actions** are steps your page does for the customer: filling in a form, opening a
dialog. Add an action on the connector **Our website or app (screen actions)**, and handle it
in your page:
```js
window.TexaWidget.on("fill_delivery_address", async ({ street, city }) => {
form.street.value = street;
form.city.value = city;
return { filled: true }; // the action's results, for Texa AI
});
```
The page has 30 seconds to answer; otherwise Texa AI is told it didn't, and says so. The script
loads `async`: call these after the `texa:ready` event, or set them before it loads with
`window.TexaWidgetSettings = { handlers: {...}, onOpenPage: fn }`.
## 6. In your app
React Native and Expo, with `@texadesk/react-native`:
```tsx
(user ? await api.post("/texa/session") : null)}
signedIn={!!user}
actions={{ fill_delivery_address: async (inputs) => ({ filled: true }) }}
onOpenPage={(url, page) => navigation.navigate(page)}
onPickFile={pickDocument} // your picker: { uri, name, type }
/>
```
Flutter, with `texa_flutter` (coming soon: it isn't on pub.dev yet):
```dart
TexaChat(
siteKey: 'pk_live_...',
getSession: () async => user == null ? null : WidgetSession.fromJson(await api.texaSession()),
signedIn: user != null,
actions: {'fill_delivery_address': (inputs) async => {'filled': true}},
onOpenPage: (url, page) => Navigator.pushNamed(context, url),
)
```
Both components are also clients without a screen (`TexaWidgetClient`), for your own design.
The routes they call are in the [API reference](/api-reference/introduction), under widgets.
## 7. Files, documents and the typing preview
- Customers send photos, PDFs, Word documents and recordings with the paper clip (10 MB at most).
Texa AI reads them.
- Documents Texa AI sends show as document messages; a spreadsheet downloads as a CSV.
- While a chat is shown in Texa Desk, your team sees what the customer is typing, with a line
telling the customer so. The widget masks it before it leaves the page: any run of six or more
digits, and whatever follows *pin*, *otp*, *code*, *cvv* or *password*. Your own client must
do the same before `POST .../typing`.
## 8. Your team
With **Every chat in Texa Desk** on (the default), each chat shows in the **App conversations**
inbox from its first message, waiting with Texa AI, with the customer's name, email, phone and
city. When Texa AI hands over, or doesn't answer within 2 minutes, the same conversation opens
for your team; their replies reach the customer in the chat.
## Before you go live
- Your secret key stays on your server; only the session token and the site key reach a page.
- Your websites are listed under **API keys**.
- Each action that touches an account is kept for signed-in customers.
- Try it all with a sandbox key and the sandbox site key first: nothing reaches a real customer.
---
# A WhatsApp bot in 15 minutes
Source: https://developers.texadesk.com/guides/whatsapp-bot
**No code at all?** Connect your WhatsApp number in Texa Desk (Settings → Inboxes → Add
inbox → WhatsApp) and switch Texa AI on for it under **Texa AI → Agent and inboxes**.
This guide is for when your own server already talks to WhatsApp through Meta's Cloud
API and you want Texa AI to answer.
## What you need
- A WhatsApp Business number on Meta's Cloud API: its **phone number id** and an
**access token**. Meta must send its webhook to your server.
- A Texa test key (`texa_test_…`), from Texa AI → Developers.
- Python 3.10+ with `fastapi`, `uvicorn` and `httpx`.
## The whole bot
```python
# bot.py: run with uvicorn bot:app --port 8000
import os
import httpx
from fastapi import FastAPI, Request
TEXA = httpx.Client(base_url="https://api.texadesk.com/v1", timeout=90,
headers={"Authorization": f"Bearer {os.environ['TEXA_API_KEY']}"})
META = httpx.Client(base_url="https://graph.facebook.com/v21.0", timeout=30,
headers={"Authorization": f"Bearer {os.environ['META_TOKEN']}"})
PHONE_ID = os.environ["META_PHONE_NUMBER_ID"]
conversations: dict[str, str] = {} # customer's number -> Texa conversation (use your DB)
app = FastAPI()
def send_whatsapp(to: str, text: str) -> None:
META.post(f"/{PHONE_ID}/messages", json={
"messaging_product": "whatsapp", "to": to, "type": "text", "text": {"body": text}})
def conversation_for(number: str, name: str | None) -> str:
if number not in conversations:
r = TEXA.post("/conversations", headers={"Idempotency-Key": f"wa-open-{number}"},
json={"channel": "whatsapp", "customer_ref": f"wa:{number}",
"first_name": name})
conversations[number] = r.json()["conversation_id"]
return conversations[number]
@app.post("/whatsapp") # Meta's webhook (verify its signature too)
async def incoming(request: Request):
body = await request.json()
for entry in body.get("entry", []):
for change in entry.get("changes", []):
value = change.get("value", {})
names = {c["wa_id"]: c["profile"]["name"] for c in value.get("contacts", [])}
for m in value.get("messages", []):
if m.get("type") != "text":
continue
number = m["from"]
cid = conversation_for(number, names.get(number))
r = TEXA.post(f"/conversations/{cid}/messages",
json={"message_id": m["id"], "text": m["text"]["body"]})
if r.status_code == 409 and r.json()["error"]["code"] == "conversation_closed":
conversations.pop(number) # it ended: start a new one
cid = conversation_for(number, names.get(number))
r = TEXA.post(f"/conversations/{cid}/messages",
json={"message_id": m["id"], "text": m["text"]["body"]})
for event in r.json().get("events", []):
if event["type"] == "reply":
send_whatsapp(number, event["text"])
return {"ok": True}
```
Run it, point Meta's webhook at `https://your-server/whatsapp`, and message your number.
## Why it is safe to retry
Meta sends a webhook again when your server is slow. Passing Meta's message id as
`message_id` means Texa answers a repeat with the first answer (`"replayed": true`) and
never replies twice. Opening with an `Idempotency-Key` means one number gets one
conversation, however often the first message arrives.
## Things WhatsApp decides
- **The 24-hour window.** You can send a free-form message only within 24 hours of the
customer's last one. After that, Meta takes only an approved template. A reply that
arrives later (see below) may need one.
- **Replies that come later.** A voice note or photo is understood before Texa AI answers,
so its reply can come after your request has returned. Subscribe a
[webhook](/guides/webhooks) to `conversation.reply` and send those too. Use `reply_id` to
make sure you send each reply once.
- **People taking over.** When an event is a `handover`, Texa AI stops replying. With
handover to Texa Desk, your team answers there, and each answer reaches you as the
`conversation.human_message` webhook: send its `text` to the customer on WhatsApp. See
[handing over](/guides/handover).
- **Voice notes and photos.** Upload the file with `POST /v1/media` (with the
`conversation_id`), then send a message whose `attachments` holds `{"media_id": ...}`.
## Going live
Make a live key, swap it in, and nothing else changes. Everything is counted against your
prepaid credits. Watch it in Texa Desk under **Texa AI → Credits and billing**.
---
# Voice in your app
Source: https://developers.texadesk.com/guides/voice
Customers talk to Texa AI from your app or website, with the same agent, knowledge and
procedures as on WhatsApp and the phone. Your **server** opens a voice conversation and
asks for a single-use token. Your **app** opens the voice line with that token. The app
never holds a key.
## 1. Your server: a voice conversation and a token
```python
cid = texa.post("/conversations", json={
"channel": "voice", "customer_ref": "user-1842", "first_name": "Tolu"}).json()["conversation_id"]
session = texa.post(f"/conversations/{cid}/voice-sessions", json={"ttl_s": 120}).json()
# {"conversation_id": "cnv_...", "token": "...", "expires_at": "...",
# "url": "wss://voice.texadesk.com/v1/voice?token=", "options": {}}
```
Give the app `session["token"]` and `session["url"]`. The token opens one session, once,
before `expires_at` (`ttl_s` is 15 to 900 seconds). Anything about the session (text or
voice mode, the voice) is set here in `options`, never by the app:
| Option | Meaning |
|---|---|
| `mode` | `voice` (default) or `text`: the same line with typed turns |
| `voice` | a voice from Texa Desk's Agent settings, e.g. `spitch:lina` |
| `agent_name`, `agent_gender` | how the agent introduces itself |
## 2. Your app: the line
Open `wss://voice.texadesk.com/v1/voice?token=&codec=&rate=`:
- `codec`: `opus` (default; best on mobile networks) or `pcm` (16-bit little-endian mono).
- `rate`: the sample rate, `8000` (default) or `16000`.
The first message tells you exactly what the line carries:
```json
{"type": "ready", "conversation_id": "cnv_...", "rate": 16000,
"audio": {"codec": "pcm", "sample_rate": 16000, "sample_width": 2},
"persona": {"name": "Amaka", "language": "en", "gender": "female"}}
```
Then:
| From Texa (text frames) | Do |
|---|---|
| binary frames | play them: Texa AI speaking |
| `{"type": "utterance", "speaker": ..., "text": ...}` | a caption: what was said, by the customer or by Texa AI |
| `{"type": "flush"}` | the customer interrupted: drop the audio you have queued |
| `{"type": "ended", "end_reason": ..., "our_fault": false, "outcome": {...}}` | the call is over; `our_fault: true` means it ended on Texa's side |
| `{"type": "error", "error": ...}` | the line could not open or failed; open a new session |
| From your app | Means |
|---|---|
| binary frames | the microphone, in the codec and rate you asked for |
| `{"type": "hangup"}` | the customer ended the call |
| `{"type": "stats", "playout_underruns": n}` | optional: how often your playback ran dry, which helps Texa find a breaking voice |
| `{"type": "say", "text": "..."}` | in `text` mode, one typed turn |
If `ready` carries a `resume_token`, a line that drops (a tunnel, a lift) can rejoin within
`rejoin_grace_s` seconds by opening `.../v1/voice?resume=`. Texa holds the
call until then.
## A browser sketch (PCM at 16 kHz)
```js
const ws = new WebSocket(`${url.replace('', token)}&codec=pcm&rate=16000`);
ws.binaryType = 'arraybuffer';
const ctx = new AudioContext({ sampleRate: 16000 });
let playAt = 0;
ws.onmessage = ({ data }) => {
if (typeof data === 'string') {
const msg = JSON.parse(data);
if (msg.type === 'flush') playAt = ctx.currentTime; // drop what is queued
if (msg.type === 'utterance') showCaption(msg.speaker, msg.text);
if (msg.type === 'ended') stopMicrophone();
return;
}
const pcm = new Int16Array(data);
const buf = ctx.createBuffer(1, pcm.length, 16000);
buf.getChannelData(0).set(Float32Array.from(pcm, s => s / 32768));
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
playAt = Math.max(playAt, ctx.currentTime);
src.start(playAt);
playAt += buf.duration;
};
// microphone -> 16-bit PCM frames of 20 ms (use an AudioWorklet in production)
const mic = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true } });
const node = ctx.createScriptProcessor(320, 1, 1);
node.onaudioprocess = e => {
const f = e.inputBuffer.getChannelData(0);
if (ws.readyState === 1) ws.send(Int16Array.from(f, s => Math.max(-1, Math.min(1, s)) * 32767).buffer);
};
ctx.createMediaStreamSource(mic).connect(node);
node.connect(ctx.destination);
```
Turn on echo cancellation, or Texa AI hears itself. Texa's own voice button and mobile
components do all of this for you ([SDKs and widgets](/sdks)).
## After the call
The conversation's transcript is `GET /v1/conversations/{id}/transcript`. Its events
(`call.started`, `call.ended` with the minutes, `conversation.closed`) are on the feed and
in webhooks. Voice minutes are counted against your prepaid credits.
---
# Handing over to people
Source: https://developers.texadesk.com/guides/handover
Texa AI hands a conversation to people when a procedure says so, when the customer asks
for a person, when actions fail twice, or when it cannot help. The answer (and the feed,
and the `conversation.handover` webhook) then carries:
```json
{"type": "handover", "handover_id": "hnd_...", "reason": "customer_asked",
"priority": "high", "summary": "Sent N80,000 to the account ending 4471, meant 4417. Verified. Upset.",
"facts": {"account_last4": "4471"}}
```
From then on **Texa AI does not reply**. The customer's messages are still recorded, so
the conversation's record stays whole. Choose who answers, per environment, in Texa Desk
under **Texa AI → Developers**:
## Your team, in Texa Desk (the default)
The conversation appears in the **App conversations** inbox of your Texa Desk, with Texa
AI's summary, what it established, the conversation so far, and the team, priority and
labels your handover routes give (Texa AI → Agent and inboxes → Handover). Your team gets
the usual Texa Desk notifications, on the web and in the mobile app.
- Keep sending the customer's messages to `POST /v1/conversations/{id}/messages` as
usual: they appear in the Desk conversation.
- Each reply your team writes reaches your server as the
**`conversation.human_message`** webhook: show `data.text` to the customer in your app.
Subscribe to it, or replies from the Desk will not reach the customer. The Desk warns
your team when there is no such webhook.
```json
{"type": "conversation.human_message", "conversation_id": "cnv_...",
"data": {"conversation_id": "cnv_...", "message_id": "cw_1022", "author": "Bola",
"text": "I've found your transfer. It will be reversed within 24 hours.",
"sent_at": 1790000123, "attachments": []}}
```
- When your team sets the conversation to **pending**, or assigns it back to Texa AI,
Texa AI carries on. Its next replies come as `conversation.reply` webhooks. When they
**resolve** it, the conversation closes.
## Your own system
Your people answer in your own tools. Texa tells you with the `conversation.handover`
webhook and does nothing more. Then:
- `POST /v1/conversations/{id}/human-messages` with `{"text", "author"}` records what your
person said, so Texa AI knows it when it takes over again. Show it to the customer
yourself.
- `POST /v1/conversations/{id}/handback` with a `note` of what was done gives the
conversation back to Texa AI. Its answer carries Texa AI's next reply.
- `POST /v1/conversations/{id}/close` closes it.
## Handing over yourself
`POST /v1/conversations/{id}/handover` with a `reason` hands a conversation to people at
any time, for example when your app's user taps "talk to a person".
`POST /v1/conversations/{id}/assist` gives the person holding it a suggested reply. It is
screened like every reply, and never sent by Texa.
---
# The sandbox
Source: https://developers.texadesk.com/guides/sandbox
A test key (`texa_test_…`) reaches your sandbox: the same agent and knowledge, and the
procedures your team approved, in a separate environment where nothing reaches a real
customer and actions are answered by **fixtures** you set. Build everything here, then swap
in a live key.
## Fixtures: what an action answers
```bash
# answer get_transfer_status with this
curl -X PUT https://api.texadesk.com/v1/sandbox/actions/get_transfer_status \
-H "Authorization: Bearer $TEXA_TEST_KEY" -H "Content-Type: application/json" \
-d '{"response": {"status": "succeeded", "data": {"status": "paid_out", "amount": 80000}}}'
# a named scenario: open a conversation with {"scenario": "reversed"} to use it
curl -X PUT https://api.texadesk.com/v1/sandbox/actions/get_transfer_status \
-H "Authorization: Bearer $TEXA_TEST_KEY" -H "Content-Type: application/json" \
-d '{"scenario": "reversed", "response": {"data": {"status": "reversed"}}}'
# a fault, to see how Texa AI tells the customer
curl -X PUT https://api.texadesk.com/v1/sandbox/actions/refund_payment \
-H "Authorization: Bearer $TEXA_TEST_KEY" -H "Content-Type: application/json" \
-d '{"response": {"status": "succeeded"}, "fault": {"kind": "timeout"}}'
```
A fixture's `response` is `{"status": "succeeded" | "failed", "data": {...}, "error": "..."}`.
A `fault` imitates what real systems do:
| `kind` | What happens |
|---|---|
| `slow` | answers after `ms` milliseconds |
| `error` | answers failed, with `error` |
| `timeout` | never answers: the action's own timeout fires |
| `malformed` | answers with fields of the wrong type and fields nobody defined |
| `injection` | answers with an instruction hidden in a text field, to show that Texa AI does not obey it |
`GET /v1/sandbox/actions` lists your fixtures, and `DELETE /v1/sandbox/actions/{action}`
(with `?scenario=`) removes one. These routes answer only a test key. An action with no
fixture is not mocked: it reaches you, or your connector, exactly as it would live, so you
can test your own code in the sandbox.
An action connected in Texa Desk (a connector) runs against the sandbox's own connector
settings, so you can point it at your staging system instead of a fixture.
## What is different from live
| | Sandbox | Live |
|---|---|---|
| Customers | none: you play them | real |
| Actions | fixtures, or your staging connectors | your systems |
| Webhooks | sandbox endpoints; `http` allowed | live endpoints; `https` only |
| Handing over | as set for the sandbox; Desk conversations carry the label `sandbox` | as set for live |
| Rate limit | lower (see Texa Desk) | higher |
| Usage | counted as `sandbox_requests` | counted per meter against your credits |
Texa AI's replies in the sandbox come from the same models as live, so what you see is what
your customers will get.
---
# Customers and memory
Source: https://developers.texadesk.com/guides/customers
Texa AI remembers each customer across conversations: what they asked before, what was done, the
facts it established. In the Texa API it knows them by **your** reference, `customer_ref`.
## Choose a reference
Use an id from your own system that never changes for that person: `user-1842`, not an email
address or a phone number they might change. Prefixes keep sources apart: `wa:2348031234567` for a
WhatsApp number you haven't matched to an account yet.
## What Texa AI remembers
```bash
curl https://api.texadesk.com/v1/customers/user-1842 -H "Authorization: Bearer $TEXA_API_KEY"
```
The answer is what Texa AI keeps about the customer. Use it to show your team what Texa AI knows, or
to check what it will bring into the next conversation.
You can give Texa AI facts at the start of a conversation with `known_facts` ("Gold customer since
2021; two cards, one blocked"): it uses them in that conversation without having to ask.
## Two references, one person
When you learn that two references are the same person (a WhatsApp number that signs in to
account `user-1842`), merge them so their history is one:
```bash
curl -X POST https://api.texadesk.com/v1/customers/wa:2348031234567/merge \
-H "Authorization: Bearer $TEXA_API_KEY" -H "Content-Type: application/json" \
-d '{"into": "user-1842", "by": "sign-in"}'
```
`into` is the reference that stays. `by` says who or what merged them, for the record.
## Forget a customer
When a customer asks you to delete their data (their right under the NDPA and similar laws), forget
them:
```bash
curl -X POST https://api.texadesk.com/v1/customers/user-1842/forget \
-H "Authorization: Bearer $TEXA_API_KEY" -H "Content-Type: application/json" \
-d '{"requested_by": "privacy@yourcompany.com, ticket 4471"}'
```
Texa AI's memory of the customer and their Texa API conversations are deleted. `requested_by` is
kept in your audit log, so you can show the request was handled. A contact in Texa Desk is deleted
there, through the Desk ([Delete contact](/api-reference/desk/contacts/delete-contact)).
## Texa Desk contacts
Customers who reach you on a channel are **contacts** in Texa Desk, with their phone, email and
custom attributes. Texa AI fills in what it learns (a name, a city) on the contact. Read and change
contacts with the [Contacts API](/api-reference/desk/contacts/list-contacts), and hear about changes
with the `contact_created` and `contact_updated` [webhooks](/guides/desk-webhooks).
---
# Knowledge
Source: https://developers.texadesk.com/guides/knowledge
Texa AI answers only from knowledge your team approved: documents, web pages and help centre
articles. Most companies add knowledge in Texa Desk (**Texa AI → Knowledge**), where they can also
read a whole website (up to 10,000 pages from its sitemap: the first 30 at once, the rest in the
background) or connect their help centre. The API is for keeping knowledge in step with a
system of yours: a product catalogue, a price list, a policy library.
## Add a document
```bash
curl -X POST https://api.texadesk.com/v1/knowledge/documents \
-H "Authorization: Bearer $TEXA_API_KEY" -H "Content-Type: application/json" \
-d '{"title": "Transfer limits", "kind": "policy",
"body": "Daily transfer limits: Tier 1 accounts ₦50,000; Tier 2 ₦200,000; Tier 3 ₦5,000,000.",
"uploaded_by": "policy-sync"}'
```
A document is **not used until a person approves it** in Texa Desk. This is deliberate: what Texa
AI tells customers is your company's word, so a named person says yes to it. Approvers see the
document under **Texa AI → Knowledge**, with who sent it (`uploaded_by`).
## See what's there
```bash
curl https://api.texadesk.com/v1/knowledge/documents -H "Authorization: Bearer $TEXA_API_KEY"
```
Each document has its approval state, so a sync job can tell what is waiting for a person.
## Writing knowledge that works
- **One topic per document**, with a title a customer would recognise.
- **Facts, not marketing:** numbers, steps, conditions, exceptions.
- **Say what's not possible**, too ("We don't deliver outside Lagos and Abuja"). Texa AI says it
doesn't know rather than guess, so a missing fact becomes a handover.
- **What changes often** (balances, order states) is not knowledge: it is an [action](/guides/actions).
---
# Voice notes, photos and documents
Source: https://developers.texadesk.com/guides/media
Customers send more than text. Texa AI understands voice notes, photos, documents and videos: it
transcribes, reads and describes them, then answers as if they had typed.
## Send a file
Upload the file, then send a message that carries it:
```bash
curl https://api.texadesk.com/v1/media \
-H "Authorization: Bearer $TEXA_API_KEY" \
-F conversation_id=cnv_8a7f0c... \
-F file=@voice-note.ogg
```
```json
{"media_id": "med_31c...", "kind": "audio", "status": "processing", "size": 48213,
"sha256": "9f2c..."}
```
```bash
curl https://api.texadesk.com/v1/conversations/cnv_8a7f0c.../messages \
-H "Authorization: Bearer $TEXA_API_KEY" -H "Content-Type: application/json" \
-d '{"message_id": "wamid.HBg...", "attachments": [{"media_id": "med_31c..."}]}'
```
The turn waits for the file to be understood. A long recording can take longer than your request,
so Texa AI's reply may arrive **later**, as a `conversation.reply` [webhook](/guides/webhooks) and
on the event feed. Texa sends `media.understood` (or `media.failed`) when it's done.
- A **location** goes as an attachment too: `{"type": "location", "latitude": 6.45, "longitude": 3.39}`.
- `GET /v1/media/{media_id}` gives what was understood (the transcript, the description);
`GET /v1/media/{media_id}/content` gives the bytes back.
- Uploads are at most 64 MB through the API; the widget takes files of up to 10 MB.
## Replies as voice notes
Customers who speak often want to hear the answer. Open the conversation with `"reply_audio": true`
(or send it on a message to switch from then on), and each reply also carries `audio_media_id`: an
OGG/Opus voice note of the reply, ready for WhatsApp. Fetch it from `/v1/media/{id}/content`.
## Counted
Each file understood counts as a `media_items`, and audio and video by the second
(`media_seconds`). See [usage and credits](/api-basics/usage).
---
# Transcripts, quality and reports
Source: https://developers.texadesk.com/guides/reports
## One conversation
| Route | Gives |
|---|---|
| `GET /v1/conversations/{id}/transcript` | what was said, in order, by the customer, Texa AI and people |
| `GET /v1/conversations/{id}/export` | everything: the transcript, every action with its inputs and result, handovers, and the procedure followed. For audits and disputes |
| `GET /v1/conversations/{id}/quality` | the quality scores of a closed conversation |
```bash
curl https://api.texadesk.com/v1/conversations/cnv_8a7f0c.../export -H "Authorization: Bearer $TEXA_API_KEY"
```
Fields Texa AI marks as private (an account number, a balance) are masked in what you read here,
as they are in Texa Desk.
## Quality
Every closed conversation is scored: whether it was resolved, whether Texa AI followed the
procedure, whether its actions were right, and whether a handover was appropriate. A score arrives
as the `quality.scored` webhook. The same scores drive **Texa AI → Quality** in Texa Desk.
## Reports
```bash
curl "https://api.texadesk.com/v1/reports/support?since=2026-10-01&until=2026-10-19" \
-H "Authorization: Bearer $TEXA_API_KEY"
```
| Report | Carries |
|---|---|
| `GET /v1/reports/support` | `conversations`, `handover_rate`, `contact_reasons` (what customers wrote about, grouped into themes with quotes), `procedures` (conversations per procedure), `handovers`, `actions` with their `failure_rate`, and `knowledge_gaps`: what customers asked that your knowledge doesn't answer |
| `GET /v1/reports/quality` | `conversations`, `resolved_rate`, `handover_rate`, `procedure_followed`, `actions_correct`, `handover_appropriate`, `first_reply_s`, `turns_to_resolution` |
Both take `since` and `until` (ISO dates) and give back the `period`. Conversations in your own
shadow mode (people answer, Texa AI drafts) are counted apart as `shadow_conversations`.
For Texa Desk's own reports (your team's response times, conversations by inbox, agent and team),
use the [Reports API](/api-reference/desk/reports/get-account-reports). For questions in plain
words ("what are customers complaining about this week?"), connect your AI tools through
[MCP](/guides/mcp).
---
# Actions
Source: https://developers.texadesk.com/guides/actions
When Texa AI needs something from your systems, such as a transfer's status, a refund or
a balance band, it asks with an **action request**. The request uses the action's name and
typed inputs. What each action is (inputs, outputs, whether it changes something, whether
the customer must say yes or be verified first) is set in Texa Desk under **Texa AI →
Actions**. A named person approves it there. Texa AI never invents an action or runs one
the procedures do not allow.
There are three ways to run an action. You choose per action.
| Way | Who holds your credentials | You write |
|---|---|---|
| **Texa calls your API**: a connector in Texa Desk (HTTP, OAuth 2, HMAC, a ready-made one such as Paystack or Flutterwave) | Texa, encrypted | nothing |
| **Texa calls your server**: a signed webhook connector | you | one endpoint |
| **You run it**: from the API's answer | you | a few lines around your API calls |
If your systems speak MCP, a connector of type **MCP server** turns your tools into
actions ([MCP](/guides/mcp)).
The first two run by themselves. In the API's answer, the `action_request` event is
marked `"handled_by": "texa"` with its result's status, and Texa AI's next reply follows it
in the same `events`. Banks and fintechs that keep credentials inside use the second way,
or the Texa Relay inside their network.
## You run it
```json
{"type": "action_request", "request_id": "act_3f9c...", "action": "get_transfer_status",
"inputs": {"reference": "TX-9B2LC"}, "idempotency_key": "idem_act_3f9c...",
"expires_at": "2026-09-27T10:05:00Z"}
```
Run it, then give the result:
```python
r = texa.post(f"/conversations/{cid}/action-results", json={
"request_id": event["request_id"],
"status": "succeeded", # or "failed"
"data": {"status": "paid_out", "amount": 80000}})
for e in r.json()["events"]: # Texa AI carries on: show its reply
...
```
- **Answer before `expires_at`.** Texa AI tells the customer it is checking, and past that
time it moves on.
- **A failure is a failure.** Send `"status": "failed"` with an `error` (at most 500
characters, for the log). Texa AI then says it did not work and offers a person. It never
claims an action succeeded when it did not.
- **Nothing found is not a failure.** When a lookup finds no record (no transfer with that
reference), send `"status": "not_found"` with no data. Texa AI tells the customer no record
matches and asks them to check it, instead of saying the check did not work. When Texa calls
your API itself, a read's HTTP 404 means the same.
- **Writes use `idempotency_key`.** If the same action request arrives twice (a retry, a
timeout), your system must do it once. Pass `idempotency_key` to your payment or ledger
API.
- **Only return the fields the action declares.** Anything the action marks as private
never reaches the customer.
## Texa calls your server (signed webhook)
In Texa Desk, add a connector of type **Webhook** with your URL. You get a signing
secret, shown once. For each request, Texa POSTs:
```
POST https://your-server/texa/actions
X-Texa-Timestamp: 1790000000
X-Texa-Signature: sha256=" with your secret>
X-Texa-Request-Id: act_3f9c...
{"request_id": "act_3f9c...", "action": "get_transfer_status",
"inputs": {"reference": "TX-9B2LC"}, "idempotency_key": "idem_act_3f9c...",
"expires_at": "...", "conversation_id": "cnv_...", "environment": "live",
"customer": {"ref": "cw:12:345", "name": "Ngozi Eze", "phone": "+2348031234567",
"email": null},
"reply_url": "https://hooks.texadesk.com/actions/results/..."}
```
`customer` is the person as your Texa Desk contact knows them: the phone a WhatsApp
message came from, the address an email came from. It says who is asking, not that
they proved it. Texa AI checks identity before any action marked for verified
customers only. An action you mark **Load at the start** (Actions screen) runs
before Texa AI's first reply in every conversation, with `inputs` empty and this
`customer`, so look the customer up by `phone` or `email`. `conversation_id` is
`null` then.
Check the signature and that the timestamp is within 5 minutes, then either:
- answer **200** with `{"status": "succeeded", "data": {...}}`, `{"status": "not_found"}` or
`{"status": "failed", "error": "..."}`, within the action's timeout, or
- answer **202** and later POST the same body to `reply_url`, signed the same way with the
same secret.
```python
import hashlib, hmac, time
def signed_by_texa(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = "sha256=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
```
## In the sandbox
With a test key, set what an action answers, including faults, before you build it:
```bash
curl -X PUT https://api.texadesk.com/v1/sandbox/actions/get_transfer_status \
-H "Authorization: Bearer $TEXA_TEST_KEY" -H "Content-Type: application/json" \
-d '{"response": {"status": "succeeded", "data": {"status": "paid_out", "amount": 80000}}}'
```
See [the sandbox](/guides/sandbox) for scenarios and faults.
---
# Ready-made connectors
Source: https://developers.texadesk.com/guides/connectors
A ready-made connector gives Texa AI actions in a system you already use. You paste your keys,
review the actions, and name them in your procedures. Set them up in Texa Desk under
**Texa AI → Actions → Connectors**, separately for the sandbox (test keys) and live.
| Connector | Category | Actions |
|---|---|---|
| **Paystack** | payments | `get_payment_status` · `get_transfer_status` (verified customers) · `refund_payment` (changes something; verified, with the customer's yes) |
| **Flutterwave** (v4) | payments | `get_payment_status` · `get_transfer_status` (verified) · `refund_payment` (verified, with the customer's yes) |
| **Monnify** (Moniepoint) | payments | `get_payment_status` |
| **M-Pesa** (Daraja) | payments | `get_mpesa_payment_status`: whether an M-Pesa Express request was paid |
| **Shopify** | commerce | `get_order` (verified) · `cancel_order` for an unshipped order (verified, with the customer's yes) |
| **WooCommerce** | commerce | `get_order` (verified) |
| **HubSpot** | CRM | `find_customer` by phone or email · `open_ticket` |
| **Salesforce** | CRM | `find_customer` by phone · `open_case` |
| **Zoho CRM** | CRM | `find_customer` by phone · `add_crm_note` |
"Verified" means Texa AI checks who the customer is before it runs the action (a one-time code to
the phone or email on record, or your own sign-in). "With the customer's yes" means it states
exactly what it will do and waits for them to agree.
## What you control
For each action you can change, before you save it:
- **Private outputs.** An amount or a recipient's account marked private is never said back to an
unverified customer.
- **Who may use it** in your website or app chat: anyone, or signed-in customers only.
- **Limits** on actions that move money, such as the largest refund.
- **Load at the start:** a lookup that runs before Texa AI's first reply, so it already knows the
customer's record.
Every call is recorded under the action (**the last 50 calls**), with account and card numbers,
PINs, keys, emails, phone numbers and balances hidden. Test an action from the same screen: reads
run against live, writes only in the sandbox.
## Not listed?
Connect [your own API](/guides/your-api) (an OpenAPI file, or a documentation page Texa AI reads),
[your own server](/guides/action-server), or an [MCP server](/guides/mcp). Ask for a connector at
[info@texadesk.com](mailto:info@texadesk.com).
---
# Connect your own API
Source: https://developers.texadesk.com/guides/your-api
If your system has an HTTP API, Texa can call it for Texa AI with no code on your side. Your
credentials are stored encrypted and never shown again; Texa AI never sees them.
## 1. Add the connector
In Texa Desk, **Texa AI → Actions → Connectors → Your own system**, choose **Texa calls your API**
and give its base address (`https://api.yourcompany.com`). Then pick how Texa signs in:
| Sign-in | Sends |
|---|---|
| API key | a header (`X-Api-Key: …`) or a query value you name |
| Bearer token | `Authorization: Bearer …` |
| Basic | `Authorization: Basic …` from a username and password |
| OAuth 2 client credentials | gets a token from your token URL, keeps it until it expires and refreshes it before |
| HMAC signing | a signature header over the body (or `timestamp.body`), SHA-256, hex or base64 |
| The signed-in customer's own token | the customer's token from your website or app, in the header you name, on that customer's actions only ([how](/guides/website-and-app#3-acting-as-the-customer)) |
## 2. Make the actions
- **Import an OpenAPI 3 file:** every operation becomes a draft action.
- **No OpenAPI file?** Give the address of your API's documentation page. Texa AI drafts the
actions from it.
- **Or write one by hand.**
Nothing is saved until you review each draft: its description, inputs, outputs, which outputs are
private, whether it changes something, and whether the customer must be verified first.
## 3. The request and the answer
Under the hood, an action is a request template and a mapping of the answer. You can edit both:
```yaml
request:
method: GET
path: "/transfers/{{ inputs.reference | urlencode }}"
response:
success_when: {path: "$.status", equals: true}
error: {path: "$.message"}
outputs:
status: {path: "$.data.status", map: {success: paid_out, pending: processing, "*": failed}}
amount: {path: "$.data.amount", divide: 100}
sent_at: {path: "$.data.created_at", date: true}
```
- **Placeholders:** `{{ inputs.name }}`, `{{ secrets.api_key }}`, `{{ idempotency_key }}`, with
filters `urlencode`, `kobo` (naira to kobo), `upper`, `lower`, `digits`, `int`, `str`, `json`,
`hexkey` (a 40-character key for APIs that take letters and digits only). In a JSON body, a
value that is only a placeholder keeps its type. A missing value is an error that names it,
never an empty value sent by accident.
- **Paths** are a small, safe part of JSONPath: `$`, `.key`, `[n]`, `[*]`. No expressions, no scripts.
- **A read's HTTP 404** means "no such record" (Texa AI asks the customer to check), not a failure.
- **Writes carry `idempotency_key`:** pass it to your API so a retry doesn't do it twice.
## Where Texa will and won't call
- Only the hosts the connector is set up for: its base address, plus any you list.
- `https` for live connectors.
- **Never a private address** (`10.x`, `192.168.x`, `localhost`, cloud metadata), however the name
resolves, checked again at connect time. Redirects are not followed.
To reach systems inside your network, use [your own server](/guides/action-server) or the
[Texa Relay](/guides/relay).
---
# Run actions on your server
Source: https://developers.texadesk.com/guides/action-server
When you'd rather keep your credentials and logic on your side, Texa sends each action request to
your server and you answer with the result. There are two ways to receive them.
| | Signed webhook | Socket |
|---|---|---|
| Texa connects to | your `https` URL | nothing: your server connects to Texa |
| Needs an inbound port | yes | no |
| Good for | a public API server | a server behind a firewall, a laptop while you build |
Both are set up in Texa Desk under **Texa AI → Actions → Connectors → Your own system**.
## Signed webhook
Texa POSTs each request, signed with the connector's secret:
```http
POST https://your-server/texa/actions
X-Texa-Timestamp: 1790000000
X-Texa-Signature: sha256=">
X-Texa-Request-Id: act_3f9c...
{"request_id": "act_3f9c...", "action": "get_transfer_status",
"inputs": {"reference": "TX-9B2LC"}, "idempotency_key": "idem_act_3f9c...",
"expires_at": "2026-10-19T10:05:00Z", "conversation_id": "cnv_...", "environment": "live",
"customer": {"ref": "cw:12:345", "name": "Ngozi Eze", "phone": "+2348031234567", "email": null},
"reply_url": "https://hooks.texadesk.com/actions/results/..."}
```
[Check the signature](/authentication#checking-what-texa-sends-you), then either:
- answer **200** with `{"status": "succeeded", "data": {...}}`, `{"status": "not_found"}` or
`{"status": "failed", "error": "..."}` within the action's timeout, or
- answer **202**, and later POST the same body to `reply_url`, signed the same way.
`customer` says who is asking as Texa Desk knows them (the phone a WhatsApp message came from), not
that they proved it. Texa AI checks identity first for actions marked for verified customers only.
A request from your website or app chat also says `"signed_in": true` when your server signed the
customer in.
## Socket
Your server opens one WebSocket to Texa and keeps it open; requests come down it.
```text
wss://hooks.texadesk.com/actions/connect
Authorization: Bearer
```
| Direction | Message |
|---|---|
| Texa → you | `{"type": "ready"}` once connected |
| Texa → you | `{"type": "action_request", "reply_token", "request_id", "action", "inputs", "idempotency_key", "expires_at", "conversation_id"}` |
| you → Texa | `{"type": "action_result", "reply_token", "status", "data", "error"}` |
| you → Texa | `{"type": "ping"}`, answered `{"type": "pong"}` |
```python
# pip install websockets
import asyncio, json, os, websockets
async def main():
async with websockets.connect("wss://hooks.texadesk.com/actions/connect",
additional_headers={"Authorization": f"Bearer {os.environ['TEXA_SOCKET_KEY']}"}) as ws:
async for raw in ws:
msg = json.loads(raw)
if msg["type"] != "action_request":
continue
data = await run(msg["action"], msg["inputs"], msg["idempotency_key"])
await ws.send(json.dumps({"type": "action_result", "reply_token": msg["reply_token"],
"status": "succeeded", "data": data}))
asyncio.run(main())
```
Reconnect when the socket drops (with a pause that grows). A request with no socket connected, or no
answer before `expires_at`, fails, and Texa AI tells the customer honestly that it couldn't check.
## Or run it from the API's answer
If your own server is already the one calling the Texa API, you don't need either: action
requests come back in the answer's `events`, and you answer with
`POST /v1/conversations/{id}/action-results`. See [actions](/guides/actions#you-run-it).
---
# MCP
Source: https://developers.texadesk.com/guides/mcp
Texa works with the Model Context Protocol in both directions.
## Your AI tools ask Texa
Texa is an MCP server. An assistant your team already uses can ask about your customers,
and every answer is backed by real conversations. It connects over Streamable HTTP:
- **URL:** `https://api.texadesk.com/mcp`
- **Header:** `Authorization: Bearer texa_live_…`
| Tool | What it does |
|---|---|
| `ask_customers` | Answers a question such as "what are customers complaining about this week?" from your conversations, citing the conversation and the exact words for every claim. When the conversations do not say, it answers that it cannot answer from the data. Needs a live key |
| `list_conversations` | Recent conversations, by state or customer |
| `get_transcript` | What was said in one conversation |
| `support_report` | The support report for the last few days |
| `usage` | What has been counted this month |
Every tool only reads. Tool calls count as requests, and each question as an Ask AI
question, against your credits. They appear in your request log.
## Your MCP server runs Texa's actions
If your systems already speak MCP, add a connector of type **MCP server** in Texa Desk
(**Texa AI → Actions → Connectors**), with your server's `https` address and, if it needs
one, a bearer token. **Import tools** lists your tools as draft actions. Review each one
(its outputs, which fields are private, whether the customer must be verified, and for
tools that change something, that the customer must say yes), then save it. Texa AI then
asks for those actions like any other: gated, recorded, and told honestly when they fail.
A tool's `structuredContent`, or its JSON text, is the action's result, and `isError` is a
failure.
---
# The Texa Relay
Source: https://developers.texadesk.com/guides/relay
!!! note "On request"
The Texa Relay is built and tested. Its connection point on Texa's hosting is set up per
company: ask [info@texadesk.com](mailto:info@texadesk.com) before you plan a rollout.
The Texa Relay lets Texa AI look things up in your systems (a transfer's status, an account's
state) **without your data or credentials leaving your network** and **without opening any inbound
port**. It is a single program (also a container image) that runs on a server of yours, connects
*out* to Texa over mutual TLS, and runs only the operations your administrator wrote, reviewed and
signed.
## What it can and cannot do
- It runs **only the named operations in your signed file**: a `SELECT` your team wrote, or a call
to one of your internal services. Texa sends an operation's name and values for its declared
parameters, **never SQL, a URL or a header**.
- **Queries are read-only.** Each is checked to be a single `SELECT`, the database login must be
read-only (checked at start), and every query runs in a read-only transaction.
- **Limits you set:** rows, time, response size, requests a minute, how many at once.
- **Masking:** account numbers to the last four digits, card numbers to the first six and last
four, BVN and NIN hashed, dates of birth to the year. Per column, or by column name everywhere.
- **Customer checks:** an operation can require that Texa has verified the customer first.
- **An audit log on your disk**, hash-chained, so a changed or removed line shows.
- **Your kill switch:** create `/var/lib/texa-relay/KILL` (or send `SIGUSR1`) and nothing runs
until it's removed. Texa can't lift your switch.
## Databases and systems
Postgres, MySQL, SQL Server and Oracle, and HTTP services answering JSON, XML, OFS or text.
Templates are included for **Finacle**, **Temenos Transact (T24)** and **Oracle FLEXCUBE**.
## Setting it up
The full guide for your IT team, with the configuration file, signing, enrolment and operations, is
in [Hosting and security → Install the Texa Relay](/hosting/relay-install). In outline:
1. Your administrator makes a signing key and keeps the private half off the Relay server.
2. Your team writes `relay.yaml`: the databases, and each operation's query, parameters, outputs and
masking. Start from a template.
3. The administrator signs the file. The Relay refuses an unsigned or changed file.
4. In Texa Desk, **Texa AI → Actions → Relay → Add a relay**, then run the enrol command it shows on
your server. The token works once, for 24 hours.
5. **Bind** each operation to an action, and name the action in your procedures.
You see each relay's health, operations and alerts on the same screen, and can switch one off.
---
# Texa API webhooks
Source: https://developers.texadesk.com/guides/webhooks
Texa POSTs each event to your server as it happens. Add an endpoint in Texa Desk under
**Texa AI → Developers → Webhooks**: its URL, live or sandbox events, and every event type
or the ones you pick. You get a **signing secret**, shown once.
- Live endpoints must be `https` and reachable from the internet. Sandbox endpoints may use
`http` while you build (through a tunnel such as ngrok: private addresses are never
called).
- **Send a test event** in Texa Desk posts a `ping` at once and shows the answer.
## What arrives
```
POST https://your-server/texa/webhooks
Content-Type: application/json
X-Texa-Timestamp: 1790000000
X-Texa-Signature: sha256=5d41402abc4b2a76b9719d911017c592...
X-Texa-Event-Id: evt_431
X-Texa-Event-Type: conversation.reply
X-Texa-Delivery-Id: whd_12
{"id": "evt_431", "type": "conversation.reply", "created_at": "2026-09-27T10:00:03Z",
"environment": "live", "conversation_id": "cnv_8a7f0c...",
"data": {"type": "reply", "reply_id": "rpl_...", "format": "chat", "text": "..."}}
```
The body is the same event as on `GET /v1/events`, plus `environment`.
## Check it came from Texa
Compute the signature over the **raw** body, before any JSON parsing, and compare in
constant time. Reject a timestamp more than 5 minutes old.
```python
import hashlib, hmac, time
def verify(secret: str, headers, raw_body: bytes) -> bool:
ts = headers["X-Texa-Timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
expected = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, headers["X-Texa-Signature"])
```
```js
import crypto from 'node:crypto';
export function verify(secret, headers, rawBody) {
const ts = headers['x-texa-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', secret)
.update(`${ts}.`).update(rawBody).digest('hex');
const given = headers['x-texa-signature'] || '';
return given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
```
The same signature scheme is used on every request Texa sends you: action webhooks and
monitor alerts too.
## Answer fast, handle once
- Answer any **2xx within 10 seconds**, then do the work. Anything else, or no answer, is
tried again after 1 minute, 5 minutes, 30 minutes, then 2, 5, 10 and 10 hours: 8 attempts
over about a day. After that the delivery is marked failed, and you can send it again from
Texa Desk.
- Delivery is **at least once**: use `X-Texa-Event-Id` (or the body's `id`) to ignore a
repeat.
- Order is not guaranteed across retries. Use `created_at`, or the feed, when order
matters.
- An endpoint whose every delivery has failed for **5 days** is switched off, and Texa Desk
says so. Switch it on again once it is fixed.
- Each delivery's attempts (time, status, error) are in Texa Desk, for 30 days.
The feed, `GET /v1/events?after=`, is the complete record. If your server was
down, read the feed from your last cursor instead of waiting for retries.
## Event types
| Type | When |
|---|---|
| `conversation.opened` | a conversation was opened |
| `conversation.reply` | Texa AI replied, including replies that came after your request returned |
| `conversation.action_request` | Texa AI asked for an action (codes the customer typed are masked here) |
| `conversation.handover` | Texa AI handed the conversation to people |
| `conversation.handback` | people handed it back to Texa AI |
| `conversation.closed` | it was closed; `resolved` says whether Texa AI resolved it |
| `conversation.human_message` | a person answered from Texa Desk: show `text` in your app ([handing over](/guides/handover)) |
| `call.started`, `call.ended` | a phone or in-app call started or ended |
| `media.understood`, `media.failed` | a voice note, photo, document or video was understood, or could not be |
| `quality.scored` | a closed conversation was scored |
| `monitor.finding` | one of your monitors found something |
| `interview.completed` | a research interview was completed |
| `report.ready` | a report is ready |
| `survey.response` | a customer completed a survey |
| `ping` | a test event you sent from Texa Desk |
New types may be added. Ignore types you do not know, and subscribe to types by name if
you want only those.
---
# Texa Desk webhooks
Source: https://developers.texadesk.com/guides/desk-webhooks
Texa Desk webhooks tell your server what happens in the helpdesk: new conversations, every message
(from customers, your team and Texa AI), contact changes and tickets. Use them to copy conversations
into your own systems, update a CRM, or trigger your own work.
For events from **Texa API** conversations (replies, action requests, handovers), use the
[Texa API webhooks](/guides/webhooks) instead.
## Add a webhook
In Texa Desk, **Settings → Integrations → Webhooks → Add new webhook**: your URL and the events you
want. Each webhook has a **secret** (shown with an eye button, and copyable) that signs every
delivery. Or use the API: [Add a webhook](/api-reference/desk/webhooks/add-a-webhook).
## Events
| Event | When |
|---|---|
| `conversation_created` | a conversation starts |
| `conversation_updated` | a conversation's attributes change (assignee, team, labels, custom attributes, its ticket) |
| `conversation_status_changed` | it becomes open, pending, snoozed or resolved |
| `message_created` | a message is added: incoming, outgoing, or a private note |
| `message_updated` | a message changes (delivered, read, failed, edited) |
| `contact_created`, `contact_updated` | a contact is added or changed (including a merge) |
| `conversation_typing_on`, `conversation_typing_off` | someone in your team starts or stops typing |
| `webwidget_triggered` | a visitor opens the live chat on your website |
| `inbox_created`, `inbox_updated` | an inbox is added or changed |
| `ticket_created`, `ticket_updated`, `ticket_merged` | a [ticket](/desk/tickets) is made, changed (state, type, person) or merged |
## What arrives
```http
POST https://your-server/texa-desk/webhooks
Content-Type: application/json
X-Chatwoot-Delivery: 7b0d0d6e-0f6c-4b1e-9c0e-2b0d6a3a9f11
X-Chatwoot-Timestamp: 1790000000
X-Chatwoot-Signature: sha256=" with the webhook's secret>
```
```json
{
"event": "message_created",
"id": 50211,
"content": "Hello, my transfer of 80,000 has not arrived",
"message_type": "incoming",
"content_type": "text",
"private": false,
"created_at": "2026-10-19T10:00:03.112Z",
"source_id": "wamid.HBgN...",
"sender": {"id": 345, "name": "Ngozi Eze", "phone_number": "+2348031234567", "type": "contact"},
"conversation": {"id": 171, "inbox_id": 12, "status": "pending", "...": "..."},
"inbox": {"id": 12, "name": "WhatsApp"},
"account": {"id": 1, "name": "Pilot Bank"}
}
```
- **Messages** carry `content`, `message_type` (`incoming`, `outgoing`, `activity`, `template`),
`private` (a note your team only), `sender`, `conversation`, `inbox`, `account`, and
`attachments` when there are files.
- **Conversation events** carry the conversation (with its `meta.sender`, `meta.assignee`, labels,
custom attributes and `ticket`) and, for updates, `changed_attributes`: each changed field as
`[before, after]`.
- **Contact events** carry the contact: `id`, `name`, `email`, `phone_number`, `identifier`,
`custom_attributes`, `additional_attributes`.
- **Ticket events** carry the ticket (reference, title, type, state, person, team, promise),
`contact`, `conversations`, and for `ticket_updated` its `changed_attributes`, the state and type
by name: `"state": ["New", "Waiting on customer"]`.
## Check the signature
The signature is HMAC-SHA256 over `"."` with the webhook's secret,
written as `sha256=`. Compute it over the **raw** body, before parsing, compare in constant
time, and reject a timestamp older than five minutes. The code is the same as for
[Texa's own signatures](/authentication#checking-what-texa-sends-you), with these header names.
These header names come from the open-source Chatwoot helpdesk Texa Desk is built on, and are kept
so that existing integrations work unchanged.
## Answer fast
- Texa Desk waits **5 seconds** for your answer and sends each event **once**: there are no
retries. Answer `200` at once and do the work afterwards (put the event on your own queue).
- Use `X-Chatwoot-Delivery` to spot a repeat.
- If your server was down, catch up from the API: [list conversations](/api-reference/desk/conversations/conversations-list)
updated since then, and [their messages](/api-reference/desk/messages/get-messages).
## Per inbox: API inboxes
An [API inbox](/desk/api-channel) has its own webhook URL, which gets that inbox's events, signed
with the inbox's secret. That's how your own channel hears your team's replies.
---
# Live chat on your website
Source: https://developers.texadesk.com/desk/live-chat
Live chat puts a chat bubble on your website. Every chat lands in Texa Desk, where your team
(and Texa AI, if you turn it on) answers it. This page covers adding it to any website, telling
it who the visitor is, and checking that nobody pretends to be someone else.
## Add it to your website {#install}
In Texa Desk, open **Settings → Inboxes → Add inbox → Website** and give it your website's
name and address. The last step shows your code. You can copy it again at any time from the
inbox's **Configuration** tab. It looks like this:
```html
```
Paste it on every page, just before `