Routing plans
What happens when the phone rings.
A routing plan is a small graph of steps: an entry step and a map of named steps, each leading to the next by name. Every path must end in voicemail or a hangup: a plan with a loop is refused, and a menu repeats only through its own retries.
A plan
business hours, a queue, voicemail
{
"entry": "hours",
"steps": {
"hours": { "type": "hours", "scheduleId": "<schedule id>", "open": "sales", "closed": "vm" },
"sales": { "type": "queue", "queueId": "<queue id>", "maxWaitSecs": 300,
"overflow": "vm", "offerCallback": false },
"vm": { "type": "voicemail", "greeting": { "tts": "Thanks for calling Acme. Leave a message." } }
},
"recording": { "enabled": true, "disclosure": true }
}Steps
| Type | What it does |
|---|---|
hours | Branch on a schedule: open or closed (scheduleId). |
menu | Play a prompt and collect one key; each key leads to a step, noInput after maxRetries. |
ring | Ring members, application users or cell numbers, simultaneous or sequential; next on no answer. |
queue | Wait in a queue for the next agent; overflow after maxWaitSecs, optional callback offer. |
ai_answer | Hand the caller to an AI agent (provider, agentId); onCap when the AI-minute cap is reached, next when it cannot. |
forward | Dial an external number; next on no answer. |
voicemail | Take a message (greeting text or audio, or the default). |
hangup | End the call, optionally after a message. |
- Ring targets are
{ memberId }(a SiteCall member),{ userId }(one of your application users, the id fromPOST /api/v1/users) or{ cell }(an E.164 number). Each may setmode:ring(default),silent(shown without a ring or a phone push) ormuted(not offered). - Prompts are
{ tts }text (up to 1000 characters) or{ audioUrl }. - Limits: ring and forward timeouts 5–120 s, menu timeouts 3–20 s with up to 3 retries, queue waits 30–3600 s.
- Recording:
enabledrecords the calls the plan connects, anddisclosurespeaks the recording notice on answer. Absent: no recording. - AI answering needs an agent from
/api/v1/ai-agentsand draws on the workspace's AI minutes (GET /api/v1/ai-limit).
Versions, publishing and numbers
POST /api/v1/routing-plans/validatechecks a definition against the workspace (its members, users, queues, schedules, AI agents and destination policy) without saving it, and answers{ valid, issues }.POST /api/v1/routing-planssaves the plan and its first version, a draft unlesspublish: true. EveryPATCHwith a definition saves a new immutable version.POST /api/v1/routing-plans/{id}/publishmakes a version (default the newest) the one new calls run, after checking it again. Calls already in progress finish on the version they started with.POST /api/v1/routing-plans/{id}/assign{ phoneNumberId }routes a number with the plan (a 409not_publisheduntil a version is published). The number's calls must be on SiteCall's voice lane (see Numbers).
with @sitecall/cli
const check = await sitecall.routingPlans.validate({ definition: plan });
if (!check.valid) console.log(check.issues); // [{ stepId, code, message }]
const saved = await sitecall.routingPlans.create({ name: "Main line", definition: plan, publish: true });
await sitecall.routingPlans.assign(saved.id, { phoneNumberId });A save or publish of an invalid plan is a 422 invalid_plan; its error envelope carries the same issues. Archiving a plan or deleting a queue or schedule a plan still uses is a 409 in_use.
The plan's JSON Schema
Generated from the validator the engine and the API use.
Routing plan JSON Schema
{
"type": "object",
"properties": {
"entry": {
"type": "string"
},
"steps": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "hours"
},
"scheduleId": {
"type": "string"
},
"open": {
"type": "string"
},
"closed": {
"type": "string"
}
},
"required": [
"type",
"scheduleId",
"open",
"closed"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "menu"
},
"prompt": {
"anyOf": [
{
"type": "object",
"properties": {
"tts": {
"type": "string"
}
},
"required": [
"tts"
]
},
{
"type": "object",
"properties": {
"audioUrl": {
"type": "string"
}
},
"required": [
"audioUrl"
]
}
]
},
"options": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"*",
"#"
]
},
"additionalProperties": {
"type": "string"
}
},
"noInput": {
"type": "string"
},
"timeoutSecs": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"maxRetries": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
}
},
"required": [
"type",
"prompt",
"options",
"noInput",
"timeoutSecs",
"maxRetries"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "ring"
},
"targets": {
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"properties": {
"memberId": {
"type": "string"
},
"mode": {
"type": "string",
"enum": [
"ring",
"silent",
"muted"
]
}
},
"required": [
"memberId"
]
},
{
"type": "object",
"properties": {
"userId": {
"type": "string"
},
"mode": {
"type": "string",
"enum": [
"ring",
"silent",
"muted"
]
}
},
"required": [
"userId"
]
},
{
"type": "object",
"properties": {
"cell": {
"type": "string"
},
"mode": {
"type": "string",
"enum": [
"ring",
"silent",
"muted"
]
}
},
"required": [
"cell"
]
}
]
}
},
"strategy": {
"type": "string",
"enum": [
"simultaneous",
"sequential"
]
},
"timeoutSecs": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"next": {
"type": "string"
}
},
"required": [
"type",
"targets",
"strategy",
"timeoutSecs",
"next"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "queue"
},
"queueId": {
"type": "string"
},
"maxWaitSecs": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"overflow": {
"type": "string"
},
"offerCallback": {
"type": "boolean"
}
},
"required": [
"type",
"queueId",
"maxWaitSecs",
"overflow",
"offerCallback"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "ai_answer"
},
"provider": {
"type": "string",
"enum": [
"sitecall",
"gemini",
"elevenlabs"
]
},
"agentId": {
"type": "string"
},
"onCap": {
"type": "string"
},
"next": {
"type": "string"
}
},
"required": [
"type",
"provider",
"agentId",
"onCap",
"next"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "forward"
},
"to": {
"type": "string"
},
"timeoutSecs": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"next": {
"type": "string"
}
},
"required": [
"type",
"to",
"timeoutSecs",
"next"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "split"
},
"percentToA": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"a": {
"type": "string"
},
"b": {
"type": "string"
}
},
"required": [
"type",
"percentToA",
"a",
"b"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "voicemail"
},
"greeting": {
"anyOf": [
{
"anyOf": [
{
"type": "object",
"properties": {
"tts": {
"type": "string"
}
},
"required": [
"tts"
]
},
{
"type": "object",
"properties": {
"audioUrl": {
"type": "string"
}
},
"required": [
"audioUrl"
]
}
]
},
{
"type": "null"
}
]
}
},
"required": [
"type",
"greeting"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "hangup"
},
"message": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"type",
"message"
]
}
]
}
},
"recording": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"disclosure": {
"type": "boolean"
}
},
"required": [
"enabled",
"disclosure"
]
}
},
"required": [
"entry",
"steps"
]
}