Skip to content

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.

Tools · webhook · your APIs become tools

Despite 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.

Each route becomes a tool

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 cardBecomes
Route check_orderTool check_order, with its own parameter schema
description + whenToUse + responseDescriptionThe description the model reads to decide whether to call it
parameters[]The JSON Schema properties of the tool (with required)
enabled: falseThe 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 Ordercheck_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).

What the model sees

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.

descrição da tool, como é montada
{description}

WHEN TO USE:
{whenToUse}

RESPONSE:
{responseDescription}
descrição de cada parâmetro
{description} ({howToObtain}) Exemplo: {example}

A well-described route reaches the model like this:

a tool que o modelo recebejson
{
  "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.

How parameters become a request

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
pathTextual substitution in the path: /orders/{{order_id}}/orders/PED-10492.
queryParamsTextual substitution in each **value** of the object: {"cpf": "{{cpf}}"}. Values must be strings.
bodyTemplateSubstitution 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.

The quoting rule in bodyTemplate

In the body, the placeholder with quotes becomes a JSON value; without quotes, numbers and JSON enter raw. The rule by type:

Parameter typeWrite like thisArrives 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.
bodyTemplatejson
{
  "cliente_id": "{{cliente_id}}",
  "observacao": "{{observacao}}",
  "itens": {{itens}},
  "quantidade": {{quantidade}},
  "urgente": {{urgente}}
}
StatusWhat the runtime does
**Optional** parameter that the model did not fillThe 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 valueClear error to the model listing the missing parameters (path parameter must be required).
Template that does not become valid JSONExplicit error to the model with the problem position — the body is **never** sent as broken text.

Authentication

Authentication is for the entire card — applies to all routes.

auth.typeFieldsWhat goes in the request
noneNothing.
bearertokenAuthorization: Bearer <token>
api_keyapiKey, apiKeyName, apiKeyLocationHeader <apiKeyName>: <apiKey> (default) or, with apiKeyLocation: "query", ?<apiKeyName>=<apiKey>.
basicusername, passwordAuthorization: Basic <base64(user:password)>
custom_headercustomHeaderName, customHeaderValueThe 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/jsonconfig.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.

Execution: timeout, retries and errors

BehaviorDetail
Timeoutconfig.timeout in **milliseconds** (default 30000), applied to the entire call.
RetriesOnly with retryOnError: true (builder default). Default maxRetries 2 → up to 3 attempts, with 1 s and 2 s waits.
What is retriedOnly **network** failures: connection refused, timeout, HTTP client exception.
What is **not** retriedHTTP response with status ≥ 400 — returns immediately, no retry.
Successful responseJSON is returned formatted to the model; non-JSON response comes as text, truncated to **5,000** characters.
Error responseError: 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.

Reference: the card

namestringobrigatório

Card name on canvas (e.g., `Order API`). Does not affect tool names.

config.baseUrlstringobrigatório

API base URL (e.g., `https://api.store.com/v1`). The trailing slash is removed before concatenating with `path`.

config.descriptionstring

API description for whoever edits the card.

config.authobjectpadrão: { type: "none" }

Authentication for all routes — see the table above.

config.defaultHeadersobject

Headers applied to all routes. Only configurable via API/JSON.

config.timeoutnumber (ms)padrão: 30000

Timeout for each call.

config.retryOnErrorbooleanpadrão: true

Retries network failures.

config.maxRetriesnumberpadrão: 2

Maximum number of retries (up to 3 attempts total).

config.routes[]WebhookRoute[]obrigatório

The routes — each becomes a tool.

Reference: the route

namestringobrigatório

Tool name, sanitized (`create_order`). Unique across the entire workflow.

descriptionstringobrigatório

What the route does — first line of the description the model reads.

whenToUsestringobrigatório

When to use. Enters as `WHEN TO USE:` in the description — this is what separates similar routes.

methodGET | POST | PUT | PATCH | DELETEobrigatóriopadrão: GET

HTTP method. Only `POST`/`PUT`/`PATCH` send a body.

pathstringobrigatório

Path 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.

queryParamsobject

Fixed query string or with `{{placeholders}}`. Only configurable via API/JSON.

headersobject

Headers specific to this route. Only configurable via API/JSON.

responseDescriptionstring

What the response contains. Enters as `RESPONSE:` in the tool description.

enabledbooleanpadrão: true

Disables the route without deleting it — it is no longer registered as a tool.

Reference: the parameter

namestringobrigatório

Name used in `{{name}}` and in the tool schema. Lowercase, no accents, with `_`.

typestring | number | boolean | array | objectpadrão: string

Type declared in the tool's JSON Schema.

requiredbooleanpadrão: true

Enters the `required` list of the schema — the model cannot omit it.

descriptionstringobrigatório

What the parameter is, in the voice of someone instructing the model.

howToObtainstring

How the agent gets the value (e.g., "ask the client" or "use `fetch_client` first"). Goes in parentheses in the description.

examplestring

An example value — becomes `Example: ...` at the end of the description. Greatly reduces format errors.

defaultValuestring

Suggested 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.

Advanced features of routes

FieldWhat it does
responseMappingExtracts 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).
maxRequestsPerMinuteLimit of calls per minute for the card (sliding window). Exceeded, the model receives a clear error asking to wait.
dependsOn / triggersBecome 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.

Scope, security and limits

PointHow it works
Who sees the toolsOnly 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 furtherIn 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.
NetworkThe 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).
CredentialsStay in the node config, never in the prompt or stream. Still, use a key **with minimal scope** — the agent can call any enabled route.
ValidationA Webhook card without baseUrl **and** without routes fails workflow validation (webhook_no_config) — see [Validate, sandbox and publish](/en/api/agents/publish).
CostThe 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.

Works well with

The Webhook rarely stands alone. The combinations that work best:

WithWhy
[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.

What you can build

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.

In the builder, step by step

  1. 1

    Drag the Webhook card to the canvas

    It's in Tools, on the left palette.

  2. 2

    Connect to the Agent slot

    The right output of the Webhook enters a slot-N of the Agent. Slots grow on their own — there's always a free one.

  3. 3

    Fill in the connection

    Card name, baseUrl, authentication and timeout. It's worth testing the base URL in a curl first.

  4. 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. 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. 6

    Test in the sandbox

    Run the agent in the editor itself and watch the call. The tool_use event shows exactly what the model sent.

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

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".

agente de atendimento com duas rotasjson
{
  "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" }
    ]
  }
}
criar e rodarbash
# 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.

When something doesn't work

SymptomLikely cause
The agent never calls the routewhenToUse 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 allowedThe 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 reachedThe card's maxRequestsPerMinute exceeded in the 1-minute window.
Error: Route 'x' not foundThe name changed after publishing, or the route is in another card. The message lists available routes.
Error: Route 'x' is disabledenabled: false on the route.
The API receives the call without the parameterGET/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 behaviorNames that collide after sanitization (Criar-Pedido and criar pedido become the same criar_pedido).
Error: Timeout even with a fast APItimeout is in milliseconds: 30 means 0.03 s.
The response arrives truncatedBody 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.

Was this page helpful?