atteq API
  • Guides
  • Recipes
  • Cookbook
  • API Reference
How it fits togetherHow to write an agentQuickstartAuthenticationFor agentsErrorsTools and their parametersHTTP tools and connections
powered by Zudoku
Guides

HTTP tools and connections

Your system is reached live, during a conversation, by an HTTP tool: one configured instance per action, with a schema the model fills in. The tool holds the address and the shape of the call; it never holds the key. What authorizes the call is a connection, created once and reused by every tool, thread and automation in your organization.

One tool per action

TerminalCode
curl -X POST https://YOUR_PLATFORM_ORIGIN/api/v1/tools \ -H "Authorization: Bearer $ATTEQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "check_availability", "description": "Free slots for one day at one clinic.", "source": "builtin", "config": {"endpoint": "https://clinic.example.com/api/availability", "method": "GET"}, "input_schema": { "type": "object", "properties": { "day": {"type": "string", "description": "The day, as YYYY-MM-DD."}, "clinic": {"type": "string", "description": "The clinic code."} }, "required": ["day"] }, "deadline_seconds": 20 }'
  • input_schema is what the model is shown, so each property's description is an instruction to it. Keep the schema to the one action: a job with several steps or a branch is a flow of tools, not one tool that does everything.
  • config.endpoint is the address, config.method the verb (POST when you send none). GET, HEAD and DELETE carry the call's values in the query string, added to whatever query you wrote into the endpoint; every other method carries them as a JSON body.
  • The endpoint may name gaps in its path — .../patients/{patient_id}/visits — each filled from a value of the call and encoded as one path segment. Gaps in the host, the query or the fragment are refused.
  • deadline_seconds is how long a conversation waits for your system: 1 to 300, and 30 if you send none. When it passes, the agent says so and the conversation continues.

Test it by running it. POST /tools/{tool_id}/test really makes the call, with this instance's own connection and deadline — nothing is simulated — and records what happened, so a wrong address or a refused key shows up here first rather than in somebody's conversation.

What your system receives

Three layers, and nothing else:

  1. what the model asked for, against the schema above;
  2. what you fixed on the instance (fixed_parameters), which the model is not shown, cannot ask for and cannot overwrite;
  3. what a flow step mapped onto it by name (param_overrides), when the tool is a step of a flow.

A value fixed by a person wins over the model's; a value the flow step mapped wins over both. Nothing of ours travels with them: the platform's own organization and conversation ids are not sent to your system, and a flow's inputs reach a step only through a value the step maps or a parameter the instance declares — never by sharing a name with one of them.

Two headers the platform sets itself: Idempotency-Key on every method but GET, HEAD and OPTIONS — the same value on each retry of one call, so a retried write is written once — and whichever header the connection's own authorization needs. A call is attempted at most three times, always inside the deadline, and only when the failure was worth retrying — a refusal from your system is an answer and is handed to the model instead.

Authorizing with a key

A system of your own behind a key is one connection provider, http_key. Its secret holds the key and where the key goes, so the tool spells none of it. Create the connection once:

A key in a header

TerminalCode
curl -X POST https://YOUR_PLATFORM_ORIGIN/api/v1/connections \ -H "Authorization: Bearer $ATTEQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "http_key", "account_label": "Clinic booking system", "scopes": [], "grant_secret": "{\"header\":\"Authorization\",\"prefix\":\"Bearer \",\"value\":\"clinic_live_replace_me\"}" }'

grant_secret is a string: the JSON object above, serialized. prefix is joined to the key verbatim, which is why "Bearer " carries its trailing space; leave it out for a bare X-Api-Key-style key. scopes is empty and that is the rule rather than an omission — your key can do whatever your system lets it, and the platform can neither see nor narrow that.

A key in a query parameter

Code
{ "provider": "http_key", "account_label": "Price list", "scopes": [], "grant_secret": "{\"placement\":\"query\",\"parameter\":\"api_key\",\"value\":\"pl_live_replace_me\"}" }

The key is added to the query of every attempt, beside whatever the endpoint already carries, and it outranks a value of the same name from the model or from your fixed values. There is no prefix for this placement.

A user name and a password

Code
{ "provider": "http_key", "account_label": "Warehouse API", "scopes": [], "grant_secret": "{\"placement\":\"basic\",\"username\":\"attek\",\"password\":\"replace_me\"}" }

Sent as Authorization: Basic base64(username:password). The platform builds the header; you supply the two halves.

An omitted placement means header, so a connection you created before the other two existed keeps working untouched.

Pointing the tool at it

create_connection answers the connection, including its id. Put it on the instance — the column, never a header:

TerminalCode
curl -X PATCH https://YOUR_PLATFORM_ORIGIN/api/v1/tools/$TOOL_ID \ -H "Authorization: Bearer $ATTEQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{"connection_id": "'$CONNECTION_ID'"}'

One connection serves as many instances as you like. A tool whose own config.headers would carry a credential is refused when it is created or changed — a header whose name says so (Authorization, anything with key, token or secret in it) — naming the header and never its value: the key belongs in a connection, where it is rotated once and where the platform can tell you what stopped working if it is refused.

Which brings us to the one thing worth knowing about a key that stops working: when your system answers 401 or 403 to it, the connection is marked broken and GET /connections says so, with the reason and the tool that hit it. Every instance behind it is refused until the key is replaced (PATCH /connections/{connection_id} with a new grant_secret, which makes it healthy again). The refusal your system wrote is also handed to the agent, so it can say something true to the person it is talking to.

Nothing of the key reaches a record, a refusal or a log, in any placement: a key in the query is as absent from them as one in a header, and so is a basic password — including where your system quotes the request back in its own error message.

Authorizing as an account you consented to

Where your system is behind a provider's consent screen rather than a key — a Google account of your organization's — the connection is authorized in the dashboard, because a consent screen needs a browser and a person who can agree to it. The tool then carries that account's bearer, renewed by the platform as the provider's rules require, and may do what the account consented to and nothing wider.

From the API, that connection is like any other: read it with GET /connections (provider is google) and put its id on the instance's connection_id. One difference is worth knowing:

  • the provider answering "this account is no longer authorized" (401) breaks the connection, exactly as a refused key does;
  • the provider answering "the consent does not cover this request" (403) refuses that one tool call and breaks nothing — the authorization is intact, so every other instance behind it keeps working, and the agent is told why this call could not be made.

A deployment with no OAuth client with the provider configured refuses such an instance saying so, rather than calling without a credential.

An MCP server behind a connection

If your actions already exist behind an MCP server, import it: one configured instance per remote tool, each with a name, fixed parameters and a deadline of its own. Pass connection_id and the imported tools carry no credential of their own — and the import's own listing call goes out with that connection too.

TerminalCode
curl -X POST https://YOUR_PLATFORM_ORIGIN/api/v1/tools/import-mcp \ -H "Authorization: Bearer $ATTEQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "server_url": "https://mcp.vendor.example/mcp", "connection_id": "'$CONNECTION_ID'", "name_prefix": "vendor_" }'
  • An http_key connection whose key goes in a header sends that header, under the name your server asks for, with its prefix; a basic one sends Authorization: Basic …; a consented account sends its bearer.
  • A key the connection places in the query is not a way to reach an MCP server: such an instance is refused before the server is contacted, naming the reason. Give the server's key a header.
  • name_prefix is the lever for one collision: two servers may each publish a search, and a person attaching both to one agent needs to tell them apart.
  • Importing the same server again refreshes what the server owns — each tool's description and parameter schema — and keeps what you changed: the instance's name, its fixed parameters, its deadline. A remote tool that disappeared is left alone rather than deleted.

Connect an MCP server that signs you in

Some MCP servers — Notion's, Linear's and others — ask whoever calls them to sign in on their own screen. Connect one from Connections: pick the product, or An MCP server and give its address, sign in on the server's screen, and you come back connected. No key is pasted and nothing is registered beforehand. Then import the server with that connection: the listing and every imported tool use it, and its token is sent to that server and no other.

  • What the agent may do there is what you allowed on the server's screen, in the server's words. A call that needs more is refused saying what is missing; connect the same server again under the same name to widen it.
  • It renews itself while the server allows; it breaks only when the server's sign-in service refuses to renew it, or the server still refuses after one renewal.
  • A server that asks for no sign-in, or that this door cannot use yet, is connected with a key instead (above).

Import an API description (OpenAPI or Google Discovery)

If your system publishes a description of its interface, you do not write one tool per endpoint: you choose the operations you want and the platform makes each of them an ordinary HTTP tool — editable, testable, attachable to an agent like one made by hand — with one connection for the whole set. Reading the description involves no model and costs nothing.

It takes two calls. The first lists; the second writes.

1. Preview: what could become a tool

TerminalCode
curl -X POST https://YOUR_PLATFORM_ORIGIN/api/v1/tools/import-openapi/preview \ -H "Authorization: Bearer $ATTEQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://petstore3.swagger.io/api/v3/openapi.json", "name_prefix": "pets_" }'

Send exactly one of url (fetched by the platform, from the public internet only, within the fetch deadline) or document (the text of the JSON file, as a string). Both, or neither, is a 422. name_prefix is optional and shows the names with a prefix. Nothing is written.

Code
{ "title": "Swagger Petstore - OpenAPI 3.0", "kind": "openapi", "servers": [ { "url": "https://petstore3.swagger.io/api/v3", "description": "", "suggestion": { "provider": "http_key", "placement": "header", "header": "api_key", "parameter": null, "prefix": "", "note": "" } } ], "operations": [ { "key": "getPetById", "method": "GET", "path": "/pet/{petId}", "summary": "Find pet by ID.", "name": "pets_getPetById", "description": "Find pet by ID. Returns a single pet.", "input_schema": { "type": "object", "properties": {"petId": {"type": "integer", "format": "int64", "description": "ID of pet to return"}}, "required": ["petId"] }, "locations": {} } ], "unimportable": [ { "key": "uploadFile", "method": "POST", "path": "/pet/{petId}/uploadImage", "summary": "Uploads an image.", "reason": "its body is not JSON (application/octet-stream)" } ], "max_operations_per_import": 50 }
  • kind is openapi or discovery.
  • servers: pick one for the set. A server written with variables comes with the description's defaults filled in. A url of null is a server the file gives relative to itself: type it yourself when you import.
  • operations are in the description's order. key is the operation's own identifier, or METHOD /path where it has none; it is what you send back. name, description and input_schema are what the tool would carry, and locations is where each value goes (see below); a gap in the path is filled from the endpoint and never listed. The text is the description's own, with markup removed and the tail cut; no model writes it.
  • unimportable are listed with the reason and cannot be chosen.

The example above is abbreviated to two entries. A description the platform refuses (see below), or an import naming an operation that does not exist or cannot be imported, answers 400 with the reason; 422 is only for the shape of the request itself.

2. Import: the tools you chose

TerminalCode
curl -X POST https://YOUR_PLATFORM_ORIGIN/api/v1/tools/import-openapi \ -H "Authorization: Bearer $ATTEQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://petstore3.swagger.io/api/v3/openapi.json", "server": "https://petstore3.swagger.io/api/v3", "operations": ["getPetById", "findPetsByStatus"], "connection_id": "'$CONNECTION_ID'", "name_prefix": "pets_" }'

Send the same url or document as the preview, plus:

  • server: the one you chose for the set.
  • operations: the key of each operation, at least one and at most 50.
  • connection_id or connection_group (the label of a group of connections, as on create_tool) — not both; or neither, for an interface that asks for no key. Every tool created points at it.

It answers 201:

Code
{ "tools": [ { "id": "…", "name": "pets_getPetById", "description": "Find pet by ID. Returns a single pet.", "source": "builtin", "input_schema": {"type": "object", "properties": {"petId": {"type": "integer", "format": "int64"}}, "required": ["petId"]}, "fixed_parameters": {}, "deadline_seconds": 30, "connection_id": "…", "connection_group": null, "…": "…" } ], "missing": [] }

tools are the tools created or refreshed, in the shape GET /tools returns (abbreviated above). Like any tool, they never return their config. Everything is created or nothing is: a set that fails while it is written leaves no tool behind.

The connection it suggests

The preview's suggestion on each server comes from the description's securitySchemes. You confirm or change it; the import never creates a connection itself.

The description declaresIt suggests
a key in a header or in the queryan http_key connection with the key placed where the description says (placement, header or parameter)
a user name and passwordan http_key connection with placement basic
a bearer tokenan http_key connection in the Authorization header with the prefix Bearer
consent through a provider's screenyour consented account with that provider, where the platform holds that provider's consent screen — on a Google server, your Google account
anything elsenothing; note says why when the description asked for something the platform cannot hold

A scheme the platform cannot connect does not stop the import: point the set at a generic connection or at none. A credential is never a parameter of the tool. These are left to the connection and kept off the tool: any header or query parameter the description names as its key; the Authorization, Proxy-Authorization, Cookie and X-API-Key headers; any other header whose name contains key, token, secret or auth (an X-Request-Token is dropped from the schema without a notice); and Idempotency-Key, which the platform sets itself.

What the model is shown

Each tool's schema holds the operation's own parameters (path, query, headers) and the properties of its JSON body, cut to what the operation needs: every required property, the optional ones in the description's order up to the per-object bound, and nesting down to the depth bound, below which a value of any shape is accepted. A name used in more than one place is told apart for the model — of path, query, headers and body, in that order, the first keeps the name and each later one becomes query_id, header_name, body_name — while the value is sent under its original name. A tool's name is the operation's identifier (or its method and path) behind your prefix; two that would share a name get a number.

What cannot be imported

Such an operation appears in unimportable with its reason:

  • a body that is not JSON (a form, a file, several parts), or a JSON body that is not an object;
  • a callback or webhook — your system would call it rather than be called;
  • a reference the description does not resolve within the chain bound (one that loops back to itself is cut there and accepted as a value of any shape);
  • a gap in the path that no parameter names;
  • a cookie the operation requires that is not its key;
  • parameters that expand past a bound: more than 2,000 parts once references are followed, or a schema of more than 64 KB (see Bounds below).

A Google Discovery document is read the same way: its methods are the operations, its server is the rootUrl plus servicePath, and Google's global parameters are left out. A path written {+name} cannot be imported.

A description that is not JSON, is not OpenAPI 3.x or Discovery, is over the size or operation bound, or would take more than 400,000 steps to read in all (see Bounds below) is refused whole, naming what was found or the bound. YAML is not read yet: hand over the JSON form of the file. A client who needs part of a larger interface hands over a description cut to that part. The platform keeps the tools, never the description.

Importing again

Import the same description with the same server and the tools made before are refreshed rather than duplicated. The platform remembers, on each tool, the server and the operation it was imported for; a tool is the same one when both match, and the description's title plays no part. You need do nothing but import again with the same server.

RefreshedKept
address, method, locations, parameter schema, descriptionname, fixed parameters, deadline, connection, the agents and nodes it is attached to
  • A chosen operation with no tool yet is created, pointing at the connection you chose for this import.
  • A tool whose operation you did not choose is not touched.
  • A tool whose operation the description no longer holds is left exactly as it is — it may be attached to a published agent — and named in missing: {"operation": "…", "tool_id": "…", "name": "…"}. Nothing is deleted.
  • A different server is a new set, so a tool never moves to another server while keeping its connection.
  • A value you fixed on a parameter the operation no longer has stays on the tool and reaches nothing.

Bounds

BoundValue
A description's sizeat most 16 MB
Waiting for a description fetched by its addressat most 30 seconds
Operations a description may hold to be listedat most 3,000
Operations created by one importat most 50
References followed in one chainat most 32
Optional properties kept per object of a bodyat most 20, in the description's order
Nesting shown to the model5 levels; below them, a value of any shape
Steps to read one operation's parameters, references followedat most 2,000; past it the operation is unimportable
One operation's parameter schemaat most 64 KB; past it the operation is unimportable
Steps to read the whole descriptionat most 400,000; past it the description is refused whole
An imported tool's name, prefix includedat most 64 characters
An imported tool's descriptionat most 1,024 characters
An imported parameter's descriptionat most 300 characters

Saying where a parameter goes

Any HTTP tool, imported or written by hand, may say where each of its parameters goes with config.locations. A tool that says nothing sends them by its method: in the query for GET, HEAD and DELETE, in a JSON body otherwise.

Code
{ "name": "create_visit", "source": "builtin", "config": { "endpoint": "https://clinic.example.com/api/clinics/{clinic}/visits", "method": "POST", "locations": { "dry_run": "query", "tenant": {"in": "header", "name": "X-Tenant"}, "patient_id": "body" } }, "input_schema": {"type": "object", "properties": {"clinic": {"type": "string"}, "dry_run": {"type": "boolean"}, "tenant": {"type": "string"}, "patient_id": {"type": "string"}}} }

A place is "query", "header" or "body"; a gap in the endpoint's path ({clinic} above) is filled from the value of that name and needs no entry. Write {"in": …, "name": …} when the name on the wire differs from the one the model sees; the value is sent under name. A header that carries a credential, or Idempotency-Key, cannot be named: the tool is refused when it is created or changed — the connection places the credential.

Next

  • Sync a lead to the client's system — an HTTP tool and a connection inside a flow and an automation.
  • Quote from a price list — one read tool the agent calls mid-conversation.
  • API Reference — create_tool, test_tool, import_mcp_server, preview_openapi_import, import_openapi, create_connection, with every field.
Last modified on November 4, 2026
Tools and their parameters
On this page
  • One tool per action
  • What your system receives
  • Authorizing with a key
    • A key in a header
    • A key in a query parameter
    • A user name and a password
    • Pointing the tool at it
  • Authorizing as an account you consented to
  • An MCP server behind a connection
  • Connect an MCP server that signs you in
  • Import an API description (OpenAPI or Google Discovery)
    • 1. Preview: what could become a tool
    • 2. Import: the tools you chose
    • The connection it suggests
    • What the model is shown
    • What cannot be imported
    • Importing again
    • Bounds
  • Saying where a parameter goes
  • Next
JSON
JSON
JSON
JSON
JSON