Create a hosted voice AI agent with one REST call
Turn the agent definition you would serve yourself into a resource SignalWire hosts, with one POST, then put a phone number on it with one more.
The claim
Every agent recipe here serves a document from your host. This one hands the same definition to the platform instead. AgentBase renders the ai verb as it would for a call. Its prompt, params and post_prompt become the body of a create request, and the response is a resource with an id.
Why it holds
The vendored REST spec, tools/openapi/rest.json, is the authority.
POST /api/fabric/resources/ai_agentsrequiresnameandprompt. The spec describesprompt,paramsandpost_promptby pointing at the SWMLaireference, so the objects the SDK renders are the objects the endpoint wants.POST /api/fabric/resources/{id}/phone_routesrequiresphone_route_idand ahandler, whose enum iscallingormessaging.- The number’s id comes from
GET /api/relay/rest/phone_numberswithfilter_number, which is a contains match, so the recipe compares the number exactly and refuses one the project does not hold.
A hosted agent has no tool webhooks of yours to call, because there is no host of yours. This one has none. An agent with tools stays a served agent, or uses serverless tools. That pattern is [Let the agent call an API with no server of yours](../call-an-api-without-a-backend/).
How it works
class FrontDesk(AgentBase):
def __init__(self):
super().__init__(name=NAME, route="/front-desk")
self.prompt_add_section("Role", "You answer the phone for Ridgeline Cycles...")
self.set_post_prompt("Summarise the call in one sentence.")
self.set_params({"end_of_speech_timeout": 700})
def definition():
doc = json.loads(FrontDesk()._render_swml())
(ai,) = [step["ai"] for step in doc["sections"]["main"] if "ai" in step]
return {"name": NAME, "prompt": ai["prompt"], "params": ai["params"],
"post_prompt": ai["post_prompt"]}
client.fabric.ai_agents.create(**definition())What the platform receives, from the Python SDK:
{"name": "ridgeline-front-desk",
"prompt": {"pom": [{"title": "Role", "body": "You answer the phone for ..."},
{"title": "Hours", "body": "Open Monday to Friday, ..."},
{"title": "Limits", "body": "You cannot book repairs. ..."}]},
"params": {"end_of_speech_timeout": 700},
"post_prompt": {"text": "Summarise the call in one sentence."}}The two SDKs render the same sections in different forms. Python 3.0.1 emits a pom list; @signalwire/sdk 2.0.5 emits them as markdown text with one ## heading per section. The ai.prompt schema allows both, and the verifier validates each as SWML before trusting it as a request body.
The second call is the one every no-server recipe shares: client.fabric.resources.assign_phone_route(agent_id, phone_route_id=..., handler="calling").
Limitations
The verifier proves the requests and the documents, not the call. What the hosted agent says when the number rings is live behaviour.
A hosted agent cannot call tool webhooks on a host you do not have. Keep tools serverless, or keep the agent served.
What to change first
Add self.define_tool(...) with a webhook handler to FrontDesk and run the verifier. The rendered ai now carries a SWAIG object pointing at your host, and definition() drops it. That is the point: the fields this recipe posts are the ones a hosted agent can honour.