An agent can call your own systems during a conversation, for example to look up a policy, check an order or book a callback. You describe your API once in the dashboard as a custom integration, add an endpoint for each operation, and turn the endpoints you want into agent tools. When the agent decides to use a tool, Subcontext makes the HTTP request to your server and hands the answer back to the agent.
This page covers what arrives at your endpoint and what to send back. The dashboard help articles Custom integrations, Integration endpoints and Exposing an endpoint as an agent tool walk through the screens.
How the pieces fit#
- The custom integration (choose Outbound) holds what every request shares: the Base URL, the Auth type and its secret, and any Default headers.
- An endpoint is one operation: a Path added to the base URL, an HTTP method, a Content format (JSON, form or XML), a Body template for writes, Success conditions and extra Headers.
- The agent tool gives the endpoint a Tool name, a Description (the agent's whole instruction for when and how to use it), the Parameters the agent fills in, the Response fields it sees, and optionally Require human approval.
- The tool is ticked under Custom tools on an agent version, and goes live when that version is promoted.
What arrives at your endpoint#
The address#
The address is the Base URL followed by the endpoint's Path. Write a
path parameter as {{name}}, and it's filled from the parameter of that name:
| Base URL | Path | The agent sends | Subcontext calls |
|---|---|---|---|
https://api.example.com/v2 |
/policies/{{reference}} |
reference: "POL-1001" |
https://api.example.com/v2/policies/POL-1001 |
Values are encoded the way web forms encode them, so a space arrives as + and
a / as %2F. A path parameter is always required. If the agent leaves it out,
the call stops before anything is sent.
GET requests#
Every parameter that isn't in the path is sent in the query string:
GET /v2/policies/POL-1001?date_of_birth=1980-04-12
POST, PUT and PATCH requests#
The body is made from the endpoint's Body template, with the agent's
parameters as variables. Without a template, no body is sent, so a write
endpoint almost always needs one. Use the json filter to put each value in
safely (it adds the quotes and escaping):
{
"reference": {{ reference | json }},
"phone": {{ phone | json }},
"preferred_time": {{ preferred_time | json }}
}
which arrives as:
POST /v2/callbacks
Content-Type: application/json
Idempotency-Key: (unique to this tool call)
{"reference":"POL-1001","phone":"+447700900123","preferred_time":"tomorrow at 10am"}
The Content format sets the Content-Type and what the template must produce:
| Content format | Content-Type |
What the template must produce |
|---|---|---|
| JSON | application/json |
Valid JSON. |
| Form | application/x-www-form-urlencoded |
A flat JSON object of plain values, which is sent form-encoded. Empty (null) values are left out. |
| XML | application/xml |
The XML exactly as you want it sent. Use xml_escape on each value. |
Templates use the Liquid language. As well as the agent's parameters they can
use the integration's variables ({{ secret.NAME }} and {{ static.NAME }}),
lookup tables ({{ cover_type | lookup: "cover_codes" }}) and these filters:
json, xml_escape, base64_encode, url_encode, sha256, sha1, md5,
hmac_sha256, hmac_sha1, to_int, to_float, trim, pad_left,
pad_right, camelcase, snakecase, default, and Liquid's other standard
filters. The finished body can be up to 1 MB.
DELETE requests#
A DELETE sends no body. As with a GET, every parameter that isn't in the path goes in the query string:
DELETE /v2/callbacks/CB-881?reason=customer_cancelled
Headers#
| Header | When |
|---|---|
| Your credential | Every request. See Authentication. |
Content-Type |
Requests with a body, from the Content format. |
Idempotency-Key |
POST, PUT and PATCH. Unique to the tool call, and the same if that call is ever sent again. See Duplicate protection. |
| Your Default headers and the endpoint's Headers | Every request. A value written exactly as secret.NAME or static.NAME is replaced by that variable. Use this to send a second credential. |
What isn't sent#
Subcontext doesn't add anything about the conversation, such as the conversation id, the customer's name or the agent's name. Your endpoint gets only the parameters the tool declares, and the agent can't add others. If you need the customer's reference or email address, add a parameter for it and say in the tool's Description when to ask for it.
Authentication#
Choose the Auth type on the custom integration. Each value comes from a secret variable on the integration, so create the variable first.
| Auth type | What your endpoint receives |
|---|---|
| Api key | X-API-Key: <the key>, or the header name you set in API key header. |
| Bearer token | Authorization: Bearer <the token>. |
| Basic auth | Authorization: Basic <base64 of username:password>. |
| Oauth2 client credentials | Authorization: Bearer <access token>. Subcontext gets the token from your Token URL (below). |
For OAuth2 client credentials, Subcontext sends this to your Token URL:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET
If your token URL wants the client id and secret in a Basic header instead,
set Send the client id and secret to As a Basic Authorization header.
Then the body is only grant_type=client_credentials, and the request carries
Authorization: Basic <base64 of client_id:client_secret>, each part
form-encoded first as the OAuth2 standard asks.
Subcontext expects JSON back with access_token and, ideally, expires_in in
seconds. It keeps the token and gets a new one a minute before it runs out.
Without expires_in, it keeps the token for an hour. Either way, if your API
answers 401, it gets a new token and tries the request once more.
Check the credential on every request and answer 401 when it's wrong.
Human approval and policies#
With Require human approval ticked, the conversation pauses before the request is sent and waits for someone in your firm to approve it. Your endpoint is only called after approval. Your organisation's policies can also hold or refuse a tool call before it's sent. Approval is recommended for anything that writes or changes records.
What the agent sees#
When it works#
A 2xx answer (that passes the endpoint's Success conditions, if it has
any) goes back to the agent like this:
{
"status": 200,
"message": "The look_up_policy call succeeded. Answer using only fields present in the body: never invent values.",
"body": { "reference": "POL-1001", "status": "active", "renewal_date": "2027-03-01", "monthly_premium": "18.40" }
}
- Response fields on the tool limit
bodyto the top-level keys you list. Leave it blank to pass everything. - If
bodyis longer than 8,000 characters (written as JSON), it's cut short and sent asbody_excerpt, with "The response was truncated." added to the message. - A body that isn't JSON reaches the agent as
{"raw": "the text"}. - Any secret variable's value that appears in your answer is replaced with
[redacted].
When it doesn't#
Otherwise the agent gets one of these messages, and uses it to decide what to say:
| What happened | The agent is told |
|---|---|
| A required parameter is missing (nothing is sent) | missing required parameter(s): reference |
Your endpoint answered 4xx (except 401 and 429), or a 2xx that failed the success conditions |
the API rejected the request (status 404): {"error":"No policy matches that reference and date of birth. Ask the customer to check both."}. The message holds the status and the first 500 characters of your body. |
429, 5xx, a timeout or no connection |
the external system is temporarily unavailable: tell the customer and offer to try again shortly |
401, or a problem with the set-up |
this integration is misconfigured or unavailable: tell the customer this action can't be completed right now |
Because the agent reads your 4xx body, make it a short, plain sentence that
says what to do next. "Ask the customer to check the reference" is more
useful to the agent than ERR_4471.
Designing endpoints for an agent#
- Answer within 15 seconds. The customer is waiting in the conversation.
- Keep answers small and flat, with clear field names, and only what the agent needs. Less personal data in the answer means less in the conversation.
- Check identity in your endpoint as well as in the agent's instructions. Ask for something only the customer knows (a date of birth, a postcode) as a parameter, and refuse when it doesn't match.
- Make writes safe to repeat with the
Idempotency-Key. - Use
404with a helpful message for "not found", instead of a200with an empty body. - Write the tool's Description for the agent: when to use it, what to ask the customer first, and what the answer means.
Example endpoints#
These three files are the same small endpoint in Node, Python and PHP. Each offers a policy lookup and a callback booking, checks the API key, and answers a repeated write with its first answer. Set them up in Subcontext like this:
- A custom integration, Outbound, with your server's address as the Base URL, Auth type Api key, and a secret variable holding the key you start the example with.
- An endpoint
look_up_policy: GET/policies/{{reference}}. Its tool takesreferenceanddate_of_birth, both required strings. - An endpoint
book_callback: POST/callbacks, JSON, with the body template shown in POST, PUT and PATCH requests. Its tool takesreference,phoneandpreferred_time.
-
agent-tool-endpoint.mjs
Node: an endpoint for agent tools, with a policy lookup (GET) and a callback booking (POST), API key check and Idempotency-Key handling.
-
agent_tool_endpoint.py
Python: the same agent tool endpoint, with the standard library only.
-
agent-tool-endpoint.php
PHP: the same agent tool endpoint, for PHP's built-in web server or your own.
Try each endpoint with Test on the integration's page before you give it to an agent. Test sends a real request, so a write endpoint makes the change.
Seeing the calls#
Every call is in Integration logs, with the Agent tool that made it, the request and your answer. The agent's side (its arguments and what it was told) is under Decisions, in the tool calls. See Troubleshooting.