> For the complete documentation index, see [llms.txt](https://docs.maiagent.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.maiagent.ai/tech/en/api-integration/api-tool-engineering-guide.md).

# API Tool Engineering Guide

Connect your REST API to a MaiAgent Agent as a tool: request assembly, authentication, timeouts and retries, error responses to the model, and record lookup.

This page is for engineers who want an Agent to call their own systems: how MaiAgent turns the parameters chosen by the Agent into an HTTP request, how to design your API for model use, and what the model sees when a call fails.

For step-by-step instructions in the admin console (where to add a tool and what to enter in each field), see [Create an API Tool](https://docs.maiagent.ai/build/tools/setup_api_tool) in the user manual. This page does not repeat those steps. You can also create and manage tools through the REST API, using the same fields described here. See “Tools and Connectors” in the API documentation.

## Call Flow <a href="#call-flow" id="call-flow"></a>

```mermaid
sequenceDiagram
    participant U as User
    participant A as MaiAgent Agent
    participant P as MaiAgent Platform
    participant S as Your API

    U->>A: "Check the status of order A123"
    A->>A: Choose a tool and parameters based on its description and parameter schema
    A->>P: tool call (tool name + JSON parameters)
    P->>P: Apply parameter defaults and substitute {{variables}} in path parameters, headers, and Query parameters
    P->>S: HTTP request (place parameters in the path, query string, JSON body, or headers according to their locations)
    S-->>P: Response (any text; JSON recommended)
    P->>P: Mask sensitive values, truncate to fit the context window, and write a tool execution record
    P-->>A: Raw response (or error message)
    A-->>U: Summarize the results in natural language
```

The Agent only decides which tool to call and which parameters to provide. The platform sends the actual HTTP request. Your API does not need to know anything about the model; it only needs to be an HTTP endpoint accessible from the public internet.

## Tool Definition Reference <a href="#tool-definition" id="tool-definition"></a>

One API tool corresponds to **one endpoint and one HTTP method**. The platform currently does not import OpenAPI documents. If your API has an OpenAPI specification, create a separate tool for each operation you want to expose to the Agent, then combine the path parameter, query, and requestBody schemas and paste them into “Parameter Schema.”

For field values, format restrictions, and locations in the interface, refer to [Create an API Tool: Detailed API Configuration](https://docs.maiagent.ai/build/tools/setup_api_tool#step-6-api-configure-details) in the user manual. Engineers integrating the API should also know the following:

* **The tool's `name` is the identifier the model uses to call it.** Before it is sent to the model, it is normalized to match `^[a-zA-Z][a-zA-Z0-9_]*$`, with a maximum of 64 characters. Hyphens become underscores; names that exceed the limit are truncated and given a hash suffix. Use `snake_case` from the start, such as `get_order_status`.
* **The model decides when to call a tool based only on its description.** If `prompt` has a value, it is used; otherwise, `description` is used. Clearly describe what the tool does, when to use it, and what it returns.
* **Field names when creating a tool through the REST API**: API URL is `apiUrl`, HTTP method is `httpMethod` (`get`, `post`, `put`, `patch`, `delete`), headers are `rawHeaders`, Query parameters are `rawQueryParams`, parameter schema is `rawParametersSchema`, and display name is `displayName`.

## Parameter Schema (JSON Schema) <a href="#parameters-schema" id="parameters-schema"></a>

The platform converts the parameter schema into the model's function-calling schema, then validates the parameters returned by the model against it.

```json
{
  "type": "object",
  "properties": {
    "order_id": {
      "type": "string",
      "description": "Order number, in a format such as A123456"
    },
    "include_items": {
      "type": "boolean",
      "description": "Whether to include item details in the response",
      "default": false
    },
    "status": {
      "type": "string",
      "enum": ["pending", "shipped", "delivered"],
      "description": "Set this when querying a specific status only"
    }
  },
  "required": ["order_id"]
}
```

Rules and limitations:

* The top level must be `type: "object"`. Supported features include `properties`, `required`, `default`, `enum`, nested `object`, and `array`.
* Parameter names may contain only English letters, digits, underscores, dots, hyphens, and Chinese characters, and must be 1–64 characters long. Invalid names are rejected when the tool is saved.
* **The platform supplies `default` values**: if the model does not provide a parameter, the platform fills in its `default` before sending the request. To always send a fixed value without leaving it to the model, use `default` or enter it directly in Query parameters.
* Write a `description` for every parameter. The quality of the parameters supplied by the model depends almost entirely on this text. A format example (“in a format such as A123456”) is much more effective than specifying only the type.
* The parameter name `timeout` is reserved: if the model provides it, the platform uses it as the timeout in seconds for this request and **does not** send it to your API. Do not use this name for a business parameter.
* The model's function calling is responsible for honoring `required` and types. When saving a tool, the platform only checks that the parameter schema itself is valid JSON Schema. Before sending a request, it only checks that all path parameters are present. The model may still occasionally omit required parameters. Have your API return `400` and explain which field is missing; the model can then fill in the parameters again and call the tool (see [How Responses Reach the Model](#response-to-model)).
* **Parameter schema changes take effect immediately**: the tool definition is read when processing begins for each message. The next message after saving uses the new schema, including in an ongoing conversation; you do not need to attach the tool again. For MCP tools, however, the “Allowed Tools” list is a snapshot retrieved when the tool is saved. Retrieve it again after adding tools on the server. See [Create an MCP Tool](https://docs.maiagent.ai/build/tools/mcp-setup#step-5-fetch-allowed-tools) in the user manual.

{% hint style="warning" %}
Do not declare an API key as a parameter and supply it through `default`. The model can see the parameter schema, so the key would enter the model's context. Put keys in headers or Query parameters instead; see the next section.
{% endhint %}

## Request Assembly <a href="#request-assembly" id="request-assembly"></a>

Each parameter in the parameter schema can specify a location (the JSON Schema property's `location`: `path`, `query`, `body`, or `header`). The platform places the value supplied by the model in that part of the request. If no location is specified, the HTTP method determines where it goes:

| Method                 | Parameters Without a Specified Location                    | Body                                                        |
| ---------------------- | ---------------------------------------------------------- | ----------------------------------------------------------- |
| `GET`                  | Placed in the query string                                 | Sent only when parameters are explicitly assigned to `body` |
| `POST`, `PUT`, `PATCH` | Placed in the JSON body (`Content-Type: application/json`) | Always sends a JSON object                                  |
| `DELETE`               | **Not sent**; specify a location to pass a value           | Sent only when parameters are explicitly assigned to `body` |

Key points:

* **Path parameters**: use single braces `{parameter_name}` in the API URL, such as `https://api.example.com/orders/{order_id}`, and set the parameter location to `path`. Values are URL-encoded before substitution. If any path parameter value is missing for a call, the platform does not send the request and reports an error directly. For rules and interface instructions, see [Path Parameters and Parameter Locations](https://docs.maiagent.ai/build/tools/setup_api_tool#path-parameters) in the user manual.
* **Configured Query parameters take precedence over model parameters with the same name**: if a configured “Query parameter” has the same name as a model parameter, the configured value is used. Put keys that must be sent in the query string in Query parameters so the model cannot override them.
* **Configured headers take precedence over parameters with the same name**: parameters with location `header` are sent as headers using their parameter names. If the tool's “Headers” or contact credentials already contain a header with the same name (case-insensitive), the configured value is used.
* The body is always a JSON object. form-data, file uploads, and XML are not supported. If your API requires these formats, provide a JSON interface on your side.

### `{{variables}}` in Headers and Query Parameters <a href="#template-variables" id="template-variables"></a>

Header and Query parameter values can reference conversation context variables (`{{contact_id}}`, `{{source_id}}`, `{{conversation_id}}`, etc.) and parameters declared in the parameter schema using `{{parameter_name}}`. The platform substitutes them on each call. For available variables and substitution rules, see [Substituted `{{…}}` Variables](https://docs.maiagent.ai/build/tools/setup_api_tool#template-variables) in the user manual.

`{{source_id}}` is the variable ISVs use most often. It is the contact's external ID—the `sourceId` you provide when embedding Web Chat or synchronizing contacts. Your system recognizes its own user IDs. Put this ID in a header (for example, `X-User-Id: {{source_id}}`) or the query string so your API can query data as that user, without asking the model to “guess” who the user is through the parameter schema.

## Authentication <a href="#authentication" id="authentication"></a>

Three methods can be used together, in order of precedence from lowest to highest:

1. **Fixed tool-level credentials**: enter `Authorization: Bearer ...` or `X-API-Key: ...` in headers, or enter a key in Query parameters. Calls to your API represent the “entire organization.” After saving, values are masked with `****` in the admin console. Query parameter values in tool execution records and API request records are also masked.
2. **Context variables**: as described above, use `{{source_id}}` and similar variables to tell your API “who is asking,” and handle authorization on your side. Credentials remain at the tool level.
3. **Contact-level credentials (API Credential)**: bind a set of headers to a specific contact. On a call, they **override tool-level headers with the same names**. This suits cases where each end user has their own token (your system needs to make calls as the user, rather than as an integration account).

Your backend writes contact-level credentials after the user logs in:

```http
POST /api/contacts/{contact_id}/api-credentials/
Authorization: Api-Key <Organization API key>
Content-Type: application/json

{
  "tool": "<Tool ID>",
  "headers": {
    "Authorization": "Bearer <This user's token in your system>"
  }
}
```

Sending credentials again for the same contact and tool updates the existing credentials; call the endpoint again when the token expires. Credentials apply only in “that contact's conversations.” Other contacts calling the same tool still use tool-level credentials.

{% hint style="info" %}
Tokens are usually written to contact credentials during identity synchronization when embedding Web Chat. See [Contact Identity Synchronization and Token Updates](/tech/en/serve/contacts/contact-credentials-sync.md).
{% endhint %}

### Where to Put Keys and Who Can See Them <a href="#secrets-and-masking" id="secrets-and-masking"></a>

The platform has no “secret variable” mechanism. Custom names such as `{{SECRET_XXX}}` are not substituted and are rejected when saving a tool. Enter keys directly in the tool's headers or Query parameters, or write them to contact-level credentials. The only substitutable `{{variables}}` are the context variables and parameter names listed in the [previous section](#template-variables).

| Location                                           | Masking Rules                                                                                                                                                                           |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tool editing screen                                | After saving, header and Query parameter values show only their first and last 4 characters, with `****` in between. Values of 8 characters or fewer are displayed entirely as `****`   |
| Model context                                      | Header and Query parameter values are not provided to the model. The model sees only the tool name, description, and parameter schema                                                   |
| `requestHeaders` in tool execution records         | Values are displayed as `***` for headers whose names match the platform's sensitive header list, and for all headers from contact-level credentials. Name matching is case-insensitive |
| Request URL and response in tool execution records | Values configured in the tool's “Query parameters” do not appear in the request URL. If a response or error message echoes these values, they are also replaced with `***`              |

The platform's sensitive header list is `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie`, `X-API-Key`, `Api-Key`, `X-Auth-Token`, and `X-Access-Token`. Custom headers with other names (such as `X-Partner-Secret`) appear in plain text in tool execution records. Therefore:

* If you can choose the header name, put keys in a header from the list above.
* If you must use a custom header name (such as `x-functions-key`), set `sensitiveHeaders` on the tool through the REST API: an array of header names, with at most 50 entries, each up to 256 characters long. Matching is case-insensitive. Only API tools accept this field, and it is not available in the admin console's tool settings. Once configured, masking also applies when viewing existing execution records. When reading a tool, the read-only `globalSensitiveHeaders` field returns the platform's current sensitive header list.
* Organization administrators cannot modify the platform list. On-premises deployments can replace the entire list using the `TOOL_API_SENSITIVE_HEADERS` environment variable.
* Do not put keys in the API URL. The URL (including the query string after `?`) appears unchanged in tool execution records.

For the corresponding explanation in the interface, see [Who Can See These Values](https://docs.maiagent.ai/build/tools/setup_api_tool#credential-visibility) in the user manual.

## Passing Conversation Attachments to Tools <a href="#conversation-attachments" id="conversation-attachments"></a>

API tools can access files uploaded by users in conversations by **passing URLs**. The platform does not put file contents in the request. Instead, the model fills a parameter you declare with the attachment's download URL, and your API downloads it.

How it works:

1. After a user uploads files, the model sees each attachment's `id`, `filename`, and `file` (download URL). Image attachments also include a filename and URL.
2. Declare a string parameter in the parameter schema to receive the URL. When calling the tool, the model fills it with the corresponding attachment's URL.
3. Your API downloads the URL with `GET`.

```json
{
  "type": "object",
  "properties": {
    "file_url": {
      "type": "string",
      "description": "Download URL of the file to process. When the user uploads an attachment, enter its file URL. Copy it exactly without modification"
    },
    "filename": {
      "type": "string",
      "description": "File name, including its extension"
    }
  },
  "required": ["file_url"]
}
```

Key points:

* **URL format**: `file` is a stable platform URL (`https://<API domain>/api/media/...`) that redirects to a currently valid signed storage URL on each download. Your download code must **follow HTTP redirects**. Downloading from this URL does not require an API key.
* **The platform corrects URLs rewritten by the model**: when copying a URL, the model may occasionally get the host or protocol wrong. If a URL in a parameter value can be matched to an attachment in this conversation, the platform replaces it with the correct URL before sending the request. URLs that do not match any attachment are sent unchanged.
* **Multiple files in one call**: declare the parameter as an array of strings (`"type": "array", "items": {"type": "string"}`) and explain in `description` that each element is an attachment URL.
* **Requests are still JSON**: the body does not support form-data or binary uploads (see [Request Assembly](#request-assembly)), so the platform cannot send “the file itself” to your API.
* **Size and format**: allowed file types and per-file size limits depend on the conversation platform's attachment settings. See [API Conventions: Upload Size Limits](/tech/en/api-integration/api-conventions.md#upload-limits). Your API should check `Content-Length` and file type itself.
* **Retention**: conversation attachments have a retention period. Files are deleted and URLs stop working after it expires. If you need to keep a file long term, download it to your own storage when the tool is called. For retention rules, see [Credits Billing: Conversation Attachment Storage](https://docs.maiagent.ai/others/credits#attachment-and-derived-storage) in the user manual.
* **Parameter types**: parameters support `string`, `number`, `integer`, `boolean`, `enum`, nested `object`, and `array` (see [Parameter Schema](#parameters-schema)). There is no dedicated “file” type; files are always passed as URL strings.

{% hint style="warning" %}
Attachment download URLs can be accessed without authentication. Anyone who knows the URL can download the file. Your API must not write it to publicly visible logs or responses, or pass it to third parties.
{% endhint %}

## Timeouts, Retries, and TLS <a href="#timeout-retry-tls" id="timeout-retry-tls"></a>

| Item    | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Timeout | The default timeout for each HTTP request is 30 seconds (a platform-level setting, configurable on-premises). It is not the total time limit for the entire tool call. Calls that time out are recorded as `timeout`, and the model receives “API request timed out.”                                                                                                                                                       |
| Retries | Up to 3 retries after the initial request, for a maximum of 4 attempts, with backoff delays of 1, 2, and 4 seconds. Request errors, including timeouts, and `408`, `409`, `429`, `500`, `502`, `503`, and `504` are retried. Other 4xx responses are not retried.                                                                                                                                                           |
| TLS     | The certificate chain and hostname are always verified. If the connection fails because your certificate chain uses an older root certificate without a Subject Key Identifier (some local CAs), only MaiAgent can enable this exception in system administration, per tool or per domain. This option is not available in the admin console's tool settings; contact MaiAgent. Self-signed certificates are not supported. |

What this means for your API:

* Write endpoints must be **idempotent**. Retries can send the same `POST` twice. Deduplicate using a business key in the parameters (order number or external ID), or return `409` so platform retries still refer to the same record.
* The entire tool call accumulates the time spent on all requests and backoff delays. Repeated timeouts may take more than 120 seconds, so 30 seconds is not the total upper limit on the model's wait. Keep query endpoints within a few seconds. For long-running work, use “create task → return task ID” and provide a separate query tool.
* Returning `Retry-After` with `429` has no effect; the platform uses fixed backoff. Estimate rate limits assuming 3 retries.

## How Responses Reach the Model <a href="#response-to-model" id="response-to-model"></a>

The model sees your **raw response body**, without structural parsing:

| Situation                                  | What the Model Sees                                                                           |
| ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| 2xx                                        | Raw body (`response.text`). An empty body becomes the string `None`.                          |
| Non-2xx (still unsuccessful after retries) | A message starting with `API request failed`, followed by `HTTP <status code>: <raw body>`.   |
| Timeout                                    | A message starting with `API request timed out`, followed by an error description.            |
| Connection error, DNS, or TLS failure      | A message starting with `API request failed`, followed by the exception message.              |
| Insufficient organization Credits          | The call is not sent, and the model receives an explanation that the balance is insufficient. |

If the response exceeds a certain proportion of the model's context window (80% by default), the platform retains the beginning and end, and adds a “truncated” notice at the start. The middle is removed.

API design recommendations:

* **Return concise JSON**. Return only the fields the model needs, rather than an entire ORM object. Limit each list response to a few dozen records; provide pagination parameters so the model can query additional records.
* **Use readable error messages**, rather than error codes alone. `{"error": "order_id A123 does not exist; please check the number"}` helps the model correct parameters or explain the issue to the user better than `{"code": 40401}`.
* **4xx responses provide feedback to the model**: for missing or malformed parameters, return `400` and identify the field so the model can try supplying parameters again. Do not wrap errors in `200` responses; the model will summarize them for the user as successful results.
* If a response echoes the query string (as some frameworks' error pages do), the platform masks the configured Query parameter values before passing the response to the model. Headers stay out of the body only if your API does not print them there.
* To let the model tell the user “no data found,” explicitly return `404` with an explanation, rather than `200` with an empty array.
* **Image URLs in responses**: when your API returns an image URL, the model includes it in its reply using Markdown image syntax (`![Description](https://…)`). When Web Chat displays it, the **user's browser** fetches the image directly from that URL. These are two independent requests: platform → your API (with tool headers and credentials), and browser → image URL (without any tool headers). The image URL must therefore be publicly accessible without authentication and remain valid throughout the conversation. When a short-lived signed URL (such as Azure SAS) expires, images in older messages fail to display. See [Creating Conversations and Messages: Images in Message Content](/tech/en/api-integration/conversations-and-messages.md#images-in-content).

## Records and Debugging <a href="#records-and-debugging" id="records-and-debugging"></a>

Every call writes a **tool execution record**. Find these in the admin console under “AgentOps → Tool Execution Records,” or query them through the API:

```http
GET /api/tool-execution-records/?tool=<Tool ID>&status=failure&start_date=2026-09-01&end_date=2026-09-11
Authorization: Api-Key <Organization API key>
X-Organization-Id: <Organization ID>
```

Available filters: `tool`, `tool_type=api`, `status` (`success`, `failure`, `pending`, `timeout`), `chatbot`, `start_date`, `end_date`, and `query` (full text). Each record contains:

* The input parameters supplied by the model, `inputParameters`, and the actual `requestMethod`, `requestHeaders` (sensitive values masked), and `requestBody` sent by the platform.
* The raw response, `outputResult` (after truncation), or `errorMessage`.
* Execution time and status, plus the associated message and Agent.

To read this API, members need the AgentOps “Tool Execution Records” permission. Organization owners are exempt from this restriction.

Suggested debugging order:

1. Check whether `requestHeaders` and `requestBody` in the record have the expected structure. A `{{variable}}` that was not substituted usually indicates a misspelled variable name or a missing parameter with that name in the parameter schema.
2. If `errorMessage` starts with `HTTP <code>`, your API returned the error. The raw body follows it.
3. If `status=timeout` but your logs show a response was sent, the response took more than 30 seconds. Check your p95 first.
4. If the model “does not call the tool,” no record is created. In this case, adjust the tool description and parameter `description`, rather than the API.

## Billing <a href="#billing" id="billing"></a>

Each API tool call that is successfully sent incurs one “API Tool Call” Credits charge. Calls are not sent when Credits are insufficient. Retries for failed or timed-out calls are not charged separately. Specific prices depend on the organization's plan; see the Credits explanation in the user manual.

## Pre-launch Checklist <a href="#checklist" id="checklist"></a>

* [ ] The endpoint is accessible from the public internet over HTTPS, with a complete certificate chain.
* [ ] Query endpoints have a p95 within a few seconds; write endpoints are idempotent.
* [ ] Every parameter in the parameter schema has a `description` and format example; `required` includes only genuinely required parameters.
* [ ] Keys are in headers or Query parameters, not in the parameter schema.
* [ ] If calls need to be made “as the user,” choose between `{{source_id}}` and contact-level credentials, and implement authorization in your backend.
* [ ] Error responses contain readable messages; `4xx` explains what is missing, and errors are not wrapped in `200` responses.
* [ ] Responses are concise, and lists support pagination.
* [ ] Test a complete call in a real conversation, then confirm in the tool execution record that the request and response are as expected.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.maiagent.ai/tech/en/api-integration/api-tool-engineering-guide.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
