For Developers
Agents

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

idstringrequired
A UUID you generate, unique within the agent. Keep it the same across saves, because stored header values are matched to the function by this ID.
namestringrequired
Up to 40 letters, numbers and underscores, starting with a letter. Unique within the agent.
descriptionstringrequired
Up to 2,000 characters. Tell the agent what the function does and when to use it.
urlstringrequired
Your endpoint, up to 2,048 characters. It must use HTTPS on port 443, have no credentials or fragment, and resolve only to public addresses.
methodstringrequired
GET, POST, PUT, PATCH or DELETE.
timeoutSecondsintegerrequired
1 to 20. Time allowed across all attempts.
maxRetriesintegerrequired
0 to 2. Must be 0 unless method is GET.
headersarrayrequired
Up to 16 objects with name and value. Values are encrypted and never returned.
queryParametersarrayrequired
Up to 20 objects with name and value, added to every request.
fieldsarrayrequired
Up to 20 values the agent fills in. See below.
messagestringrequired
What the agent says while the function runs, up to 1,000 characters. Use an empty string for nothing.
enabledbooleanrequired
Whether the agent can use this function on calls.

Field

namestringrequired
Same format as a function name. Unique within the function. constructor and prototype are reserved.
typestringrequired
string, number, integer or boolean.
descriptionstringrequired
Up to 1,000 characters. Tell the agent what to collect. Can be empty.
requiredbooleanrequired
Whether the agent must provide this field.

Some 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.

  1. Read the current version
    cURL
    curl https://backend.synthesys.app/api/v1/agents/YOUR_AGENT_ID/custom-tools \
      -H "SYNTH_API_KEY: YOUR_API_KEY"
    JSON
    {
      "success": true,
      "data": {
        "version": null,
        "syncStatus": "synced",
        "tools": []
      }
    }
  2. 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
        }]
      }'
  3. Check syncStatus

    synced means the functions are ready for calls. pending means they were saved but aren't live yet, and the agent can't use them on calls. Save again with the returned version to retry.

Handle a version conflict

If someone else saved first, you get a 409 and nothing changes.

JSON
{
  "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

StatusWhen
400The body fails validation, the URL breaks the rules above, or the hostname can't be resolved to public addresses.
403The key doesn't have write permission.
404The agent doesn't exist on your account.
409expectedVersion 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.

HTTP
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.

  1. Generate a long random value and store it on your server.
  2. Add it to the function's headers, as in the example above.
  3. 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 responseWhat the agent gets
2xx with JSONThe status and the parsed body.
2xx with textThe status and the text.
Any other status, including redirectsA failure with the status code. Redirects aren't followed.
Timeout or connection errorA failure saying the outcome is unknown and the write shouldn't be repeated.
Body over 16 KB, or a compressed bodyThe 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.

Endpoint
// 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
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"}'