> 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

Wire your REST API to the MaiAgent AI assistant as a tool - how requests are assembled, how auth is passed, timeouts and retries, how errors are returned to the model, and how to inspect logs.

This page is for engineers who want the AI assistant to "call back into their own system": how MaiAgent turns the parameters the AI assistant decides on into an HTTP request, what your API should look like to be model-friendly, and what the model sees when a call fails.

For the step-by-step admin console walkthrough (which screen to add a tool on, what to fill into each field), see [Creating an API Tool](https://docs.maiagent.ai/tools/setup) in the user manual - this page does not repeat those steps. Tools can also be created and managed via the REST API, with fields matching this page; see "Tools and Connectors" in the API docs.

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

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

    U->>A: "Help me check the status of order A123"
    A->>A: Decide which tool to call and what parameters to fill, based on the tool description and parameter schema
    A->>P: tool call (tool name + JSON parameters)
    P->>P: Apply parameter defaults, render {{variables}} in headers and query params
    P->>S: HTTP request (GET with query string / POST with JSON body)
    S-->>P: Response (any text, JSON recommended)
    P->>P: Mask sensitive values, truncate to fit context window, write tool execution record
    P-->>A: Raw response (or error message)
    A-->>U: Summarize the result in natural language
```

The AI assistant is only responsible for "deciding the call and its parameters" - the actual HTTP request is sent by the platform. Your API doesn't need to know anything about the model; it just needs to be an HTTP endpoint reachable from the public internet.

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

One API tool maps to **one endpoint + one HTTP method**. The platform does not currently import OpenAPI specs; if your API has an OpenAPI spec, create one tool per operation you want to expose to the assistant, and paste the requestBody or query schema into "Parameter Schema."

| Field (admin console name / API field)          | Audience | Description                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tool name `name`                                | Model    | The identifier the model uses when calling the tool. Only letters, digits, underscores, and hyphens are allowed; before being sent to the model it's further normalized to `^[a-zA-Z][a-zA-Z0-9_]*$`, capped at 64 characters, with hyphens converted to underscores and overlong names truncated with a hash suffix appended. Use `snake_case` from the start, e.g. `get_order_status`. |
| Display name `displayName`                      | Human    | Shown in the admin console list and tool execution records; no format restrictions.                                                                                                                                                                                                                                                                                                      |
| Tool description `description`, prompt `prompt` | Model    | The sole basis the model uses to decide "when to use this tool." If `prompt` has a value it's used, otherwise `description` is used. Clearly describe what it does, when to use it, and what it returns.                                                                                                                                                                                 |
| API URL `apiUrl`                                | Platform | The full URL, including `https://`. The path is fixed; any value that varies by parameter goes into the query string or body.                                                                                                                                                                                                                                                            |
| HTTP method `httpMethod`                        | Platform | `get`, `post`, `put`, `patch`, `delete`. Determines whether model parameters go into the query string or the JSON body - see next section.                                                                                                                                                                                                                                               |
| Headers `rawHeaders`                            | Platform | A JSON object sent with every request. Values support `{{variables}}`.                                                                                                                                                                                                                                                                                                                   |
| Query params `rawQueryParams`                   | Platform | A JSON object appended to the query string on every request. Values support `{{variables}}`; **when a name collides with a model parameter, this value takes precedence.**                                                                                                                                                                                                               |
| Parameter schema `rawParametersSchema`          | Model    | A JSON Schema declaring which parameters the model can (or must) provide.                                                                                                                                                                                                                                                                                                                |

## 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, and validates the parameters the model returns against it.

```json
{
  "type": "object",
  "properties": {
    "order_id": {
      "type": "string",
      "description": "Order number, e.g. A123456"
    },
    "include_items": {
      "type": "boolean",
      "description": "Whether to also return line item details",
      "default": false
    },
    "status": {
      "type": "string",
      "enum": ["pending", "shipped", "delivered"],
      "description": "Fill in only when querying a specific status"
    }
  },
  "required": ["order_id"]
}
```

Rules and constraints:

* The top level must be `type: "object"`; `properties`, `required`, `default`, `enum`, and nested `object`/`array` are supported.
* Parameter names may only contain letters, digits, underscores, dots, hyphens, and Chinese characters, 1-64 characters long; names that don't match are rejected at save time.
* **`default` is filled in by the platform**: when the model doesn't provide that parameter, the platform inserts `default` before sending the request. To always send a fixed value without letting the model decide, use `default`, or set it directly as a Query param.
* Write a `description` for every parameter. The quality of the model's parameter filling depends almost entirely on this text - including a format example (e.g. "e.g. A123456") is far more effective than the type alone.
* The parameter name `timeout` has a reserved meaning: if the model supplies it, the platform uses it as the timeout in seconds for this request and **does not** forward it to your API. Don't use this name for a business parameter.

{% hint style="warning" %}
Don't declare an API key as a parameter and inject it via `default`. The model can see the parameter schema, so the key would end up in the model's context. Put keys in headers or Query params instead - see the next section.
{% endhint %}

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

The platform decides where model parameters go based on the HTTP method:

| Method                 | Where model parameters go                    | "Query params" field                                                               | Body                            |
| ---------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------- |
| `GET`                  | Query string                                 | Also added to the query string, **overriding model parameters with the same name** | None                            |
| `POST`, `PUT`, `PATCH` | JSON body (`Content-Type: application/json`) | Query string                                                                       | JSON object of model parameters |
| `DELETE`               | Not sent                                     | Query string                                                                       | None                            |

Key points:

* `DELETE` does not carry model parameters. If you need to specify the deletion target, use `POST` instead, or put the target into a Query param's `{{variable}}`.
* The body is always a JSON object; form-data, file uploads, and XML are not supported. If your API needs those formats, wrap them behind a JSON interface on your end.
* When a model parameter and a "Query param" share a name, the configured value wins - this is intentional: put keys that must go through the query string in Query params so the model cannot override them.

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

Header and Query param values can reference two kinds of variables, which the platform substitutes on every call:

| Variable              | Content                                                                                                |
| --------------------- | ------------------------------------------------------------------------------------------------------ |
| `{{contact_id}}`      | The current conversation's contact ID (MaiAgent internal ID)                                           |
| `{{source_id}}`       | The contact's external ID - i.e. the `sourceId` you supplied during Web Chat embedding or contact sync |
| `{{contact_name}}`    | Contact name                                                                                           |
| `{{conversation_id}}` | Conversation ID                                                                                        |
| `{{inbox_id}}`        | Inbox ID                                                                                               |
| `{{organization_id}}` | Organization ID                                                                                        |
| `{{parameter_name}}`  | The value of the same-named parameter in the parameter schema (filled by the model)                    |

`{{source_id}}` is the one ISVs use most: your system only recognizes its own user IDs, so putting it into a header (e.g. `X-User-Id: {{source_id}}`) or the query string lets your API look up data as that user, without having the model "guess" who the user is via the parameter schema.

Substitution is literal text, with no type conversion; if the model doesn't provide a referenced parameter, the call aborts with an error rather than sending `{{parameter_name}}` literally. Variables are only available when there's conversation context - calls without a conversation, such as "Test Tool" in the admin console, cannot render contextual variables.

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

Three methods can be combined, in ascending priority:

1. **Tool-level fixed credentials**: `Authorization: Bearer ...` or `X-API-Key: ...` written into headers, or a key placed in Query params. Represents "the whole organization" calling your API. Once saved, it's shown masked as `****` in the admin console, and Query param values are also masked in tool execution records and API request logs.
2. **Context variables**: as in the previous section, using `{{source_id}}` etc. to tell your API "who is asking," with authorization handled on your end. The credential is still the tool-level one.
3. **Contact-level credentials (API Credential)**: bind a set of headers to a specific contact; at call time these **override tool-level headers of the same name**. Suited to cases where each end user holds their own token (your system needs to call as the user themselves, not as the integration account).

Contact-level credentials are written by your backend 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 this again for the same contact and tool updates the existing credential; when the token expires, just call it again. The credential only takes effect in "that contact's conversation" - other contacts calling the same tool still use the tool-level credential.

{% hint style="info" %}
Writing a token into a contact credential is typically done during the identity-sync flow when embedding Web Chat - see [Contact Identity Sync and Token Refresh](/tech/en/authorization-integration/contact-credentials-sync.md).
{% endhint %}

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

| Item    | Behavior                                                                                                                                                                                                                                                                                                            |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Timeout | Each HTTP request has a default timeout of 30 seconds (a platform-level setting, adjustable on-premise) - this is not the total time limit for the whole tool call. A timed-out call is recorded as `timeout`, and the model receives "API request timed out."                                                      |
| Retry   | Up to 3 retries beyond the initial request, for up to 4 attempts total, with backoff of 1, 2, 4 seconds. Retries apply to request errors including timeouts, plus `408`, `409`, `429`, `500`, `502`, `503`, `504`; other 4xx errors are not retried.                                                                |
| TLS     | Certificate chain and hostname are always verified. If your certificate chain uses an older root certificate lacking a Subject Key Identifier (common with some local CAs), you can enable "Relax RFC 5280 strict" in the tool settings, which relaxes only that check. Self-signed certificates are not supported. |

Implications for your API:

* Write endpoints must be **idempotent**. Retries can send the same `POST` twice - dedupe using a business key in the parameters (order number, external ID), or return `409` so the platform's retry lands on the same record.
* The whole tool call accumulates the time of every request plus backoff; consecutive timeouts can exceed 120 seconds, so don't treat 30 seconds as the model's total wait cap. Keep query endpoints to a few seconds; for long-running jobs, switch to a "create job → return job ID" pattern with a separate polling tool.
* Returning `Retry-After` for `429` has no effect - the platform uses a fixed backoff; plan for rate limiting based on the 3-retry volume.

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

The model sees your **raw response body**, unparsed:

| Case                                  | What the model sees                                                             |
| ------------------------------------- | ------------------------------------------------------------------------------- |
| 2xx                                   | Raw body (`response.text`). An empty body becomes the string `None`.            |
| Non-2xx (still failing after retries) | Starts with `API request failed`, followed by `HTTP <status code>: <raw body>`. |
| Timeout                               | Starts with `API request timed out`, followed by an error description.          |
| Connection error, DNS, TLS failure    | Starts with `API request failed`, followed by the exception message.            |
| Insufficient organization Credits     | The call is not sent; the model receives a message about insufficient credits.  |

When a response exceeds a set proportion of the model's context window (80% by default), the platform keeps the head and tail and prepends a "truncated" notice - the middle section is what gets cut.

Recommendations for your API design:

* **Return lean JSON.** Only return the fields the model needs, avoiding whole ORM objects. Keep a single list under a few dozen items, and offer pagination parameters for the model to query further.
* **Use readable error messages**, not just error codes. `{"error": "order_id A123 does not exist, please verify the number"}` helps the model correct parameters or explain to the user far better than `{"code": 40401}`.
* **4xx is feedback for the model**: for missing or malformed parameters, return `400` explaining which field is at issue, giving the model a chance to refill parameters and retry the call; don't wrap errors in a `200`, or the model will present them as a successful result to the user.
* If your response echoes the query string back (as some frameworks' error pages do), the platform masks the tool's configured Query param values before showing them to the model, but headers are only kept out of the body if your API itself doesn't print them.
* To let the model tell the user "no data found," return an explicit `404` with an explanation rather than a `200` with an empty array.

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

Every call writes a **tool execution record**, found in the admin console under "AgentOps → Tool Execution Records," and also queryable via 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`, `query` (full text). Each record includes:

* The model-filled input parameters `inputParameters`, plus the platform's actual `requestMethod`, `requestHeaders` (sensitive values masked), and `requestBody` sent.
* The raw response `outputResult` (truncated version) or `errorMessage`.
* Execution time and status, along with the related message and AI assistant.

Reading this API requires the member to have the AgentOps "Tool Execution Records" permission; organization owners are unrestricted.

Suggested debugging order:

1. Check whether `requestHeaders`/`requestBody` in the record have the shape you expect. `{{variables}}` not being substituted is usually a misspelled variable name, or no matching parameter in the parameter schema.
2. If `errorMessage` starts with `HTTP <code>`, it's an error from your API - the raw body follows.
3. If `status=timeout` but your logs show a response was sent, the response took longer than 30 seconds - check your p95 first.
4. The model "not calling the tool" produces no record. In that case, fix the tool description and parameter `description`s, not the API.

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

Every successfully sent API tool call deducts one "API tool call" worth of Credits; if credits are insufficient, the call is not sent. Retries of failed or timed-out calls don't count separately. Exact pricing depends on the organization's plan - see the Credits section of the user manual.

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

* [ ] The endpoint is reachable over HTTPS from the public internet, with a complete certificate chain.
* [ ] Query endpoints have a p95 of a few seconds; write endpoints are idempotent.
* [ ] Every parameter in the schema has a `description` with a format example; `required` lists only truly mandatory fields.
* [ ] Keys are placed in headers or Query params, not in the parameter schema.
* [ ] When you need to call "as the user," decide between `{{source_id}}` and contact-level credentials, and complete authorization on your backend.
* [ ] Error responses are readable messages; `4xx` explains what's missing, instead of wrapping errors in `200`.
* [ ] Responses are lean, with pagination for lists.
* [ ] Walk through one real conversation end to end, and confirm in the tool execution records that the request sent and the response received both match expectations.


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
