AI Studio API

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.

Quickstart in 5 minutes

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.

1

Make a test key

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_...
2

Install a library (or use curl)

# 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
3

Place a (test) call

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"])
4

Read what happened

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"])
5

Go live

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.

Common jobs

Copy, paste, replace AGENT_ID. Every example works with a test key first.

Place a call

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"})

Book a call for later

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"])

Upload leads and start a campaign

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"])

Read calls and results

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"])

Receive webhooks

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}

Make a voice-over

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.mp3
import { 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")

Check the wallet

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 first
import { 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"])

Test mode

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.

What a test key does

RequestIn test mode
POST /calls/outboundA 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_atA simulated booking, moved into your calling hours like a real one; read or cancel it at /calls/bookings/{id}. It is never dialled.
POST /campaignsA 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/ttsA silent MP3/WAV of the right length (no voice, no charge).
Any readThe 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 changeRefused with 403 test_mode_read_only: a test key cannot edit agents, settings, keys or anything else.
ScopesA test key holds scopes like a live key: placing a test call needs calls:write, a test campaign campaigns:write, otherwise 403 insufficient_scope.

Choose the outcome with the last digit of the number

Ends inWhat the simulated customer does
0Answers, is interested and agrees to a visit/meeting tomorrow (call.ended, lead.interested, visit.booked)
1Answers and is interested, no visit yet (call.ended, lead.interested)
2Answers and says no thanks (call.ended)
3Rings out, nobody answers (the call's status no-answer; call.ended status no_answer)
4The line is busy (the call's status no-answer; call.ended status busy)
5Goes to voicemail (the call's status completed; call.ended status voicemail)
6The call fails, e.g. a wrong number (the call's status failed; call.ended status failed)
7-9Same 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.

Send us your leads (and have them called at once)

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 called
import { 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.

Your own variables

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:

  1. The call request: dynamic_variables (or dynamic_vars) on POST /v1/calls/outbound, the Test call form, MCP variables.
  2. The lead: every column of its uploaded list or Google Sheet (a column treatment fills {{treatment}}).
  3. Your workspace default: Settings, Variables, or PUT /v1/settings/variables.
  4. What the Studio knows: 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).

Send to my system: webhooks, Zapier, Make, your CRM

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.endedevery 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.createdthe first time a call finds someone interested; lead.fields carries the lead's own columns
lead.updateda later call on the same person: status and next step
follow-up.scheduledwe booked the next call, with its time
appointment.*booked, confirmed, rescheduled, reschedule_requested, cancel_requested, cancelled, completed, no_show
message.senta 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

Connect your CRM

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.

Libraries

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.

AI assistants (MCP)

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.

Remote server (nothing to install)

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_..." } } } }

Local server (Claude Desktop and other apps that start a command)

{
  "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

Tools

ToolDoesScope
get_wallet_balanceThe workspace's prepaid balance in credits (1 rupee = 10 credits), its plan and per-minute rates.billing:readread
list_agentsThe 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:readread
list_voicesThe voices this workspace can use for agents and voice-overs.agents:readread
list_recent_callsThe latest calls, newest first, with status, length and who was called.calls:readread
get_callEverything about one call: status, length, summary, the full transcript and what the analysis found.calls:readread
search_conversationsFind conversations by words said or summarised (e.g.calls:readread
list_campaignsOutbound calling campaigns, newest first, with status and how many leads were called and answered.campaigns:readread
campaign_statusHow one campaign is doing: status, leads called, answered, voicemail, no answer, interested, still to call.campaigns:readread
list_interested_leadsPeople the lead judge found interested (hot or warm) in the last 30 days, newest first, with the summary.campaigns:readread
recent_resultsWhat calls produced (the outcome and the details collected, e.g.calls:readread
list_booked_callsCalls booked for a later time, newest first.calls:readread
place_callAn AI agent phones someone now.calls:writecosts money: shows the price first
book_callBook 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:writecosts money: shows the price first
create_leadAdds 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:writecosts money: shows the price first
cancel_booked_callCancel a call booked for later, while it is still waiting.calls:writechanges things
list_appointmentsSite visits / meetings, newest first: status (scheduled, confirmed, rescheduled, reschedule_requested, cancel_requested, cancelled, completed, no_show), time, lead.appointments:readread
book_appointmentBook a site visit / meeting / demo / consultation at a time (ISO 8601, e.g.appointments:writechanges things
confirm_appointmentThe customer reconfirmed: the appointment becomes confirmed at the same time.appointments:writechanges things
reschedule_appointmentMove an appointment to a new time (ISO 8601 with offset).appointments:writechanges things
make_voice_overTurn text (up to 2,500 characters) into speech in one of the workspace's voices and return the audio file.agents:writecosts money: shows the price first

Website widget

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.

Authentication

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.

Scopes

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 ["*"].

ScopeLets the key
agents:readRead agents, their prompts, voices, versions, analysis config, languages and the voice catalogue.
agents:writeCreate, edit, duplicate, roll back and delete agents; upload clips; change voice tuning.
calls:readRead call records, transcripts, call statistics, the do-not-call list and exports.
calls:writePlace calls. Add to and edit the do-not-call list. Hand this one out deliberately.
campaigns:readRead campaigns, their leads, lead lists, statistics and keypad reports.
campaigns:writeCreate, start, pause, edit and delete campaigns; add leads; re-queue.
recordings:readFetch call audio. Separate from calls:read so metadata can be shared without the audio.
usage:readRead usage by day, the CSV export, and the cost and health dashboards.
keys:readList API keys — prefixes and scopes only, never a secret. Still privileged: it is reconnaissance.
keys:writeCreate, rotate and revoke API keys.
tools:readRead the organisation tool library.
tools:writeCreate, edit and delete tools an agent may call mid-conversation.
numbers:readRead your phone numbers, their health, their assignments and your number requests.
numbers:writeRotate caller ID, flag and unflag numbers, request more numbers.
billing:readRead your plan, this month’s billed usage and invoices. There is no billing:write — plans are assigned by the platform.
settings:readRead organisation settings (provider keys appear masked).
settings:writeChange organisation settings and provider keys. Never grant this to a third party.
settings:secretsRead 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:readList knowledge documents and preview what the agent would retrieve.
knowledge:writeAdd and delete knowledge documents.
trunks:readPlatform administrators only — carrier trunk health.
trunks:writePlatform administrators only — carrier trunk caps and priority.
system:readPlatform administrators only — outbound pause state, channels, audit log, reserves.
system:writePlatform administrators only — the global outbound pause and channel reserves.
whatsapp:readThe WhatsApp channel (without its set-up secrets) and its conversations.
whatsapp:writeChange the WhatsApp channel and post-call templates, send a test message, hand a conversation to a person.
appointments:readList site visits / meetings and read one with its history.
appointments:writeBook, 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.

Errors

Every error is JSON and always carries error, a sentence you can act on. Failures you should branch on also carry a stable code.

StatuscodeMeansWhat to do
400did_not_ownedThe 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.
400tool_not_ownedA 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.
401legacy_key_not_allowed_on_v1The 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.
402no_provider_keyThis 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.
403insufficient_scopeThe 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.
403dndThe 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.
423outbound_pausedYour organisation’s outbound calling is paused.Resume it in Studio. Queue the work; do not retry in a loop.
423system_pausedPlatform-wide outbound calling is paused.Wait. This is the kill switch and it is deliberate.
425outside_calling_hoursOutside 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.
429daily_capYour organisation’s calls-per-day limit is spent.Resume tomorrow, or ask for a higher limit.
429concurrency_capYour 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.

Rate limits

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
}

Reference

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.

Agents

The AI agent — prompt, greeting, voice, language, caller ID.

MethodPathWhat it doesScope
GET/v1/agentsList agentsagents:read
POST/v1/agentsCreate an agentagents:write
GET/v1/agents/{id}Fetch one agentagents:read
PUT/v1/agents/{id}Update an agentagents:write
DELETE/v1/agents/{id}Delete an agentagents:write
POST/v1/agents/{id}/duplicateCopy an agentagents:write
GET/v1/agents/{id}/versionsPrompt version historyagents:read
GET/v1/agents/{id}/versions/{versionId}One stored version, with its full textagents:read
POST/v1/agents/{id}/rollbackRoll a prompt column back to a stored versionagents:write
GET/v1/agents/{id}/languagesLanguages the agent speaksagents:read
PUT/v1/agents/{id}/languagesSet the agent's languagesagents:write
GET/v1/languagesSupported languagesagents:read
GET/v1/agents/{id}/promptWhat the model receivesagents:read
GET/v1/agents/{id}/variablesWhich variables a call in this workspace fillsagents:read
POST/v1/agents/{id}/variablesCheck an unsaved draft's variablesagents:write
GET/v1/agents/{id}/branchesSaved named versionsagents:read
POST/v1/agents/{id}/branchesSave the agent as a named versionagents:write
POST/v1/agents/{id}/branches/{branchId}/applyMake a saved version liveagents:write
DELETE/v1/agents/{id}/branches/{branchId}Delete a saved versionagents:write
GET/v1/agents/{id}/evals/scenariosList simulation testsagents:read
POST/v1/agents/{id}/evals/scenariosAdd a simulation testagents:write
POST/v1/agents/{id}/evals/scenarios/starterAdd the starter testsagents:write
PUT/v1/agents/{id}/evals/scenarios/{scenarioId}Edit a simulation testagents:write
DELETE/v1/agents/{id}/evals/scenarios/{scenarioId}Delete a simulation testagents:write
GET/v1/agents/{id}/evalsPast test runsagents:read
POST/v1/agents/{id}/evalsRun the simulation testsagents:write
GET/v1/agents/{id}/evals/{runId}One test run — progress, then resultsagents:read
POST/v1/agents/{id}/evals/{runId}/cancelStop a test runagents:write

Analysis

Per-agent success criteria and structured data extracted from each call.

MethodPathWhat it doesScope
GET/v1/agents/{id}/analysisThe agent's success criteria and data-collection fieldsagents:read
PUT/v1/agents/{id}/analysisReplace the agent's analysis configurationagents:write

Calls

Placing calls, and the record of every call placed or received.

MethodPathWhat it doesScope
GET/v1/callsList callscalls:read
GET/v1/calls/{id}One call, with transcriptcalls:read
GET/v1/calls/statsAggregates over the same filters as the listcalls:read
POST/v1/calls/outboundPlace an outbound callcalls:write
GET/v1/calls/bookingsCalls booked for latercalls:read
GET/v1/calls/bookings/{id}One booked callcalls:read
DELETE/v1/calls/bookings/{id}Cancel a booked callcalls:write
GET/v1/calls/contactEvery call with one phone numbercalls:read
GET/v1/resultsPull call results since your last readcalls:read

Recordings

Call audio, behind a short-lived signed link.

MethodPathWhat it doesScope
GET/v1/recordings/{id}Play a call recordingrecordings:read
GET/v1/recordings/{id}/downloadDownload a call recordingrecordings:read

Campaigns

Outbound campaigns and the leads inside them.

MethodPathWhat it doesScope
GET/v1/campaignsList campaignscampaigns:read
POST/v1/campaignsCreate a campaigncampaigns:write
GET/v1/campaigns/{id}Fetch one campaigncampaigns:read
PATCH/v1/campaigns/{id}Update a campaign — including starting and pausing itcampaigns:write
DELETE/v1/campaigns/{id}Delete a campaign and its leadscampaigns:write
GET/v1/campaigns/{id}/leadsLeads in a campaigncampaigns:read
POST/v1/campaigns/{id}/leadsAdd leads to a campaigncampaigns:write
GET/v1/campaigns/{id}/call-logCalls made by a campaigncampaigns:read
GET/v1/campaigns/{id}/statsLifetime counters for a campaigncampaigns:read
GET/v1/campaigns/{id}/daywiseDay-by-day activitycampaigns:read
GET/v1/campaigns/{id}/interestLeads that showed interestcampaigns:read
POST/v1/campaigns/{id}/didsAdd a number to a campaigncampaigns:write
POST/v1/campaigns/{id}/dids/{number}/rotateRotate one of a campaign's numberscampaigns:write
GET/v1/campaigns/{id}/preflightPre-launch sanity checkcampaigns:read
GET/v1/campaigns/{id}/pipeline-metricsFunnel countscampaigns:read
POST/v1/campaigns/{id}/retryCall the unanswered leads againcampaigns:write
GET/v1/campaigns/{id}/retry-jobProgress of the latest retry jobcampaigns:read
POST/v1/campaigns/{id}/retry-job/stopStop a running retry jobcampaigns:write
GET/v1/campaigns/{id}/pipeline-statsDid the system do its partcampaigns:read
GET/v1/campaigns/live-channelsYour lines in use right nowcampaigns:read
GET/v1/campaigns/leads/hot-warmHot and warm leads (the lead judge's verdict)campaigns:read
GET/v1/campaigns/didsWhich numbers dialled for which campaignscampaigns:read
GET/v1/campaigns/liveLive calling snapshotcampaigns:read
GET/v1/campaigns/active-callsCalls in flight right nowcampaigns:read
GET/v1/campaigns/scoped-statsCampaign totals for today, yesterday or the last 7 dayscampaigns:read
GET/v1/campaigns/{id}/summaryCompact progress summarycampaigns:read
GET/v1/campaigns/{id}/retry-statsHow many leads a retry would re-queuecampaigns:read
GET/v1/campaigns/lead-groupsList reusable lead listscampaigns:read
POST/v1/campaigns/lead-groupsCreate a reusable lead listcampaigns:write
DELETE/v1/campaigns/lead-groups/{id}Delete a lead listcampaigns:write
POST/v1/campaigns/lead-groups/{id}/leadsAppend leads to a lead listcampaigns:write
POST/v1/campaigns/lead-groups/{id}/recountRecount a lead listcampaigns:write
POST/v1/campaigns/preview-leadsCheck which phones you already holdcampaigns:write

Numbers

The phone numbers you call from, their health, and requests for more.

MethodPathWhat it doesScope
GET/v1/didsList your numbersnumbers:read
GET/v1/dids/assignmentsWhich number belongs to which agent or campaignnumbers:read
GET/v1/dids/next-freeThe next number of yours that no agent is usingnumbers:read
POST/v1/dids/rotateRotate onto a fresh caller-ID numbernumbers:write
POST/v1/dids/{number}/spamMark one of your numbers as spam-flaggednumbers:write
POST/v1/dids/{number}/unspamClear the spam flagnumbers:write
GET/v1/numbers/requestsYour requests for more numbersnumbers:read
POST/v1/numbers/requestsRequest more numbersnumbers:write
POST/v1/numbers/verified-requestAsk for a verified caller IDnumbers:write
GET/v1/numbersYour phone numbers and who uses themnumbers:read

Knowledge

Documents the agent answers from.

MethodPathWhat it doesScope
GET/v1/knowledgeList knowledge documentsknowledge:read
POST/v1/knowledgeAdd a documentknowledge:write
GET/v1/knowledge/searchPreview what the agent would retrieveknowledge:read
GET/v1/knowledge/foldersYour foldersknowledge:read
GET/v1/knowledge/{docId}One document, with its textknowledge:read
PUT/v1/knowledge/{docId}Edit a document's textknowledge:write
PATCH/v1/knowledge/{docId}Move, re-scope or set auto-syncknowledge:write
DELETE/v1/knowledge/{docId}Remove a document and its chunksknowledge:write
GET/v1/knowledge/{docId}/agentsAgents that can use this documentknowledge:read
POST/v1/knowledge/{docId}/resyncRe-crawl a URL document nowknowledge:write
GET/v1/knowledge/{docId}/jobsRecent re-crawls of a documentknowledge:read

Tools

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.

MethodPathWhat it doesScope
GET/v1/toolsList your toolstools:read
POST/v1/toolsCreate a tooltools:write
GET/v1/tools/{id}Fetch one tooltools:read
PUT/v1/tools/{id}Update a tooltools:write
DELETE/v1/tools/{id}Delete a tooltools:write

Usage

What you used, per day. The basis for billing.

MethodPathWhat it doesScope
GET/v1/usageUsage by dayusage:read
GET/v1/usage/export.csvThe same data as CSVusage:read

API keys

Issue, rotate and revoke keys.

MethodPathWhat it doesScope
GET/v1/api-keysList keyskeys:read
POST/v1/api-keysCreate a keykeys:write
POST/v1/api-keys/{id}/rotateIssue a replacement and revoke the old keykeys:write
PATCH/v1/api-keys/{id}Rename a key or change its limitskeys:write
DELETE/v1/api-keys/{id}Revoke a keykeys:write

Settings

Organisation settings — pipeline defaults, webhooks, secrets your tools send, recording and privacy settings.

MethodPathWhat it doesScope
GET/v1/settingsOrganisation settingssettings:read
PUT/v1/settingsUpdate organisation settingssettings:write
GET/v1/settings/webhooksPost-call and initiation webhookssettings:read
PUT/v1/settings/webhooksUpdate webhook settingssettings:write
POST/v1/settings/webhooks/testSend a signed test eventsettings:write
GET/v1/settings/webhooks/deliveriesRecent webhook deliveriessettings:read
GET/v1/settings/send-toWhere your call results gosettings:read
PUT/v1/settings/send-toChoose where your call results gosettings:write
GET/v1/settings/send-to/inboxYour test address and what arrived theresettings:read
GET/v1/settings/variablesYour variables and their defaultssettings:read
PUT/v1/settings/variablesReplace your variablessettings:write
PUT/v1/settings/variables/{name}Make or change one variablesettings:write
DELETE/v1/settings/variables/{name}Remove one variablesettings:write
POST/v1/settings/variables/previewWhat an agent would say with these valuessettings:write
GET/v1/settings/crmYour CRM connectionsettings:read
PUT/v1/settings/crmConnect your CRM, change its URL, or switch it offsettings:write
POST/v1/settings/crm/testSend a signed sample to your CRMsettings:write
POST/v1/settings/crm/zoho/startStart connecting Zoho CRMsettings:write
POST/v1/settings/crm/secretMake a new signing secretsettings:write
GET/v1/settings/complianceRecording and privacy settingssettings:read
GET/v1/secretsYour secrets (names only)settings:read

Billing

Your plan, this month's billed usage and invoices. Read-only.

MethodPathWhat it doesScope
GET/v1/creditsCredits left, plan and ratesbilling:read
GET/v1/credits/ledgerEvery charge and credit, newest firstbilling:read
GET/v1/credits/statementA month, line by line, with the call behind every chargebilling:read
GET/v1/credits/statement.csvThe monthly statement as CSVbilling:read
GET/v1/billing/planYour current planbilling:read
GET/v1/billing/usageBilled usage for a monthbilling:read
GET/v1/billing/invoicesList invoicesbilling:read
GET/v1/billing/invoices/{id}One invoice with its linesbilling:read

Do not call

The opt-out list. A number here is refused before the switch is touched.

MethodPathWhat it doesScope
GET/v1/dndList opted-out numberscalls:read
POST/v1/dnd/bulkAdd numbers in bulkcalls:write
PATCH/v1/dnd/{id}Update an entrycalls:write
DELETE/v1/dnd/{id}Remove an entrycalls:write

Leads

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.

MethodPathWhat it doesScope
GET/v1/leadsList the leads you sent incalls:read
POST/v1/leadsSend a lead in (and have it called)calls:write
GET/v1/leads/{id}One lead, its call now, the calls made and its appointmentcalls:read

Appointments

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.

MethodPathWhat it doesScope
GET/v1/appointmentsList appointmentsappointments:read
POST/v1/appointmentsBook an appointmentappointments:write
GET/v1/appointments/{id}One appointment with its historyappointments:read
PATCH/v1/appointments/{id}Change an appointment (any action)appointments:write
POST/v1/appointments/{id}/confirmConfirm (the customer reconfirmed)appointments:write
POST/v1/appointments/{id}/rescheduleMove to a new timeappointments:write
POST/v1/appointments/{id}/reschedule_requestedHe will come later, no new time yetappointments:write
POST/v1/appointments/{id}/cancel_requestedHe may not come (a person decides)appointments:write
POST/v1/appointments/{id}/cancelCancelappointments:write
POST/v1/appointments/{id}/completeMark doneappointments:write
POST/v1/appointments/{id}/no_showMark no-showappointments:write

Integrations

REST hooks and the auth test used by the Zapier and Make apps (and any API client) - call results as they happen.

MethodPathWhat it doesScope
GET/v1/meWho am Icalls:read
GET/v1/hooksYour REST hookscalls:read
POST/v1/hooksSubscribe a URL to an event (REST hook)calls:write
DELETE/v1/hooks/{id}Unsubscribe a REST hookcalls:write
POST/v1/hooks/{id}/testSend a signed sample of the hook's eventcalls:write
GET/v1/hooks/recentThe newest items of an event (polling)calls:read

Catalogue

Voices, text to speech, models and voice tuning.

MethodPathWhat it doesScope
GET/v1/voicesList voicesagents:read
GET/v1/subjectsList what your campaigns are aboutagents:read
GET/v1/voices/{voiceId}/sampleHear a voiceagents:read
POST/v1/voices/{voiceId}/tryHear your own line in a voiceagents:write
POST/v1/voices/ttsText to speechagents:write
GET/v1/modelsLLM model ids by provideragents:read
GET/v1/llm/modelsAI models you can pick, and what each adds per minuteagents:read
GET/v1/voice-tuningCurrent voice tuningagents:read
PUT/v1/voice-tuningUpdate voice tuningagents:write

Reports

Exports and dashboard reporting.

MethodPathWhat it doesScope
GET/v1/export/leadsExport calls (and, for a workspace whose own CRM keeps its leads, its campaigns' call results)calls:read
GET/v1/export/filtersFilters available for exportcalls:read
GET/v1/dashboard/healthCall health over the last hourusage:read
GET/v1/analytics/overviewCalls, answer rate and latency over a date rangeusage:read
GET/v1/dashboard/reportsCall outcome reportusage:read

Platform

Platform-wide status that affects every account.

MethodPathWhat it doesScope
GET/v1/system/pauseIs outbound calling paused?system:read