Guides / Use cases
Voice agents
For phone lines and voice assistants: an agent that knows who is calling and what happened last time, without adding silence to the call. You read the caller's memory while the call connects, store each utterance in one fast request that runs next to your model, and leave anything that takes seconds until after the hang-up.
What memory adds
Without memory, every call starts with "how can I help?" and the same questions. With it, the agent knows the story so far:
| Without memory | With Recall | |
|---|---|---|
| Monday's call | The caller reports that the internet drops out every evening. The agent resets the line and promises to check it again on Friday. | |
| Friday, same caller, verified | "What is the reason for your call today?" | "Is this about the evening drop-outs? We said we would check your line today." |
| A month later | "Which router do you have?" | "Is it still the white router we sent in August? And shall I call you back after six, like last time?" |
Recall does not hear or speak. Your speech-to-text, model and text-to-speech do; Recall stores the transcript and gives your model the caller's memory.
How to model it
| On your line | In Recall | Why |
|---|---|---|
| Your phone line or voice product | Workspace, e.g. support-line | One per product and environment, so test calls never mix with real callers. |
| The caller | Peer: your account id (cust-1042) once verified, otherwise caller- and a keyed hash of the number | One stable id per person, and never the raw number. |
| Your voice agent | Peer voice-agent, with observeMe: false | Recall does not learn from the agent's lines, only reads them as context, so they cost less. |
| A call | Session, e.g. call-7f3e9a2c, from your telephony provider's call id | One transcript per call, its own running summary, and one place to delete it. |
| What the caller said on earlier calls | The peer card and facts, returned with the context | Ready for the first prompt, before the caller says a word. |
Why not the phone number? Ids show up in request paths, logs, exports and the dashboard, and cannot be renamed. A plain SHA-256 of a number protects nothing, because every possible number can be hashed and compared. A keyed hash (an HMAC with a secret only your server holds) cannot be reversed without the key, and the same number always gives the same id, so you can still find a caller to delete them. A number is still not a person, though: numbers change hands and a household can share one line. Once a caller is verified against your accounts, key memory by your account id and keep the number-to-account mapping in your own system; use the hashed number only for callers you cannot identify.
Build it
Install the SDK (npm install @recall-memory/sdk or pip install recall-mem) and set RECALL_API_KEY on the server that handles your call events. Configure the agent once at startup; per call you only need two handles, which make no request.
import { createHmac } from "node:crypto";
import { Recall } from "@recall-memory/sdk";
const recall = new Recall({ workspaceId: "support-line" });
// Ids show up in URLs, logs and exports, so never use the raw number. A plain hash of a phone
// number is easy to reverse; a keyed hash is not, without the key from your secret store.
const secret = process.env.CALLER_ID_SECRET ?? "local-dev-only";
const callerId = (phone: string) =>
`caller-${createHmac("sha256", secret).update(phone).digest("hex").slice(0, 32)}`;
// Once, at startup: Recall reads the agent's lines but learns nothing from them, and keeps a
// running summary of each call every 10 messages (the default is 20).
const agent = recall.peer("voice-agent", { configuration: { observeMe: false } });
await recall.setConfiguration({ summary: { messagesPerShortSummary: 10 } });
// Per call: the caller, and a session named after your telephony provider's call id.
// Handles make no request; the first utterance creates the session.
const caller = recall.peer(callerId("+447700900123")); // or your account id, once verified
const call = recall.session("call-7f3e9a2c");
import hashlib
import hmac
import os
from concurrent.futures import ThreadPoolExecutor
from recall_memory import PeerConfig, Recall
recall = Recall(workspace_id="support-line")
# Ids show up in URLs, logs and exports, so never use the raw number. A plain hash of a phone
# number is easy to reverse; a keyed hash is not, without the key from your secret store.
secret = os.environ.get("CALLER_ID_SECRET", "local-dev-only").encode()
def caller_id(phone: str) -> str:
return "caller-" + hmac.new(secret, phone.encode(), hashlib.sha256).hexdigest()[:32]
# Once, at startup: Recall reads the agent's lines but learns nothing from them, and keeps a
# running summary of each call every 10 messages (the default is 20).
agent = recall.peer("voice-agent", configuration=PeerConfig(observe_me=False))
recall.set_configuration({"summary": {"messages_per_short_summary": 10}})
# Per call: the caller, and a session named after your telephony provider's call id.
# Handles make no request; the first utterance creates the session.
caller = recall.peer(caller_id("+447700900123")) # or your account id, once verified
call = recall.session("call-7f3e9a2c")
Before the first word: read the caller's memory. As soon as the call connects, while your greeting plays, read the session context with the caller as peerTarget. It is one request (a context call measured 0.3 to 0.7 seconds) and works before the session exists, so a first-time caller gets empty context rather than an error. The result holds the caller's peer card and facts, ready as system messages. Until the caller is verified, keep personal details from it out of what the agent says.
// The call is connecting: read what Recall knows about the caller while your greeting plays,
// so the first reply needs no lookup. One request; a new caller just has no memory yet.
const start = await call.context({ peerTarget: caller, tokens: 1500 });
const system = start.toOpenAI(agent); // peer card and known facts, as system messages
# The call is connecting: read what Recall knows about the caller while your greeting plays,
# so the first reply needs no lookup. One request; a new caller just has no memory yet.
start = call.context(peer_target=caller, tokens=1500)
system = start.to_openai(assistant=agent) # peer card and known facts, as system messages
Each utterance: one request, off the critical path. When speech-to-text gives you the final transcript of an utterance, send it with turn, together with the agent's previous line, so one request stores both. A turn measured 0.3 to 0.7 seconds, which is too long to add to every reply. Your model does not need to wait for it: it already has the memory from the start of the call and the call so far, so start the model call and the turn together. Use the context the turn returns for the next utterance; on a long call it replaces older turns with a summary, so the prompt stays within tokens.
// Each finished utterance (the final transcript from speech-to-text): one request that stores
// it with the agent's previous line and returns fresh context. Start it next to your model call,
// not before it: the model already has `system` and the call so far.
const greeting = "Thanks for calling. How can I help?";
const heard = "Hi, it's me again. The internet keeps dropping out in the evenings.";
const stored = call.turn([agent.message(greeting), caller.message(heard)], {
peerTarget: caller,
tokens: 1500,
});
// Your voice model replies now, from `system`, the call so far and `heard`.
const reply = "Sorry about that. Is it the router we replaced in August?";
const { context } = await stored; // for the next utterance; older turns become a summary
# Each finished utterance (the final transcript from speech-to-text): one request that stores
# it with the agent's previous line and returns fresh context. Start it next to your model
# call, not before it: the model already has `system` and the call so far. (In an asyncio
# voice stack, use AsyncRecall and asyncio.create_task instead of a thread.)
greeting = "Thanks for calling. How can I help?"
heard = "Hi, it's me again. The internet keeps dropping out in the evenings."
with ThreadPoolExecutor(max_workers=1) as pool:
stored = pool.submit(
call.turn,
[agent.message(greeting), caller.message(heard)],
peer_target=caller,
tokens=1500,
)
# Your voice model replies now, from `system`, the call so far and `heard`.
reply = "Sorry about that. Is it the router we replaced in August?"
context = stored.result().context # for the next utterance; older turns become a summary
Questions wait until the call is over. chat took 2 to 4.5 seconds at minimal or low, which is dead air on a phone line. Never call it inside a turn. Ask before the call when you can (an outbound call you are about to place), or after it, for your CRM or to prepare the next call. If a question must inform this call, start it in the background when the call connects and use the answer only if it arrives in time.
After the hang-up: the summary and anything slow. Store the agent's last line. Recall keeps a running summary of each session, written every 20 messages by default (at least 10, set with messagesPerShortSummary), and summaries() returns it; a short call may not have one. A queue.empty webhook tells your server when learning for the call has finished, so you do not need to poll. For a note on every call, short ones included, ask a question as below ($0.005 at low), or have your own model write it from the transcript you already hold.
// Hang-up: store the agent's last line. Anything slow happens now, off the call.
await call.addMessages([agent.message(reply)]);
// (waitForIdle is for this demo; in production the queue.empty webhook says when to read.)
await recall.waitForIdle();
// Recall's running summary of the call (every 10 messages, as set at startup); a call as
// short as this one has none yet.
const { shortSummary } = await call.summaries();
// A question for your CRM or the next call: one model call of a few seconds, fine now.
const followUp = await caller.chat("What have we promised this caller, and by when?", {
reasoningLevel: "low",
});
# Hang-up: store the agent's last line. Anything slow happens now, off the call.
call.add_messages([agent.message(reply)])
# (wait_for_idle is for this demo; in production the queue.empty webhook says when to read.)
recall.wait_for_idle()
# Recall's running summary of the call (every 10 messages, as set at startup); a call as
# short as this one has none yet.
summary = call.summaries().short_summary
# A question for your CRM or the next call: one model call of a few seconds, fine now.
follow_up = caller.chat(
"What have we promised this caller, and by when?", reasoning_level="low"
)
A caller asks to be forgotten. Verify them, derive the id from their number with the same key, delete their calls (the agent's lines quote them), then the caller. delete() erases their messages, every fact and card about them, and cached answers.
// A caller asks to be forgotten: derive the same id from their number, delete their calls
// (the agent's lines quote them), then the caller.
const person = recall.peer(callerId("+447700900123"));
for (const s of await (await person.sessions()).toArray()) await s.delete();
const removed = await person.delete();
# A caller asks to be forgotten: derive the same id from their number, delete their calls
# (the agent's lines quote them), then the caller.
person = recall.peer(caller_id("+447700900123"))
for s in list(person.sessions()):
s.delete()
removed = person.delete()
Other languages and voice platforms
Recall has no telephony SDK; call it from whatever handles your call events. Over REST, the call start and each utterance are one request each. The context read:
curl -G $RECALL_URL/v3/workspaces/support-line/sessions/call-7f3e9a2c/context \
-d tokens=1500 -d peer_target=cust-1042 \
-H "Authorization: Bearer $RECALL_API_KEY"
Each utterance, with the agent's previous line. The Idempotency-Key header makes a retry safe; the speech-to-text segment id is a good value:
curl $RECALL_URL/v3/workspaces/support-line/sessions/call-7f3e9a2c/turn \
-H "Authorization: Bearer $RECALL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: call-7f3e9a2c-utt-7" \
-d '{"messages":[
{"peer_id":"voice-agent","content":"Thanks for calling. How can I help?"},
{"peer_id":"cust-1042","content":"Hi, me again. The internet keeps dropping out."}
],
"context":{"tokens":1500,"peer_target":"cust-1042"}}'
The answer is {"messages": [...], "context": {...}}: the stored messages and the context for the next prompt. The first turn creates the session and the caller. See the REST API.
Settings that matter
| Setting | Use |
|---|---|
tokens | The context goes into every prompt, so keep the budget small: a shorter prompt is faster for your model. Older turns become a summary. |
observeMe: false on the agent | Recall learns about the caller only, and the agent's lines cost less. |
summary.messagesPerShortSummary (workspace or session) | How often the running summary of a call is written: every 20 messages by default, at least 10. |
idempotencyKey on turn (idempotency_key in Python) | The SDKs add one per call, so their own retries never store an utterance twice. Pass your own, such as the speech-to-text segment id, if another worker may send it again. |
timeout and maxRetries (max_retries in Python) | On a live call, give up early and carry on without memory rather than leave the caller in silence. TypeScript also takes timeout per request. |
reasoningLevel: "low" | For questions after the call. See Reasoning levels and models. |
Cost and speed
Measured from Europe against the hosted API (Frankfurt), October 2026: a turn or context call took 0.3 to 0.7 seconds, a question at minimal or low 2 to 4.5 seconds. Learning happens in the background and never holds up a call. Speech-to-text, your model and text-to-speech already sit between the caller and the reply, so keep Recall beside that path, not on it: context while the call connects, each turn next to your model, questions after the hang-up.
Context reads and turns are included; you pay for learning from the messages a turn stores, $1.75 per million message tokens with every message counting as at least 50, and for questions. A caller who calls four times a month, with 30 short utterances and 30 agent lines of about 30 tokens per call, and one question after each call:
| Item | Per month |
|---|---|
| Caller utterances: 4 × 30 × 50 tokens × $1.75 / 1M | $0.0105 |
| Agent lines, read but not learned from: 4 × 30 × 30 tokens × $1.75 / 1M | $0.0063 |
Questions after the call at low: 4 × $0.005 | $0.0200 |
| Total | about $0.04 per caller a month |
More in Cost and speed.
Privacy and forgetting
This is practical guidance, not legal advice. See Privacy, consent and forgetting for the general picture.
- Tell callers, and get consent where it is required. Say at the start that the call is transcribed and that the assistant remembers earlier calls. The rules on recording and transcribing calls differ between countries, and in some countries between states; some require every party's consent. Recall stores the text you send, never audio.
- Caller ID is not identity. Numbers can be spoofed. Verify the caller, with a code, account details or a call back, before the agent repeats anything personal from memory.
- Keep secrets out of the transcript. Redact card numbers, PINs, one-time codes and answers to security questions before an utterance reaches Recall.
- Say where the data goes. Transcripts are stored in the EU (Frankfurt) and processed by AI model providers in the US through OpenRouter, restricted to providers that do not collect the data; they are not used to train models.
- Forget on request. Delete the caller and their calls as above. Facts about other people that merely mention them stay; delete those with
DELETE …/conclusions/{id}if needed (Forget a person).
Recall's own terms are in the privacy policy and the DPA.
Pitfalls
- Asking during the call. A question takes seconds, which is silence on a phone line. Read context during the call; ask before or after it.
- Recall in series with your model. Waiting for each turn before you start your model adds its 0.3 to 0.7 seconds to every reply. Start both together.
- The raw number as id. It ends up in logs and URLs, and memory follows the number to its next owner. Use your account id, or a keyed hash for callers you cannot identify.
- Trusting caller ID. Anyone can show someone else's number. Verify before the agent uses personal memory.
- Expecting a summary after every call. The running summary is written every 20 messages by default, so short calls have none. For call notes, ask a question or use your own model.
- Expecting Recall to listen. It works on text. Bring your own speech-to-text, model and voice.
Next: Cost and speed · Customer support