Integrating External APIs
This worked example demonstrates how to connect Maitento agents to external REST APIs using OpenAPI connectors. We’ll build a complete integration with JSONPlaceholder, a free fake REST API, then show how to bind operations to agents so they can retrieve and create data.
What You’ll Learn
- Creating OpenAPI connectors for REST APIs
- Defining external operations with parameter bindings
- Securely handling authentication with secrets
- Binding operations to agents
- How agents invoke external tools at runtime
Prerequisites
- A Maitento account with
Tenant.UserorTenant.Adminrole - An API key or Bearer token for authentication
- Basic understanding of REST APIs and JSON
Overview: The Integration Architecture
Maitento uses a layered approach to external API integration:
+-------------------+
| AI Agent | <-- Uses tools to accomplish tasks
+-------------------+
|
v
+-------------------+
| External Operation| <-- Adds bindings, auth, and AI guidance
+-------------------+
|
v
+-------------------+
| Connector | <-- Defines API endpoint and schema
+-------------------+
|
v
+-------------------+
| External API | <-- JSONPlaceholder, your API, etc.
+-------------------+
Connectors define the technical interface: URL, HTTP method, parameters, and schemas.
External Operations wrap connectors with bindings (pre-filled values), authentication, and guidance for the AI.
Agents use these operations as tools, letting the AI decide when and how to call them.
Part 1: Setting Up the Connector
We’ll start by creating a connector that can fetch posts from JSONPlaceholder’s /posts/{id} endpoint.
Step 1.1: Create the OpenAPI Connector
The connector defines the API’s structure. For a GET request that retrieves a post by ID:
API Endpoint: https://jsonplaceholder.typicode.com/posts/{id}
MTR Definition:
{
"$mtr": "1.0",
"type": "Connector.OpenApi.Create",
"namespace": "examples",
"name": "jsonplaceholder-get-post",
"url": "https://jsonplaceholder.typicode.com/posts/{id}",
"description": "Retrieves a single post by its ID from JSONPlaceholder",
"authenticationType": "None",
"httpMethod": "GET",
"pathParameters": [
{
"name": "id",
"dataType": "Integer",
"isRequired": true,
"description": "The unique identifier of the post to retrieve"
}
],
"queryParameters": [],
"responseSchemaJson": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"userId\":{\"type\":\"integer\"},\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"}}}"
}
Equivalent cURL:
curl -X POST "https://api.maitento.com/namespaces/examples/connectors/open-api" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "jsonplaceholder-get-post",
"url": "https://jsonplaceholder.typicode.com/posts/{id}",
"description": "Retrieves a single post by its ID from JSONPlaceholder",
"authenticationType": "None",
"httpMethod": "GET",
"pathParameters": [
{
"name": "id",
"dataType": "Integer",
"isRequired": true,
"description": "The unique identifier of the post to retrieve"
}
],
"queryParameters": [],
"responseSchemaJson": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"userId\":{\"type\":\"integer\"},\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"}}}"
}'
Response:
{
"id": "a1b2c3d4-1111-2222-3333-444455556666",
"name": "jsonplaceholder-get-post",
"url": "https://jsonplaceholder.typicode.com/posts/{id}",
"description": "Retrieves a single post by its ID from JSONPlaceholder",
"authenticationType": "None",
"httpMethod": "GET",
"pathParameters": [
{
"name": "id",
"dataType": "Integer",
"isRequired": true,
"description": "The unique identifier of the post to retrieve"
}
],
"queryParameters": [],
"responseSchema": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"userId\":{\"type\":\"integer\"},\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"}}}"
}
Save the id from the response - you’ll need it to create the external operation.
Step 1.2: Understanding Connector Properties
| Property | Purpose |
|---|---|
name | Unique identifier within the namespace (max 150 chars) |
url | The API endpoint. Use {paramName} for path parameters |
description | Human-readable documentation |
httpMethod | HTTP verb: GET, POST, PUT, DELETE, PATCH |
pathParameters | Parameters embedded in the URL path |
queryParameters | Parameters added as query strings |
responseSchemaJson | JSON Schema for validating responses |
authenticationType | None, Header, or Maitento |
Part 2: Creating External Operations
External operations wrap connectors with additional configuration. They specify how parameters should be bound and provide guidance for the AI.
Step 2.1: Create the Get Post Operation
MTR Definition:
{
"$mtr": "1.0",
"type": "ExternalOperation.OpenApi.Create",
"namespace": "examples",
"name": "get-post",
"connectorId": "a1b2c3d4-1111-2222-3333-444455556666",
"requestBindings": [],
"pathBindings": [],
"queryBindings": [],
"promptGuidance": "Use this tool to retrieve a blog post by its ID. The post includes a title, body content, and the userId of the author. Posts are numbered 1-100."
}
Equivalent cURL:
curl -X POST "https://api.maitento.com/namespaces/examples/external-operations/open-api" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "get-post",
"connectorId": "a1b2c3d4-1111-2222-3333-444455556666",
"requestBindings": [],
"pathBindings": [],
"queryBindings": [],
"promptGuidance": "Use this tool to retrieve a blog post by its ID. The post includes a title, body content, and the userId of the author. Posts are numbered 1-100."
}'
Notice that pathBindings is empty - this means the AI must provide the id parameter when invoking the tool.
Step 2.2: Pre-binding Parameters
Sometimes you want to fix certain parameters. For example, create a connector for listing posts filtered by user:
Connector for listing posts by user:
{
"$mtr": "1.0",
"type": "Connector.OpenApi.Create",
"namespace": "examples",
"name": "jsonplaceholder-list-posts",
"url": "https://jsonplaceholder.typicode.com/posts",
"description": "Lists posts, optionally filtered by userId",
"authenticationType": "None",
"httpMethod": "GET",
"pathParameters": [],
"queryParameters": [
{
"name": "userId",
"dataType": "Integer",
"isRequired": false,
"description": "Filter posts by this user ID"
},
{
"name": "_limit",
"dataType": "Integer",
"isRequired": false,
"description": "Maximum number of posts to return"
}
]
}
External operation with pre-bound limit:
{
"$mtr": "1.0",
"type": "ExternalOperation.OpenApi.Create",
"namespace": "examples",
"name": "list-user-posts",
"connectorId": "CONNECTOR_ID_HERE",
"requestBindings": [],
"pathBindings": [],
"queryBindings": [
{
"target": "_limit",
"value": "10"
}
],
"promptGuidance": "List posts for a specific user. Always returns a maximum of 10 posts. Provide the userId to filter by author."
}
Now when the AI invokes list-user-posts, it only needs to provide userId. The _limit=10 is automatically applied.
Part 3: Handling Authentication with Secrets
Most real-world APIs require authentication. Let’s create an integration with a hypothetical authenticated API.
Step 3.1: Store the API Key as a Secret
First, store your API credentials securely:
curl -X POST "https://api.maitento.com/namespaces/examples/secrets" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "weather-api-key",
"value": "your-actual-api-key-here"
}'
Response:
{
"id": "secret-1234-5678-90ab-cdef12345678",
"name": "weather-api-key"
}
Save the secret id - you’ll reference it in your external operation.
Step 3.2: Create an Authenticated Connector
{
"$mtr": "1.0",
"type": "Connector.OpenApi.Create",
"namespace": "examples",
"name": "weather-api",
"url": "https://api.weatherapi.com/v1/current.json",
"description": "Gets current weather for a location",
"authenticationType": "Header",
"httpMethod": "GET",
"pathParameters": [],
"queryParameters": [
{
"name": "q",
"dataType": "String",
"isRequired": true,
"description": "Location query - city name, coordinates, or postal code"
},
{
"name": "aqi",
"dataType": "String",
"isRequired": false,
"description": "Include air quality data (yes/no)"
}
],
"responseSchemaJson": "{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"object\"},\"current\":{\"type\":\"object\"}}}"
}
Step 3.3: Create Operation with Authentication Header
{
"$mtr": "1.0",
"type": "ExternalOperation.OpenApi.Create",
"namespace": "examples",
"name": "get-weather",
"connectorId": "WEATHER_CONNECTOR_ID",
"requestBindings": [],
"pathBindings": [],
"queryBindings": [
{
"target": "aqi",
"value": "no"
}
],
"authenticationHeader": {
"headerName": "X-API-Key",
"secretId": "secret-1234-5678-90ab-cdef12345678"
},
"promptGuidance": "Get current weather conditions for any location. Provide a city name (e.g., 'London'), coordinates (e.g., '48.8567,2.3508'), or postal code. Returns temperature, conditions, wind, and humidity."
}
Equivalent cURL:
curl -X POST "https://api.maitento.com/namespaces/examples/external-operations/open-api" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "get-weather",
"connectorId": "WEATHER_CONNECTOR_ID",
"requestBindings": [],
"pathBindings": [],
"queryBindings": [
{ "target": "aqi", "value": "no" }
],
"authenticationHeader": {
"headerName": "X-API-Key",
"secretId": "secret-1234-5678-90ab-cdef12345678"
},
"promptGuidance": "Get current weather conditions for any location. Provide a city name, coordinates, or postal code. Returns temperature, conditions, wind, and humidity."
}'
When this operation executes, Maitento automatically:
- Retrieves the secret value
- Adds the header
X-API-Key: your-actual-api-key-hereto the request
Part 4: Binding Operations to an Agent
Now we’ll create an agent and bind our external operations to it.
Step 4.1: Create the Agent
MTR Definition:
{
"$mtr": "1.0",
"type": "Agent.Create",
"namespace": "examples",
"name": "research-assistant",
"description": "An assistant that can look up posts and weather information",
"systemPrompt": "You are a helpful research assistant. You can look up blog posts and check weather conditions. When asked about posts, use the get-post tool with the appropriate ID. When asked about weather, use the get-weather tool with the location.",
"llmConfigurationId": "YOUR_LLM_CONFIG_ID",
"externalOperationReferences": [
{
"externalOperationId": "GET_POST_OPERATION_ID",
"requestBindings": [],
"pathBindings": [],
"queryBindings": []
},
{
"externalOperationId": "GET_WEATHER_OPERATION_ID",
"requestBindings": [],
"pathBindings": [],
"queryBindings": []
}
]
}
Equivalent cURL:
curl -X POST "https://api.maitento.com/namespaces/examples/agents" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "research-assistant",
"description": "An assistant that can look up posts and weather information",
"systemPrompt": "You are a helpful research assistant. You can look up blog posts and check weather conditions. When asked about posts, use the get-post tool with the appropriate ID. When asked about weather, use the get-weather tool with the location.",
"llmConfigurationId": "YOUR_LLM_CONFIG_ID",
"externalOperationReferences": [
{
"externalOperationId": "GET_POST_OPERATION_ID",
"requestBindings": [],
"pathBindings": [],
"queryBindings": []
},
{
"externalOperationId": "GET_WEATHER_OPERATION_ID",
"requestBindings": [],
"pathBindings": [],
"queryBindings": []
}
]
}'
Step 4.2: Understanding Operation Binding Levels
External operations can be bound at three levels:
| Level | Scope | Use Case |
|---|---|---|
| Agent-level | Available whenever this agent is used | Core capabilities of the agent |
| Interaction-Agent-level | Available for this agent in a specific interaction | Interaction-specific tools |
| Interaction-level | Available to all agents in the interaction | Shared tools |
The 5-level binding hierarchy allows values to be overridden:
- Session-level for single agent (highest priority)
- Session-level for entire interaction
- Interaction-level for single agent
- Interaction-level for entire interaction
- External operation level (lowest priority)
Part 5: Creating POST Requests
Let’s add the ability to create new posts.
Step 5.1: Create the POST Connector
{
"$mtr": "1.0",
"type": "Connector.OpenApi.Create",
"namespace": "examples",
"name": "jsonplaceholder-create-post",
"url": "https://jsonplaceholder.typicode.com/posts",
"description": "Creates a new post",
"authenticationType": "None",
"httpMethod": "POST",
"pathParameters": [],
"queryParameters": [],
"requestSchemaJson": "{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"},\"userId\":{\"type\":\"integer\"}},\"required\":[\"title\",\"body\",\"userId\"]}",
"responseSchemaJson": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"},\"userId\":{\"type\":\"integer\"}}}"
}
Step 5.2: Create the POST Operation
{
"$mtr": "1.0",
"type": "ExternalOperation.OpenApi.Create",
"namespace": "examples",
"name": "create-post",
"connectorId": "CREATE_POST_CONNECTOR_ID",
"requestBindings": [
{
"target": "userId",
"value": "1"
}
],
"pathBindings": [],
"queryBindings": [],
"promptGuidance": "Create a new blog post. You must provide a title and body. The post will be created under the default user (userId 1). Returns the created post including its new ID."
}
Note that userId is pre-bound to 1, so the AI only needs to provide title and body.
Part 6: Using Template Placeholders
Template placeholders ({{variable}}) allow dynamic value injection at runtime.
Step 6.1: Create an Operation with Placeholders
{
"$mtr": "1.0",
"type": "ExternalOperation.OpenApi.Create",
"namespace": "examples",
"name": "create-user-post",
"connectorId": "CREATE_POST_CONNECTOR_ID",
"requestBindings": [
{
"target": "userId",
"value": "{{current_user_id}}"
}
],
"pathBindings": [],
"queryBindings": [],
"promptGuidance": "Create a new blog post for the current user. Provide title and body content."
}
Step 6.2: Inject Values at Runtime
When starting an interaction session, you can provide binding values:
curl -X POST "https://api.maitento.com/interactions/INTERACTION_ID/sessions" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"externalOperationBindings": [
{
"externalOperationId": "CREATE_USER_POST_OPERATION_ID",
"requestBindings": [
{
"target": "current_user_id",
"value": "42"
}
]
}
]
}'
Now when the agent uses create-user-post, the userId will automatically be set to 42.
Part 7: Complete Working Example
Here’s a complete MTR file that sets up the full JSONPlaceholder integration:
{
"$mtr": "1.0",
"resources": [
{
"type": "Connector.OpenApi.Create",
"namespace": "examples",
"name": "jsonplaceholder-get-post",
"url": "https://jsonplaceholder.typicode.com/posts/{id}",
"description": "Retrieves a single post by ID",
"authenticationType": "None",
"httpMethod": "GET",
"pathParameters": [
{
"name": "id",
"dataType": "Integer",
"isRequired": true,
"description": "Post ID (1-100)"
}
],
"queryParameters": [],
"responseSchemaJson": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"userId\":{\"type\":\"integer\"},\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"}}}"
},
{
"type": "Connector.OpenApi.Create",
"namespace": "examples",
"name": "jsonplaceholder-list-posts",
"url": "https://jsonplaceholder.typicode.com/posts",
"description": "Lists posts with optional filtering",
"authenticationType": "None",
"httpMethod": "GET",
"pathParameters": [],
"queryParameters": [
{
"name": "userId",
"dataType": "Integer",
"isRequired": false,
"description": "Filter by user ID"
},
{
"name": "_limit",
"dataType": "Integer",
"isRequired": false,
"description": "Max posts to return"
}
],
"responseSchemaJson": "{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"userId\":{\"type\":\"integer\"},\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"}}}}"
},
{
"type": "Connector.OpenApi.Create",
"namespace": "examples",
"name": "jsonplaceholder-create-post",
"url": "https://jsonplaceholder.typicode.com/posts",
"description": "Creates a new post",
"authenticationType": "None",
"httpMethod": "POST",
"pathParameters": [],
"queryParameters": [],
"requestSchemaJson": "{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"},\"userId\":{\"type\":\"integer\"}},\"required\":[\"title\",\"body\",\"userId\"]}",
"responseSchemaJson": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"},\"userId\":{\"type\":\"integer\"}}}"
},
{
"type": "ExternalOperation.OpenApi.Create",
"namespace": "examples",
"name": "get-post",
"connectorRef": "jsonplaceholder-get-post",
"requestBindings": [],
"pathBindings": [],
"queryBindings": [],
"promptGuidance": "Retrieve a blog post by its ID. Posts are numbered 1-100. Returns title, body, and author userId."
},
{
"type": "ExternalOperation.OpenApi.Create",
"namespace": "examples",
"name": "list-posts",
"connectorRef": "jsonplaceholder-list-posts",
"requestBindings": [],
"pathBindings": [],
"queryBindings": [
{ "target": "_limit", "value": "5" }
],
"promptGuidance": "List blog posts. Optionally filter by userId to see posts from a specific author. Returns up to 5 posts."
},
{
"type": "ExternalOperation.OpenApi.Create",
"namespace": "examples",
"name": "create-post",
"connectorRef": "jsonplaceholder-create-post",
"requestBindings": [],
"pathBindings": [],
"queryBindings": [],
"promptGuidance": "Create a new blog post. You must provide title, body, and userId. Returns the created post with its new ID."
}
]
}
Part 8: Testing the Integration
Step 8.1: Create an Interaction
curl -X POST "https://api.maitento.com/namespaces/examples/interactions" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "api-test-interaction",
"mode": "SingleAgent",
"agentBindings": [
{
"agentId": "RESEARCH_ASSISTANT_AGENT_ID"
}
]
}'
Step 8.2: Start a Session and Send a Message
# Start session
curl -X POST "https://api.maitento.com/interactions/INTERACTION_ID/sessions" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
# Send message
curl -X POST "https://api.maitento.com/sessions/SESSION_ID/messages" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Can you show me post number 5 and tell me what it is about?"
}'
The agent will:
- Recognize it needs to fetch a post
- Invoke the
get-posttool withid=5 - Receive the response from JSONPlaceholder
- Summarize the post content for the user
Execution Flow
When the agent invokes an external tool, here’s what happens:
1. Agent decides to call "get-post" with id=5
|
v
2. Maitento finds the matching external operation
|
v
3. Bindings are applied (5-level hierarchy)
|
v
4. URL is constructed: https://jsonplaceholder.typicode.com/posts/5
|
v
5. Authentication headers are added (from secret)
|
v
6. HTTP GET request is sent
|
v
7. Response is validated against schema
|
v
8. Data is returned to the agent
|
v
9. Agent uses the data to formulate a response
Error Handling
Maitento handles several error types:
| Error Type | Description | What Happens |
|---|---|---|
| Authentication | Secret not found, invalid header | Error returned to agent |
| Schema Validation | Request/response doesn’t match schema | Error with details |
| HTTP Error | Non-2xx status code | Error with status code |
| Network Error | Connection failed | Error with description |
| Binding Error | Duplicate targets, missing required bindings | Error at operation time |
The agent receives structured error information and can decide how to proceed or inform the user.
Best Practices
Connector Design
- One connector per endpoint - Keep connectors focused on a single API operation
- Provide response schemas - Helps with validation and AI understanding
- Use descriptive names -
jsonplaceholder-get-postis clearer thanjp-gp
External Operations
- Pre-bind static values - API versions, format preferences, pagination limits
- Write clear prompt guidance - Help the AI know when and how to use each tool
- Use placeholders for dynamic values -
{{user_id}}for runtime injection
Security
- Always use secrets - Never hardcode API keys in connectors
- Minimal permissions - Only request the access you need
- Rotate secrets - Update API keys periodically
Performance
- Limit results - Pre-bind pagination limits to avoid large responses
- Use specific endpoints - Prefer
GET /posts/5over filtering fromGET /posts - Cache when appropriate - Use Maitento’s response caching for stable data
Next Steps
- Learn about MCP Connectors for AI tool server integrations
- Explore Webhooks for event-driven workflows
- See Multi-Agent Workflows for complex orchestration