Hang up a live call with a reason, send it to a new destination, or unbridge it from its peer, each with one REST command addressed to the call id.
rest
The claim
POST /api/calling/calls takes a command, the call id at the top level, and a params object. Any process that holds a call id can steer the call, whether or not it placed the call. The id arrives in an AI agent’s tool webhook, in a status callback, or in the response to a dial. The vendored REST spec, tools/openapi/rest.json, documents three commands for ending or moving a call.
Why it holds
calling.end takes a reason: one of hangup, cancel, busy, noAnswer, decline or error. The recipe refuses any other value before it is sent.
calling.transfer requires dest, which the spec describes as “a SIP URI, phone number, SWML URL, or an inline Calling SWML document”.
calling.disconnect has no params. The spec’s command table describes it as “Disconnect bridged calls without hanging up either leg”, so it separates two connected calls and ends neither.
The verifier proves the three requests. The spec is the authority for what the platform does with them.
How it works
client = RestClient()
def hang_up(call_id, reason="hangup"):
if reason not in END_REASONS:
raise ValueError(...)
return client.calling.end(call_id, reason=reason)
def transfer(call_id, dest):
return client.calling.transfer(call_id, dest=dest)
def unbridge(call_id):
return client.calling.disconnect(call_id)
The SDK’s calling namespace, rest/namespaces/calling.py, sends every call command to that one path and puts the call id in id. The TypeScript SDK, @signalwire/sdk, does the same: client.calling.end(callId, { reason }) produces the body above, which is what the verifier checks. The spec marks command, id and params required on all three commands. dest accepts a string or an object. An object carries an inline SWML document, for when the new leg needs a document of its own.
A transfer over REST is different from connect inside a SWML document. The document bridges a call it is already running. The REST command moves a call from outside, which is what a supervisor console or a CRM needs.
Limitations
The verifier proves the request, not the call’s fate. Whether a dest answers, and what the far end hears for each reason, are live behaviour.
calling.disconnect presumes a bridge exists. On a call with no peer leg the platform’s response, not the recipe, says what happens.
What to change first
Change "busy" in the verifier’s first helper call to "rejected". The recipe raises before the request, which is the point. The six reasons are the whole vocabulary, and the check sits in your code rather than in a 400.