Give agents custom functions
Let an agent call your API during a conversation and use the response in what it says next.
A custom function describes one HTTPS endpoint of yours. During a call, the agent decides when to use it and fills in the fields you defined. Synthesys sends the request and passes your response back to the agent.
Each agent has its own set of up to 8 functions. Saving replaces the whole set. All routes need a key with write permission, including the read route, because endpoint addresses and query parameters can contain tokens. See the custom functions reference for every parameter.
Define a function
Every property below is required on every save.
Function
idstringrequirednamestringrequireddescriptionstringrequiredurlstringrequiredmethodstringrequiredGET, POST, PUT, PATCH or DELETE.timeoutSecondsintegerrequiredmaxRetriesintegerrequiredmethod is GET.headersarrayrequiredname and value. Values are encrypted and never returned.queryParametersarrayrequiredname and value, added to every request.fieldsarrayrequiredmessagestringrequiredenabledbooleanrequiredField
namestringrequiredconstructor and prototype are reserved.typestringrequiredstring, number, integer or boolean.descriptionstringrequiredrequiredbooleanrequiredSome header names are managed by Synthesys and rejected: Host, Content-Length, Connection, Transfer-Encoding, Content-Type, Accept-Encoding, Cookie, Set-Cookie, Proxy-Authorization, Idempotency-Key, and anything starting with X-Synthesys-.
For GET functions, query parameter names and field names must not overlap.
Save with expectedVersion
expectedVersion stops two writers from overwriting each other. Send the version you last read. Send null if the agent has never had functions saved.
- Read the current version
curl https://backend.synthesys.app/api/v1/agents/YOUR_AGENT_ID/custom-tools \ -H "SYNTH_API_KEY: YOUR_API_KEY"{ "success": true, "data": { "version": null, "syncStatus": "synced", "tools": [] } } - Save the full set
The response has the same shape as the read, with a new
version.curl -X PUT https://backend.synthesys.app/api/v1/agents/YOUR_AGENT_ID/custom-tools \ -H "SYNTH_API_KEY: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "expectedVersion": null, "tools": [{ "id": "8f14e45f-ea5d-4c1e-9a55-3f1c2b7d6e10", "name": "book_callback", "description": "Book a callback when the caller asks a person to call them back.", "url": "https://example.com/agent/callbacks", "method": "POST", "timeoutSeconds": 10, "maxRetries": 0, "headers": [{ "name": "X-Function-Secret", "value": "YOUR_SHARED_SECRET" }], "queryParameters": [], "fields": [ { "name": "preferred_time", "type": "string", "description": "When the caller wants the callback.", "required": true }, { "name": "reason", "type": "string", "description": "Why the caller wants the callback.", "required": false } ], "message": "One moment while I book that.", "enabled": true }] }' - Check syncStatus
syncedmeans the functions are ready for calls.pendingmeans they were saved but aren't live yet, and the agent can't use them on calls. Save again with the returnedversionto retry.
Handle a version conflict
If someone else saved first, you get a 409 and nothing changes.
{
"success": false,
"error": "Tools changed. Reload before saving.",
"code": "CONFLICT",
"requestId": "6f1d2c3b-8a47-4e90-b5d1-0c9e7a6f4b21",
"timestamp": "2026-10-31T09:00:00.000Z"
}Read the functions again, reapply your change to the new set, and save with the new version. Don't retry the same request, because it would fail the same way.
Update without resending secrets
Reads never return header values. Each header comes back as { "name": "X-Function-Secret", "value": null, "hasValue": true }.
To keep a stored value, send the header with value: null. Remove hasValue first, because the save route rejects unknown properties.
const url = 'https://backend.synthesys.app/api/v1/agents/YOUR_AGENT_ID/custom-tools';
const headers = { SYNTH_API_KEY: 'YOUR_API_KEY', 'Content-Type': 'application/json' };
const { data } = await (await fetch(url, { headers })).json();
const tools = data.tools.map((tool) => ({
...tool,
headers: tool.headers.map(({ name, value }) => ({ name, value })),
// Your change goes here.
timeoutSeconds: 15,
}));
const res = await fetch(url, {
method: 'PUT',
headers,
body: JSON.stringify({ expectedVersion: data.version, tools }),
});
console.log(res.status, await res.json());A new header must include a real value. If you change a function's URL to a different host, you must also re-enter every header value. This stops stored credentials from being sent to a new destination.
Errors when saving
| Status | When |
|---|---|
400 | The body fails validation, the URL breaks the rules above, or the hostname can't be resolved to public addresses. |
403 | The key doesn't have write permission. |
404 | The agent doesn't exist on your account. |
409 | expectedVersion doesn't match the current version. |
The URL's address is checked when you save an enabled function with a new URL, or when you enable one.
What your endpoint receives
For GET, your query parameters and the agent's fields are in the query string. Values are converted to strings.
For other methods, your query parameters are in the URL and the agent's fields are the JSON body.
POST /agent/callbacks HTTP/1.1
Host: example.com
Content-Type: application/json
Accept: application/json, text/plain
Accept-Encoding: identity
User-Agent: Synthesys-Custom-Tool/1.0
Idempotency-Key: 7d1f0c9e4b2a6f83c5e1d0a97b3c4e26f18a5d7c0b9e2f436a1c8d5e7f09b2a4
X-Function-Secret: YOUR_SHARED_SECRET
{"preferred_time":"tomorrow at 3pm","reason":"Billing question"}Synthesys checks the agent's values before sending. If a required field is missing, a type is wrong, or there's an unknown field, no request is made.
Verify requests
Function requests aren't signed. Authenticate them with a secret you set yourself.
- Generate a long random value and store it on your server.
- Add it to the function's
headers, as in the example above. - Reject any request where the header doesn't match. Use a constant-time comparison.
Don't put the secret in url or queryParameters. Those are returned in plain text when the functions are read, and header values aren't.
Use Idempotency-Key to avoid repeating work. It's a hash that stays the same for one function call, including GET retries. Store the key with your result, and return the stored result if the key appears again.
What to return
| Your response | What the agent gets |
|---|---|
2xx with JSON | The status and the parsed body. |
2xx with text | The status and the text. |
| Any other status, including redirects | A failure with the status code. Redirects aren't followed. |
| Timeout or connection error | A failure saying the outcome is unknown and the write shouldn't be repeated. |
| Body over 16 KB, or a compressed body | The same failure as a timeout. |
Keep responses short and written in terms the agent can repeat. Respond well inside timeoutSeconds.
POST, PUT, PATCH and DELETE requests are never retried. GET requests are retried up to maxRetries times on a connection failure or a 5xx.
Complete example
This endpoint serves the book_callback function saved above. It checks the secret, deduplicates on the idempotency key, and returns a short JSON result.
Your endpoint must be reachable over HTTPS on port 443, so run it behind a TLS proxy. Set FUNCTION_SECRET to the same value you saved in the function's header.
// callbacks.mjs. Requires Node 18 or later.
import { createServer } from 'node:http';
import { timingSafeEqual } from 'node:crypto';
const SECRET = Buffer.from(process.env.FUNCTION_SECRET);
const results = new Map(); // Use your database in production.
function secretMatches(value) {
const given = Buffer.from(value ?? '');
return given.length === SECRET.length && timingSafeEqual(given, SECRET);
}
function send(res, status, body) {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(body));
}
createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/agent/callbacks') return send(res, 404, { error: 'Not found' });
if (!secretMatches(req.headers['x-function-secret'])) return send(res, 401, { error: 'Unauthorized' });
const key = req.headers['idempotency-key'];
if (results.has(key)) return send(res, 200, results.get(key));
let raw = '';
for await (const chunk of req) raw += chunk;
const { preferred_time, reason } = JSON.parse(raw);
// Create the callback in your own system here.
console.log('Callback requested', { preferred_time, reason });
const result = { booked: true, confirmed_time: preferred_time };
results.set(key, result);
send(res, 200, result);
}).listen(8080);To try the endpoint without placing a call, send the request a function call would produce.
curl -X POST https://example.com/agent/callbacks \
-H "X-Function-Secret: YOUR_SHARED_SECRET" \
-H "Idempotency-Key: test-1" \
-H "Content-Type: application/json" \
-d '{"preferred_time":"tomorrow at 3pm","reason":"Billing question"}'