Updated
Quickstart
- Create a key under API keys in the dashboard. It acts as your account and spends your credit, so keep it on your server.
- From your server, start a call and describe the agent in the same request:
curl https://llamacalls.com/api/calls \
-H "Authorization: Bearer $LLAMACALLS_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"name": "Priya",
"instructions": "You are Priya, a hiring manager interviewing for a senior designer role. Ask one question at a time.",
"greeting": "Hi, this is Priya from the design team. Ready to start?",
"capabilities": {
"avatarId": "8b86dda1-98ed-4acd-8a4e-b1a00ba69268",
"language": "en",
"recording": "video",
"analysis": ["conversation", "eye-contact"]
}
},
"metadata": { "yourUserId": "u_42" }
}'The answer:
{
"room": "api-6f1c…",
"joinUrl": "https://llamacalls.com/embed/api-6f1c…#url=…&token=…&name=Priya",
"url": "https://realtime.llamacalls.com",
"token": "…"
}- Hand
joinUrlto your page and embed it. Your user presses Join, talks to the agent, and hangs up from the same frame:
<iframe
src="{joinUrl}"
allow="microphone; camera; autoplay"
style="width: 100%; height: 640px; border: 0; border-radius: 16px"
></iframe>The allow attribute is required: without it the browser refuses the microphone inside the frame. In a mobile app, open joinUrl in a WebView with media playback and microphone access enabled.
joinUrl opens this one call and nothing else, so it is safe to give to the person taking the call. Your API key is not. The credentials sit after the #, which browsers never send to a server, so they stay out of logs.
Let the agent point at things on your page
Add the guide script to the page that holds the iframe. The agent can then read what is on that page and draw on it while it talks the caller through it: point at a button, circle a field, highlight a row, write a short note beside something.
<script src="https://llamacalls.com/guide.js" async></script>The script only answers an embed from llamacalls.com, sends nothing but the visible elements and text of the page, and draws on a layer that never takes a click. Without it the agent still has its own screen-share: a whiteboard it draws on, pages it browses, and PDFs and files it opens and reads.
Starting a call
POST /api/calls takes a saved agent, a whole agent described inline, or both. There is nothing to create first: an app with thousands of users makes one call per user, each with its own persona, face and voice, and never manages an agent per user.
{ "agentId": "…" } // a saved agent, as configured
{ "agent": { … } } // everything inline, nothing saved
{ "agentId": "…", "agent": { … } } // a saved agent, changed for this callagent takes every setting the agent page in the dashboard has, in the same shape as PATCH /api/agents/:id. Each one applies to this call only. Anything you leave out comes from the saved agent, or, with no agentId, from your account's API calls agent: it is created on your first call, has no face unless a call asks for one, and shows in the dashboard, so tools you attach to it there reach every such call.
The agent object
| Field | What it sets |
|---|---|
name | What the agent is called, and answers to |
instructions | Who the agent is and what the call is for, up to 8,000 characters |
greeting | The first thing the agent says, word for word. Leave it out and the agent opens in character |
roleCard | A structured persona, instead of or alongside the instructions |
voiceId | A voice from GET /api/voices, or any Fish voice as fish:<id>. Leave it out with a stock face and the face speaks in its own voice |
model | The language model behind the agent |
connectorIds | The tools it can use on this call: your connectors from GET /api/connectors. Replaces the saved agent's tools |
mcpServers | MCP servers for this call only, as [{ name, url, token? }]: added to the agent's tools, so a product can run one server per customer without a connector each. One named like a connector replaces it for this call |
agent.capabilities
| Field | Values |
|---|---|
face | lifelike or none. Setting an avatarId turns the face on |
avatarId | A stock face or one you uploaded, from GET /api/avatars |
language | en es fr de hi ru pt ja it nl zh ms ta |
gender | female or male: how the agent refers to itself |
recording | off, transcript, audio, video or full |
analysis | Post-call reports: conversation (timing, voice and language) and eye-contact. Needs recording video or full |
analysisSkills | Which communication skills the conversation report scores, by id from GET /api/skills. Absent means the core set |
vision | Whether the agent can see the caller's camera |
screenShare | Whether the agent can open a browser and share it |
respond | always, or when-addressed on calls with several people |
interrupt | Whether the agent may cut in mid-sentence |
randomProfile | A different person on every call, drawn from criteria you set (below) |
A different person every call
For practice and training, randomProfile draws a new person for each call: their name, face, voice, background and temperament. Narrow who can turn up with the criteria, and opt into prejudice when the training calls for it.
"capabilities": {
"randomProfile": {
"enabled": true,
"criteria": {
"ageBands": ["senior"],
"temperaments": ["hostile"],
"prejudices": ["accent"],
"prejudiceLevels": ["subtle"],
"prejudiceShare": 30
},
"trainee": "a new call-centre agent"
}
}Every criteria list is optional and empty means anyone. prejudices is the exception: empty means none. prejudiceShare is the percentage of calls whose person brings one. trainee tells the person who they are talking to.
Your own faces
Upload a portrait once with POST /api/avatars?label=…, sending the image as the raw body (image/jpeg or image/png, 256 to 4,096 pixels a side, up to 8 MB). The answer holds its id. Pass that as agent.capabilities.avatarId on any call.
Everything else in the request
| Field | What it does |
|---|---|
metadata | Your own data, up to 4 KB of JSON, such as your user's id. Stored with the call and returned by GET /api/calls/:room. The agent never sees it |
analysisContext | Which reference bands the post-call analysis judges against: general, sales, interview, negotiation or difficult-conversation |
Fields we don't recognise are refused with a 400 that names them, so a misspelt setting never silently does nothing. So are voices, faces and connectors that aren't yours.
After the call
| Request | Returns |
|---|---|
GET /api/calls/:room | The call: when it started and ended, what was recorded, your metadata |
GET /api/calls/:room/transcript | The transcript, one JSON line per utterance |
GET /api/calls/:room/analysis | { status, result }: the report, once status is ready, with a 1–5 level, quotes and a suggested rewrite per skill; a 404 when the call had no analysis |
GET /api/calls/:room/video | The recording as MP4, with range requests |
GET /api/calls/:room/audio | Whose audio was captured; each speaker's at /audio/:speaker |
GET /api/calls?page=1 | All your calls, newest first |
DELETE /api/calls?room=:room | Ends a call from your server |
A call ends when the caller hangs up in the embed, when you end it, or a minute after the caller closes the page.
iOS
The Swift package joins a call natively: add github.com/kalashshah/llamacalls-swift with Swift Package Manager (iOS 17+), start the call on your server, and pass room, url and token to LlamaCall.connect. The package handles the connection, microphone, camera, the agent's voice and face, captions and hang-up; your app draws everything.
import LlamaCalls
import SwiftUI
struct CallScreen: View {
@State private var call = LlamaCall()
let room: String, url: URL, token: String
var body: some View {
VStack {
LlamaCallVideoView(call.faceTrack)
Text(call.captions.last?.text ?? "")
Button("Hang up") { Task { await call.hangUp() } }
}
.task {
try? await call.connect(room: room, url: url, token: token)
try? await call.setMicrophone(true)
}
// A call is not torn down when the view goes away; leave it, or it keeps the mic open.
.onDisappear { Task { await call.leave() } }
}
}Lists and saved agents
| Request | Returns |
|---|---|
GET /api/voices | Every voice, with its language |
GET /api/skills | Every skill the analysis can score, by pack, with what each level looks like |
GET /api/avatars | Your uploaded faces and the stock ones (catalogue) |
GET /api/connectors | Your tools. Connecting a new one happens in the dashboard |
/api/agents, /api/agents/:id | List, create, read, change and delete saved agents, if you prefer to keep some |
Errors and pricing
Errors are JSON, { "error": "…" }. 401: the key is missing, wrong or revoked. 404: not in your account. 402: your credit can't cover a minute of the call. 429: every call slot is busy, so retry after the retry-after seconds.
API calls cost what dashboard calls cost, by the second, from your prepaid credit: $0.03 a minute for voice, $0.05 more with a face, and $0.03 more with a video recording. Transcripts and analysis are free. No monthly plan.