LlamaCalls: AI agents that join your calls. Try it now

Put an AI agent on a call inside your product

One request from your server starts a voice or video call with an agent you describe on the spot. Drop the call into your page with an iframe. Read the transcript and analysis when it ends.

Updated

Quickstart

  1. Create a key under API keys in the dashboard. It acts as your account and spends your credit, so keep it on your server.
  2. 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": "…"
}
  1. Hand joinUrl to 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 call

agent 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

FieldWhat it sets
nameWhat the agent is called, and answers to
instructionsWho the agent is and what the call is for, up to 8,000 characters
greetingThe first thing the agent says, word for word. Leave it out and the agent opens in character
roleCardA structured persona, instead of or alongside the instructions
voiceIdA 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
modelThe language model behind the agent
connectorIdsThe tools it can use on this call: your connectors from GET /api/connectors. Replaces the saved agent's tools
mcpServersMCP 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

FieldValues
facelifelike or none. Setting an avatarId turns the face on
avatarIdA stock face or one you uploaded, from GET /api/avatars
languageen es fr de hi ru pt ja it nl zh ms ta
genderfemale or male: how the agent refers to itself
recordingoff, transcript, audio, video or full
analysisPost-call reports: conversation (timing, voice and language) and eye-contact. Needs recording video or full
analysisSkillsWhich communication skills the conversation report scores, by id from GET /api/skills. Absent means the core set
visionWhether the agent can see the caller's camera
screenShareWhether the agent can open a browser and share it
respondalways, or when-addressed on calls with several people
interruptWhether the agent may cut in mid-sentence
randomProfileA 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

FieldWhat it does
metadataYour 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
analysisContextWhich 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

RequestReturns
GET /api/calls/:roomThe call: when it started and ended, what was recorded, your metadata
GET /api/calls/:room/transcriptThe 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/videoThe recording as MP4, with range requests
GET /api/calls/:room/audioWhose audio was captured; each speaker's at /audio/:speaker
GET /api/calls?page=1All your calls, newest first
DELETE /api/calls?room=:roomEnds 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

RequestReturns
GET /api/voicesEvery voice, with its language
GET /api/skillsEvery skill the analysis can score, by pack, with what each level looks like
GET /api/avatarsYour uploaded faces and the stock ones (catalogue)
GET /api/connectorsYour tools. Connecting a new one happens in the dashboard
/api/agents, /api/agents/:idList, 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.