# 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

```bash
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

```bash
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

```json
{
  "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

```json
{
  "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:

```bash
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.

```bash
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

```bash
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.

```json
{
  "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

```bash
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](/how-it-fits#one-key-per-person), 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`:

```json
{
  "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 declares | It suggests |
|---|---|
| a key in a header or in the query | an `http_key` connection with the key placed where the description says (`placement`, `header` or `parameter`) |
| a user name and password | an `http_key` connection with `placement` `basic` |
| a bearer token | an `http_key` connection in the `Authorization` header with the prefix `Bearer ` |
| consent through a provider's screen | your consented account with that provider, where the platform holds that provider's consent screen — on a Google server, your Google account |
| anything else | nothing; `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`.

| Refreshed | Kept |
|---|---|
| address, method, `locations`, parameter schema, description | name, 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

| Bound | Value |
|---|---|
| A description's size | at most 16 MB |
| Waiting for a description fetched by its address | at most 30 seconds |
| Operations a description may hold to be listed | at most 3,000 |
| Operations created by one import | at most 50 |
| References followed in one chain | at most 32 |
| Optional properties kept per object of a body | at most 20, in the description's order |
| Nesting shown to the model | 5 levels; below them, a value of any shape |
| Steps to read one operation's parameters, references followed | at most 2,000; past it the operation is unimportable |
| One operation's parameter schema | at most 64 KB; past it the operation is unimportable |
| Steps to read the whole description | at most 400,000; past it the description is refused whole |
| An imported tool's name, prefix included | at most 64 characters |
| An imported tool's description | at most 1,024 characters |
| An imported parameter's description | at 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.

```json
{
  "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](/recipes/sync-lead-to-client-system) —
  an HTTP tool and a connection inside a flow and an automation.
- [Quote from a price list](/recipes/quote-from-price-list) — one read tool the
  agent calls mid-conversation.
- [API Reference](/api) — `create_tool`, `test_tool`, `import_mcp_server`,
  `preview_openapi_import`, `import_openapi`, `create_connection`, with every
  field.
