Creating a webhook tool is the most common way to connect an agent to an external API, and it fails a lot, almost all from schema mistakes in api_schema.request_body_schema. This skill gets it right on the first write.
All calls use xi-api-key: $API_KEY against https://api.elevenlabs.io.
1. Gather current state first
Before writing anything, make these reads (in parallel where independent):
GET /v1/convai/agents/$AGENT_ID?branch_id=$BRANCH_IDto confirm the agent and whether it is a single-node agent or a workflow (the workflow is in this config; it changes where you attach the tool later).GET /v1/convai/toolsto check the tool does not already exist and to match the naming style already in use.
Get the endpoint URL, method, auth method, and request/response shape from the user before you construct the tool. If any are missing, ask for them or the API docs first.
2. Create the tool
POST /v1/convai/tools
with the tool config discriminated by type (webhook/server). The webhook config lives under api_schema, not at the top level.
3. Schema gotchas (the specific things that cause failures)
- STATIC URL.
api_schema.urlmust be a static string. Do NOT put{{variables}}in the path. Dynamic values go intorequest_body_schema(orpath_params_schema/query_params_schema), never interpolated into the URL. - EXACTLY ONE value source per property. Every property in
request_body_schema.propertiesgets its value from exactly one of: a non-emptydescription(LLM fills it),dynamic_variable(e.g."user_phone_number"or a system var like"system__conversation_id"),constant_value(a fixed literal), oris_system_provided: true. Two sources on one property is the most common validation failure; ZERO sources is equally invalid. A bare{"type":"string"}or a property whose only non-type field is an emptyconstant_valuehas zero sources and is rejected.constant_value: ""does NOT count as a constant. - path_params_schema / query_params_schema are ARRAYS of parameter objects, NOT a
{properties:{...}}object. Each item needs:id,typeas a BARE string ("string"|"number"|"integer"|"boolean"; nullable uses a 2-element array like["string","null"], never{"type":"string"}),description,value_type("llm_prompt"|"dynamic_variable"|"constant"),dynamic_variable(empty string unless bound),constant_value(always a string/number/boolean, NEVER null or omitted), andrequired(boolean). A working llm_prompt item:{"id":"city","type":"string","description":"The city to look up.","value_type":"llm_prompt","dynamic_variable":"","constant_value":"","required":true}. - ARRAY properties REQUIRE an
itemsschema and MUST NOT carryconstant_value. requiredmust reference only property names that exist inproperties.request_headersis an OBJECT, not an array. For secret API keys, reference a workspace secret by id ({"secret_id": "..."}, list existing viaGET /v1/convai/secrets) rather than pasting the key.response_timeout_secsdefaults to 20, max 120. Raise it only for genuinely slow APIs; high timeouts hurt voice latency.- Name and description drive INVOCATION. Make the description specific ("Look up a customer by phone number", not "Customer lookup"); a vague description is the top reason a correctly-saved tool never gets called.
4. Extract response values into the conversation
To feed API response fields into {{dynamic_variables}}, add entries to the tool's top-level assignments array (a sibling of api_schema). Each maps a response field to a variable: source response, a dot-notation value_path (data.items.0.id, NOT data.items[0].id, bracket indexing silently misses), and the target dynamic_variable. Then reference {{customer_name}} in the prompt so the agent speaks it.
5. Attach and wire usage
After creating, attach the tool to the agent. On a single-node agent add its id to the base tool_ids; on a workflow agent keep base tool_ids: [] and add it to the right node's additional_tool_ids so it is only in scope where it fires. Apply the attachment via a targeted PATCH /v1/convai/agents/$AGENT_ID?branch_id=$BRANCH_ID on the relevant path. In the prompt, tell the agent exactly when to call the tool and add a fallback line ("If the lookup returns an error or no results, apologize and offer to take a message").
6. Recovery
schema_mismatch(often no field detail): re-check the section-3 rules one by one, most often two value sources on one property, a{{var}}in the URL, orconstant_valueon an array. Fix and resend; do not blind-retry the identical payload.validation: a concrete arg is wrong (arequiredname with no matching property, an array missingitems, a badvalue_path, a non-objectrequest_headers). The message names the field; correct that one.not_found: a bad agent id or a workflow node id that does not exist. Re-read current ids and retry.