API Reference
Numbers

Per-country outbound call rates for a tenant number

GET
/v1/numbers/{id}/call-rates

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.

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

Response Body

application/json

application/json

application/json

curl -X GET "https://example.com/v1/numbers/497f6eca-6276-4993-bfeb-53cbbbba6f08/call-rates"
{  "data": {}}
{  "error": {    "code": "validation.bad_request",    "message": "string",    "request_id": "string",    "details": [      "string"    ],    "resolution": "string",    "reason": "invalid_characters",    "suggested_e164": "+254727524723"  }}
{  "error": {    "code": "validation.bad_request",    "message": "string",    "request_id": "string",    "details": [      "string"    ],    "resolution": "string",    "reason": "invalid_characters",    "suggested_e164": "+254727524723"  }}

Update a tenant number's routing config PUT

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`: ```json { "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): ```json { "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: ```json { "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): ```json { "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: ```json { "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).

Register a customer-owned number against the workspace's SIP trunk POST

Adds a number you already own — and already pay your own carrier for — to the workspace's SIP trunk. Unlike `POST /v1/numbers/{id}/claim`, this performs no price lookup and no wallet debit at creation time: there is no monthly rent on a BYO number, only per-minute call charges. Requires an `active` SIP trunk; see [Bring your own SIP trunk](https://sautikit.com/developers/concepts/numbers#bring-your-own-sip-trunk).