The inbound call webhook lets your server decide how to answer each inbound call before the agent picks up. Tried sends the caller and the dialled number, waits for your reply, and uses it to:
- Route the call to a different agent or agent version.
- Pass dynamic variables (customer name, account balance, open tickets) into the prompt.
- Attach metadata to the call record.
- Override agent, LLM or flow settings for this one call.
- Reject the call.
Unlike the other webhooks, this one is synchronous: the caller waits while your server responds.
When it fires#
For every inbound call to a phone number that has an inbound webhook URL. The steps around it:
- The call arrives and Tried finds the phone number.
- The caller's number is checked against
allowed_inbound_country_list. Callers from other countries are rejected before the webhook. - An inbound agent is picked from the number's weighted
inbound_agents. - The inbound call webhook is sent.
- Your response is applied.
- If no agent version was chosen yet, the number's version binding applies (a version,
latest,latest_publishedor an environment tag). - Pre-call workflows run. Their outputs are added to the dynamic variables.
- The call is admitted under the concurrency limit (or queued) and the agent joins.
It does not fire for outbound calls, web calls or inbound SMS. Inbound SMS messages are answered by the number's inbound agent without a webhook.
Set the URL#
The URL is per phone number.
- Dashboard: Phone Numbers, select the number, then under Inbound Call Agent tick Add an inbound webhook ("override agent or variables per call"), enter the URL and press Save. Untick to remove it.
- API:
inbound_webhook_urloncreate-phone-number,import-phone-numberandupdate-phone-number. Sendnullon update to remove it.
curl -X PATCH https://api.tried.com.au/update-phone-number/+61291234567 \
-H "Authorization: Bearer $TRIED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"inbound_webhook_url": "https://example.com/hooks/inbound"}'A number can have an inbound webhook and no inbound agents. Your webhook must then return override_agent_id, or the call is rejected.
Request#
{
"event": "call_inbound",
"call_inbound": {
"call_id": "call_5e1b9d3a7c0f4e2b8a6d1c75",
"agent_id": "agent_5b0e2c7d9a1f4e3b8c6d2a10",
"agent_version": null,
"from_number": "+61412345678",
"to_number": "+61291234567"
}
}| Field | Type | Description |
|---|---|---|
event |
string | Always call_inbound |
call_inbound.call_id |
string | The id the call will have if it is accepted. The same on every retry, so you can de-duplicate on it |
call_inbound.agent_id |
string or null | The agent picked from the number's inbound_agents. null when the number has none |
call_inbound.agent_version |
integer or null | The version pinned on that inbound_agents entry, else null |
call_inbound.from_number |
string | Caller number. A bare 8 to 15 digit number from the carrier gets a + prefix |
call_inbound.to_number |
string | The Tried number that was dialled, E.164 |
Headers are the same as for other webhooks, including x-tried-signature and x-retell-signature. Verify them as described in Secure webhooks.
Response#
Reply 200 with a JSON body. Every field is optional. An empty object, or {} for call_inbound, keeps the defaults.
{
"call_inbound": {
"override_agent_id": "agent_9d2f6b0e4a8c1d5f3b7e2a61",
"override_agent_version": 4,
"dynamic_variables": {
"customer_name": "Mia Chen",
"balance_due": "120.00",
"is_vip": true
},
"metadata": {
"crm_id": "0035g00000XyZ12",
"segment": "returning"
},
"agent_override": {
"agent": { "max_call_duration_ms": 600000 },
"retell_llm": { "begin_message": "Hi {{customer_name}}, welcome back to Bondi Dental." }
}
}
}| Field | Type | Effect |
|---|---|---|
call_inbound.override_agent_id |
string | Answer with this agent instead of the picked one |
call_inbound.override_agent_version |
integer | Version of override_agent_id. Without it, the number's version binding applies |
call_inbound.dynamic_variables |
object | Merged over the variables from SIP headers. Non-string values are converted to JSON strings (true becomes "true") |
call_inbound.metadata |
object | Merged into the call's metadata |
call_inbound.agent_override.agent |
object | Agent fields to change for this call only. response_engine is ignored |
call_inbound.agent_override.retell_llm |
object | LLM fields for this call (applies when the agent uses a single or multi prompt LLM) |
call_inbound.agent_override.conversation_flow |
object | Flow fields for this call (applies when the agent uses a conversation flow) |
call_inbound.reject |
boolean | true hangs up the call. No call record is created |
Override fields use the same names and validation as update-agent, update-retell-llm and update-conversation-flow. They are validated when the agent starts, and nothing is saved to the stored agent.
Variable precedence#
Later sources win:
- SIP
X-headers and pass-through headers (User-to-User,Diversion,History-Info,P-Asserted-Identity), lower-cased. dynamic_variablesfrom your response.- Pre-call workflow outputs.
Environment tag variables and contact fields are merged in when the agent starts, below these.
Metadata#
Inbound SIP headers are stored in metadata.sip_headers. Your metadata keys are merged on top. When an environment tag decides the version, metadata.environment_tag holds the tag name. All of it appears on the call object in later webhooks.
Timeout and retries#
| Setting | Value |
|---|---|
| Timeout per attempt | 10 seconds, fixed |
| Retries | 2 after the first attempt (3 attempts in total) |
| Wait between attempts | 0.5 s, then 1 s |
| Success | Any 2xx |
Every attempt is stored in the delivery log with event call_inbound and no call ID.
The caller hears ringing while your server works, so keep the handler fast. Look-ups should finish well under a second.
What happens on failure#
| Situation | Result |
|---|---|
All attempts time out or return non-2xx |
The call continues with the picked agent and no overrides |
2xx with an empty or non-JSON body |
Same: defaults are used |
reject: true |
The call is ended |
No agent picked and no override_agent_id |
The call is ended |
override_agent_id does not exist in the workspace |
The call is ended |
Example handler#
import express from "express";
import { verifyWebhook } from "./verify-webhook";
const app = express();
app.post("/hooks/inbound", express.raw({ type: "application/json" }), async (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyWebhook(raw, process.env.TRIED_WEBHOOK_KEY!, req.get("x-tried-signature") ?? "")) {
return res.sendStatus(401);
}
const { call_inbound } = JSON.parse(raw);
const customer = await crm.findByPhone(call_inbound.from_number);
if (customer?.blocked) return res.json({ call_inbound: { reject: true } });
res.json({
call_inbound: {
...(customer?.vip ? { override_agent_id: process.env.VIP_AGENT_ID } : {}),
dynamic_variables: {
customer_name: customer?.name ?? "",
is_returning: customer ? "yes" : "no",
},
metadata: { crm_id: customer?.id ?? null },
},
});
});import json, os
from flask import Flask, request, jsonify, abort
from verify_webhook import verify_webhook
app = Flask(__name__)
@app.post("/hooks/inbound")
def inbound():
raw = request.get_data()
if not verify_webhook(raw, os.environ["TRIED_WEBHOOK_KEY"], request.headers.get("x-tried-signature", "")):
abort(401)
ci = json.loads(raw)["call_inbound"]
customer = crm.find_by_phone(ci["from_number"])
if customer and customer.blocked:
return jsonify({"call_inbound": {"reject": True}})
return jsonify({
"call_inbound": {
"dynamic_variables": {"customer_name": customer.name if customer else ""},
"metadata": {"crm_id": customer.id if customer else None},
}
})Use the variables in the agent's prompt as {{customer_name}}. See Dynamic variables.
Related#
- Inbound calls for inbound agents, weights and allowed countries.
call_started, the first event after an accepted inbound call.