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.User or Tenant.Admin role
  • 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

PropertyPurpose
nameUnique identifier within the namespace (max 150 chars)
urlThe API endpoint. Use {paramName} for path parameters
descriptionHuman-readable documentation
httpMethodHTTP verb: GET, POST, PUT, DELETE, PATCH
pathParametersParameters embedded in the URL path
queryParametersParameters added as query strings
responseSchemaJsonJSON Schema for validating responses
authenticationTypeNone, 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:

  1. Retrieves the secret value
  2. Adds the header X-API-Key: your-actual-api-key-here to 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:

LevelScopeUse Case
Agent-levelAvailable whenever this agent is usedCore capabilities of the agent
Interaction-Agent-levelAvailable for this agent in a specific interactionInteraction-specific tools
Interaction-levelAvailable to all agents in the interactionShared tools

The 5-level binding hierarchy allows values to be overridden:

  1. Session-level for single agent (highest priority)
  2. Session-level for entire interaction
  3. Interaction-level for single agent
  4. Interaction-level for entire interaction
  5. 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:

  1. Recognize it needs to fetch a post
  2. Invoke the get-post tool with id=5
  3. Receive the response from JSONPlaceholder
  4. 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 TypeDescriptionWhat Happens
AuthenticationSecret not found, invalid headerError returned to agent
Schema ValidationRequest/response doesn’t match schemaError with details
HTTP ErrorNon-2xx status codeError with status code
Network ErrorConnection failedError with description
Binding ErrorDuplicate targets, missing required bindingsError at operation time

The agent receives structured error information and can decide how to proceed or inform the user.


Best Practices

Connector Design

  1. One connector per endpoint - Keep connectors focused on a single API operation
  2. Provide response schemas - Helps with validation and AI understanding
  3. Use descriptive names - jsonplaceholder-get-post is clearer than jp-gp

External Operations

  1. Pre-bind static values - API versions, format preferences, pagination limits
  2. Write clear prompt guidance - Help the AI know when and how to use each tool
  3. Use placeholders for dynamic values - {{user_id}} for runtime injection

Security

  1. Always use secrets - Never hardcode API keys in connectors
  2. Minimal permissions - Only request the access you need
  3. Rotate secrets - Update API keys periodically

Performance

  1. Limit results - Pre-bind pagination limits to avoid large responses
  2. Use specific endpoints - Prefer GET /posts/5 over filtering from GET /posts
  3. Cache when appropriate - Use Maitento’s response caching for stable data

Next Steps