API Reference
Numbers

Update a tenant number's routing config

PUT
/v1/numbers/{id}/routing

Patches voice_callback_url, events_url, and/or inbound_agent on a number's routing config. voice_callback_url/events_url accept http or https; empty string clears either field; omitted fields are left untouched. The Idempotency-Key header is honoured for replay safety.

Assign an AI agent to this number

Send an inbound_agent object to make one of your AI agents answer inbound calls to this number instead of POSTing to voice_callback_url:

{  "inbound_agent": {    "enabled": true,    "agent_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",    "mode": "always",    "handover_callback_url": "https://example.com/sautikit/voice"  }}
  • agent_id — an agent in this workspace that has at least one PUBLISHED REVISION. An agent you are currently editing still qualifies: its live published revision keeps answering while the new draft is unpublished.
  • mode — always (answer every inbound call) or after_hours (answer only outside the schedule windows; see InboundAgentConfig).
  • handover_callback_url — REQUIRED while enabled unless you set handover_forward or handover_voicemail (either one answers the handover itself); defaults to this number's voice_callback_url when omitted. Sautikit POSTs the CallHandover event here when the agent transfers the call (e.g. to a human); you respond with voice actions (dial, say, …). See the CallHandoverCallback schema.

To STOP an AI agent answering, send inbound_agent: null (clears the binding). To pause without clearing, send the object with enabled: false.

inbound_agent uses TRI-STATE semantics distinct from the two URL fields above (which only distinguish set-non-empty vs clear-via-empty-string):

  • omitted from the request body — leave the current binding untouched.
  • JSON null — clear the binding (no AI agent answers this number; routing falls through to voice_callback_url / legacy voice-actions).
  • a JSON object — validate (see InboundAgentConfig) and set.

Forward a number

Send a forward object to ring one or more destinations when this number is called directly (the same shape also appears under InboundAgentConfig.handover_forward for AI-agent handover):

{  "forward": {    "destinations": ["+254700000001", "+254700000002"],    "sequential": false,    "first_answer_wins": true,    "ring_timeout": 20  }}

forward uses the SAME tri-state semantics as inbound_agent: omit to keep the current target, send null to clear it, or send a ForwardTarget object to validate and set it (numbers.invalid_forward on failure).

Queue the call

Send a queue object to hold callers in an ordered line, announce their position, and offer them to a pool one caller at a time:

{  "queue": {    "enabled": true,    "greeting_text": "All of our team are busy. Please hold.",    "agents": { "destinations": ["1001", "1002"], "first_answer_wins": true, "ring_timeout": 20 },    "announce_every_seconds": 90,    "on_timeout": "voicemail",    "voicemail": { "greeting_text": "Leave a message." }  }}

A four-digit destination is an internal extension and rings the device registered to it. queue uses the same tri-state semantics as inbound_agent, and OUTRANKS forward and voicemail when enabled — so a number may keep a forward configured as the fallback it reverts to when the queue is switched off.

There is no hold ceiling: the switch's own queue owns the waiting, so a caller holds until an agent answers, until max_wait_seconds if you set one, or until they hang up. Set sharing_mode to choose the ring order — longest_free by default, or chosen_order to try agents.destinations in the order you listed them.

Saving a queue also PROVISIONS it on the switch. A save that stored the configuration but could not reach the switch answers 202 with a queue_provisioning object naming the agents it created and the ones it skipped; an extension with no device signed in is skipped rather than created logged-out. 202 is an expected response, not an error — the scheduled reconcile carries the change.

Send to voicemail

Send a voicemail object to configure a reusable voicemail fallback (a text-to-speech greeting or a pre-recorded greeting URL, plus recording limits):

{  "voicemail": {    "greeting_text": "Sorry we missed your call. Leave a message after the tone.",    "max_length": 120,    "beep": true  }}

voicemail is also tri-state: omit to keep, null to clear, an object to validate and set (numbers.invalid_voicemail on failure). beep is accepted and stored but not yet rendered on the live call (reserved for a future release).

Opening hours

Send an hours object to give the number its own opening hours and decide what a caller meets outside them:

{  "hours": {    "enabled": true,    "timezone": "Africa/Nairobi",    "weekly": [      { "days": [1, 2, 3, 4, 5], "start": "08:00", "end": "17:00" },      { "days": [6], "start": "09:00", "end": "13:00" }    ],    "observe_holidays": true,    "exceptions": [      { "date": "2026-12-24", "start": "08:00", "end": "13:00", "name": "Christmas Eve" }    ],    "closed": { "mode": "say", "say": "We are closed. Please call back at eight." }  }}

A number with no hours is ALWAYS OPEN, which is how every number behaved before this field existed. The gate is evaluated once, on the initial inbound edge: a caller already holding in a queue when the number closes keeps their place.

Precedence is fixed: a dated exceptions entry wins outright, then the workspace public-holiday calendar when observe_holidays is set, then weekly. Windows are start-inclusive and end-exclusive (a window ending "17:00" is shut at 17:00), and a window whose end is BEFORE its start runs past midnight — the day list names the day the window STARTS on, so 22:00-06:00 on Monday is open at Tuesday 02:00.

hours uses the same tri-state semantics as inbound_agent: omit to keep, null to clear (always open again), an object to validate and set. A rejected object answers 400 numbers.invalid_hours with the specific rule in details (e.g. invalid_timezone, closed_required, invalid_closed_mode).

A number cannot carry both hours and inbound_agent.mode: "after_hours" — that is two schedules on one number disagreeing about when it is open, and it is refused with detail hours_conflict. Bind the agent as hours.closed.mode: "agent" instead.

Outbound routing mode

Set outbound_mode to control how calls PLACED FROM this number route on the outbound edge (it has no effect on inbound):

  • server (default; also the value when unset) — forward to voice_callback_url when set, else auto-bridge the requested destination.
  • connect — auto-bridge the requested destination directly with no webhook round trip, IGNORING voice_callback_url on the outbound leg (that URL still serves the inbound "your server" mode).

outbound_mode is a string field: omit to keep the current value, send an empty string to reset to server, or send connect (numbers.invalid_outbound_mode on any other value).

Authorization

bearerAuth
AuthorizationBearer <token>

Long-lived ES256 JWT minted from the dashboard (https://app.sautikit.com/developers/api-keys). Signed by the platform keyring. Carries workspace_id and scopes claims; revoked via the platform deny-list.

In: header

Path Parameters

id*string
Formatuuid

Header Parameters

Idempotency-Key?string
Lengthlength <= 64

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X PUT "https://example.com/v1/numbers/497f6eca-6276-4993-bfeb-53cbbbba6f08/routing" \  -H "Content-Type: application/json" \  -d '{    "voice_callback_url": "https://example.com/sautikit/voice",    "events_url": "https://example.com/sautikit/events",    "inbound_agent": {      "enabled": true,      "agent_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",      "mode": "after_hours",      "handover_callback_url": "https://example.com/sautikit/voice",      "schedule": {        "timezone": "Africa/Nairobi",        "hours": [          {            "days": [              1,              2,              3,              4,              5            ],            "start": "08:00",            "end": "17:30"          }        ]      }    }  }'
{  "number": {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",    "e164": "string",    "friendly_name": "string",    "status": "active",    "assigned_at": "2019-08-24T14:15:22Z"  },  "routing": {    "voice_callback_url": "string",    "events_url": "string",    "inbound_agent": {      "enabled": true,      "agent_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",      "mode": "after_hours",      "schedule": {        "timezone": "Africa/Nairobi",        "hours": [          {            "days": [              1,              2,              3,              4,              5            ],            "start": "08:00",            "end": "17:30"          }        ]      },      "handover_callback_url": "https://hooks.example.com/handover"    },    "forward": {      "destinations": [        "+254700000001",        "+254700000002"      ],      "sequential": false,      "first_answer_wins": true,      "ring_timeout": 20,      "caller_id": "+254712345678"    },    "queue": {      "enabled": true,      "agents": {        "destinations": [          "+254700000001",          "+254700000002"        ],        "sequential": false,        "first_answer_wins": true,        "ring_timeout": 20,        "caller_id": "+254712345678"      },      "engine": "legacy",      "name": "string",      "sharing_mode": "longest_free",      "greeting_text": "string",      "greeting_play_url": "string",      "greeting_voice": "man",      "hold_play_url": "string",      "hold_duration_seconds": 0,      "announce_position": true,      "announce_every_seconds": 15,      "tick_seconds": 15,      "max_wait_seconds": 0,      "max_depth": 1,      "on_timeout": "hangup",      "voicemail": {        "greeting_text": "Sorry we missed your call. Leave a message after the tone.",        "max_length": 120,        "beep": true      },      "overflow_forward": {        "destinations": [          "+254700000001",          "+254700000002"        ],        "sequential": false,        "first_answer_wins": true,        "ring_timeout": 20,        "caller_id": "+254712345678"      }    },    "voicemail": {      "greeting_text": "Sorry we missed your call. Leave a message after the tone.",      "max_length": 120,      "beep": true    },    "hours": {      "enabled": true,      "timezone": "Africa/Nairobi",      "weekly": [        {          "days": [            1,            2,            3,            4,            5          ],          "start": "08:00",          "end": "17:00"        },        {          "days": [            6          ],          "start": "09:00",          "end": "13:00"        }      ],      "observe_holidays": true,      "closed": {        "mode": "say",        "say": "We are closed. Please call back at eight."      }    },    "outbound_mode": "server"  },  "queue_provisioning": {    "queue": "string",    "sharing_mode": "string",    "live": true,    "message": "string",    "agents": [      "string"    ],    "skipped": [      {        "extension": "string",        "reason": "no_line",        "message": "string"      }    ]  }}
{  "number": {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",    "e164": "string",    "friendly_name": "string",    "status": "active",    "assigned_at": "2019-08-24T14:15:22Z"  },  "routing": {    "voice_callback_url": "string",    "events_url": "string",    "inbound_agent": {      "enabled": true,      "agent_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",      "mode": "after_hours",      "schedule": {        "timezone": "Africa/Nairobi",        "hours": [          {            "days": [              1,              2,              3,              4,              5            ],            "start": "08:00",            "end": "17:30"          }        ]      },      "handover_callback_url": "https://hooks.example.com/handover"    },    "forward": {      "destinations": [        "+254700000001",        "+254700000002"      ],      "sequential": false,      "first_answer_wins": true,      "ring_timeout": 20,      "caller_id": "+254712345678"    },    "queue": {      "enabled": true,      "agents": {        "destinations": [          "+254700000001",          "+254700000002"        ],        "sequential": false,        "first_answer_wins": true,        "ring_timeout": 20,        "caller_id": "+254712345678"      },      "engine": "legacy",      "name": "string",      "sharing_mode": "longest_free",      "greeting_text": "string",      "greeting_play_url": "string",      "greeting_voice": "man",      "hold_play_url": "string",      "hold_duration_seconds": 0,      "announce_position": true,      "announce_every_seconds": 15,      "tick_seconds": 15,      "max_wait_seconds": 0,      "max_depth": 1,      "on_timeout": "hangup",      "voicemail": {        "greeting_text": "Sorry we missed your call. Leave a message after the tone.",        "max_length": 120,        "beep": true      },      "overflow_forward": {        "destinations": [          "+254700000001",          "+254700000002"        ],        "sequential": false,        "first_answer_wins": true,        "ring_timeout": 20,        "caller_id": "+254712345678"      }    },    "voicemail": {      "greeting_text": "Sorry we missed your call. Leave a message after the tone.",      "max_length": 120,      "beep": true    },    "hours": {      "enabled": true,      "timezone": "Africa/Nairobi",      "weekly": [        {          "days": [            1,            2,            3,            4,            5          ],          "start": "08:00",          "end": "17:00"        },        {          "days": [            6          ],          "start": "09:00",          "end": "13:00"        }      ],      "observe_holidays": true,      "closed": {        "mode": "say",        "say": "We are closed. Please call back at eight."      }    },    "outbound_mode": "server"  },  "queue_provisioning": {    "queue": "string",    "sharing_mode": "string",    "live": true,    "message": "string",    "agents": [      "string"    ],    "skipped": [      {        "extension": "string",        "reason": "no_line",        "message": "string"      }    ]  }}
{  "error": {    "code": "numbers.invalid_inbound_agent",    "message": "inbound_agent config is invalid: schedule is required when mode is after_hours",    "request_id": "req-a1b2c3d4",    "details": [      "schedule_required"    ]  }}
{  "error": {    "code": "validation.bad_request",    "message": "string",    "request_id": "string",    "details": [      "string"    ],    "resolution": "string",    "reason": "invalid_characters",    "suggested_e164": "+254727524723"  }}
{  "error": {    "code": "calls.invalid_destination",    "message": "forward.destinations \"+254254727524723\" is not a valid phone number: doubled country code: the number repeats 254 after +254; did you mean +254727524723?",    "request_id": "req-a1b2c3d4",    "reason": "doubled_country_code",    "suggested_e164": "+254727524723"  }}