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

# Server-side AI tools

> Register tools your own server answers, so Ola can call them while your app is closed

Your app can give Ola tools that run on **your server**, not in the dashboard
iframe. Register up to 32 tools with a name, a description, a JSON Schema and an
HTTPS `toolUrl`. When Ola calls one, Salesive makes a signed `POST` to that URL
and returns the answer to Ola. Your app does not need to be open, and it does not
need to run an MCP server.

## Three ways to give Ola tools

| | WebMCP tools | Server-side tools (this page) | MCP server tools |
| - | - | - | - |
| Where the code runs | Your embedded page | Your HTTPS endpoint, one per tool | Your MCP server |
| App page must be loaded | Yes | No | No |
| Protocol | `modelContext.registerTool` | One HTTPS `POST` per tool | MCP over Streamable HTTP |
| How tools are declared | From the page | `PUT /api/v1/app/tools` | MCP `tools/list` |
| Merchant approval | Yes | Yes | Yes |
| Best for | Tools that need the page's state | A few actions on your backend | An MCP server you already run |

[WebMCP tools](https://docs.salesive.com/apps/webmcp-tools.md) run in your iframe. The dashboard preloads
installed apps in the background, but the tool still depends on that page being
loaded in the merchant's browser. Server-side tools do not: the call goes from
Salesive's servers to yours.

## Register your tools

Send the whole list with `PUT /api/v1/app/tools` and your app token. The
installation, and therefore the store, comes from the token.

```bash theme={null}
curl -X PUT https://api.salesive.com/api/v1/app/tools \
  -H "Authorization: Bearer app_<your-app-access-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "tools": [
      {
        "name": "get_stock",
        "description": "Read the stock level for a SKU in this store.",
        "inputSchema": {
          "type": "object",
          "properties": { "sku": { "type": "string" } },
          "required": ["sku"]
        },
        "toolUrl": "https://api.example.com/salesive/tools/get-stock"
      }
    ]
  }'
```

* The request **replaces the whole list**. Send every tool every time; a tool you
  leave out is removed.
* Nothing is written until every tool passes validation. A rejected request
  leaves the previous list in place and answers `400` with a message that names
  the tool and the rule, plus a `data.code` such as `too_many_tools`,
  `duplicate_name`, `schema_too_large` or `unsafe_url`.
* `GET /api/v1/app/tools` returns the list for the installation, including tools
  the merchant switched off.

### Tool fields

| Field | Rules |
| - | - |
| `name` | Lowercase letters, digits, `_` and `-`, 1 to 30 characters, starting with a letter or digit. Unique per installation after `-` and `_` are treated as the same character, so `get-stock` and `get_stock` cannot both be registered. |
| `description` | Required, up to 500 characters. Ola sees it. |
| `inputSchema` | Optional JSON Schema object. `type` must be `"object"` (it is added when missing). Up to 8 KB serialized. |
| `toolUrl` | Required HTTPS URL, up to 2048 characters. Public host only: no credentials, no localhost, no private, loopback or link local address. |

`toolUrl` is checked when you save it and again before every call. A URL that
stops passing the guard is not called.

### What the API returns

```json theme={null}
{
  "status": 200,
  "success": true,
  "message": "App tools saved",
  "data": {
    "tools": [
      {
        "id": "672...",
        "name": "get_stock",
        "description": "Read the stock level for a SKU in this store.",
        "inputSchema": {
          "type": "object",
          "properties": { "sku": { "type": "string" } }
        },
        "toolUrl": "https://api.example.com/salesive/tools/get-stock",
        "enabled": true,
        "createdAt": "2026-11-07T10:00:00.000Z",
        "updatedAt": "2026-11-07T10:00:00.000Z"
      }
    ]
  }
}
```

No OAuth scope is required for these endpoints. A tool row is bound to the
installation in your token, so you can only register tools for the store that
installed you. The registration route accepts a 512 KB JSON body, because 32
tools with 8 KB schemas do not fit the API's default limit.

## How Ola calls a tool

When Ola decides to use one of your tools, Salesive sends:

```http theme={null}
POST https://api.example.com/salesive/tools/get-stock
Content-Type: application/json
X-Salesive-Signature: t=1700000000,v1=<base64 HMAC-SHA256>

{
  "tool": {
    "id": "672...",
    "name": "get_stock",
    "description": "Read the stock level for a SKU in this store."
  },
  "arguments": { "sku": "C3" },
  "shopId": "6680aabbccddeeff00112200",
  "installationId": "672..."
}
```

* `arguments` is the object Ola built from your `inputSchema`. It is `{}` when
  there is nothing.
* `shopId` and `installationId` say which install the call is for. They are
  inside the signed body.
* There is no merchant identity, no session token and no `Authorization` header.
  The signature is the only credential.
* The call has 20 seconds to answer. A response over 256 KB is refused.
* A `3xx` is refused, and a non `2xx` status is a failed call.
* Return JSON or plain text. Salesive parses JSON when it can and passes text
  otherwise.

### Verify the signature

The signature uses your app's webhook signing secret and the same construction
as [webhooks](https://docs.salesive.com/apps-api/webhooks.md#verify-the-signature) and the
[app proxy](https://docs.salesive.com/apps/app-proxy.md#verify-the-signature): base64 HMAC-SHA256 of
`<t>.<raw body>`, in `X-Salesive-Signature: t=<unix seconds>,v1=<signature>`.
Reject a timestamp more than 300 seconds from your clock, in either direction.
Verify the raw body before parsing.

```js Node.js (Express) theme={null}
import express from "express";
import crypto from "crypto";

const app = express();
const SIGNING_SECRET = process.env.SALESIVE_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 300;

const safeEqual = (a, b) => {
  const left = Buffer.from(a);
  const right = Buffer.from(b);
  return left.length === right.length && crypto.timingSafeEqual(left, right);
};

app.post(
  "/salesive/tools/:name",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const parts = Object.fromEntries(
      (req.get("X-Salesive-Signature") || "")
        .split(",")
        .map((part) => part.split("=", 2)),
    );
    const timestamp = Number(parts.t);
    const signature = parts.v1 || "";

    if (
      !Number.isFinite(timestamp) ||
      Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS
    ) {
      return res.status(401).send("stale signature");
    }

    const expected = crypto
      .createHmac("sha256", SIGNING_SECRET)
      .update(`${timestamp}.`)
      .update(req.body)
      .digest("base64");

    if (!safeEqual(signature, expected)) {
      return res.status(401).send("invalid signature");
    }

    const { tool, arguments: args, shopId } = JSON.parse(
      req.body.toString("utf8"),
    );
    if (tool.name !== req.params.name) {
      return res.status(400).send("tool mismatch");
    }

    // ... look the store up by shopId and answer
    res.json({ stock: 7 });
  },
);
```

> [!NOTE]
>
> A development install is signed with your development signing secret;
> production uses the production pair. Tool calls use the app's current secret
> for the installation's environment, not a release snapshot's secret. `toolUrl`
> still has to be a public HTTPS address, so use a tunnel (for example
> `cloudflared` or `ngrok`) if your server runs locally.

## Approval and the merchant's switch

In the dashboard chat, every call shows an approval card first. The card shows
your app's name and logo from the installation record, and the tool name.

* **Approve** runs this call.
* **Reject** tells Ola the merchant declined. Your server is never called.
* **Always allow** runs this call, and future calls of this tool for this
  installation without asking.

A pending card waits up to **2 minutes**, and Ola's turn waits with it. If the
merchant ignores it, the call ends as an error and Ola continues without the
tool.

Merchants can switch a tool off at any time. In the dashboard, open
**Apps > Installed apps**, open your app, and use **Tools Ola can use**. The
switch is enforced at call time: the next call is refused with `tool_disabled`
and your server is not called. The list Ola sees can take up to 30 seconds to
catch up, but the call itself is refused immediately.

Re-registering your list does not re-enable a tool the merchant switched off. The
choice is carried across by name. A tool you remove and add again later is a new
row, so it starts on.

Only the store owner or a store admin can see and switch tools. Other staff get
`403`.

### Scheduled tasks and headless runs

Ola also runs where nobody can click a card, such as **scheduled tasks**. There
she only uses tools the merchant chose **Always allow** for. These tools have no
read-only annotation, so even a read-only tool needs **Always allow** to run in a
scheduled task. Workflow AI steps never get app tools.

## Audit

Every call that reaches a registered tool writes one audit row: the app,
installation, tool, whether it succeeded, how long it took, and a short error
code. Arguments and results are never stored, so the audit cannot carry customer
data. Audit rows are not shown in the developer console yet.

## Limits

| Limit | Value |
| - | - |
| Tools per installation | 32 |
| Tool name | 1 to 30 characters, lowercase letters, digits, `_` and `-` |
| Description | 500 characters |
| `inputSchema` | 8 KB serialized, `type: "object"` |
| `toolUrl` | HTTPS, 2048 characters |
| Registration request body | 512 KB |
| Call timeout | 20 seconds |
| Response from your server | 256 KB |
| Merchant approval | 2 minutes |
| Tools offered per store | 48, within 96 KB of definitions (this path's budget) |
| Tool list refresh | About 30 seconds |
| Result shown to Ola | 16,000 characters |

## Merchant endpoints

The merchant's view and switch use a merchant session, never an app token:

```bash theme={null}
# List the tools an installation registered
curl https://api.salesive.com/api/v1/apps/installed/<installationId>/tools \
  -H "Authorization: Bearer <merchant-session-token>" \
  -H "x-shop-id: <shopId>"

# Switch one tool off, or back on
curl -X PATCH https://api.salesive.com/api/v1/apps/installed/<installationId>/tools/<toolId> \
  -H "Authorization: Bearer <merchant-session-token>" \
  -H "x-shop-id: <shopId>" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'
```

`GET` returns `data.tools`, the same shape the app sees. `PATCH` returns
`data.tool` with the new `enabled` value.

## Security model

* The app token is bound to one installation, so you can only register tools for
  the store that installed you.
* No OAuth scope is required, because registering a tool is not a write to store
  data.
* Every call is signed with your webhook secret. Verify it and reject stale
  timestamps.
* Salesive never sends your `toolUrl` or secret to the model. Ola sees the name,
  the description and the schema.
* Results reach Ola marked as untrusted third-party data and are capped at
  16,000 characters.
* The merchant can switch any tool off, and uninstalling the app stops calls
  immediately.
* Your server never receives a merchant session or user identity. Use `shopId`
  and `installationId` from the signed body.

## Next steps

> [card] [AI tools (WebMCP)](https://docs.salesive.com/apps/webmcp-tools.md)
>
> Tools that run inside your embedded page.

> [card] [AI tools from your server (MCP)](https://docs.salesive.com/apps/mcp-server-tools.md)
>
> A full MCP server, with its own session tokens and tool listing.

> [card] [Storefront app proxy](https://docs.salesive.com/apps/app-proxy.md)
>
> Serve your own pages on the merchant's storefront domain.


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