Webhook
The card that transforms any HTTP API into agent tools: each route becomes a tool. Complete reference, substitution rules, combinations with other cards, and ready-made applications.
Updated on Sep 02, 2026
The Webhook is the bridge between the agent and your system. You describe an API — base URL, authentication, and a set of routes — and each route becomes a tool that the model can call on its own, at the right time, with the right parameters.
It's the card that makes the agent step out of the conversation and act: look up an order, open a ticket, schedule, charge, update a CRM.
webhook · your APIs become toolsDespite its name, this card does not receive calls from outside: it makes HTTP calls to your API. The agent's events are received by the run stream.
A Webhook card with four routes delivers four tools to the agent — not a generic "call API" tool. The model chooses between them by name and description, just as it would choose between web search and a calculator.
| On the card | Becomes |
|---|---|
Route check_order | Tool check_order, with its own parameter schema |
description + whenToUse + responseDescription | The description the model reads to decide whether to call it |
parameters[] | The JSON Schema properties of the tool (with required) |
enabled: false | The route is not registered — disappears from the tools list |
The route name is sanitized
The name becomes the tool name after losing accents, becoming lowercase, and replacing spaces/hyphens with _ (max. 64 characters): Check Order → check_order. Names that collide overwrite each other — between routes on the same card, between two Webhook cards in the same workflow, and with native tools (don't name a route summarize or web_search).
This is the part that decides whether the agent gets the call right or wrong. The runtime builds the tool description from three fields of the route, and the description of each parameter from three others. It's not decoration: it's the only context the model has.
{description}
WHEN TO USE:
{whenToUse}
RESPONSE:
{responseDescription}{description} ({howToObtain}) Exemplo: {example}A well-described route reaches the model like this:
{
"name": "consultar_pedido",
"description": "Consulta o status e os itens de um pedido pelo número.\n\nWHEN TO USE:\nUse quando o cliente perguntar sobre um pedido, entrega, rastreio ou nota fiscal.\n\nRESPONSE:\nRetorna status, data de envio, código de rastreio e a lista de itens.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Número do pedido do cliente (peça ao cliente ou obtenha com buscar_pedidos_do_cliente) Exemplo: PED-10492"
}
},
"required": ["order_id"]
}
}Write the whenToUse as a rule, not as a summary
"Looks up orders" doesn't help the model decide. "Use when the customer mentions an order number or asks about delivery, tracking, or invoice — don't use for exchanges, which have their own route" resolves ambiguity between similar routes.
The model returns the parameters as an object. The runtime doesn't guess where each one goes: you tell it, using {{name}} in the right place on the route.
Where you write {{param}} | What happens |
|---|---|
path | Textual substitution in the path: /orders/{{order_id}} → /orders/PED-10492. |
queryParams | Textual substitution in each **value** of the object: {"cpf": "{{cpf}}"}. Values must be strings. |
bodyTemplate | Substitution in the JSON body, in POST/PUT/PATCH — with a quoting rule (below). |
Nowhere (POST without bodyTemplate) | The parameters become the entire request body **as is**, the way the model sent them. |
| Nowhere (GET/DELETE) | The parameter is **discarded** — doesn't become a query string on its own. |
Error #1 in GET routes
In GET and DELETE parameters don't automatically become query strings. A cpf parameter in a GET /clients route disappears from the call: to include it, put it in the path (/clients/{{cpf}}) or declare queryParams: {"cpf": "{{cpf}}"}. The call "works", returns the full list, and the agent responds with the wrong client.
In the body, the placeholder with quotes becomes a JSON value; without quotes, numbers and JSON enter raw. The rule by type:
| Parameter type | Write like this | Arrives at your API as |
|---|---|---|
string | "{{text}}" | "value" — JSON string, with proper escaping of quotes and line breaks. |
number | {{quantity}} (no quotes) | 3 — number. With quotes it becomes the string "3". |
boolean | {{flag}} (either way) | true/false — real JSON, both forms. |
array / object | {{items}} (either way) | ["A1","B2"] — real JSON, both forms. |
{
"cliente_id": "{{cliente_id}}",
"observacao": "{{observacao}}",
"itens": {{itens}},
"quantidade": {{quantidade}},
"urgente": {{urgente}}
}| Status | What the runtime does |
|---|---|
| **Optional** parameter that the model did not fill | The placeholder becomes null in the body, and the entry is **removed** from the query string — no literal {{param}} reaches your API. |
| Placeholder in **path** without value | Clear error to the model listing the missing parameters (path parameter must be required). |
| Template that does not become valid JSON | Explicit error to the model with the problem position — the body is **never** sent as broken text. |
Authentication is for the entire card — applies to all routes.
auth.type | Fields | What goes in the request |
|---|---|---|
none | — | Nothing. |
bearer | token | Authorization: Bearer <token> |
api_key | apiKey, apiKeyName, apiKeyLocation | Header <apiKeyName>: <apiKey> (default) or, with apiKeyLocation: "query", ?<apiKeyName>=<apiKey>. |
basic | username, password | Authorization: Basic <base64(user:password)> |
custom_header | customHeaderName, customHeaderValue | The header you set — for APIs with their own schema. |
Call headers are assembled in this order, each layer overwriting the previous: Content-Type/Accept: application/json → config.defaultHeaders → route headers → authentication header.
The token does not reach the model
Card configuration (including auth) is explicitly excluded from the input sent to the model and the stream's tool_use event — the runtime injects the config directly into the HTTP call. The model only sees route parameters; anyone following the run via SSE also does. Your credentials do not leak into the conversation history.
| Behavior | Detail |
|---|---|
| Timeout | config.timeout in **milliseconds** (default 30000), applied to the entire call. |
| Retries | Only with retryOnError: true (builder default). Default maxRetries 2 → up to 3 attempts, with 1 s and 2 s waits. |
| What is retried | Only **network** failures: connection refused, timeout, HTTP client exception. |
| What is **not** retried | HTTP response with status ≥ 400 — returns immediately, no retry. |
| Successful response | JSON is returned formatted to the model; non-JSON response comes as text, truncated to **5,000** characters. |
| Error response | Error: HTTP 404 - <body>, with body truncated to **500** characters. |
Error becomes context, not exception
Every error returns to the model as text (Error: Route 'x' is disabled, Error: Webhook base URL not configured, Error: HTTP 422 - ...). The agent does not break: it reads the error and decides — try another route, ask for missing data, or explain to the user. That's why it pays to put effort into your own API's error messages: they become instructions for the model.
namestringobrigatórioCard name on canvas (e.g., `Order API`). Does not affect tool names.
config.baseUrlstringobrigatórioAPI base URL (e.g., `https://api.store.com/v1`). The trailing slash is removed before concatenating with `path`.
config.descriptionstringAPI description for whoever edits the card.
config.authobjectpadrão: { type: "none" }Authentication for all routes — see the table above.
config.defaultHeadersobjectHeaders applied to all routes. Only configurable via API/JSON.
config.timeoutnumber (ms)padrão: 30000Timeout for each call.
config.retryOnErrorbooleanpadrão: trueRetries network failures.
config.maxRetriesnumberpadrão: 2Maximum number of retries (up to 3 attempts total).
config.routes[]WebhookRoute[]obrigatórioThe routes — each becomes a tool.
namestringobrigatórioTool name, sanitized (`create_order`). Unique across the entire workflow.
descriptionstringobrigatórioWhat the route does — first line of the description the model reads.
whenToUsestringobrigatórioWhen to use. Enters as `WHEN TO USE:` in the description — this is what separates similar routes.
methodGET | POST | PUT | PATCH | DELETEobrigatóriopadrão: GETHTTP method. Only `POST`/`PUT`/`PATCH` send a body.
pathstringobrigatórioPath concatenated to `baseUrl`. Accepts `{{parameters}}`.
parameters[]WebhookParam[]The parameters the model fills in — become the tool's JSON Schema.
bodyTemplatestring (JSON)Body with `{{placeholders}}`. Without it, parameters become the entire body.
queryParamsobjectFixed query string or with `{{placeholders}}`. Only configurable via API/JSON.
headersobjectHeaders specific to this route. Only configurable via API/JSON.
responseDescriptionstringWhat the response contains. Enters as `RESPONSE:` in the tool description.
enabledbooleanpadrão: trueDisables the route without deleting it — it is no longer registered as a tool.
namestringobrigatórioName used in `{{name}}` and in the tool schema. Lowercase, no accents, with `_`.
typestring | number | boolean | array | objectpadrão: stringType declared in the tool's JSON Schema.
requiredbooleanpadrão: trueEnters the `required` list of the schema — the model cannot omit it.
descriptionstringobrigatórioWhat the parameter is, in the voice of someone instructing the model.
howToObtainstringHow the agent gets the value (e.g., "ask the client" or "use `fetch_client` first"). Goes in parentheses in the description.
examplestringAn example value — becomes `Example: ...` at the end of the description. Greatly reduces format errors.
defaultValuestringSuggested value when the model does not provide one. Documented in the catalog; today **not** automatically filled by the runtime — mention the default in `description` and declare the parameter as required.
validValuesstring[]Accepted values. Same: describe them in `description` so the model respects them.
| Field | What it does |
|---|---|
responseMapping | Extracts from the response only the declared fields, by path ({"order": "$.order.id", "sku": "$.items[0].sku"}) — fewer tokens in context. If no path matches, the full response is returned (wrong mapping does not hide the data). |
maxRequestsPerMinute | Limit of calls per minute for the card (sliding window). Exceeded, the model receives a clear error asking to wait. |
dependsOn / triggers | Become chaining instruction in the description the model reads: DEPENDS ON: call first: fetch_client / AFTER THIS: consider calling: track. |
No effect yet
responseExample and parameters[].in are accepted and ignored (the parameter location is defined by {{placeholder}} in path/query/body). defaultValue/validValues are not applied automatically — describe the default and accepted values in the parameter's description, which is what the model reads.
| Point | How it works |
|---|---|
| Who sees the tools | Only the agent in whose **slot** the card is enabled (or who receives it via pipeline) — the same scope as MCP cards. Enable the card in more than one slot to share. |
| How to restrict further | In the Agent card, config.tools_filter with the list of allowed names: when two agents share the same card, each exposes only the subset of their specialty. |
| Network | The runtime **blocks private/loopback/metadata destination** — a card can only reach public addresses (installations that need internal API allow specific hosts via AGENT_EGRESS_ALLOW). |
| Credentials | Stay in the node config, never in the prompt or stream. Still, use a key **with minimal scope** — the agent can call any enabled route. |
| Validation | A Webhook card without baseUrl **and** without routes fails workflow validation (webhook_no_config) — see [Validate, sandbox and publish](/en/api/agents/publish). |
| Cost | The HTTP call is not charged; what matters is the return entering the model's context. Huge responses = more tokens in every following turn. |
Routes that change the world demand confirmation
For POST/DELETE that charge, cancel, or delete, place a User Approval card before the flow section that uses them — or require an explicit confirmation parameter and state in whenToUse that it comes only from the user.
The Webhook rarely stands alone. The combinations that work best:
| With | Why |
|---|---|
| [Agent](/en/api/agents/cards/essentials) | The basics: routes become tools in the agent's slot. Describe in system_prompt the usage policy ("never make up an order number"). |
| [Transform](/en/api/agents/cards/data) | In the pipeline after the Webhook, extracts only the fields that matter from the JSON response — fewer tokens and less distraction for the next step. It's the practical replacement for responseMapping. |
| [Knowledge (RAG)](/en/api/agents/cards/tools) | RAG answers "how does the exchange policy work"; Webhook answers "what's the status of **your** order". Together they cover static knowledge and live data. |
| [User Approval](/en/api/agents/cards/logic) | Human gate before routes that spend money or delete data. |
| [If / Else](/en/api/agents/cards/logic) | Branches by result: order delivered goes to satisfaction survey, delayed goes to the logistics team. |
| [Router](/en/api/agents/cards/coordination) | One agent per domain (orders, finance, support), each with its own Webhook card and tools_filter. |
| [Parallel](/en/api/agents/cards/coordination) | Queries multiple APIs at the same time (inventory + shipping + credit) and combines the responses into one. |
| [Start](/en/api/agents/cards/essentials) | Run variables ({{customer_id}}, {{tenant}}) arrive via variables in the call and identify the end user in your API. |
Routes: check_order (GET), open_ticket (POST), request_exchange (POST).
The agent uses RAG for the exchange policy and the Webhook for the specific case. whenToUse separates "question about the rule" from "I want to exchange mine". The exchange goes through User Approval before the POST.
Routes: list_times (GET with queryParams for date), schedule (POST with bodyTemplate), cancel (DELETE with {{id}} in path).
The howToObtain for slot_id says "use list_times first" — the chaining that dependsOn would promise, done by text.
Routes: search_company (GET), create_lead (POST), update_stage (PATCH).
The agent converses, enriches with web search, creates the lead and moves the stage. A Transform card after search_company reduces the CRM response to the 5 fields that matter.
Routes: service_status (GET), reprocess_queue (POST), escalate_oncall (POST).
Here tools_filter is mandatory: only the "operator" agent sees the write routes; the first-level agent gets only the read routes.
Routes: check_invoice (GET), generate_duplicate (POST), register_payment_promise (POST).
Short and objective responses in responseDescription — in financial routes, the model must repeat values, never recalculate them. If you need calculation, also connect the Calculator.
- 1
Drag the Webhook card to the canvas
It's in Tools, on the left palette.
- 2
Connect to the Agent slot
The right output of the Webhook enters a
slot-Nof the Agent. Slots grow on their own — there's always a free one. - 3
Fill in the connection
Card name,
baseUrl, authentication and timeout. It's worth testing the base URL in acurlfirst. - 4
Create the first route
Name, method, path, description and when to use. Start with a read route (
GET) — it's the easiest to validate. - 5
Declare the parameters
Name, type, required or not, and description. Use
{{name}}in the path or body, otherwise the value won't reach the API. - 6
Test in the sandbox
Run the agent in the editor itself and watch the call. The
tool_useevent shows exactly what the model sent. - 7
Publish
Only the published version responds on
/run.
The builder edits a subset
The card modal edits name, baseUrl, description, authentication, timeout and routes (name, method, path, description, when to use, bodyTemplate, responseDescription and parameters). Fields like defaultHeaders, queryParams, headers per route, retryOnError/maxRetries and howToObtain/example work at runtime, but today they only enter through the workflow JSON, via API.
The Webhook card is a webhook node in the workflow JSON. It doesn't enter the flow: it connects to the agent through an edge with targetHandle: "slot-N".
{
"name": "Atendimento Loja",
"workflow": {
"nodes": [
{ "id": "start", "type": "start", "data": { "label": "Início", "variables": [
{ "name": "customer_id", "type": "input", "required": true,
"description": "Cliente autenticado na sua aplicação" }
] } },
{ "id": "agent-1", "type": "agent", "data": {
"name": "Atendente",
"model": "hinow/himax",
"system_prompt": "Você atende clientes da loja. Nunca invente número de pedido: consulte sempre. O cliente atual é {{customer_id}}.",
"config": { "tools_filter": ["consultar_pedido", "abrir_chamado"] }
} },
{ "id": "hook-1", "type": "webhook", "data": {
"name": "API da Loja",
"config": {
"baseUrl": "https://api.loja.com/v1",
"timeout": 15000,
"retryOnError": true,
"maxRetries": 2,
"auth": { "type": "bearer", "token": "sk_loja_..." },
"defaultHeaders": { "X-Origem": "agente-hinow" },
"routes": [
{
"id": "r1",
"name": "consultar_pedido",
"description": "Consulta status e itens de um pedido pelo número.",
"whenToUse": "Use quando o cliente citar um número de pedido ou perguntar por entrega, rastreio ou nota fiscal.",
"responseDescription": "Retorna status, data de envio, código de rastreio e itens.",
"method": "GET",
"path": "/orders/{{order_id}}",
"queryParams": { "customer_id": "{{customer_id}}" },
"parameters": [
{ "id": "p1", "name": "order_id", "type": "string", "required": true,
"description": "Número do pedido",
"howToObtain": "Peça ao cliente; ele aparece no e-mail de confirmação",
"example": "PED-10492" },
{ "id": "p2", "name": "customer_id", "type": "string", "required": true,
"description": "Id do cliente autenticado", "example": "cust_881" }
],
"enabled": true
},
{
"id": "r2",
"name": "abrir_chamado",
"description": "Abre um chamado de suporte vinculado a um pedido.",
"whenToUse": "Use quando o problema não puder ser resolvido pela consulta — produto avariado, atraso acima de 5 dias, cobrança indevida.",
"responseDescription": "Retorna o número do chamado e o prazo de resposta.",
"method": "POST",
"path": "/tickets",
"bodyTemplate": "{\"order_id\": \"{{order_id}}\", \"motivo\": \"{{motivo}}\", \"prioridade\": {{prioridade}}}",
"parameters": [
{ "id": "p3", "name": "order_id", "type": "string", "required": true,
"description": "Pedido relacionado",
"howToObtain": "Use consultar_pedido antes", "example": "PED-10492" },
{ "id": "p4", "name": "motivo", "type": "string", "required": true,
"description": "Resumo do problema em uma frase" },
{ "id": "p5", "name": "prioridade", "type": "number", "required": false,
"description": "1 a 5. Use 3 quando o cliente não indicar urgência", "example": "3" }
],
"enabled": true
}
]
}
} },
{ "id": "end-1", "type": "end", "data": { "label": "Fim", "status": "success" } }
],
"edges": [
{ "id": "e1", "source": "start", "target": "agent-1" },
{ "id": "e2", "source": "hook-1", "target": "agent-1", "targetHandle": "slot-1" },
{ "id": "e3", "source": "agent-1", "target": "end-1" }
]
}
}# 1. cria o agent (o workflow passa pelo validador antes de salvar)
curl -X POST https://agents.hinow.ai/v1/agents \
-H "Authorization: Bearer hi_SUA_API_KEY" \
-H "Content-Type: application/json" \
-d @workflow.json
# → {"agent": {"id": "agt_...", "status": "draft"}}
# 2. publica
curl -X POST https://agents.hinow.ai/v1/agents/agt_.../publish \
-H "Authorization: Bearer hi_SUA_API_KEY"
# 3. roda — 'variables' alimenta o {{customer_id}} do Início
curl -X POST https://agents.hinow.ai/v1/agents/agt_.../run \
-H "Authorization: Bearer hi_SUA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message": "cadê meu pedido PED-10492?",
"variables": {"customer_id": "cust_881"},
"stream": true}'Follow the call in the stream
Each route call appears as a tool_use event (with the parameters the model chose) followed by the result — including when the result is Error: HTTP 500. It's the fastest way to discover that a parameter wasn't arriving. See Execution events.
| Symptom | Likely cause |
|---|---|
| The agent never calls the route | whenToUse is vague or similar to another route's; or the card is not in **this agent's slot** (scope is per slot); or the agent's tools_filter doesn't include the route name. |
Error: destination not allowed | The baseUrl resolves to a private/loopback/metadata address — the runtime only reaches public destinations. Expose the API publicly or allow the host via AGENT_EGRESS_ALLOW. |
Error: rate limit reached | The card's maxRequestsPerMinute exceeded in the 1-minute window. |
Error: Route 'x' not found | The name changed after publishing, or the route is in another card. The message lists available routes. |
Error: Route 'x' is disabled | enabled: false on the route. |
| The API receives the call without the parameter | GET/DELETE route with the parameter outside path and outside queryParams — only what appears in a template arrives. |
Error: bodyTemplate produced invalid JSON… | The template, after substitution, didn't form valid JSON — the message points to the position. The body is never sent broken. |
A field arrived as null | **Optional** parameter that the model didn't fill — the runtime replaces the placeholder with null to keep the JSON valid. If the field is essential, declare it required. |
| Two routes with swapped behavior | Names that collide after sanitization (Criar-Pedido and criar pedido become the same criar_pedido). |
Error: Timeout even with a fast API | timeout is in milliseconds: 30 means 0.03 s. |
| The response arrives truncated | Body larger than 5,000 characters. Reduce it in the API or chain a Transform card. |
Reference for all cards
Each builder node, with fields, defaults, and connections.

