Update a tenant number's routing config
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) orafter_hours(answer only outside theschedulewindows; seeInboundAgentConfig).handover_callback_url— REQUIRED whileenabledunless you sethandover_forwardorhandover_voicemail(either one answers the handover itself); defaults to this number'svoice_callback_urlwhen omitted. Sautikit POSTs theCallHandoverevent here when the agent transfers the call (e.g. to a human); you respond with voice actions (dial,say, …). See theCallHandoverCallbackschema.
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 tovoice_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 tovoice_callback_urlwhen set, else auto-bridge the requested destination.connect— auto-bridge the requested destination directly with no webhook round trip, IGNORINGvoice_callback_urlon 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 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
uuidHeader Parameters
length <= 64Request 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" }}Release a tenant number DELETE
Marks the number released, returns the DID to the available pool, and emits a `number.released` event. Idempotent — re-releasing a released number returns 204 with no event.
Per-country outbound call rates for a tenant number GET
Returns the workspace's currently-effective per-country outbound rate card for the given number, sourced from the provider series rates resolved against the number's origin country and the workspace currency.