API Capabilities¶
An API capability (type: "api") turns any HTTP(S) endpoint into a tool your agent can call. You describe the endpoint — its URL, method, and parameters — and PrimeThink builds a typed tool from it. When the model decides to use the tool, the platform makes the request and returns the response back to the model.
No code is required: an API capability is just a configuration blob. This is the simplest way to connect an agent to a third-party REST API or one of your own internal services.
For the bigger picture of how capabilities fit together, see Capabilities.
Options schema¶
The capability's behavior is defined entirely by its options:
{
"name": "get_weather",
"description": "Get current weather for a location",
"method": "GET",
"url": "https://api.weather.com/v1/current",
"params": {
"location": { "type": "string", "description": "City name", "required": true }
},
"headers": {
"Authorization": "Bearer ${WEATHER_API_KEY}"
},
"body": {
"query": { "type": "string", "description": "Search query", "required": true }
}
}
Field reference¶
| Key | Required | Default | Description |
|---|---|---|---|
url | Yes | — | Endpoint URL. Must be HTTPS. May contain ${SETTING} placeholders and {slot} call-time path params. Surrounding whitespace is stripped automatically. |
path_params | No | — | Schema for the {slot} fields in url. Undeclared slots default to required strings. See Path templating. |
name | No* | falls back to the capability name, slugified | The function name the model sees. Must be valid (letters, digits, underscores). |
description | No | falls back to the capability description, then "Call the {tool_name} API" | Tells the model when to use the tool. |
method | No | GET | HTTP method. Allowed: GET, POST, PUT, PATCH. (DELETE and others are rejected.) Case-insensitive. |
params | No | — | Query-string parameters → field schema. Become tool arguments sent in the URL query string. |
body | No | — | Flat JSON-body fields → field schema. Become tool arguments sent in the request body. Mutually exclusive with body_template. |
body_template | No | — | Templated request body — nested JSON or a raw string, with ${SETTING} / {slot} interpolation. Mutually exclusive with body and files. See Request bodies. |
body_params | No | — | Schema for the {slot} fields in body_template. Undeclared slots default to required strings. |
files | No | — | Multipart file fields; each argument is a chat document_id. Switches the request to multipart/form-data. See File uploads. |
headers | No | — | Static HTTP headers. Values support ${SETTING} placeholders. Not exposed as tool arguments. |
* name must come from somewhere: either in options or from the capability's own name. If neither is set, the tool won't build.
Field schema (for params, body, path_params, and body_params)¶
Each entry under params, body, path_params, or body_params describes one argument the model can fill in:
{
"param_name": {
"type": "string",
"description": "What this parameter does",
"required": true,
"default": "optional default value"
}
}
| Attribute | Values | Notes |
|---|---|---|
type | string, integer, number, boolean | Unknown types default to string. |
description | string | Shown to the model — describe the argument well. |
required | bool (default false) | Required arguments have no default; optional ones are nullable. |
default | any | Used for optional arguments when the model omits them. |
files fields use a different schema (description, required, multiple) — see File uploads.
Every argument name belongs to exactly one section
A given name may be declared in only one of path_params, params, body, body_params, or files. Duplicates across sections are rejected at build time — rename so each maps unambiguously. (The one deliberate exception: a {slot} may be reused in both the url and the body_template — that's one argument filling two places. See Naming rules.)
Name and description fallback¶
So you don't repeat yourself, when name/description are omitted from options:
- name → the capability's name, slugified into a valid function name. For example, a capability named "Open-Meteo Weather" becomes the tool
open_meteo_weather. - description → the capability's description; if that's also empty →
"Call the {tool_name} API".
Values in options always override these fallbacks.
Two kinds of interpolation: ${SETTING} vs {slot}¶
Strings in your options can carry two different kinds of placeholder. They look different, resolve at different times, and can coexist in a single string — for example https://${TENANT_HOST}/v1/chats/{chat_id}.
| Placeholder | Resolved | Source | Where |
|---|---|---|---|
${SETTING_NAME} | Once, at build time | Your user/group settings | url, headers, body_template |
{arg_name} | On every call | A tool argument the model supplies | url (path params), body_template |
Use ${SETTING} for values that are fixed for a given install — API keys, tenant hostnames, base URLs. Use {slot} for values the model chooses per call — a chat id, a message body, a search term you want inside the path or a JSON body.
Build-time settings (${SETTING_NAME})¶
Never hard-code secrets like API keys or tokens. Any of the supported strings in options can contain ${SETTING_NAME} placeholders, resolved at build time from your user/group settings.
- The name must be word characters (letters, digits, underscore):
\${WEATHER_API_KEY}. - Example:
"Authorization": "Bearer ${WEATHER_API_KEY}". - Resolved in
url,headers, andbody_template. - If a referenced setting is missing or empty, the tool fails to build and is skipped (logged). Define the setting before enabling the capability.
You configure settings on the Settings page (or via the admin API):
The value is then available as ${WEATHER_API_KEY} in any capability's options.
Call-time slots ({arg_name})¶
A {slot} is a placeholder for a value the model fills in each time it calls the tool. Every slot becomes a typed tool argument:
- Declare it (for a custom type or description) in
path_params(for slots in theurl) orbody_params(for slots in thebody_template). - Or omit the declaration — an undeclared slot auto-registers as a required string.
The following two sections show slots in action.
Path templating¶
Put {name} segments in the url; each becomes a call-time tool argument that is URL-encoded and substituted into the path before the request is made.
- Declare slots in
path_paramsto give them a custom type or description, or omit them — undeclared slots auto-register as required strings. - Values are URL-encoded with no safe characters (
/→%2F), so a slot can't break out of its path segment — no host swap, no../traversal. The fully-filled URL is then re-validated for SSRF (see Security) before the request.
Example — a path-scoped GET:
{
"name": "Get Chat Messages",
"code": "get_chat_messages",
"type": "api",
"options": {
"name": "get_chat_messages",
"description": "List the messages in a chat",
"method": "GET",
"url": "https://api.example.com/v1/chats/{chat_id}/messages",
"path_params": {
"chat_id": { "type": "string", "description": "The chat id", "required": true }
},
"params": {
"limit": { "type": "integer", "description": "Max messages", "required": false, "default": 20 }
},
"headers": { "Authorization": "Bearer ${API_TOKEN}" }
}
}
The model calls it with chat_id and an optional limit; the tool requests .../chats/<chat_id>/messages?limit=....
Every declared path param needs a matching slot
A path_params entry with no corresponding {slot} in the url is rejected at build time.
Request bodies¶
There are two mutually exclusive ways to describe a request body. Pick one.
body — flat scalar fields¶
The body option maps a flat schema of scalar fields to a flat JSON object. Each field becomes a typed tool argument and is sent in the JSON body. This is the simplest option and hasn't changed — see the Send Email example.
body_template — nested JSON or raw text¶
For anything richer than a flat object, use body_template. It supports the same ${SETTING} + {slot} interpolation as the url, and takes one of two forms.
(a) JSON structure (a dict or list) → sent as JSON. Static values, ${SETTING}, and {slot} arguments mix freely; nesting and arrays are supported.
- A leaf that is exactly
"{slot}"keeps the argument's native type — an integer stays an integer, a boolean stays a boolean. - A slot inside surrounding text (
"Hello {name}") is string-interpolated.
(b) Raw string → sent as the raw request body, honoring the Content-Type header — text/plain, application/x-www-form-urlencoded, XML, and so on.
Slots used in a body_template are declared in body_params (for a custom type or description) or auto-register as required strings.
Example — nested JSON mixing static, dynamic, and typed fields:
{
"name": "Post Message",
"code": "post_message",
"type": "api",
"options": {
"name": "post_message",
"description": "Post a message to a chat",
"method": "POST",
"url": "https://api.example.com/v1/chats/{chat_id}/messages",
"headers": { "Authorization": "Bearer ${API_TOKEN}" },
"body_template": {
"source": "primethink",
"message": { "text": "{text}", "pinned": "{pinned}" },
"tags": ["inbound", "{tag}"]
},
"body_params": {
"text": { "type": "string", "description": "Message text", "required": true },
"pinned": { "type": "boolean", "description": "Pin it", "required": true },
"tag": { "type": "string", "description": "Extra tag", "required": true }
}
}
}
Called with chat_id=42, text="hi", pinned=true, tag="urgent", it POSTs:
{ "source": "primethink", "message": { "text": "hi", "pinned": true }, "tags": ["inbound", "urgent"] }
Note that pinned is emitted as a real boolean (whole-leaf type preservation), and chat_id fills the URL path — the same name can be used in both the URL and the body (one argument, two fills).
Example — a raw form-urlencoded body (with a ${SETTING} inside the body too):
{
"name": "Send SMS (form)",
"code": "send_sms_form",
"type": "api",
"options": {
"name": "send_sms_form",
"description": "Send an SMS via a form-encoded endpoint",
"method": "POST",
"url": "https://api.sms-provider.com/v1/messages",
"headers": {
"Authorization": "Bearer ${SMS_API_KEY}",
"Content-Type": "application/x-www-form-urlencoded"
},
"body_template": "To={to}&Body={body}",
"body_params": {
"to": { "type": "string", "required": true },
"body": { "type": "string", "required": true }
}
}
}
Body validation
Setting both body and body_template is rejected. So is a body_params without a body_template, or a body_params entry with no matching {slot}.
File uploads¶
Endpoints that accept file uploads use a files section — the request then becomes multipart/form-data.
Because the model can't emit raw bytes, each file field's argument is a PrimeThink document_id — a document that already exists in the current chat (what the model can see). PrimeThink resolves the id to its bytes server-side and attaches it as a multipart part.
multiple: false(the default) → the argument is a singledocument_id(an integer).multiple: true→ the argument is a list of ids, each sent as its own part under the same field name.- A flat
bodyalongsidefilesbecomes the multipart text fields.body_templatecannot be combined withfiles. Content-Typeis set automatically for multipart — do not set it inheaders.
A files field is described with description, required, and multiple (rather than the type/default field schema used elsewhere).
Example — attach a document with an optional caption:
{
"name": "Attach File to Chat",
"code": "attach_file",
"type": "api",
"options": {
"name": "attach_file",
"description": "Upload a document from this chat as an attachment",
"method": "POST",
"url": "https://api.example.com/v1/chats/{chat_id}/attachments",
"files": {
"file": {
"description": "The document to upload (a document_id from this chat)",
"required": true,
"multiple": false
}
},
"body": {
"caption": { "type": "string", "description": "Optional caption", "required": false }
},
"headers": { "Authorization": "Bearer ${API_TOKEN}" }
}
}
Called with chat_id=5, file=7, caption="quarterly report", it sends a multipart POST to .../chats/5/attachments with document 7's bytes as the file part and caption as a text field. If document 7 isn't in chat 5, the tool returns an error and sends nothing.
File uploads need a chat, and stay inside it
File-bearing capabilities require a chat context — they're unavailable outside a chat. Combining files with body_template is rejected. See Security for the chat-scoping and size guarantees.
Naming rules¶
These rules apply across every section and are all enforced at build time:
- One home per name. Every argument name is declared in exactly one of
path_params,params,body,body_params, orfiles. Duplicates across sections are rejected. - A slot may fill two places. A
{slot}name may appear in both theurland thebody_template— that's a single argument filling two places, and it's allowed. - Slots and sent-fields don't mix. A template
{slot}may not collide with a field that's sent verbatim (params,body, orfiles). A name is either a template slot or a sent field — never both.
How a call works¶
When the model calls the tool:
- Any
{slot}values are URL-encoded and substituted into theurlpath, and (when present) into thebody_template. - Remaining arguments are split — those declared in
paramsgo to the query string; those inbodygo to the JSON body (or multipart text fields whenfilesis set). Empty values are dropped. - When
filesis present, eachdocument_idis authorized against the current chat and resolved to bytes; the request is sent asmultipart/form-data. - The fully-filled URL is re-resolved and re-validated for safety (see Security).
- An HTTPS request is made with a 30-second timeout.
- On success, the response body is returned to the model as text.
Errors the model sees¶
These failures are returned to the model as text (so it can react), not raised as errors:
| Condition | Returned to the model |
|---|---|
| URL fails safety validation | API request blocked: {detail} |
A file document_id isn't in the current chat | an error — and no request is made |
| Non-2xx HTTP status | API error: HTTP {status_code} |
| Network failure, timeout, etc. | API request failed: {detail} |
Build-time problems prevent the tool from being created — the capability is skipped and the issue is logged, so the agent simply won't have that tool. These include: a missing url/name; a bad method; an unresolved ${SETTING} placeholder; a name declared in more than one section; setting both body and body_template; combining body_template with files; a path_params/body_params entry with no matching {slot}; or a body_params with no body_template.
Dot-notation keys¶
The capability editor stores options as flat name/value pairs and can't natively express nested structures. To get around this, any key containing a dot is expanded into nested objects:
Rules:
- Keys without a dot are kept as-is.
- Multiple dotted keys sharing a prefix are merged under the same parent.
- Dotted keys merge into any already-nested value at the same path, so existing nested JSON keeps working — you can mix styles freely.
Tip
If your editor lets you enter JSON objects directly, you can nest without dot-notation. Dot-notation is just the escape hatch for flat key/value editors. A rich body_template is far easier to express as real nested JSON than as dotted keys.
Security¶
API capabilities are guarded to prevent them from reaching internal services or exfiltrating data:
- HTTPS only.
http://URLs are rejected. - SSRF protection. Before each request the hostname is resolved and every resolved IP is checked. Requests are blocked if the target resolves to a private, loopback, link-local, reserved, multicast, or carrier-grade-NAT (
100.64.0.0/10) address. This blocks attempts to reach internal services or cloud metadata endpoints (such as169.254.169.254). Because a filled-in{slot}can change the URL, this check runs against the fully-filled URL on every call, and slot values are URL-encoded so they can't alter the host or escape their path segment. - The original hostname is preserved for the request (so TLS/SNI stays correct); the validated IP is used only for connection targeting.
- Chat-scoped uploads. For
files, a document is uploaded only if it belongs to the current chat; any other id is refused (the tool returns an error and makes no request). This prevents an agent from exfiltrating documents it can't see. A per-document size cap of 50 MiB guards against oversized uploads, and file-bearing capabilities are unavailable outside a chat context.
Examples¶
The three examples below use real, live endpoints so you can adapt them directly. For the newer features, see the worked examples in Path templating, Request bodies, and File uploads.
GET with an API key — recent news (NewsAPI)¶
When you'd use this: give a research or briefing agent the ability to pull recent articles on demand. NewsAPI takes its key in an X-Api-Key header, so the key stays in headers (a secret) and never becomes a model-visible argument.
{
"name": "Search News",
"type": "api",
"options": {
"name": "search_news",
"description": "Search recent news articles by keyword.",
"method": "GET",
"url": "https://newsapi.org/v2/everything",
"params": {
"q": { "type": "string", "description": "Search keywords or phrase", "required": true },
"sortBy": { "type": "string", "description": "relevancy, popularity, or publishedAt", "required": false, "default": "publishedAt" },
"language": { "type": "string", "description": "2-letter language code, e.g. 'en'", "required": false, "default": "en" }
},
"headers": {
"X-Api-Key": "${NEWSAPI_KEY}"
}
}
}
POST with a JSON body — send an email (Resend)¶
When you'd use this: let an agent send a transactional email — a summary, an alert, a follow-up — at the end of a workflow. The API key lives in a header; the email fields become typed tool arguments the model fills in.
{
"name": "Send Email",
"type": "api",
"options": {
"name": "send_email",
"description": "Send an email via Resend.",
"method": "POST",
"url": "https://api.resend.com/emails",
"body": {
"from": { "type": "string", "description": "Verified sender, e.g. 'agent@yourdomain.com'", "required": true },
"to": { "type": "string", "description": "Recipient email address", "required": true },
"subject": { "type": "string", "description": "Email subject line", "required": true },
"html": { "type": "string", "description": "HTML body of the email", "required": true }
},
"headers": {
"Authorization": "Bearer ${RESEND_API_KEY}",
"Content-Type": "application/json"
}
}
}
No auth, using the flat editor — weather forecast (Open-Meteo)¶
When you'd use this: a genuinely free, key-less API — great for a first capability. Open-Meteo needs no sign-up. This example also shows the flat editor: name/description are left out and fall back to the capability — named "Open-Meteo Weather", it becomes the tool open_meteo_weather.
method = GET
url = https://api.open-meteo.com/v1/forecast
params.latitude.type = number
params.latitude.required = true
params.longitude.type = number
params.longitude.required = true
params.current.type = string
params.current.default = temperature_2m
Quick reference¶
Minimal API capability:
(method defaults to GET; name can be omitted if the capability name is set.)
Rules to remember:
- HTTPS only; methods
GET/POST/PUT/PATCH; no private/internal IPs. - Two interpolations:
${SETTING}(build-time, from settings) and{slot}(call-time, a tool argument). They can share one string. {slot}in theurl→ a path param (path_params);{slot}in abody_template→body_params. Undeclared slots become required strings.- Bodies:
body(flat JSON) orbody_template(nested JSON / raw text) — never both. - Uploads:
filesmakes the request multipart; each argument is a chatdocument_id, scoped to the current chat. - Every argument name lives in exactly one section; a
{slot}may fill the URL and the body, but never doubles as a sent field. - Secrets →
${SETTING_NAME}placeholders, defined in settings first. - Nested config in a flat editor → dot-notation keys (
params.x.type).
Related Topics¶
- Capabilities — what capabilities are and how they're used
- MCP Capabilities — connect a remote MCP server instead
- Documents and Collections in Chats — where the
document_idvalues used byfilescome from - Working with AI Agents — assigning capabilities to an agent