# Texa API webhooks

Signed events for every conversation, reply, action, handover and call, retried for about a day.


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.

<!-- tabs -->
```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));
}
```
<!-- /tabs -->

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=<cursor>`, 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.
