Skip to main content
When someone calls your number, Unpod hands the call to your own app or an Unpod AI agent, whether it reached Unpod on an Unpod number or through your carrier’s trunk. For your app, Unpod asks your answer URL what to do while the call is still ringing, then runs the XML or JSON you reply with.

Three ways to answer

For your app to answer, attach the number with attach_type="human_call" (see the Quick Start). On a number attached to an AI agent, the agent always answers: your answer URL can only reject the call (a reply that starts with <Hangup/>), and no Hangup event reaches the answer URL.

Point your number at your app

Dashboard: open the number in the Unpod dashboard and set its answer URL. API:
set_did sets the config for one number. In the URL, + becomes %2B. A PUT replaces the number’s own config; fields you leave out come from its trunk. get_did returns the config in effect. The number must be one of your org’s numbers, otherwise 404.
Inbound configuration needs the Call API switched on for your org (see the Quick Start): until then set_did (or the PUT) returns 403 call_api_disabled. If the PUT returns a 5xx, Unpod has saved your config and keeps retrying it in the background.

InboundConfig fields

The webhook secret

Your org has one signing key for every signed request Unpod sends you: inbound answer-URL requests and status callbacks alike. Outbound answer-URL requests and recording events aren’t signed yet (see Which requests are signed). Create the key with webhook_secret.rotate() (org owner or editor). The full secret is shown only in that response, so store it as UNPOD_WEBHOOK_SECRET.
Rotating creates a new key and invalidates the old one. A server still verifying with the old key rejects every request with 401 until you give it the new one, and on inbound calls that 401 makes Unpod apply the fallback.
Unpod signs inbound POST requests once your org has a secret, so create it before you point a number at a server that verifies. GET requests are not signed. Verify each request with parse_call_event, as in the server below. See Webhooks for the signature format and every event field.

Handle the call

Unpod sends your answer URL three events of its own. Reply to StartApp with instructions (see Answer URL & XML for every verb) and return 200 for the rest. Point status_callback_url at the same route, as in the config above. Unpod sends the answer URL’s Hangup only once, but retries a status callback a few times over about a minute (Delivery and retries), so a missed Hangup usually arrives again. The route then also receives call events, such as Incoming, Ringing, Answered and a Hangup per leg: return 200 for them. Hangup can arrive more than once, which on_call.pop(ev.call_id, None) below handles. Every event for a call carries the same ev.call_id; ev.leg_id differs per leg. This server routes each caller to a free agent’s phone, or tells them everyone is busy:
app.py
Run it with python app.py and expose it as in the Quick Start. After a valid signature it always returns 2xx: Unpod treats any other status as a failure and may fall back. <Dial><Number> is a blind transfer: Unpod hands the call to that number and runs nothing after it. If the chosen agent doesn’t pick up or is busy, Unpod doesn’t try the next agent. Unpod answered the caller before dialing, so the answer URL’s Hangup reports completed; the agent’s busy or no-answer cause comes on its leg’s Hangup status callback (ev.hangup_cause).
Whether inbound calls are recorded follows the recording policy (set by the Unpod team), not the number’s record setting (see Recording). Don’t put <Record/> before <Dial>: the caller waits in silence for the recording to finish (300 s by default) before Unpod dials.

When your server is down

Unpod waits answer_timeout_ms (500-10000, default 3000) for your reply. If your app doesn’t answer, Unpod applies the number’s answer_fallback: The fallback applies on a timeout, a connection error, any non-2xx status (including your own 401), a reply that doesn’t parse, or a reply containing an action URL that isn’t public https. An empty 2xx body, <Response/> or {"actions": []} is not a failure: Unpod continues with static routing. After 5 failures in a row, Unpod stops calling the answer URL for 30 s and applies the fallback straight away.

For a whole SIP trunk

Your carrier’s numbers reach Unpod over a SIP trunk. Set the same config on each number of the trunk with set_did:
set_did returns 404 for a number that isn’t one of your org’s. The numbers mapped to a trunk are in origin_endpoint.dids of the attach response.
inbound.set_trunk(name, ...) / PUT /sbc/v1/trunks/{name}/inbound set one config for a whole trunk, but name is the trunk’s internal Unpod name, which the trunk API doesn’t return yet. The name you gave the trunk (for example my-trunk) returns 404. Use set_did per number until then.
To create a trunk and attach numbers to it, see Manage Trunks.