# Tools

> Register an HTTP endpoint of yours as a tool an agent can call during a chat, with a signed request, a JSON schema for its arguments and a JSON answer.

A **tool** lets an agent reach live data or trigger an action in your systems: a CRM lookup, a stock level, a ticket creation. You register an HTTPS endpoint of your own with a description and a JSON schema for its arguments. When an agent that has the tool linked decides it needs it, **Subsidia calls your endpoint**, signs the request, reads your JSON answer and gives it to the model, which then writes its reply. Your server never talks to the model, and the model never talks to your server: Subsidia is in the middle, which is where masking, limits and logging happen.

Tools are the integration point for everything that is not a document. For documents, use [Knowledge sources](https://dev.subsidia.protypa.fr/docs/knowledge-sources.md); for facts the agent should simply know, use [memory](https://dev.subsidia.protypa.fr/docs/agent-memory.md).

**The agent asks, Subsidia calls. Your endpoint sees a signed HTTP request and returns JSON; the result goes back into the same turn.**

Flow: User message -> Agent decides to call a tool -> Subsidia signs and POSTs to your URL -> Your endpoint returns JSON -> Agent writes the answer -> toolCalls in the response

## How a tool is described

| Field | Required | What it is |
| --- | --- | --- |
| `name` | Yes | The function name the model sees. Use `snake_case` letters, digits, `_` and `-`. Any other character run is replaced by `_` before it reaches the model ("CRM lookup!" becomes "CRM_lookup"), so two names that sanitise to the same string collide. The raw name is what your endpoint receives in `X-PulseLabs-Tool`. |
| `description` | Yes | What the tool does **and when to call it**. This is the only guidance the model has. Say what comes back and name the situations ("whenever the user asks about a specific customer"). |
| `parameters` | Yes | A JSON Schema (`type: "object"`, `properties`, `required`) of the arguments. Describe every property in plain words; the model fills them from the conversation. |
| `url` | Yes | The endpoint Subsidia calls. Use HTTPS. See [What Subsidia sends](#what-subsidia-sends). |
| `agentIds` | No | On creation, the agents to link the tool to right away. You can also link later. |
| `isActive` | Update only | An inactive tool is never offered to a model, without being deleted or unlinked. |

> **TIP: A good description is half the integration**
> "Look up a customer in the CRM by company name. Returns the account status, the owner and the open invoices. Call it whenever the user asks about a specific customer" gets called at the right time with the right argument. "CRM" gets called at random. Keep arguments few and typed; prefer an `enum` over free text when the set is closed.

## What Subsidia sends

Tools registered through the public API are called with **`POST`**. The arguments the model chose, as a JSON object, are the request body.

| Part | Value |
| --- | --- |
| Method | `POST` |
| URL | The registered `url`. Placeholders written as `{name}` in the URL are replaced by the URL-encoded value of the argument of the same name, and that argument is then not repeated in the body (`https://api.example.com/clients/{clientId}/status`). |
| Body | The arguments as JSON, for example `{"company":"Atelier Moreau","include_invoices":true}`. If every argument was consumed by URL placeholders, or the model passed none, **there is no body and no `Content-Type`**: treat a missing body as `{}`. |
| `Content-Type` | `application/json`, when there is a body. |
| `X-PulseLabs-Tool` | The tool name as registered. |
| `X-PulseLabs-Signature` | `sha256=` followed by the hex HMAC-SHA256 of the string `{"tool":"<name>","arguments":<arguments>}` (compact JSON, exactly as `JSON.stringify` writes it), keyed with the tool secret. See [Verify the signature](#verify-the-signature). |
| `User-Agent` | `PulseLabs-Tools/1.0` |
| Timeout | 15 seconds for the whole call. There is no retry. |
| Redirects | Not followed: a 3xx is a failure. |

## What your endpoint must return

- **Status 2xx** means success. Anything else (4xx, 5xx, a timeout, a network error) is a failure: the model is told the call failed, with your response text as the reason, and answers with what it has. The turn itself does not fail.
- **Body: JSON.** It is parsed and handed to the model as structured data. A body that is not JSON is handed over as plain text, which also works for short answers.
- **Keep it small and relevant.** Only the first **16 KB** of the response is read; beyond that it is cut (and a cut JSON document is no longer valid JSON). Return what the model needs to answer, not a database row: names, statuses, amounts, dates, in readable keys. The model reads your keys, so `"status": "active"` works better than `"s": 1`.
- **Return errors the model can use.** `404` with `{"error": "No customer named Moreau. Did you mean Atelier Moreau?"}` lets the agent ask the user the right question.
- **Be quick.** The user is waiting on the answer. Fifteen seconds is a ceiling, not a target.

> **WARNING: Reads are easy, writes need care**
> A model decides when to call a tool, from text a user typed. Treat every argument as **untrusted input**, as you would a form field: validate it, authorise it against your own rules, and never build SQL or shell commands from it. For tools that change something (create a ticket, send an email), make the call idempotent, return what was done, and consider a two-step design (a tool that prepares, a human who confirms). The agent is told never to claim an action it has no tool result for, but your endpoint is the last line of defence.

## Security

- **Verify the signature on every request.** It proves the call comes from your Subsidia workspace and that the arguments were not altered. Compare in constant time. The signature has no timestamp, so it does not by itself stop a captured request from being replayed: for writes, make operations idempotent.
- **The secret is shown once.** Registration returns `secret` (`toolsec_` followed by 64 hex characters) in the creation response, and never again; reads omit it. There is no rotation endpoint: to change a secret, create a new tool with the same URL, link it, and delete the old one.
- **Use HTTPS** with a certificate from a public authority. Arguments contain what the user said.
- **Masking depends on where the tool lives.** A tool on your own network (localhost, private ranges such as `10.x`, `172.16-31.x`, `192.168.x`, link-local addresses, `.local` hosts) receives the arguments **as the model chose them**, with real names and values, because internal automations break otherwise. A tool on the public internet receives the same personal-data shielding as any external model call: personal values in the arguments are replaced by consistent placeholders, and your response is restored before the agent reads it. Design public endpoints to accept placeholders without failing ([Synapse](https://dev.subsidia.protypa.fr/docs/synapse.md)).
- **Least privilege.** Give the tool a read-only credential on your side, scoped to what the agent needs. Do not put API keys of your own in the `parameters` schema or the description: both are shown to the model.
- **Linking is granting.** A tool linked to an agent can be triggered by anyone who can chat with that agent. Link sensitive tools only to the agents that need them.

## Verify the signature

The signature covers the logical call, `{"tool": <name>, "arguments": <body>}`, serialised as compact JSON. To verify it, rebuild that string from the `X-PulseLabs-Tool` header and the parsed body, HMAC it with your secret, and compare with `X-PulseLabs-Signature`. Rebuild with the same compact serialisation that `JSON.stringify` produces (no spaces, keys in the original order, non-ASCII characters left as they are). Parsing and re-serialising keeps key order in both Node.js and Python 3.7+.

> **INFO: Floating-point numbers in other languages**
> Rebuilding the string in a language that formats numbers differently from JavaScript (very large integers, numbers like `1e21`, trailing `.0`) can produce a different string than the one that was signed. If your tool takes such numbers, declare them as strings in `parameters`.

## Worked example: a CRM lookup

Goal: an agent named Claire can answer "is Atelier Moreau up to date on payments?" from your CRM. Three steps: write the endpoint, register it, link it.

### 1. The endpoint

**Your server**

_Node.js (Express)_

```typescript
import crypto from 'node:crypto'
import express from 'express'

const SECRET = process.env.SUBSIDIA_TOOL_SECRET! // toolsec_... returned once at registration
const app = express()
app.use(express.json())

function signatureIsValid(req: express.Request): boolean {
  const tool = req.header('X-PulseLabs-Tool') ?? ''
  const received = req.header('X-PulseLabs-Signature') ?? ''
  // No body at all means no arguments: express.json() leaves {} in that case.
  const payload = JSON.stringify({ tool, arguments: req.body ?? {} })
  const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(payload).digest('hex')
  const a = Buffer.from(received)
  const b = Buffer.from(expected)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

// Stand-in for your real CRM client.
async function findCustomer(company: string) {
  const db: Record<string, unknown> = {
    'atelier moreau': { company: 'Atelier Moreau', status: 'active', owner: 'S. Bernard', openInvoices: [{ number: 'Q3-114', amountEur: 12480, dueDate: '2026-10-15' }] },
  }
  return db[company.trim().toLowerCase()] ?? null
}

app.post('/crm-lookup', async (req, res) => {
  if (!signatureIsValid(req)) return res.status(401).json({ error: 'bad signature' })

  const { company, include_invoices } = req.body ?? {}
  if (typeof company !== 'string' || !company.trim()) {
    return res.status(400).json({ error: 'company is required' })
  }

  const customer: any = await findCustomer(company)
  if (!customer) return res.status(404).json({ error: 'No customer named ' + company })

  // Return only what the agent needs to answer.
  const { openInvoices, ...account } = customer
  res.json(include_invoices ? { ...account, openInvoices } : account)
})

app.listen(3000)
```

_Python (FastAPI)_

```python
import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

SECRET = os.environ["SUBSIDIA_TOOL_SECRET"]  # toolsec_... returned once at registration
app = FastAPI()

CUSTOMERS = {
    "atelier moreau": {
        "company": "Atelier Moreau",
        "status": "active",
        "owner": "S. Bernard",
        "openInvoices": [{"number": "Q3-114", "amountEur": 12480, "dueDate": "2026-10-15"}],
    }
}


def verify(tool: str, arguments: dict, received: str) -> bool:
    # Compact JSON, non-ASCII kept as is: the same string JSON.stringify produced.
    payload = json.dumps({"tool": tool, "arguments": arguments}, separators=(",", ":"), ensure_ascii=False)
    expected = "sha256=" + hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(received, expected)


@app.post("/crm-lookup")
async def crm_lookup(request: Request):
    raw = await request.body()
    arguments = json.loads(raw) if raw else {}  # no body means no arguments
    tool = request.headers.get("x-pulselabs-tool", "")
    if not verify(tool, arguments, request.headers.get("x-pulselabs-signature", "")):
        raise HTTPException(status_code=401, detail="bad signature")

    company = str(arguments.get("company", "")).strip()
    if not company:
        raise HTTPException(status_code=400, detail="company is required")

    customer = CUSTOMERS.get(company.lower())
    if customer is None:
        raise HTTPException(status_code=404, detail="No customer named " + company)

    account = {k: v for k, v in customer.items() if k != "openInvoices"}
    if arguments.get("include_invoices"):
        account["openInvoices"] = customer["openInvoices"]
    return account
```

### 2. Register it

Call `POST /v1/tools` (reference below) once. Keep the `secret` from the response in your secret manager and give it to the endpoint above as `SUBSIDIA_TOOL_SECRET`. Passing `agentIds` links the tool at the same time.

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/tools \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "crm_lookup",
    "description": "Look up a customer in the CRM by company name. Returns the account status, the owner and, on request, the open invoices. Call it whenever the user asks about a specific customer.",
    "parameters": {
      "type": "object",
      "properties": {
        "company": { "type": "string", "description": "Company name as the user wrote it." },
        "include_invoices": { "type": "boolean", "description": "Also return the open invoices. Default false." }
      },
      "required": ["company"]
    },
    "url": "https://tools.example.com/crm-lookup",
    "agentIds": ["'"$AGENT_ID"'"]
  }'
```

### 3. Ask a question

Chat with the agent normally. Tools are on by default; the call shows up in `toolCalls` (and as `tool_call` / `tool_result` events when streaming).

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/agents/' + agentId + '/chat', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ message: 'Is Atelier Moreau up to date on payments? Show the open invoices.' }),
})
const turn = await res.json()
console.log(turn.content)
console.log(turn.toolCalls)
// [{ toolName: 'crm_lookup', arguments: { company: 'Atelier Moreau', include_invoices: true },
//    result: { company: 'Atelier Moreau', status: 'active', ... }, success: true, durationMs: 143 }]
```

_Python_

```python
import os, requests

turn = requests.post(
    "https://api.subsidia.protypa.fr/v1/agents/" + agent_id + "/chat",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
    json={"message": "Is Atelier Moreau up to date on payments? Show the open invoices."},
    timeout=300,
).json()
print(turn["content"])
for call in turn.get("toolCalls", []):
    print(call["toolName"], call["arguments"], "ok" if call["success"] else call.get("error"))
```

> **TIP: Debugging a tool that never fires**
> Check, in order: the tool is `isActive`; it is linked to the agent you are chatting with (`GET /v1/tools` lists `agents` for each tool); the request does not set `tools.enabled` to `false` or a `toolIds` list that excludes it; and the description tells the model when to use it. A tool that is called but fails shows `success: false` and the reason in `error` in `toolCalls`.

## How calls are bounded

Within one turn the model can ask for tools up to **three rounds** (a round may contain several calls), then it must answer. Each call has a 15-second timeout and a 16 KB response cap. Tool calls are part of the turn: they are metered with it and recorded in the audit journal of the workspace.

## Endpoints

All tool routes need the `agents` scope on restricted keys (`/v1/tools*` and `/v1/agents/*` share it). Create, update, delete and link are refused for `readonly` keys. Tools are scoped to the workspace of the key.

### POST /v1/tools

Register a tool.

Creates the tool, generates its signing secret and optionally links it to agents. The secret is returned **once**, in this response. Registration does not call your URL, so a typo is only discovered at the first use: test the endpoint yourself first.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | `string` | yes |  | Function name shown to the model. Prefer `snake_case`. |
| `description` | `string` | yes |  | What the tool does and when to call it. |
| `parameters` | `object` | yes |  | JSON Schema of the arguments (`type: "object"`, `properties`, `required`). |
| `url` | `string` | yes |  | HTTPS endpoint Subsidia will POST to. |
| `agentIds` | `string[]` | no |  | Ids of the agents of your workspace to link the tool to. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/tools \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "crm_lookup",
    "description": "Look up a customer in the CRM by company name. Call it whenever the user asks about a specific customer.",
    "parameters": {"type":"object","properties":{"company":{"type":"string"}},"required":["company"]},
    "url": "https://tools.example.com/crm-lookup"
  }'
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/tools', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'crm_lookup',
    description: 'Look up a customer in the CRM by company name. Call it whenever the user asks about a specific customer.',
    parameters: {
      type: 'object',
      properties: { company: { type: 'string', description: 'Company name as the user wrote it.' } },
      required: ['company'],
    },
    url: 'https://tools.example.com/crm-lookup',
    agentIds: [agentId],
  }),
})
const { tool, secret } = await res.json()
// Store "secret" now: it is never returned again.
```

_Python_

```python
import os, requests

res = requests.post(
    "https://api.subsidia.protypa.fr/v1/tools",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
    json={
        "name": "crm_lookup",
        "description": "Look up a customer in the CRM by company name. Call it whenever the user asks about a specific customer.",
        "parameters": {
            "type": "object",
            "properties": {"company": {"type": "string", "description": "Company name as the user wrote it."}},
            "required": ["company"],
        },
        "url": "https://tools.example.com/crm-lookup",
        "agentIds": [agent_id],
    },
)
res.raise_for_status()
body = res.json()
tool_id, secret = body["tool"]["id"], body["secret"]  # store the secret now
```

#### Responses

**201**: The tool, and its signing secret.

```json
{
  "tool": {
    "id": "cm2kd2m7r000fqz0fa4t6y9ch",
    "workspaceId": "cm1w0b0000000qz0fworksp01",
    "name": "crm_lookup",
    "description": "Look up a customer in the CRM by company name. Returns the account status, the owner and the open invoices. Call it whenever the user asks about a specific customer.",
    "parameters": {
      "type": "object",
      "properties": {
        "company": { "type": "string", "description": "Company name as the user wrote it." }
      },
      "required": ["company"]
    },
    "url": "https://tools.example.com/crm-lookup",
    "method": "POST",
    "paramLocations": null,
    "isActive": true,
    "createdAt": "2026-10-09T09:30:12.447Z",
    "updatedAt": "2026-10-09T09:30:12.447Z"
  },
  "secret": "toolsec_9f2c4b7a1e6d3085c7b1a94f2e0d68b35a7c19e4d2f0836b5c8a1e7d94f203ab"
}
```

**400**: A required field is missing.

```json
{ "error": "name, description, parameters, and url are required" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 |  | name, description, parameters or url is missing. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |
| 403 | `scope_denied` | A restricted key without the `agents` scope. |

### GET /v1/tools

List the tools of the workspace.

Newest first. Each tool lists the agents it is linked to (`id` and `name`). The secret is never included.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/tools -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_Python_

```python
import os, requests

tools = requests.get(
    "https://api.subsidia.protypa.fr/v1/tools",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
).json()["tools"]
for t in tools:
    print(t["name"], "active" if t["isActive"] else "inactive", [a["name"] for a in t["agents"]])
```

#### Responses

**200**: An object with a `tools` array.

```json
{ "tools": [
  {
    "id": "cm2kd2m7r000fqz0fa4t6y9ch",
    "workspaceId": "cm1w0b0000000qz0fworksp01",
    "name": "crm_lookup",
    "description": "Look up a customer in the CRM by company name. Returns the account status, the owner and the open invoices. Call it whenever the user asks about a specific customer.",
    "parameters": {
      "type": "object",
      "properties": {
        "company": { "type": "string", "description": "Company name as the user wrote it." }
      },
      "required": ["company"]
    },
    "url": "https://tools.example.com/crm-lookup",
    "method": "POST",
    "paramLocations": null,
    "isActive": true,
    "createdAt": "2026-10-09T09:30:12.447Z",
    "updatedAt": "2026-10-09T09:30:12.447Z",
    "agents": [{ "id": "cm2k8x1ab0001qz0f7h3d9t4e", "name": "Claire" }]
  }
] }
```

### GET /v1/tools/:id

Get one tool.

Returns the tool with its linked agents. The secret is not returned.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The tool id. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/tools/$TOOL_ID -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

#### Responses

**200**: The tool.

```json
{ "tool": {
    "id": "cm2kd2m7r000fqz0fa4t6y9ch",
    "workspaceId": "cm1w0b0000000qz0fworksp01",
    "name": "crm_lookup",
    "description": "Look up a customer in the CRM by company name. Returns the account status, the owner and the open invoices. Call it whenever the user asks about a specific customer.",
    "parameters": {
      "type": "object",
      "properties": {
        "company": { "type": "string", "description": "Company name as the user wrote it." }
      },
      "required": ["company"]
    },
    "url": "https://tools.example.com/crm-lookup",
    "method": "POST",
    "paramLocations": null,
    "isActive": true,
    "createdAt": "2026-10-09T09:30:12.447Z",
    "updatedAt": "2026-10-09T09:30:12.447Z",
    "agents": []
  } }
```

**404**: Unknown tool.

```json
{ "error": "Tool not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | No tool with this id in the workspace. |

### PUT /v1/tools/:id

Update a tool.

Partial update: send only what changes. Use `isActive: false` to take a tool out of service without losing its links. The secret and the linked agents are not changed here.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The tool id. |

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | `string` | no |  | New function name. |
| `description` | `string` | no |  | New description. |
| `parameters` | `object` | no |  | New JSON Schema. |
| `url` | `string` | no |  | New endpoint. |
| `isActive` | `boolean` | no |  | Enable or disable the tool. |

#### Request examples

_curl_

```bash
curl -X PUT https://api.subsidia.protypa.fr/v1/tools/$TOOL_ID \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" -H "Content-Type: application/json" \
  -d '{"isActive": false}'
```

#### Responses

**200**: The updated tool.

```json
{ "tool": {
    "id": "cm2kd2m7r000fqz0fa4t6y9ch",
    "workspaceId": "cm1w0b0000000qz0fworksp01",
    "name": "crm_lookup",
    "description": "Look up a customer in the CRM by company name. Returns the account status, the owner and the open invoices. Call it whenever the user asks about a specific customer.",
    "parameters": {
      "type": "object",
      "properties": {
        "company": { "type": "string", "description": "Company name as the user wrote it." }
      },
      "required": ["company"]
    },
    "url": "https://tools.example.com/crm-lookup",
    "method": "POST",
    "paramLocations": null,
    "isActive": false,
    "createdAt": "2026-10-09T09:30:12.447Z",
    "updatedAt": "2026-10-09T09:30:12.447Z"
  } }
```

**404**: Unknown tool.

```json
{ "error": "Tool not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | No tool with this id in the workspace. |

### DELETE /v1/tools/:id

Delete a tool.

Deletes the tool and its links to agents. Agents stop offering it immediately. This also invalidates its secret.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The tool id. |

#### Request examples

_curl_

```bash
curl -X DELETE https://api.subsidia.protypa.fr/v1/tools/$TOOL_ID -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

#### Responses

**204**: Deleted. No body.

**404**: Unknown tool.

```json
{ "error": "Tool not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | No tool with this id in the workspace. |

### POST /v1/agents/:id/tools/:toolId

Link a tool to an agent.

Makes the tool available to this agent in its next turn. Linking twice is harmless. Returns the agent with the list of its tools (`id` and `name`).

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |
| `toolId` | `string` | yes |  | The tool id. |

#### Request examples

_curl_

```bash
curl -X POST https://api.subsidia.protypa.fr/v1/agents/$AGENT_ID/tools/$TOOL_ID \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_Python_

```python
import os, requests

res = requests.post(
    "https://api.subsidia.protypa.fr/v1/agents/" + agent_id + "/tools/" + tool_id,
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
)
res.raise_for_status()
print([t["name"] for t in res.json()["agent"]["tools"]])
```

#### Responses

**200**: The agent, with a `tools` array.

```json
{ "agent": { "id": "cm2k8x1ab0001qz0f7h3d9t4e", "name": "Claire", "tools": [{ "id": "cm2kd2m7r000fqz0fa4t6y9ch", "name": "crm_lookup" }] } }
```

**404**: `Agent not found` or `Tool not found`.

```json
{ "error": "Tool not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | The agent or the tool does not exist in the workspace. |

### DELETE /v1/agents/:id/tools/:toolId

Unlink a tool from an agent.

The tool is kept and stays linked to its other agents. The agent stops offering it from its next turn.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |
| `toolId` | `string` | yes |  | The tool id. |

#### Request examples

_curl_

```bash
curl -X DELETE https://api.subsidia.protypa.fr/v1/agents/$AGENT_ID/tools/$TOOL_ID \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

#### Responses

**204**: Unlinked. No body.

**404**: Unknown agent.

```json
{ "error": "Agent not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | No agent with this id in the workspace. |

## Schema reference

`parameters` is a standard JSON Schema object, passed to the model unchanged. Supported in practice: `type` (`string`, `number`, `integer`, `boolean`, `array`, `object`), `properties`, `required`, `enum`, `items`, `description`. Always set `"type": "object"` at the top. Example:

_parameters_

```json
{
  "type": "object",
  "properties": {
    "company": { "type": "string", "description": "Company name as the user wrote it." },
    "include_invoices": { "type": "boolean", "description": "Also return the open invoices. Default false." }
  },
  "required": ["company"]
}
```
