Skip to content
TriedDocs
Webhooks

Inbound call webhook

A synchronous webhook Tried calls when an inbound call arrives, so your server can pick the agent, pass dynamic variables and metadata, override agent settings or reject the call.

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:

  1. The call arrives and Tried finds the phone number.
  2. The caller's number is checked against allowed_inbound_country_list. Callers from other countries are rejected before the webhook.
  3. An inbound agent is picked from the number's weighted inbound_agents.
  4. The inbound call webhook is sent.
  5. Your response is applied.
  6. If no agent version was chosen yet, the number's version binding applies (a version, latest, latest_published or an environment tag).
  7. Pre-call workflows run. Their outputs are added to the dynamic variables.
  8. 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_url on create-phone-number, import-phone-number and update-phone-number. Send null on 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:

  1. SIP X- headers and pass-through headers (User-to-User, Diversion, History-Info, P-Asserted-Identity), lower-cased.
  2. dynamic_variables from your response.
  3. 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.

  • Inbound calls for inbound agents, weights and allowed countries.
  • call_started, the first event after an accepted inbound call.