An outbound call or an API chat is started by a request of yours, which can carry vars, secrets, agent_override and userdata. An inbound phone call, an inbound WhatsApp call or a new WhatsApp or Telegram chat has no such request. The inbound webhook is that request: before the session starts, Talqing asks your server for its configuration, and applies the answer exactly as POST /v1/calls/outbound would. It is asked only by agents that turn it on (inbound_webhook: true), and only the agent that answers counts. It is never asked for web calls, API chats, outbound calls or media streams, which each have a request of their own. Nor for a WhatsApp or Telegram chat started with an agent you posted or sent in a batch: that request already said what runs.
No answer refuses the session. An agent that turns this on is saying it cannot run without your configuration. If your server does not return a valid answer within 3 seconds, the call is hung up or the chat is not answered. See refusals.

Set it up

One webhook per workspace. PUT sets or changes its URL, which must be https:// and resolve to a public address. The signing secret is generated on the first PUT and returned once; later PUTs change only the URL.
Then turn it on per agent: inbound_webhook: true in the agent’s config (Ask my server before each inbound session in the agent’s Prompt & voice card; the URL itself is set on Webhooks). Publishing such an agent is refused until the workspace has a webhook, and removing the webhook (DELETE /v1/inbound-webhook) is refused with 409 while a published agent still asks it.

The request

POST to your URL, Content-Type: application/json, User-Agent: talqing-inbound/1:
A chat asks once, when it opens; later messages in the same chat do not ask again, so an agent that needs live state reads it with a tool. A chat stays open until it is ended, so its answer stands for that long.

Verify it

This differs from the event webhooks, which sign the body alone: a request you answer must not be replayable, so reject a timestamp more than five minutes old.

The answer

Return 2xx with a JSON object. Every field is optional, {} included, and an unknown field is an error.
metadata is shown on the call and chat, carried by session.started and session.completed, and filters GET /v1/calls and GET /v1/chats (?metadata=tenant:t1, repeatable, all must match). It is kept when the session’s content is deleted.

Refusals

Any of these refuses the session with the close reason inbound_webhook_failed, and sessions.error.message says which:
  • Phone call: the caller hears no greeting and is hung up while still ringing; livekit-sip sends the carrier 486 Busy Here. The refused call is listed in Calls with its reason.
  • WhatsApp call: the call is hung up before the agent says anything.
  • Chat: the message that opened it is kept, marked with the error, and nothing is sent. The chat is closed; the person’s next message opens a new chat, which asks again.
Every refused session sends session.refused. Every ask, answered or not, is on the session’s timeline as inbound_webhook with its status code, time, the request sent and what came back: a JSON answer with each secrets value replaced by "[redacted]", or the first 2 KB of anything else.

Test it

POST /v1/inbound-webhook/test with {channel, number, peer} sends a sample session.request signed as a real one is (from = peer, to = number) and returns what came back: {status_code, duration_ms, valid, errors, response}. valid checks the answer’s shape; an agent_override is checked against an agent only on a real session.

Secrets

Workspace and session secrets.

Close reasons

Every way a session ends.