> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usefrictionless.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Actions: buttons that call your server

> Put buttons like Retry transfer or Resend OTP beside each chat.

Add actions in **Integrations → Actions**, with one HTTPS address on your server. When an agent presses a button, we send that address a POST:

```http theme={null}
POST https://api.yourapp.com/frictionless/actions
Content-Type: application/json
X-Frictionless-Timestamp: 1758801234
X-Frictionless-Signature: sha256=5f2b…

{
  "action": "retry_transfer",
  "label": "Retry transfer",
  "conversationId": "c1d2…",
  "customer": { "id": "…", "name": "Chidi Okafor", "phone": "2348035550142", "visitorId": null, "email": null },
  "contact": { "externalId": "user_42", "email": "chidi@example.com", "phone": "2348035550142", "name": "Chidi Okafor" },
  "agent": { "id": "…", "name": "Bisi Adebayo" },
  "sentAt": "2026-09-25T10:20:34.000Z"
}
```

`contact` is your own record of the person, when you've sent it through the API. Otherwise it's `null` and you can match on `customer.phone`.

## Check the signature

The signature is the HMAC-SHA256 of the timestamp, a full stop and the raw request body, keyed with your [webhook secret](/authentication).

* Refuse the request if it doesn't match, or if the timestamp is more than five minutes old.
* While a rotation overlaps, the header carries two signatures separated by a comma. Accept the request if either matches.

```js Node.js (Express) theme={null}
// Read the raw body before JSON parsing
const crypto = require("crypto");

app.post("/frictionless/actions", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Frictionless-Timestamp");
  const expected = Buffer.from("sha256=" + crypto
    .createHmac("sha256", process.env.FRICTIONLESS_WEBHOOK_SECRET)
    .update(timestamp + "." + req.body)
    .digest("hex"));
  const signatures = (req.get("X-Frictionless-Signature") || "").split(",").map((s) => Buffer.from(s.trim()));
  const matches = signatures.some((given) => given.length === expected.length && crypto.timingSafeEqual(given, expected));
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  if (!fresh || !matches) {
    return res.status(401).end();
  }

  const { action, contact } = JSON.parse(req.body);
  if (action === "retry_transfer") {
    // …retry it in your system…
    return res.json({
      message: "Transfer TRX-8842 retried. It should arrive in 2 minutes.",
      event: { name: "Transfer retried", properties: { reference: "TRX-8842" }, level: "info" },
    });
  }
  res.status(400).json({ message: "Unknown action" });
});
```

## What to send back

* Answer within **10 seconds** with a 2xx status. `message` is optional: it appears in the chat as a private note, for the team only.
* `event` is optional too, with the same shape as [`POST /events`](/context-api/server#send-an-event). It goes into the customer's history beside the chat.
* Any other status counts as failed. If you send `{ "message": "…" }`, agents see it as the reason.
* We don't follow redirects, and only public HTTPS addresses are allowed.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.