---
title: "Create or update a custom tool"
method: POST
path: "/v1/tools"
tags: ["Agents"]
---

# Create or update a custom tool

`POST /v1/tools`

## Request

- Base URL: `https://api.pyai.com`
- URL: `https://api.pyai.com/v1/tools`
- Auth: one of:
  - HTTP bearer
  - API key in header `x-api-key`

Register a custom tool. **Idempotent upsert on `(org, name)`:** if your org already has a tool with this `name`, it is updated in place (HTTP `200`, no duplicate, `hmac_secret` preserved), so re-syncing the same tool from a multi-tenant deploy is safe; otherwise a new tool is created (HTTP `201`, `hmac_secret` returned once). A prebuilt/global tool of the same name never blocks creating your org's tool. In `server` mode PyAI calls your `webhook_url` (signed, network-isolated), works on phone calls; `client` mode runs on the WebSocket. Requires the `omni:session` scope.

## Request body

- ToolCreate
  - `name` string, required
  - `description` string
  - `input_schema` object
  - `config_schema` ToolConfigSchema: Declares the settings a tool needs. Render these as a form in your agent builder; capture answers on the agent's tool binding config.
    - `fields` ToolConfigField[]
      - `key` string, required
      - `label` string, required
      - `type` 'string' | 'number' | 'boolean' | 'select', required
      - `required` boolean
      - `secret` boolean: When true, the value is encrypted at rest and never returned in plaintext.
      - `help` string
      - `placeholder` string
      - `options` string[]: Allowed values when type is 'select'.
  - `webhook_url` string, uri: HTTPS URL PyAI calls when this server tool runs. Public HTTPS only; private, loopback, and metadata addresses are rejected at registration.
  - `execution` 'server' | 'client': How the tool runs. server = PyAI calls webhook_url (default when webhook_url is set; works on phone calls). client = your connected app executes it over the Omni socket. Register server tools here; do not put webhook_url on the Omni configure frame.
  - `auth_header` string: Header name the executor injects the webhook auth value into (e.g. Authorization). Pair with auth_secret.
  - `auth_secret` string: Auth value for your webhook (e.g. 'Bearer ...'). Stored encrypted at rest, injected by the executor, never echoed back.
  - `side_effect` 'read' | 'action'
  - `timeout_ms` integer: Per-call budget (ms). Note the executor also enforces a platform hard ceiling (5000 ms by default); the effective budget is min(timeout_ms, ceiling). On timeout the model receives a synthetic soft `{ "error": "tool timed out" }` and the turn continues.

## Response `200`

Updated existing tool (idempotent upsert by name)

- Tool
  - `object` string
  - `id` string
  - `org_id` string, nullable
  - `name` string
  - `kind` 'prebuilt' | 'custom'
  - `description` string
  - `input_schema` object
  - `config_schema` ToolConfigSchema: Declares the settings a tool needs. Render these as a form in your agent builder; capture answers on the agent's tool binding config.
    - `fields` ToolConfigField[]
      - `key` string, required
      - `label` string, required
      - `type` 'string' | 'number' | 'boolean' | 'select', required
      - `required` boolean
      - `secret` boolean: When true, the value is encrypted at rest and never returned in plaintext.
      - `help` string
      - `placeholder` string
      - `options` string[]: Allowed values when type is 'select'.
  - `webhook_url` string, nullable
  - `execution` 'hosted' | 'server' | 'engine' | 'client': How the tool runs. hosted = PyAI runs it (prebuilt read catalog). server = the tool-executor calls your webhook. engine = the Omni call engine runs it natively (call control: transfer_to_human, send_dtmf, play_hold, collect, end_call), not routed to the executor. client = your connected app.
  - `auth_header` string, nullable
  - `has_auth` boolean: Whether a webhook auth secret is configured (the secret itself is never returned).
  - `side_effect` 'read' | 'action'
  - `timeout_ms` integer
  - `status` 'active' | 'disabled'
  - `created_at` integer

## Other responses

- `201`: Created tool
- `400`: Invalid field
- `401`: Missing or invalid API key (`code: unauthorized`)

---

[API](https://skmtc.dev/pyai/apis/pyai-api.md) · [All operations](https://skmtc.dev/pyai/apis/pyai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/pyai/apis/pyai-api/revisions/271a1539185b?raw)
