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

# Build an order-status tool endpoint

> Run a small backend with sample orders, verify Visito's POST format, and connect it to a custom tool.

This example gives you a working endpoint for the [custom tools walkthrough](/product-guides/ai-agent/custom-tools). It uses synthetic orders and Node.js built-in modules, with no package installation or database connection.

The example implements **Visito → your endpoint**. Registering a tool does not create the backend for you.

## Run the example

With Node.js installed, save the following as `order-status-tool.mjs`:

```javascript theme={null}
import { createServer } from "node:http";

const secret = process.env.TOOL_DEMO_SECRET;
if (!secret) throw new Error("Set TOOL_DEMO_SECRET before starting.");

const server = createServer(async (req, res) => {
  const reply = (status, data) => {
    res.writeHead(status, { "content-type": "application/json" });
    res.end(JSON.stringify(data));
  };
  if (req.method !== "POST" || req.url !== "/visito/order-status") {
    reply(404, { error: "route_not_found" });
    return;
  }
  if (req.headers.authorization !== `Bearer ${secret}`) {
    reply(401, { error: "unauthorized" });
    return;
  }
  try {
    let raw = "";
    let bytes = 0;
    for await (const chunk of req) {
      bytes += chunk.length;
      if (bytes > 4096) {
        reply(413, { error: "request_too_large" });
        return;
      }
      raw += chunk;
    }
    let body;
    try {
      body = JSON.parse(raw);
    } catch {
      reply(400, { error: "invalid_json" });
      return;
    }
    const number = body?.arguments?.order_number;
    if (typeof number !== "string" || !/^A-[0-9]{4}$/.test(number)) {
      reply(400, { error: "invalid_order_number" });
      return;
    }
    // Synthetic fixtures only. A-5000 simulates an unavailable upstream.
    if (number === "A-5000") {
      reply(503, { error: "order_service_unavailable" });
      return;
    }
    if (number !== "A-1003") {
      reply(200, { found: false, order_number: number });
      return;
    }
    reply(200, {
      found: true,
      order_number: number,
      status: "out_for_delivery",
      delivery_estimate: "today"
    });
  } catch {
    if (!res.headersSent) reply(500, { error: "internal_error" });
    else res.end();
  }
});
server.requestTimeout = 10000;
server.headersTimeout = 10000;
server.listen(4100, "127.0.0.1", () => {
  console.log("Demo endpoint: http://127.0.0.1:4100/visito/order-status");
});
```

Start the server:

```bash theme={null}
TOOL_DEMO_SECRET=local-demo-only node order-status-tool.mjs
```

`local-demo-only` is a deliberately public sample value for loopback testing. Use a new private secret before exposing the endpoint beyond your machine. The server returns fixed demo data and does not verify customer identity; it is not a production order service.

## Check the request and response

In another terminal, run:

```bash theme={null}
curl --fail-with-body http://127.0.0.1:4100/visito/order-status \
  -H 'Authorization: Bearer local-demo-only' \
  -H 'Content-Type: application/json' \
  --data '{"arguments":{"order_number":"A-1003"},"meta":{"source":"developer_test"}}'
```

Expected HTTP `200` body:

```json theme={null}
{
  "found": true,
  "order_number": "A-1003",
  "status": "out_for_delivery",
  "delivery_estimate": "today"
}
```

Try these variations before connecting Visito:

| Request change                                           | Expected result                                                   |
| -------------------------------------------------------- | ----------------------------------------------------------------- |
| Use `A-9999`                                             | HTTP `200`, `found: false`. The lookup completed without a match. |
| Use `A-5000`                                             | HTTP `503`, simulated upstream failure.                           |
| Omit `order_number` or send a number instead of a string | HTTP `400`, `invalid_order_number`.                               |
| Send malformed JSON                                      | HTTP `400`, `invalid_json`.                                       |
| Omit or change the bearer token                          | HTTP `401`, `unauthorized`.                                       |

The input schema helps the agent construct arguments; still validate requests in your backend. Don't require live-conversation metadata for direct tests: Visito's test endpoint sends `meta.tenantId` and `meta.source: "developer_test"`, while conversation calls include channel and conversation identifiers. See the [complete request contract](/api-docs/conversational-ai-api#what-visito-sends-to-your-endpoint).

## Make the endpoint reachable

The URL you save must work **from Visito's backend**, not just your browser.

| Where Visito runs                       | Endpoint setup                                                                                                                                                                                 |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Hosted Visito                           | Deploy the demo behind an authenticated HTTPS endpoint, or forward a development HTTPS tunnel to port `4100`. Use synthetic data and a private secret. Stop the tunnel after testing.          |
| Entirely local services on your machine | The loopback URL works only if the calling backend shares your host's network environment.                                                                                                     |
| Local Visito services in Docker         | `localhost` points to the calling container. Use a reachable service hostname or host address and configure the demo's bind address for that network. The sample binds to loopback by default. |

For a container-hosted demo you may need to change `127.0.0.1` to `0.0.0.0` and configure port access. Keep development access restricted to your test environment. Do not paste a loopback URL into hosted Visito and expect it to reach your laptop.

Use the resulting URL ending in `/visito/order-status` in **Build → Tool calls**. Select **POST**, **Bearer**, and your endpoint's secret. Copy the schema and description from the [product walkthrough](/product-guides/ai-agent/custom-tools#1-add-the-tool-definition).

### Connecting an existing API instead

A GET tool sends arguments as query parameters, for example `?order_number=A-1003`. A POST tool sends the wrapper `{ "arguments": { ... }, "meta": { ... } }`. It does not send a flat order object or substitute arguments into URL path templates.

If your provider expects `/orders/A-1003`, a different request body, or rotating OAuth credentials, keep those details in your adapter endpoint. The adapter validates the request, calls the provider, and returns a small JSON result. Provider authentication stays on your server; the tool's configured secret authenticates Visito to that adapter.

## Test the saved definition

You can keep **Active** off for the API test. Obtain the tool ID from [List tools](/api-docs/conversational-ai-api#list-and-read-tools), then set `VISITO_API_KEY` in your local shell from secure storage. It must be a Visito API key for the test workspace with `tools:execute`. Listing tools additionally requires `tools:read`.

Replace `YOUR_TOOL_ID` before running:

```bash theme={null}
curl --fail-with-body \
  "https://platform-api.visitoai.com/m2m/v1/tools/YOUR_TOOL_ID/test" \
  -H "Authorization: Bearer $VISITO_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"input":{"order_number":"A-1003"}}'
```

The test calls your configured endpoint and creates an invocation log. Check `ok: true` and the returned `output`; a failed execution can still return HTTP `200` from the Visito test API with `ok: false` and an `error`. A successful curl exit alone is not enough.

The Visito API key here is different from `TOOL_DEMO_SECRET`. Never put your Visito API key in the custom tool's Secret field to authenticate to this demo.

## Test the agent's behavior

In a workspace without live customer channels, enable the definition and use a new Playground chat:

* “Where is order A-1003?” should produce a completed activity and a reply grounded in the status.
* “Has my order shipped?” should prompt for an order number.
* “Where is order A-9999?” should produce a completed activity and explain that no order was found.
* “Where is order A-5000?” should produce a failed activity and an honest failure response.

These are expected test outcomes, not captured runs. Review **Build → Tool calls → Activity logs** to verify the actual input and response. See [Playground examples](/product-guides/ai-agent/custom-tools#3-try-the-customer-experience-in-playground) for the operator and customer views.

## Replace the demo with your system

Replace the fixture lookup with an authorized read from your order system. Authenticate Visito, verify the requester is allowed to see the order, and return only necessary fields. Treat conversation metadata as context, not proof of order ownership.

Use explicit status values and delivery estimates that reflect your source data. Don't return a shipment status when the provider is unavailable. Keep requests bounded and respond within your tool timeout; the configured timeout may also be limited by Visito's execution budget.

When you finish testing, disable the demo definition, stop the local server with **Ctrl+C**, and stop any development tunnel. Keep mutations such as cancellations in separately authorized tools.
