Building Voice-Enabled AI Agents with Anima
Provision a number, place a call, and follow it on the event stream — plus the two gates that decide whether the call happens at all.
Most voice APIs hand you a state machine. You author call-control markup, answer a webhook per turn, and race a timeout while your model thinks — because the caller is listening to silence the whole time.
Anima runs the conversation instead. Speech-to-text, the model, and text-to-speech all execute inside the call session, which is what keeps a turn under the threshold where someone starts talking over the gap. What you write is the part that decides what to say, not the plumbing that carries it.
Provision a number#
A voice-capable agent needs an identity with an E.164 number attached.
import { Anima } from "@anima-labs/sdk";
const anima = new Anima({ apiKey: process.env.ANIMA_API_KEY! });
const agent = await anima.agents.create({
orgId: process.env.ANIMA_ORG_ID!,
name: "Customer Support Agent",
slug: "support-voice",
});
const number = await anima.phones.provision({
agentId: agent.id,
countryCode: "US",
capabilities: ["voice", "sms"],
});Place a call#
const call = await anima.calls.create({
agentId: agent.id,
to: "+15550123456",
fromNumber: number.phoneNumber,
greeting: "Hello, this is your AI assistant calling from Anima.",
});
console.log(call.callId, call.state); // "ringing"There is no markup to author and no per-turn webhook to answer. The greeting starts the conversation; the platform runs it from there.
Two gates decide whether it dials#
Outbound is gated server-side, and your agent's code cannot route around either gate.
Consent is an org attestation, not a per-call field
Your organization completes a one-time TCPA consent attestation in the console —
consentSource plus a DNC attestation, server-stamped. There is no per-call consent
parameter to pass, and no flag on calls.create that changes this. Until the attestation
exists, outbound calls do not happen.
The second gate is spend: calls stop at your plan's included minutes unless you have opted in to metered overage.
What Anima does not do is scrub the destination against the FCC Reassigned Numbers Database. That obligation stays with you. Nothing in the platform screens who currently holds the number you are dialing.
Follow the call as it happens#
Call lifecycle arrives on the event stream rather than as requests you have to answer in time:
const stream = anima.events.connect({ channels: ["call.*"] });
stream.on("event", (event) => {
// call.started, call.ended, call.summary.ready, call.score.ready
console.log(event.eventType, event.data);
});Wildcards mean different things on the two surfaces
On the event stream, call.* is a prefix match — it covers call.summary.ready and
call.security.alert too. On webhook subscriptions the same syntax matches a single
dot-separated segment, so call.* there gets call.ended and misses both.
Same pattern, two behaviours, depending on which surface you subscribe from.
After the call#
The transcript, a summary, and a safety scan are produced for you. You do not assemble them from media frames:
const transcript = await anima.calls.getTranscript(call.callId);
console.log(transcript.text);call.summary.ready and call.score.ready fire when post-call processing finishes, which
is why they are separate events from call.ended — the call is over before the analysis
is.
What you do not manage#
Conversation state within a call is not yours to hold. The session lives on the instance that owns the call, and per-turn context is handled inside it, so there is no history for your code to accumulate and replay.
That has a boundary worth knowing: it is per-call, in-process state, not a durable store
you can query. There is no API for reading a session, and nothing survives the call
ending. Anything you need afterwards comes from the transcript, the summary, or your own
records — keyed by callId, which is the identifier that outlives the call.
Inbound keypresses are not delivered
There is no inbound DTMF event. The call events are call.started, call.ended,
call.summary.ready, call.score.ready, and two security events — that is the whole
list. An agent cannot currently react to a caller pressing 1, so a menu-driven fallback
is not something you can build on this today.
Where this leaves you#
The parts that are usually hardest — turn latency, media handling, transcription, post-call analysis — are the parts you do not write. The parts that remain yours are the ones that should be: what the agent says, when it is allowed to call, and who it is allowed to call.
That last one is worth repeating, because the platform will not catch it for you. The consent attestation gates whether your org may dial at all. It does not check whether this number still belongs to the person who consented.