# Actions

How Texa AI asks your systems for something, the three ways an action runs, and how to answer.


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=<hex HMAC-SHA256 of "1790000000.<raw body>" 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.
