Programmatic calling, AI voice agents and campaigns.
Base URL https://aistudio.housystan.com/v1. The version is part of the path and will not change in a breaking way: a breaking change becomes /v2.
Make a test key, place a test call, read what happened. A test key never dials anyone and never spends credits, so nothing here can go wrong.
In the Studio: Developers, Create API key, kind Test key. Copy it (it is shown once) and keep it in an environment variable:
export AISTUDIO_API_KEY=vs_test_...
# JavaScript / TypeScript, Node 18+ npm install https://aistudio.housystan.com/docs/sdk/aistudio-js.tgz # Python 3.8+ pip install https://aistudio.housystan.com/docs/sdk/aistudio-python.tar.gz
Pick one of your agents and call a number. With a test key the number's last digit decides what the "customer" does: ending in 0 they are interested and book a visit (see Test mode).
curl https://aistudio.housystan.com/v1/agents -H "x-api-key: $AISTUDIO_API_KEY"
# take an agent "id" from the answer, then:
curl -X POST https://aistudio.housystan.com/v1/calls/outbound -H "x-api-key: $AISTUDIO_API_KEY" \
-H "content-type: application/json" \
-d '{ "agent_id": "AGENT_ID", "lead_phone": "+919876543210", "lead_name": "Ravi" }'
# => 201 { "log_id": "...", "status": "initiated", "test": true }import { AIStudio } from '@housystan/aistudio';
const studio = new AIStudio(); // reads AISTUDIO_API_KEY
const { agents } = await studio.agents.listAgents();
const call = await studio.calls.place({ agentId: agents[0].id, phone: '+919876543210', name: 'Ravi' });
console.log('calling', call.log_id);from aistudio import AIStudio
studio = AIStudio() # reads AISTUDIO_API_KEY
agents = studio.agents.list_agents()["agents"]
call = studio.calls.place(agent_id=agents[0]["id"], phone="+919876543210", name="Ravi")
print("calling", call["log_id"])About 15 seconds later the call has ended: status, transcript and summary. If you set a webhook, it also arrives there, signed, marked "test": true.
curl https://aistudio.housystan.com/v1/calls/LOG_ID -H "x-api-key: $AISTUDIO_API_KEY"
const done = await studio.calls.waitForEnd(call.log_id); console.log(done.status, done.summary, done.transcript);
done = studio.calls.wait_for_end(call["log_id"]) print(done["status"], done["summary"])
Make a live key (vs_live_...) and use it instead. The code stays the same; now the call is real and charged to your prepaid wallet. Live keys need a plan with API access; test keys work on every plan.
Copy, paste, replace AGENT_ID. Every example works with a test key first.
Scope calls:write. variables / dynamic_vars fill the {{placeholders}} in the agent's prompt. Refusals tell you why: 402 no_credits, 403 dnd, 425 outside_calling_hours, 429 concurrency_cap. The libraries add an Idempotency-Key, so a retry never calls twice.
curl -X POST https://aistudio.housystan.com/v1/calls/outbound -H "x-api-key: $AISTUDIO_API_KEY" \
-H "content-type: application/json" -H "Idempotency-Key: order-1042-call" \
-d '{ "agent_id": "AGENT_ID", "lead_phone": "+919876543210", "lead_name": "Ravi",
"lead_id": "CRM-42", "dynamic_vars": { "product": "Gold plan" } }'import { AIStudio } from '@housystan/aistudio';
const studio = new AIStudio(); // reads AISTUDIO_API_KEY
const call = await studio.calls.place({ agentId: AGENT_ID, phone: '+919876543210', name: 'Ravi',
reference: 'CRM-42', variables: { product: 'Gold plan' } });from aistudio import AIStudio
studio = AIStudio() # reads AISTUDIO_API_KEY
call = studio.calls.place(agent_id=AGENT_ID, phone="+919876543210", name="Ravi",
reference="CRM-42", variables={"product": "Gold plan"})Scope calls:write. Send call_at (with its time zone, up to 30 days ahead): nothing is dialled now, and the call is placed when due inside your calling hours. List with GET /v1/calls/bookings, cancel while it is still waiting.
curl -X POST https://aistudio.housystan.com/v1/calls/outbound -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" \
-d '{ "agent_id": "AGENT_ID", "lead_phone": "+919876543211", "call_at": "2026-10-21T17:00:00+05:30", "lead_id": "CRM-42" }'
# => 201 { "booking": { "id": "...", "status": "booked", "scheduled_for": "..." } }
curl "https://aistudio.housystan.com/v1/calls/bookings?status=booked" -H "x-api-key: $AISTUDIO_API_KEY"
curl -X DELETE https://aistudio.housystan.com/v1/calls/bookings/BOOKING_ID -H "x-api-key: $AISTUDIO_API_KEY"import { AIStudio } from '@housystan/aistudio';
const studio = new AIStudio(); // reads AISTUDIO_API_KEY
const { booking } = await studio.calls.book({ agentId: AGENT_ID, phone: '+919876543211',
at: '2026-10-21T17:00:00+05:30', reference: 'CRM-42' });
await studio.calls.cancelCallBooking({ id: booking.id });from aistudio import AIStudio
studio = AIStudio() # reads AISTUDIO_API_KEY
booking = studio.calls.book(agent_id=AGENT_ID, phone="+919876543211",
at="2026-10-21T17:00:00+05:30", reference="CRM-42")["booking"]
studio.calls.cancel_call_booking(booking["id"])Scope campaigns:write. Leads go in with the campaign (more with POST /campaigns/{id}/leads); any extra field on a lead becomes a {{variable}} for that call. The libraries' launch uploads in chunks of 1,000 and starts it. How many calls run at once is your plan's lines.
curl -X POST https://aistudio.housystan.com/v1/campaigns -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" -d '{
"name": "October offer", "agent_id": "AGENT_ID", "start": true,
"leads": [ { "phone": "+919876543210", "name": "Ravi", "dynamic_vars": { "city": "Pune" } },
{ "phone": "+919876543211", "name": "Asha" } ] }'
# progress: dialled, answered, interested, still to call
curl https://aistudio.housystan.com/v1/campaigns/CAMPAIGN_ID/stats -H "x-api-key: $AISTUDIO_API_KEY"
# pause / resume
curl -X PATCH https://aistudio.housystan.com/v1/campaigns/CAMPAIGN_ID -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" -d '{ "status": "paused" }'import { AIStudio } from '@housystan/aistudio';
const studio = new AIStudio(); // reads AISTUDIO_API_KEY
const { campaign } = await studio.campaigns.launch({ name: 'October offer', agentId: AGENT_ID,
leads: [{ phone: '+919876543210', name: 'Ravi', dynamic_vars: { city: 'Pune' } }] });
const stats = await studio.campaigns.getCampaignStats({ id: campaign.id });
await studio.campaigns.pause(campaign.id);from aistudio import AIStudio
studio = AIStudio() # reads AISTUDIO_API_KEY
r = studio.campaigns.launch(name="October offer", agent_id=AGENT_ID,
leads=[{"phone": "+919876543210", "name": "Ravi", "dynamic_vars": {"city": "Pune"}}])
stats = studio.campaigns.get_campaign_stats(r["campaign"]["id"])
studio.campaigns.pause(r["campaign"]["id"])Scope calls:read. One call with its transcript; a list (page by page); a search over conversations; or the results feed (what calls produced, oldest first, never skipping one) for a system that cannot take webhooks.
curl https://aistudio.housystan.com/v1/calls/LOG_ID -H "x-api-key: $AISTUDIO_API_KEY" curl "https://aistudio.housystan.com/v1/calls?status=completed&limit=50&offset=0" -H "x-api-key: $AISTUDIO_API_KEY" curl "https://aistudio.housystan.com/v1/calls?view=log&q=refund&range=30" -H "x-api-key: $AISTUDIO_API_KEY" # search curl "https://aistudio.housystan.com/v1/results?since=2026-10-01T00:00:00Z" -H "x-api-key: $AISTUDIO_API_KEY" # then ?cursor=next_cursor
import { AIStudio } from '@housystan/aistudio';
const studio = new AIStudio(); // reads AISTUDIO_API_KEY
const { call } = await studio.calls.getCall({ id: LOG_ID });
for await (const c of studio.paginate('listCalls', { status: 'completed' })) console.log(c.id, c.status);
const found = await studio.calls.listCalls({ view: 'log', q: 'refund', range: '30' });
for await (const r of studio.paginate('listResults', { since: '2026-10-01T00:00:00Z' })) console.log(r.outcome, r.collected);from aistudio import AIStudio
studio = AIStudio() # reads AISTUDIO_API_KEY
call = studio.calls.get_call(LOG_ID)["call"]
for c in studio.paginate("listCalls", status="completed"):
print(c["id"], c["status"])
found = studio.calls.list_calls(view="log", q="refund", range="30")
for r in studio.paginate("listResults", since="2026-10-01T00:00:00Z"):
print(r["outcome"], r["collected"])Scope settings:write to set the URL. Events: call.ended, lead.interested, visit.booked, callback.booked. Every delivery is signed (X-Webhook-Signature = sha256 HMAC of timestamp.body with your secret); verify it on the RAW body, answer 2xx within 8 seconds, and de-duplicate on X-Webhook-Id (a retry carries the same id).
curl -X PUT https://aistudio.housystan.com/v1/settings/webhooks -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" -d '{
"post_call": { "url": "https://your-app.example.com/aistudio/webhook", "secret": "a-long-random-string", "enabled": true } }'
# a signed sample of any event:
curl -X POST https://aistudio.housystan.com/v1/settings/webhooks/test -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" -d '{ "event": "lead.interested" }'import express from 'express';
import { verifyWebhook } from '@housystan/aistudio';
const app = express();
app.post('/aistudio/webhook', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try { event = verifyWebhook(req.body, req.headers, process.env.AISTUDIO_WEBHOOK_SECRET); }
catch { return res.sendStatus(400); }
if (event.event === 'lead.interested') console.log('interested:', event.lead_name, event.lead_phone);
if (event.event === 'visit.booked') console.log('visit at', event.visit.at);
res.sendStatus(200);
});
app.listen(3000);from fastapi import FastAPI, HTTPException, Request
from aistudio import verify_webhook, WebhookVerificationError
app = FastAPI()
@app.post("/aistudio/webhook")
async def aistudio_webhook(request: Request):
try:
event = verify_webhook(await request.body(), request.headers, SECRET)
except WebhookVerificationError:
raise HTTPException(400)
if event["event"] == "lead.interested":
print("interested:", event["lead_name"], event["lead_phone"])
return {"ok": True}Scope agents:write. Up to 2,500 characters into an MP3 or WAV in one of your voices (GET /v1/voices). Charged at your AI rate per 15 seconds of audio; free and silent with a test key.
curl -X POST https://aistudio.housystan.com/v1/voices/tts -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" \
-d '{ "text": "Welcome! Today everything is ten percent off.", "format": "mp3" }' -o voice-over.mp3import { AIStudio } from '@housystan/aistudio';
const studio = new AIStudio(); // reads AISTUDIO_API_KEY
import { writeFileSync } from 'node:fs';
const audio = await studio.voices.voiceOver({ text: 'Welcome! Today everything is ten percent off.' });
writeFileSync('voice-over.mp3', audio.data);from aistudio import AIStudio
studio = AIStudio() # reads AISTUDIO_API_KEY
studio.voices.voice_over("Welcome! Today everything is ten percent off.").save("voice-over.mp3")Scope billing:read. Prepaid: when the wallet is empty, calls, campaigns and voice-overs stop until a recharge. 1 rupee = 10 credits.
curl https://aistudio.housystan.com/v1/credits -H "x-api-key: $AISTUDIO_API_KEY"
# => { "credits": 97600, "balance_inr": 9760, "plan": { "name": "...", "ai_inr_per_minute": ..., ... } }
curl "https://aistudio.housystan.com/v1/credits/ledger?limit=50" -H "x-api-key: $AISTUDIO_API_KEY" # every charge, newest firstimport { AIStudio } from '@housystan/aistudio';
const studio = new AIStudio(); // reads AISTUDIO_API_KEY
const { credits } = await studio.credits.get();
for await (const line of studio.paginate('getCreditsLedger', { limit: 100 })) console.log(line.at, line.credits, line.note);from aistudio import AIStudio
studio = AIStudio() # reads AISTUDIO_API_KEY
print(studio.credits.get()["credits"])
for line in studio.paginate("getCreditsLedger", limit=100):
print(line["at"], line["credits"], line["note"])A key that starts vs_test_ is a test key. It uses the same URLs and sees your real workspace (agents, voices, campaigns and calls to read), but it can never dial anyone and never spend a credit. Make one on the Developers page (kind: Test key); test keys work on every plan, so you can build before you buy.
| Request | In test mode |
|---|---|
POST /calls/outbound | A simulated call: 201 with a log_id. It rings, talks and ends about 15 seconds later (8 if nobody answers); GET /calls/{log_id} shows the transcript and summary; your webhook gets call.ended (and lead.interested / visit.booked when the "customer" agreed). |
POST /calls/outbound + call_at | A simulated booking, moved into your calling hours like a real one; read or cancel it at /calls/bookings/{id}. It is never dialled. |
POST /campaigns | A simulated campaign that "calls" its leads in about a minute: GET /campaigns/{id}, /stats, /leads, PATCH to pause or resume, POST /leads to add. Webhooks for the first 20 leads. |
POST /voices/tts | A silent MP3/WAV of the right length (no voice, no charge). |
| Any read | The real answer from your workspace. Your test calls are listed too, each marked "test": true: first in GET /calls, in time order in GET /results, and on the Studio’s Conversations page as a Test call. They are never in your wallet, usage or bills, and a live key never sees them. |
| Any other change | Refused with 403 test_mode_read_only: a test key cannot edit agents, settings, keys or anything else. |
| Scopes | A test key holds scopes like a live key: placing a test call needs calls:write, a test campaign campaigns:write, otherwise 403 insufficient_scope. |
| Ends in | What the simulated customer does |
|---|---|
0 | Answers, is interested and agrees to a visit/meeting tomorrow (call.ended, lead.interested, visit.booked) |
1 | Answers and is interested, no visit yet (call.ended, lead.interested) |
2 | Answers and says no thanks (call.ended) |
3 | Rings out, nobody answers (the call's status no-answer; call.ended status no_answer) |
4 | The line is busy (the call's status no-answer; call.ended status busy) |
5 | Goes to voicemail (the call's status completed; call.ended status voicemail) |
6 | The call fails, e.g. a wrong number (the call's status failed; call.ended status failed) |
7-9 | Same as 2 |
A call's status (GET /calls) is one of ringing, in-progress, completed, failed, no-answer (hyphen); the status in a webhook is one of completed, no_answer, busy, voicemail, failed (underscore). Test and live use the same words.
Every answer carries the header X-Test-Mode: true and "test": true; every webhook body carries "test": true. Test webhooks are signed like real ones, sent once and never retried. Idempotency-Key works as in live mode. Test objects are kept for 30 days.
Every lead you send is on the Leads page at once, with its source, and the call result, next call and appointment follow as for any lead. Two ways in: a lead URL (Settings › Lead sources: a secret address per source for a website form, Google Ads, Zapier / Make or a portal; no key needed) and POST /v1/leads with an API key.
call: "now" = the call is due 5 seconds after the lead arrives (in calling hours it rings within seconds; outside them at the next opening, never at night), "schedule" + call_at = at that time, "no" = only saved. Without call the workspace's API rule decides. Every call goes through your normal calling: the do-not-call list, the wallet (an empty wallet = the lead is kept and shown under Needs you, and called the moment money is added), your lines (all busy = it waits its turn, first come first called).
Identity: the same external_id (or Idempotency-Key header) twice is the SAME lead (200, "replayed": true), nothing booked twice. Never the phone: the same number again within 10 minutes is a new lead flagged as a duplicate and not called a second time. Every other field is kept as one of the lead's columns and the agent can use it as {{city}}. Webhooks: call.ended for each call, lead.created when a call finds interest (with your lead_id = the external id).
curl -X POST https://aistudio.housystan.com/v1/leads -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" \
-d '{ "name": "Ravi", "phone": "+919876543210", "email": "ravi@example.com", "external_id": "crm-5821",
"fields": { "car_model": "Creta", "city": "Pune" }, "call": "now", "agent_id": "<agent id>" }'
# 201 { "lead": { "id": "...", "ref": 1042, "call": { "state": "calling_soon", "label": "Calling now" } },
# "call": { "decision": "calling", "scheduled_for": "...", "due_in_ms": 5012, "waiting_for_line": false } }
curl https://aistudio.housystan.com/v1/leads/<id> -H "x-api-key: $AISTUDIO_API_KEY" # status, the call now, calls made, appointment
# a lead URL (no key; the token is the key) - e.g. from a website form or Zapier
curl -X POST https://aistudio.housystan.com/in/ls_xxxxxxxx -H "content-type: application/json" -d '{ "name": "Asha", "phone": "9876543210", "city": "Pune" }'
# add ?test=1 to try it: read and answered, nothing saved or calledimport { AIStudio } from '@housystan/aistudio';
const studio = new AIStudio(); // reads AISTUDIO_API_KEY
const { lead, call } = await studio.leads.createLead({
name: 'Ravi', phone: '+919876543210', external_id: 'crm-5821',
fields: { car_model: 'Creta' }, call: 'now', agent_id: AGENT_ID,
});
console.log(lead.ref, call.label);from aistudio import AIStudio
studio = AIStudio() # reads AISTUDIO_API_KEY
r = studio.leads.create_lead(name="Ravi", phone="+919876543210", external_id="crm-5821",
fields={"car_model": "Creta"}, call="now", agent_id=AGENT_ID)
print(r["lead"]["ref"], r["call"]["label"])Google Ads lead forms: Lead delivery › Webhook: the lead URL as the URL, the token (after /in/) as the key; "Send test data" is answered and never saved. Facebook / Instagram Lead Ads: today through Zapier or Make (Facebook Lead Ads › New Lead, then POST to the lead URL or our app's Create Lead action). IndiaMART, JustDial, 99acres, MagicBricks, Housing: ask them to push new enquiries to the lead URL; their usual field names (SENDER_MOBILE, mobile, leadid ...) are read as they come. A test key (vs_test_) answers POST /v1/leads with what would happen; nothing is saved or called.
Write {{any_name}} in an agent's prompt, first message, knowledge base (always-known facts and documents), a tool's URL / headers / fixed fields, or a WhatsApp template blank. It is filled on every call, like ElevenLabs' dynamic variables. Each name takes the first value it finds, in this order:
dynamic_variables (or dynamic_vars) on POST /v1/calls/outbound, the Test call form, MCP variables.treatment fills {{treatment}}).PUT /v1/settings/variables.business_name, agent_name, lead_name, lead_phone_masked, today, current_date, current_time, current_datetime, tomorrow_date, appointment_time, appointment_date, appointment_type, last_call_summary.The phone engine adds greeting_prefix (the first name, when the name is a person's) and system__caller_id, system__time_ist and the other system__ names.
A name nothing fills is never spoken: it is removed and the sentence closes up ("Hello {{lead_name}}, ..." becomes "Hello, ..."), the call records it, and the agent editor shows it in red. Give a name that may be missing a default.
# a default for every call of the workspace
curl -X PUT https://aistudio.housystan.com/v1/settings/variables -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" \
-d '{ "variables": [ { "name": "clinic_city", "default_value": "Pune" }, { "name": "offer", "default_value": "a free check-up" } ] }'
# this call's own values win over the lead's columns and the defaults
curl -X POST https://aistudio.housystan.com/v1/calls/outbound -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" \
-d '{ "agent_id": "AGENT_ID", "lead_phone": "+919876543210", "lead_name": "Priya",
"dynamic_variables": { "treatment": "root canal" } }'
# with a test key the answer shows "variables" (each value and where it came from) and the "first_message"import { AIStudio } from '@housystan/aistudio';
const studio = new AIStudio(); // reads AISTUDIO_API_KEY
await studio.settings.replaceWorkspaceVariables({ variables: [{ name: 'clinic_city', default_value: 'Pune' }] });
const call = await studio.calls.place({ agentId: AGENT_ID, phone: '+919876543210', name: 'Priya',
variables: { treatment: 'root canal' } });from aistudio import AIStudio
studio = AIStudio() # reads AISTUDIO_API_KEY
studio.settings.replace_workspace_variables(variables=[{"name": "clinic_city", "default_value": "Pune"}])
call = studio.calls.place(agent_id=AGENT_ID, phone="+919876543210", name="Priya",
variables={"treatment": "root canal"})A first message Hi {{lead_name}}, about your {{treatment}} at our {{clinic_city}} clinic then says "Hi Priya, about your root canal at our Pune clinic". Check an agent without calling: POST /v1/settings/variables/preview, or GET /v1/agents/{id}/variables (every name it uses, who fills it, and those missing on real calls).
In the Studio: Send to my system. Pick one: the Studio is your CRM (nothing is sent), your own system (a webhook), Zapier or Make, LeadSquared, Zoho CRM, or my CRM does it (your CRM books the call-backs and sends the messages; we only send it the events). The same on every plan.
call.ended | every call that ends: who was called, how it went, transcript and summary. Its recording_url is private: fetch it with your API key (header x-api-key, scope recordings:read); the answer is a redirect to a link that works for 10 minutes (add ?format=json to get { url, expires_at } instead) |
lead.created | the first time a call finds someone interested; lead.fields carries the lead's own columns |
lead.updated | a later call on the same person: status and next step |
follow-up.scheduled | we booked the next call, with its time |
appointment.* | booked, confirmed, rescheduled, reschedule_requested, cancel_requested, cancelled, completed, no_show |
message.sent | a message went to the customer after a call (details, confirmation, reminder) |
Every post is signed: X-Webhook-Signature: sha256=HMAC_SHA256(secret, X-Webhook-Timestamp + "." + raw_body); X-Webhook-Id is the event_id (dedupe on it). Answer 2xx; anything else is retried with backoff for about 17 minutes, and every delivery is listed on the page and at GET /v1/settings/webhooks/deliveries. Receivers that asked for the older names lead.interested, visit.booked or callback.booked keep getting them.
curl -X PUT https://aistudio.housystan.com/v1/settings/webhooks -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" \
-d '{ "post_call": { "url": "https://crm.example.com/ai-studio", "secret": "a-long-random-secret-1234", "enabled": true,
"events": ["call.ended", "lead.created", "lead.updated", "appointment.booked"] } }'
curl -X POST https://aistudio.housystan.com/v1/settings/webhooks/test -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" -d '{ "event": "lead.created" }'
# Zapier / Make: subscribe a hook URL to one event, then send it a sample
curl -X POST https://aistudio.housystan.com/v1/hooks -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" \
-d '{ "target_url": "https://hooks.zapier.com/hooks/catch/1/abc/", "event": "lead.created", "source": "zapier" }'
curl -X POST https://aistudio.housystan.com/v1/hooks/HOOK_ID/test -H "x-api-key: $AISTUDIO_API_KEY"
# "my CRM does it": your CRM books the call-backs and sends the messages
curl -X PUT https://aistudio.housystan.com/v1/settings/send-to -H "x-api-key: $AISTUDIO_API_KEY" -H "content-type: application/json" -d '{ "mode": "crm" }'// check a post really came from us (Node)
import { createHmac, timingSafeEqual } from 'node:crypto';
const ts = req.headers['x-webhook-timestamp'], sig = String(req.headers['x-webhook-signature']);
const want = 'sha256=' + createHmac('sha256', SECRET).update(ts + '.' + rawBody).digest('hex');
const ok = sig.length === want.length && timingSafeEqual(Buffer.from(sig), Buffer.from(want))
&& Math.abs(Date.now() / 1000 - Number(ts)) < 300;# check a post really came from us (Python) import hmac, hashlib, time ts, sig = headers["X-Webhook-Timestamp"], headers["X-Webhook-Signature"] want = "sha256=" + hmac.new(SECRET.encode(), (ts + "." + raw_body).encode(), hashlib.sha256).hexdigest() ok = hmac.compare_digest(sig, want) and abs(time.time() - int(ts)) < 300
In the Studio: Integrations, Your CRM. Or with a key holding settings:write. We then POST interested leads and call results to your URL, each one signed with your workspace secret (the same secret as the post-call webhook). Every request names its action; answer 2xx with JSON, and {} to any action you do not use.
curl -X PUT https://aistudio.housystan.com/v1/settings/crm -H "x-api-key: $KEY" -H "content-type: application/json" -d '{ "url": "https://crm.yourcompany.com/ai-studio" }'
# => { "crm": { "type": "webhook", "url": "...", "secret_set": true }, "secret": "whsec_..." } (the secret only when one was just made: copy it)
curl -X POST https://aistudio.housystan.com/v1/settings/crm/test -H "x-api-key: $KEY" # one signed sample, "test": true; shows your answer
curl -X POST https://aistudio.housystan.com/v1/settings/crm/secret -H "x-api-key: $KEY" # a new secret, shown once
curl -X PUT https://aistudio.housystan.com/v1/settings/crm -H "x-api-key: $KEY" -H "content-type: application/json" -d '{ "enabled": false }'
# What arrives (POST, JSON). Headers: x-voice-studio-action, x-voice-studio-timestamp, x-voice-studio-signature ("sha256=<hex>")
# { "id": "...", "ts": 1789000000, "org_id": "...", "action": "promote_ai_lead", "payload": { "leadName": "Rajesh", "leadPhone": "...", ... } }
# expected = "sha256=" + HMAC_SHA256(secret, ts + "." + raw_body).hex() - reject if older than 300 s.
# Answer { "crm_lead_id": "<your id>" } to promote_ai_lead so the call is linked to your lead.Official clients for JavaScript/TypeScript (Node 18+) and Python (3.8+), no dependencies. Every operation in the reference below is a typed method (generated from the OpenAPI file), plus: your key from AISTUDIO_API_KEY, retries with backoff (rate limits wait for Retry-After; a call is never resent unless it is safe), an Idempotency-Key on every create, pagination, typed errors with status and code, and webhook verification.
# no library: every example on this page has a curl tab curl https://aistudio.housystan.com/v1/agents -H "x-api-key: $AISTUDIO_API_KEY"
npm install https://aistudio.housystan.com/docs/sdk/aistudio-js.tgz
# then: import { AIStudio } from '@housystan/aistudio';pip install https://aistudio.housystan.com/docs/sdk/aistudio-python.tar.gz # then: from aistudio import AIStudio
Everything the libraries do, with examples: Libraries guide. Postman: aistudio.postman_collection.json (set the apiKey variable). The machine-readable spec: /docs/openapi.yaml.
Claude, ChatGPT, Cursor and any assistant that speaks MCP can use your workspace with an API key: list agents, read calls and campaigns, search conversations, check the wallet, and, when you allow it, place or book calls and make voice-overs. Everything runs as your key: its scopes, your wallet, calling hours and do-not-call list. A test key simulates every call.
Read-only by default. Tools that change anything appear only on https://aistudio.housystan.com/mcp?writes=1 (or aistudio-mcp --allow-writes). Price first: a tool that costs money answers with the price and does nothing; the assistant has to ask you and call it again with confirm: true.
URL https://aistudio.housystan.com/mcp (Streamable HTTP) Header Authorization: Bearer vs_live_... (or vs_test_...)
# Claude Code
claude mcp add --transport http aistudio https://aistudio.housystan.com/mcp --header "Authorization: Bearer $AISTUDIO_API_KEY"
# Cursor: ~/.cursor/mcp.json
{ "mcpServers": { "aistudio": { "url": "https://aistudio.housystan.com/mcp", "headers": { "Authorization": "Bearer vs_live_..." } } } }{
"mcpServers": {
"aistudio": {
"command": "npx",
"args": ["-y", "https://aistudio.housystan.com/docs/sdk/aistudio-mcp.tgz"],
"env": { "AISTUDIO_API_KEY": "vs_live_..." }
}
}
}
# add "--allow-writes" to args to allow calls and voice-overs| Tool | Does | Scope | |
|---|---|---|---|
get_wallet_balance | The workspace's prepaid balance in credits (1 rupee = 10 credits), its plan and per-minute rates. | billing:read | read |
list_agents | The AI agents in this workspace (who talks on calls and on the website widget): id, name, type, language, voice and the phone number they call from. | agents:read | read |
list_voices | The voices this workspace can use for agents and voice-overs. | agents:read | read |
list_recent_calls | The latest calls, newest first, with status, length and who was called. | calls:read | read |
get_call | Everything about one call: status, length, summary, the full transcript and what the analysis found. | calls:read | read |
search_conversations | Find conversations by words said or summarised (e.g. | calls:read | read |
list_campaigns | Outbound calling campaigns, newest first, with status and how many leads were called and answered. | campaigns:read | read |
campaign_status | How one campaign is doing: status, leads called, answered, voicemail, no answer, interested, still to call. | campaigns:read | read |
list_interested_leads | People the lead judge found interested (hot or warm) in the last 30 days, newest first, with the summary. | campaigns:read | read |
recent_results | What calls produced (the outcome and the details collected, e.g. | calls:read | read |
list_booked_calls | Calls booked for a later time, newest first. | calls:read | read |
place_call | An AI agent phones someone now. | calls:write | costs money: shows the price first |
book_call | Book an AI agent to phone someone at a time (ISO 8601 with zone, up to 30 days ahead); it is placed inside the workspace's calling hours. | calls:write | costs money: shows the price first |
create_lead | Adds a lead to the workspace's Leads page (POST /v1/leads) and, with call: "now" or "schedule", has an AI agent call it through the normal calling (inside calling hours, never a do-not-call number, only while the wallet can pay; busy lines = it waits its turn). | calls:write | costs money: shows the price first |
cancel_booked_call | Cancel a call booked for later, while it is still waiting. | calls:write | changes things |
list_appointments | Site visits / meetings, newest first: status (scheduled, confirmed, rescheduled, reschedule_requested, cancel_requested, cancelled, completed, no_show), time, lead. | appointments:read | read |
book_appointment | Book a site visit / meeting / demo / consultation at a time (ISO 8601, e.g. | appointments:write | changes things |
confirm_appointment | The customer reconfirmed: the appointment becomes confirmed at the same time. | appointments:write | changes things |
reschedule_appointment | Move an appointment to a new time (ISO 8601 with offset). | appointments:write | changes things |
make_voice_over | Turn text (up to 2,500 characters) into speech in one of the workspace's voices and return the audio file. | agents:write | costs money: shows the price first |
Put an agent on any website with one line: visitors talk to it out loud or type. In the Studio, Web widget: pick the agent, add the websites allowed to show it (the widget opens only there), choose the look; the Developers page shows your line and a live preview.
<script src="https://aistudio.housystan.com/embed.js" data-org="YOUR_WORKSPACE_ID" async></script>
Optional, per page: data-color="#0f766e" data-position="left" data-mode="chat" (voice, chat or both) data-title data-subtitle data-open="1". Your own button: VoiceStudioWidget.start(). Each typed reply and each 15 seconds of voice is charged at your AI rate; with an empty wallet the widget does not open.
Send your key in the x-api-key header on every request.
curl https://aistudio.housystan.com/v1/calls \ -H "x-api-key: vs_live_xxxxxxxxxxxxxxxx"
Studio sessions use Authorization: Bearer <jwt> instead. Sessions are unscoped and are governed by the signed-in user’s role; use a managed key for server-to-server work.
The legacy account-wide organisation key is not accepted on /v1. It is one un-scopable, un-rotatable string per account, so it cannot be the credential a public API is built on. Sending it returns 401 legacy_key_not_allowed_on_v1. Existing integrations on the unversioned /api paths are unaffected.
Every request is scoped to the organisation the credential belongs to. There is no org_id parameter anywhere in this API, and sending one changes nothing: a key can never read another organisation’s agents, calls, leads or recordings. An id that belongs to someone else returns 404 — never 403, which would confirm it exists.
A key carries scopes. * satisfies everything; a bare resource scope covers its verbs, so calls grants both calls:read and calls:write. Reads need the :read scope, writes the :write one. A key created without an explicit scopes array gets ["*"].
| Scope | Lets the key |
|---|---|
agents:read | Read agents, their prompts, voices, versions, analysis config, languages and the voice catalogue. |
agents:write | Create, edit, duplicate, roll back and delete agents; upload clips; change voice tuning. |
calls:read | Read call records, transcripts, call statistics, the do-not-call list and exports. |
calls:write | Place calls. Add to and edit the do-not-call list. Hand this one out deliberately. |
campaigns:read | Read campaigns, their leads, lead lists, statistics and keypad reports. |
campaigns:write | Create, start, pause, edit and delete campaigns; add leads; re-queue. |
recordings:read | Fetch call audio. Separate from calls:read so metadata can be shared without the audio. |
usage:read | Read usage by day, the CSV export, and the cost and health dashboards. |
keys:read | List API keys — prefixes and scopes only, never a secret. Still privileged: it is reconnaissance. |
keys:write | Create, rotate and revoke API keys. |
tools:read | Read the organisation tool library. |
tools:write | Create, edit and delete tools an agent may call mid-conversation. |
numbers:read | Read your phone numbers, their health, their assignments and your number requests. |
numbers:write | Rotate caller ID, flag and unflag numbers, request more numbers. |
billing:read | Read your plan, this month’s billed usage and invoices. There is no billing:write — plans are assigned by the platform. |
settings:read | Read organisation settings (provider keys appear masked). |
settings:write | Change organisation settings and provider keys. Never grant this to a third party. |
settings:secrets | Read provider keys back in plaintext (GET /settings/llm-config). Required on top of settings:read, and held today only by the CRM bridge. Never grant it to anyone else. |
knowledge:read | List knowledge documents and preview what the agent would retrieve. |
knowledge:write | Add and delete knowledge documents. |
trunks:read | Platform administrators only — carrier trunk health. |
trunks:write | Platform administrators only — carrier trunk caps and priority. |
system:read | Platform administrators only — outbound pause state, channels, audit log, reserves. |
system:write | Platform administrators only — the global outbound pause and channel reserves. |
whatsapp:read | The WhatsApp channel (without its set-up secrets) and its conversations. |
whatsapp:write | Change the WhatsApp channel and post-call templates, send a test message, hand a conversation to a person. |
appointments:read | List site visits / meetings and read one with its history. |
appointments:write | Book, confirm, reschedule, flag or cancel a site visit / meeting (the CRM and webhooks are told). |
A few pairings are worth stating outright:
/v1/agents — Versions, rollback, analysis config and languages are all agent reads/writes: a rollback replaces the live agent./v1/voices — The voice catalogue is read with agents:read./v1/subjects — What a campaign is about (your projects, courses...): picked when configuring one./v1/llm — The priced model catalogue an agent picks from./v1/clips — An IVR clip is agent content./v1/appointments — Site visits / meetings / demos (migration 322): appointments:write books, confirms, moves and cancels; the same state machine as a graded call./v1/results — What our calls produced, pulled by the customer on their own schedule. Read with calls:read - it is the outcome of a call, and a key that may read calls may read what they achieved./v1/calls — calls:write is what places a call (POST /v1/calls/outbound). Give it out deliberately./v1/recordings — Separate from calls:read so an integrator can read call metadata without the audio./v1/dnd — The do-not-call list gates dialling, so it rides with the calls scopes./v1/hooks — REST hooks (Zapier, Make): call results as they happen. Subscribing is calls:write; the recent-items list is calls:read./v1/me — Who am I - the auth test of the Zapier / Make apps. Read with calls:read./v1/leads — Leads you send in (and call now / schedule / not): sending one is calls:write, reading them calls:read./v1/dtmf-report — Keypad responses are campaign output./v1/dids — DIDs are the phone numbers. numbers:write can rotate a campaign onto fresh numbers./v1/carriers — The workspace's own calling provider (Twilio / SIP) and its imported numbers; off until the platform switches it on./v1/tools — The org tool library — webhook and transfer-to-number tools an agent may call./v1/billing — Plan, this month’s billed usage and invoices. Read-only by design — plans are assigned by the platform./v1/api-keys — keys:read lists prefixes and scopes only — the secret is never returned by anything. keys:write creates, rotates and revokes./v1/settings — Holds provider keys. Never grant settings:* to a third-party integration./v1/settings/webhooks — Delivery URL for call outcomes. Mounted before /v1/settings, so it needs its own row./v1/settings/crm — The workspace’s own CRM connection (URL, signing secret, test). Mounted before /v1/settings, so it needs its own row./v1/settings/business — What the workspace sells, in its own words (P1: the call judge and the appointment hours read it). Mounted before /v1/settings./v1/settings/send-to — Where call results go (none / webhook / Zapier-Make / LeadSquared / Zoho / "my CRM does it"). Mounted before /v1/settings, so it needs its own row./v1/settings/variables — Workspace {{variables}} and their default values (call request > lead field > workspace default > system). Mounted before /v1/settings./v1/languages — Speech-language catalogue, read with agents:read like /v1/voices./v1/secrets — Provider keys live here, so it rides with settings. Never grant settings:* to a third party./v1/analytics — Aggregates over the account, read with usage:read./v1/trunks — Platform admin only — carrier trunk capacity./v1/system — GET /v1/system/pause (system:read) works for every account; everything else here, system:write included, is platform admin only.Refused requests answer 403 naming both the scope required and the scopes granted, so the fix never needs a support ticket.
Every error is JSON and always carries error, a sentence you can act on. Failures you should branch on also carry a stable code.
| Status | code | Means | What to do |
|---|---|---|---|
400 | did_not_owned | The caller-ID number does not belong to your organisation. | Pick a number from GET /v1/dids, or ask your platform operator to assign you more. |
400 | tool_not_owned | A tool id on the agent is not in your organisation’s tool library. | Take tool ids from GET /v1/tools. An id from another organisation is never valid here. |
400 | — | Malformed body, or validation failed (invalid_tts_settings, invalid_flow_steps, a missing field). | Read details[] — it names each offending field. Do not retry unchanged. |
401 | — | No credential, or an invalid, expired or revoked one. Also an expired session. | Check the x-api-key header. If the key may be compromised, rotate it. |
401 | legacy_key_not_allowed_on_v1 | The old account-wide organisation key was used on /v1. It is not accepted here; `error` carries this code. | Issue a managed key (vs_live_…) and use that. It is scoped, rotatable and revocable. |
402 | no_provider_key | This action needs a speech or language provider key and your organisation has none. | Add the key under Settings. We never fall back to a platform key — that would bill you for something you did not buy. |
403 | insufficient_scope | The key is not scoped for this operation. required and granted are in the body. | Issue a key with the scope named in required, or use one that already has it. |
403 | — | The action needs a signed-in person (not an API key), or your role is too low. | User and organisation management is a human action. Do it in Studio. |
403 | dnd | The destination is on your do-not-call list. No call was placed. | Respect it. Remove the entry via /v1/dnd only if you have the customer’s consent. |
404 | — | No such resource in your organisation. An id belonging to someone else looks identical. | Confirm the id came from your own organisation. We answer 404, never 403 — a 403 would confirm it exists. |
423 | outbound_paused | Your organisation’s outbound calling is paused. | Resume it in Studio. Queue the work; do not retry in a loop. |
423 | system_paused | Platform-wide outbound calling is paused. | Wait. This is the kill switch and it is deliberate. |
425 | outside_calling_hours | Outside your calling window (default 10:00–20:00 Asia/Kolkata). The body carries the window and timezone. | Retry inside the window. Indian destinations are never dialled outside 10:00–21:00 IST, whatever you configure. |
429 | daily_cap | Your organisation’s calls-per-day limit is spent. | Resume tomorrow, or ask for a higher limit. |
429 | concurrency_cap | Your simultaneous-call limit is full. | Retry shortly — a slot frees as soon as a call ends. |
429 | — (rate limit) | Too many API requests this minute. Carries retry_after_seconds and a Retry-After header, no code. | Wait retry_after_seconds and retry. Default limit is 600 requests per minute per organisation. |
502 | — | The switch refused an originate, or storage did not answer for a recording. | Safe to retry a GET. Never blindly retry a call-placing POST — look the call up by log_id first. |
5xx | — | Our fault. | Retry a GET. For a POST that places a call, check /v1/calls before retrying. |
Per organisation, per minute — 600 by default. Over the limit you get an honest 429 with a Retry-After header rather than a dropped request. Studio traffic and API traffic count in separate buckets, so a campaign cannot rate-limit an operator out of their own dashboard.
{
"error": "Rate limit exceeded",
"retry_after_seconds": 12,
"limit_per_minute": 600
}Generated from the OpenAPI specification this API ships with — /docs/openapi.yaml — so the two cannot disagree. Operations marked planned have a fixed contract but no live backend yet.
The AI agent — prompt, greeting, voice, language, caller ID.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/agents | List agents | agents:read |
| POST | /v1/agents | Create an agent | agents:write |
| GET | /v1/agents/{id} | Fetch one agent | agents:read |
| PUT | /v1/agents/{id} | Update an agent | agents:write |
| DELETE | /v1/agents/{id} | Delete an agent | agents:write |
| POST | /v1/agents/{id}/duplicate | Copy an agent | agents:write |
| GET | /v1/agents/{id}/versions | Prompt version history | agents:read |
| GET | /v1/agents/{id}/versions/{versionId} | One stored version, with its full text | agents:read |
| POST | /v1/agents/{id}/rollback | Roll a prompt column back to a stored version | agents:write |
| GET | /v1/agents/{id}/languages | Languages the agent speaks | agents:read |
| PUT | /v1/agents/{id}/languages | Set the agent's languages | agents:write |
| GET | /v1/languages | Supported languages | agents:read |
| GET | /v1/agents/{id}/prompt | What the model receives | agents:read |
| GET | /v1/agents/{id}/variables | Which variables a call in this workspace fills | agents:read |
| POST | /v1/agents/{id}/variables | Check an unsaved draft's variables | agents:write |
| GET | /v1/agents/{id}/branches | Saved named versions | agents:read |
| POST | /v1/agents/{id}/branches | Save the agent as a named version | agents:write |
| POST | /v1/agents/{id}/branches/{branchId}/apply | Make a saved version live | agents:write |
| DELETE | /v1/agents/{id}/branches/{branchId} | Delete a saved version | agents:write |
| GET | /v1/agents/{id}/evals/scenarios | List simulation tests | agents:read |
| POST | /v1/agents/{id}/evals/scenarios | Add a simulation test | agents:write |
| POST | /v1/agents/{id}/evals/scenarios/starter | Add the starter tests | agents:write |
| PUT | /v1/agents/{id}/evals/scenarios/{scenarioId} | Edit a simulation test | agents:write |
| DELETE | /v1/agents/{id}/evals/scenarios/{scenarioId} | Delete a simulation test | agents:write |
| GET | /v1/agents/{id}/evals | Past test runs | agents:read |
| POST | /v1/agents/{id}/evals | Run the simulation tests | agents:write |
| GET | /v1/agents/{id}/evals/{runId} | One test run — progress, then results | agents:read |
| POST | /v1/agents/{id}/evals/{runId}/cancel | Stop a test run | agents:write |
Per-agent success criteria and structured data extracted from each call.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/agents/{id}/analysis | The agent's success criteria and data-collection fields | agents:read |
| PUT | /v1/agents/{id}/analysis | Replace the agent's analysis configuration | agents:write |
Placing calls, and the record of every call placed or received.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/calls | List calls | calls:read |
| GET | /v1/calls/{id} | One call, with transcript | calls:read |
| GET | /v1/calls/stats | Aggregates over the same filters as the list | calls:read |
| POST | /v1/calls/outbound | Place an outbound call | calls:write |
| GET | /v1/calls/bookings | Calls booked for later | calls:read |
| GET | /v1/calls/bookings/{id} | One booked call | calls:read |
| DELETE | /v1/calls/bookings/{id} | Cancel a booked call | calls:write |
| GET | /v1/calls/contact | Every call with one phone number | calls:read |
| GET | /v1/results | Pull call results since your last read | calls:read |
Call audio, behind a short-lived signed link.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/recordings/{id} | Play a call recording | recordings:read |
| GET | /v1/recordings/{id}/download | Download a call recording | recordings:read |
Outbound campaigns and the leads inside them.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/campaigns | List campaigns | campaigns:read |
| POST | /v1/campaigns | Create a campaign | campaigns:write |
| GET | /v1/campaigns/{id} | Fetch one campaign | campaigns:read |
| PATCH | /v1/campaigns/{id} | Update a campaign — including starting and pausing it | campaigns:write |
| DELETE | /v1/campaigns/{id} | Delete a campaign and its leads | campaigns:write |
| GET | /v1/campaigns/{id}/leads | Leads in a campaign | campaigns:read |
| POST | /v1/campaigns/{id}/leads | Add leads to a campaign | campaigns:write |
| GET | /v1/campaigns/{id}/call-log | Calls made by a campaign | campaigns:read |
| GET | /v1/campaigns/{id}/stats | Lifetime counters for a campaign | campaigns:read |
| GET | /v1/campaigns/{id}/daywise | Day-by-day activity | campaigns:read |
| GET | /v1/campaigns/{id}/interest | Leads that showed interest | campaigns:read |
| POST | /v1/campaigns/{id}/dids | Add a number to a campaign | campaigns:write |
| POST | /v1/campaigns/{id}/dids/{number}/rotate | Rotate one of a campaign's numbers | campaigns:write |
| GET | /v1/campaigns/{id}/preflight | Pre-launch sanity check | campaigns:read |
| GET | /v1/campaigns/{id}/pipeline-metrics | Funnel counts | campaigns:read |
| POST | /v1/campaigns/{id}/retry | Call the unanswered leads again | campaigns:write |
| GET | /v1/campaigns/{id}/retry-job | Progress of the latest retry job | campaigns:read |
| POST | /v1/campaigns/{id}/retry-job/stop | Stop a running retry job | campaigns:write |
| GET | /v1/campaigns/{id}/pipeline-stats | Did the system do its part | campaigns:read |
| GET | /v1/campaigns/live-channels | Your lines in use right now | campaigns:read |
| GET | /v1/campaigns/leads/hot-warm | Hot and warm leads (the lead judge's verdict) | campaigns:read |
| GET | /v1/campaigns/dids | Which numbers dialled for which campaigns | campaigns:read |
| GET | /v1/campaigns/live | Live calling snapshot | campaigns:read |
| GET | /v1/campaigns/active-calls | Calls in flight right now | campaigns:read |
| GET | /v1/campaigns/scoped-stats | Campaign totals for today, yesterday or the last 7 days | campaigns:read |
| GET | /v1/campaigns/{id}/summary | Compact progress summary | campaigns:read |
| GET | /v1/campaigns/{id}/retry-stats | How many leads a retry would re-queue | campaigns:read |
| GET | /v1/campaigns/lead-groups | List reusable lead lists | campaigns:read |
| POST | /v1/campaigns/lead-groups | Create a reusable lead list | campaigns:write |
| DELETE | /v1/campaigns/lead-groups/{id} | Delete a lead list | campaigns:write |
| POST | /v1/campaigns/lead-groups/{id}/leads | Append leads to a lead list | campaigns:write |
| POST | /v1/campaigns/lead-groups/{id}/recount | Recount a lead list | campaigns:write |
| POST | /v1/campaigns/preview-leads | Check which phones you already hold | campaigns:write |
The phone numbers you call from, their health, and requests for more.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/dids | List your numbers | numbers:read |
| GET | /v1/dids/assignments | Which number belongs to which agent or campaign | numbers:read |
| GET | /v1/dids/next-free | The next number of yours that no agent is using | numbers:read |
| POST | /v1/dids/rotate | Rotate onto a fresh caller-ID number | numbers:write |
| POST | /v1/dids/{number}/spam | Mark one of your numbers as spam-flagged | numbers:write |
| POST | /v1/dids/{number}/unspam | Clear the spam flag | numbers:write |
| GET | /v1/numbers/requests | Your requests for more numbers | numbers:read |
| POST | /v1/numbers/requests | Request more numbers | numbers:write |
| POST | /v1/numbers/verified-request | Ask for a verified caller ID | numbers:write |
| GET | /v1/numbers | Your phone numbers and who uses them | numbers:read |
Documents the agent answers from.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/knowledge | List knowledge documents | knowledge:read |
| POST | /v1/knowledge | Add a document | knowledge:write |
| GET | /v1/knowledge/search | Preview what the agent would retrieve | knowledge:read |
| GET | /v1/knowledge/folders | Your folders | knowledge:read |
| GET | /v1/knowledge/{docId} | One document, with its text | knowledge:read |
| PUT | /v1/knowledge/{docId} | Edit a document's text | knowledge:write |
| PATCH | /v1/knowledge/{docId} | Move, re-scope or set auto-sync | knowledge:write |
| DELETE | /v1/knowledge/{docId} | Remove a document and its chunks | knowledge:write |
| GET | /v1/knowledge/{docId}/agents | Agents that can use this document | knowledge:read |
| POST | /v1/knowledge/{docId}/resync | Re-crawl a URL document now | knowledge:write |
| GET | /v1/knowledge/{docId}/jobs | Recent re-crawls of a document | knowledge:read |
Webhook and transfer-to-number tools an agent may call mid-conversation. Making or changing one needs the Developers service switched on for your workspace (otherwise 403 `service_off`); listing and deleting always work.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/tools | List your tools | tools:read |
| POST | /v1/tools | Create a tool | tools:write |
| GET | /v1/tools/{id} | Fetch one tool | tools:read |
| PUT | /v1/tools/{id} | Update a tool | tools:write |
| DELETE | /v1/tools/{id} | Delete a tool | tools:write |
What you used, per day. The basis for billing.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/usage | Usage by day | usage:read |
| GET | /v1/usage/export.csv | The same data as CSV | usage:read |
Issue, rotate and revoke keys.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/api-keys | List keys | keys:read |
| POST | /v1/api-keys | Create a key | keys:write |
| POST | /v1/api-keys/{id}/rotate | Issue a replacement and revoke the old key | keys:write |
| PATCH | /v1/api-keys/{id} | Rename a key or change its limits | keys:write |
| DELETE | /v1/api-keys/{id} | Revoke a key | keys:write |
Organisation settings — pipeline defaults, webhooks, secrets your tools send, recording and privacy settings.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/settings | Organisation settings | settings:read |
| PUT | /v1/settings | Update organisation settings | settings:write |
| GET | /v1/settings/webhooks | Post-call and initiation webhooks | settings:read |
| PUT | /v1/settings/webhooks | Update webhook settings | settings:write |
| POST | /v1/settings/webhooks/test | Send a signed test event | settings:write |
| GET | /v1/settings/webhooks/deliveries | Recent webhook deliveries | settings:read |
| GET | /v1/settings/send-to | Where your call results go | settings:read |
| PUT | /v1/settings/send-to | Choose where your call results go | settings:write |
| GET | /v1/settings/send-to/inbox | Your test address and what arrived there | settings:read |
| GET | /v1/settings/variables | Your variables and their defaults | settings:read |
| PUT | /v1/settings/variables | Replace your variables | settings:write |
| PUT | /v1/settings/variables/{name} | Make or change one variable | settings:write |
| DELETE | /v1/settings/variables/{name} | Remove one variable | settings:write |
| POST | /v1/settings/variables/preview | What an agent would say with these values | settings:write |
| GET | /v1/settings/crm | Your CRM connection | settings:read |
| PUT | /v1/settings/crm | Connect your CRM, change its URL, or switch it off | settings:write |
| POST | /v1/settings/crm/test | Send a signed sample to your CRM | settings:write |
| POST | /v1/settings/crm/zoho/start | Start connecting Zoho CRM | settings:write |
| POST | /v1/settings/crm/secret | Make a new signing secret | settings:write |
| GET | /v1/settings/compliance | Recording and privacy settings | settings:read |
| GET | /v1/secrets | Your secrets (names only) | settings:read |
Your plan, this month's billed usage and invoices. Read-only.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/credits | Credits left, plan and rates | billing:read |
| GET | /v1/credits/ledger | Every charge and credit, newest first | billing:read |
| GET | /v1/credits/statement | A month, line by line, with the call behind every charge | billing:read |
| GET | /v1/credits/statement.csv | The monthly statement as CSV | billing:read |
| GET | /v1/billing/plan | Your current plan | billing:read |
| GET | /v1/billing/usage | Billed usage for a month | billing:read |
| GET | /v1/billing/invoices | List invoices | billing:read |
| GET | /v1/billing/invoices/{id} | One invoice with its lines | billing:read |
The opt-out list. A number here is refused before the switch is touched.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/dnd | List opted-out numbers | calls:read |
| POST | /v1/dnd/bulk | Add numbers in bulk | calls:write |
| PATCH | /v1/dnd/{id} | Update an entry | calls:write |
| DELETE | /v1/dnd/{id} | Remove an entry | calls:write |
Send a lead in and have it called at once (call now / schedule / no) - the same as a lead URL from Settings > Lead sources; read the leads and what their calls are doing.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/leads | List the leads you sent in | calls:read |
| POST | /v1/leads | Send a lead in (and have it called) | calls:write |
| GET | /v1/leads/{id} | One lead, its call now, the calls made and its appointment | calls:read |
Site visits, meetings, demos and consultations - booked, confirmed, moved or flagged by a call, an agent tool, your CRM or this API; one id, one status, one history; your CRM and webhooks are told on every change.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/appointments | List appointments | appointments:read |
| POST | /v1/appointments | Book an appointment | appointments:write |
| GET | /v1/appointments/{id} | One appointment with its history | appointments:read |
| PATCH | /v1/appointments/{id} | Change an appointment (any action) | appointments:write |
| POST | /v1/appointments/{id}/confirm | Confirm (the customer reconfirmed) | appointments:write |
| POST | /v1/appointments/{id}/reschedule | Move to a new time | appointments:write |
| POST | /v1/appointments/{id}/reschedule_requested | He will come later, no new time yet | appointments:write |
| POST | /v1/appointments/{id}/cancel_requested | He may not come (a person decides) | appointments:write |
| POST | /v1/appointments/{id}/cancel | Cancel | appointments:write |
| POST | /v1/appointments/{id}/complete | Mark done | appointments:write |
| POST | /v1/appointments/{id}/no_show | Mark no-show | appointments:write |
REST hooks and the auth test used by the Zapier and Make apps (and any API client) - call results as they happen.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/me | Who am I | calls:read |
| GET | /v1/hooks | Your REST hooks | calls:read |
| POST | /v1/hooks | Subscribe a URL to an event (REST hook) | calls:write |
| DELETE | /v1/hooks/{id} | Unsubscribe a REST hook | calls:write |
| POST | /v1/hooks/{id}/test | Send a signed sample of the hook's event | calls:write |
| GET | /v1/hooks/recent | The newest items of an event (polling) | calls:read |
Voices, text to speech, models and voice tuning.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/voices | List voices | agents:read |
| GET | /v1/subjects | List what your campaigns are about | agents:read |
| GET | /v1/voices/{voiceId}/sample | Hear a voice | agents:read |
| POST | /v1/voices/{voiceId}/try | Hear your own line in a voice | agents:write |
| POST | /v1/voices/tts | Text to speech | agents:write |
| GET | /v1/models | LLM model ids by provider | agents:read |
| GET | /v1/llm/models | AI models you can pick, and what each adds per minute | agents:read |
| GET | /v1/voice-tuning | Current voice tuning | agents:read |
| PUT | /v1/voice-tuning | Update voice tuning | agents:write |
Exports and dashboard reporting.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/export/leads | Export calls (and, for a workspace whose own CRM keeps its leads, its campaigns' call results) | calls:read |
| GET | /v1/export/filters | Filters available for export | calls:read |
| GET | /v1/dashboard/health | Call health over the last hour | usage:read |
| GET | /v1/analytics/overview | Calls, answer rate and latency over a date range | usage:read |
| GET | /v1/dashboard/reports | Call outcome report | usage:read |
Platform-wide status that affects every account.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /v1/system/pause | Is outbound calling paused? | system:read |