Inbound Calls
curl --request PUT \
--url https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound \
--header 'Content-Type: application/json' \
--data '
{
"prompt": "<string>",
"first_message": "<string>",
"voice_id": "<string>",
"language": "<string>",
"allowed_tools": [
"<string>"
],
"transfer_number": "<string>",
"business_hours": {},
"after_hours": "<string>",
"after_hours_message": "<string>",
"max_minutes": 123,
"webhook_url": "<string>",
"enabled": true
}
'import requests
url = "https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound"
payload = {
"prompt": "<string>",
"first_message": "<string>",
"voice_id": "<string>",
"language": "<string>",
"allowed_tools": ["<string>"],
"transfer_number": "<string>",
"business_hours": {},
"after_hours": "<string>",
"after_hours_message": "<string>",
"max_minutes": 123,
"webhook_url": "<string>",
"enabled": True
}
headers = {"Content-Type": "application/json"}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
prompt: '<string>',
first_message: '<string>',
voice_id: '<string>',
language: '<string>',
allowed_tools: ['<string>'],
transfer_number: '<string>',
business_hours: {},
after_hours: '<string>',
after_hours_message: '<string>',
max_minutes: 123,
webhook_url: '<string>',
enabled: true
})
};
fetch('https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'prompt' => '<string>',
'first_message' => '<string>',
'voice_id' => '<string>',
'language' => '<string>',
'allowed_tools' => [
'<string>'
],
'transfer_number' => '<string>',
'business_hours' => [
],
'after_hours' => '<string>',
'after_hours_message' => '<string>',
'max_minutes' => 123,
'webhook_url' => '<string>',
'enabled' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound"
payload := strings.NewReader("{\n \"prompt\": \"<string>\",\n \"first_message\": \"<string>\",\n \"voice_id\": \"<string>\",\n \"language\": \"<string>\",\n \"allowed_tools\": [\n \"<string>\"\n ],\n \"transfer_number\": \"<string>\",\n \"business_hours\": {},\n \"after_hours\": \"<string>\",\n \"after_hours_message\": \"<string>\",\n \"max_minutes\": 123,\n \"webhook_url\": \"<string>\",\n \"enabled\": true\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound")
.header("Content-Type", "application/json")
.body("{\n \"prompt\": \"<string>\",\n \"first_message\": \"<string>\",\n \"voice_id\": \"<string>\",\n \"language\": \"<string>\",\n \"allowed_tools\": [\n \"<string>\"\n ],\n \"transfer_number\": \"<string>\",\n \"business_hours\": {},\n \"after_hours\": \"<string>\",\n \"after_hours_message\": \"<string>\",\n \"max_minutes\": 123,\n \"webhook_url\": \"<string>\",\n \"enabled\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"prompt\": \"<string>\",\n \"first_message\": \"<string>\",\n \"voice_id\": \"<string>\",\n \"language\": \"<string>\",\n \"allowed_tools\": [\n \"<string>\"\n ],\n \"transfer_number\": \"<string>\",\n \"business_hours\": {},\n \"after_hours\": \"<string>\",\n \"after_hours_message\": \"<string>\",\n \"max_minutes\": 123,\n \"webhook_url\": \"<string>\",\n \"enabled\": true\n}"
response = http.request(request)
puts response.read_bodyVoice
Inbound Calls
Let the agent’s own phone number answer its calls
PUT
/
v1
/
tools
/
voice
/
numbers
/
{id}
/
inbound
Inbound Calls
curl --request PUT \
--url https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound \
--header 'Content-Type: application/json' \
--data '
{
"prompt": "<string>",
"first_message": "<string>",
"voice_id": "<string>",
"language": "<string>",
"allowed_tools": [
"<string>"
],
"transfer_number": "<string>",
"business_hours": {},
"after_hours": "<string>",
"after_hours_message": "<string>",
"max_minutes": 123,
"webhook_url": "<string>",
"enabled": true
}
'import requests
url = "https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound"
payload = {
"prompt": "<string>",
"first_message": "<string>",
"voice_id": "<string>",
"language": "<string>",
"allowed_tools": ["<string>"],
"transfer_number": "<string>",
"business_hours": {},
"after_hours": "<string>",
"after_hours_message": "<string>",
"max_minutes": 123,
"webhook_url": "<string>",
"enabled": True
}
headers = {"Content-Type": "application/json"}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
prompt: '<string>',
first_message: '<string>',
voice_id: '<string>',
language: '<string>',
allowed_tools: ['<string>'],
transfer_number: '<string>',
business_hours: {},
after_hours: '<string>',
after_hours_message: '<string>',
max_minutes: 123,
webhook_url: '<string>',
enabled: true
})
};
fetch('https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'prompt' => '<string>',
'first_message' => '<string>',
'voice_id' => '<string>',
'language' => '<string>',
'allowed_tools' => [
'<string>'
],
'transfer_number' => '<string>',
'business_hours' => [
],
'after_hours' => '<string>',
'after_hours_message' => '<string>',
'max_minutes' => 123,
'webhook_url' => '<string>',
'enabled' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound"
payload := strings.NewReader("{\n \"prompt\": \"<string>\",\n \"first_message\": \"<string>\",\n \"voice_id\": \"<string>\",\n \"language\": \"<string>\",\n \"allowed_tools\": [\n \"<string>\"\n ],\n \"transfer_number\": \"<string>\",\n \"business_hours\": {},\n \"after_hours\": \"<string>\",\n \"after_hours_message\": \"<string>\",\n \"max_minutes\": 123,\n \"webhook_url\": \"<string>\",\n \"enabled\": true\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound")
.header("Content-Type", "application/json")
.body("{\n \"prompt\": \"<string>\",\n \"first_message\": \"<string>\",\n \"voice_id\": \"<string>\",\n \"language\": \"<string>\",\n \"allowed_tools\": [\n \"<string>\"\n ],\n \"transfer_number\": \"<string>\",\n \"business_hours\": {},\n \"after_hours\": \"<string>\",\n \"after_hours_message\": \"<string>\",\n \"max_minutes\": 123,\n \"webhook_url\": \"<string>\",\n \"enabled\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://win.oneshotagent.com/v1/tools/voice/numbers/{id}/inbound")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"prompt\": \"<string>\",\n \"first_message\": \"<string>\",\n \"voice_id\": \"<string>\",\n \"language\": \"<string>\",\n \"allowed_tools\": [\n \"<string>\"\n ],\n \"transfer_number\": \"<string>\",\n \"business_hours\": {},\n \"after_hours\": \"<string>\",\n \"after_hours_message\": \"<string>\",\n \"max_minutes\": 123,\n \"webhook_url\": \"<string>\",\n \"enabled\": true\n}"
response = http.request(request)
puts response.read_bodyOverview
An agent’s phone number can answer its own calls. During business hours an assistant follows your prompt. After hours it takes a message, transfers the caller, or declines. Every call is transcribed, summarized, billed and receipted, and the agent is notified. Setup takes three calls.- Get a number:
POST /v1/tools/voice/numbers. It returns the agent’s existing number for free, or buys one. An agent that has made a voice call already has one. - Configure it:
PUT /v1/tools/voice/numbers/{id}/inbound. - Read the calls:
GET /v1/tools/voice/inbound, or receive a signed webhook after each call.
Authentication
Free endpoints, authenticated with the signed agent proof (X-Agent-ID + x-agent-proof), which the SDKs send for you. Changing the config needs a wallet session or an OAuth session (you, signed in through an app such as ChatGPT). Plain agent access tokens can read the config and calls but cannot change them, so a delegated credential cannot reroute your phone line.
Getting a number
POST /v1/tools/voice/numbers (no body) gets the agent a voice number without placing a call.
- If the agent already has an active number, it is returned with
is_new: falseand nothing is charged (200). - Otherwise a number is bought and the one-time phone registration fee is charged from prepaid credits (
201,is_new: true, with a signed receipt:category: communication,subcategory: voice_number). - The fee is taken before anything is bought. Too little credit returns
402and buys nothing. A failed purchase is refunded. - A second request while a number is being set up returns
409. Retry it. - Like changing the config, this needs a wallet or OAuth session.
Billing
Inbound calls are paid from the agent’s prepaid credits (POST /v1/credits/top-up), per started minute at the voice call rate, with the voice call minimum. There is no quote step, because someone else placed the call.
- A call is declined when credits cannot cover the minimum call fee.
- A call never runs longer than the credits pay for, or than
max_minutes. - A declined call is free.
- Each billed call has a signed receipt (
category: communication,subcategory: voice_inbound).
See Pricing for the voice call rate.
Request Body
string
required
How the assistant answers: who it speaks for, what it can do, what it must not do. Max 8,000 characters.
string
Greeting spoken when the call connects. Max 500 characters.
string
Voice id. Omit for the default voice.
string
en (default), es, fr, de, pt, it, nl, or multi.string[]
Call tools the assistant may use:
transfer_call, end_call. Default ["end_call"].string
E.164 number to transfer to. Required with
transfer_call or after_hours: "transfer". Emergency numbers are refused.object
{ "timezone": "America/New_York", "windows": [{ "days": ["mon","tue","wed","thu","fri"], "start": "09:00", "end": "17:00" }] }. end must be after start. Split an overnight shift into two windows. Omit (or null) for always open.string
take_message (default): a short call that takes the caller’s name, number and reason. transfer: forward to transfer_number. decline: speak after_hours_message and hang up.string
What callers hear after hours. Max 500 characters.
number
Longest call, 1–60. Default 10.
string
HTTPS URL on a public host. After each call it receives a signed
voice.inbound.completed event. The signing secret is returned once, when the URL is first set or changed.boolean
Default
true. false saves the settings without answering calls.PUT replaces the whole config. Omitted optional fields revert to defaults, and omitting webhook_url removes the webhook.
Example
const number = await agent.provisionVoiceNumber(); // existing number, or buys one
const inbound = await agent.setInboundVoice(number.id, {
prompt: 'You answer for Acme Dental. Book cleanings and check-ups; take a message for anything else.',
first_message: 'Hi, Acme Dental, how can I help?',
business_hours: {
timezone: 'America/New_York',
windows: [{ days: ['mon', 'tue', 'wed', 'thu', 'fri'], start: '09:00', end: '17:00' }],
},
after_hours: 'take_message',
webhook_url: 'https://example.com/hooks/calls',
});
// Store inbound.webhook_secret to verify webhook signatures.
const { calls } = await agent.inboundCalls({ limit: 10 });
number = client.provision_voice_number() # existing number, or buys one
client.set_inbound_voice(number["id"], {
"prompt": "You answer for Acme Dental. Book cleanings and check-ups; take a message for anything else.",
"business_hours": {
"timezone": "America/New_York",
"windows": [{"days": ["mon", "tue", "wed", "thu", "fri"], "start": "09:00", "end": "17:00"}],
},
})
calls = client.inbound_calls(limit=10)["calls"]
Webhook
After each call, thewebhook_url receives:
{
"type": "voice.inbound.completed",
"call": {
"id": "3f1c…",
"from": "+12125550199",
"to": "+14155550123",
"status": "ended",
"ended_reason": "customer-ended-call",
"duration_seconds": 75,
"summary": "Caller booked a cleaning for Tuesday at 10am.",
"transcript": "…",
"charge_usdc": "…",
"receipt_id": "rcpt_…"
}
}
X-OneShot-Signature header is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" under your webhook secret. Reject events whose t is more than a few minutes old. Redirects are not followed, and delivery is best-effort. The call always appears in GET /v1/tools/voice/inbound and in the agent’s notifications.
Other endpoints
| Endpoint | Purpose |
|---|---|
POST /v1/tools/voice/numbers | Get the agent a number (existing one free, or buy one) |
GET /v1/tools/voice/numbers | The agent’s numbers, with inbound_enabled |
GET /v1/tools/voice/numbers/{id}/inbound | Current config |
DELETE /v1/tools/voice/numbers/{id}/inbound | Stop answering (settings kept, disabled) |
GET /v1/tools/voice/inbound | Calls, newest first: limit (≤100), before, phone_number_id, include_transcript |
GET /v1/tools/voice/inbound/{id} | One call with transcript and recording URL |
Numbers idle for 30 days are released. Configuring a number and receiving calls both count as use.