Guides / Use cases

Customer support

For help desks, support bots and the human agents behind them. Recall keeps one memory per customer across tickets, chat and email, so the bot does not ask for the order number twice and the agent who takes over a conversation sees the story so far in one fast call.

What memory adds

Without memory, each ticket and each channel starts cold. With it, the customer's history comes along:

Without memoryWith Recall
Two days after emailing about a double charge, the customer opens the chat"Hi, what is your order number?""Is this about the double charge on order 77120? We refunded €49 on 6 October, so it should arrive by 11 October."
The bot hands over to a humanThe agent scrolls through the ticket and asks the customer to explain again.The agent's sidebar shows the ticket summary, the latest messages and what is known about the customer.
A colleague of the customer writes inA stranger, as far as the bot knows.Your team can ask about the account: "They want yearly billing from November and invoices sent to their finance team."

Recall does not write the replies or route tickets. Your bot's model and your help desk do that; Recall gives them what is known about this customer and learns from every conversation.

How to model it

In your help deskIn RecallWhy
Your product, or each business you serve if you run support for manyWorkspace, e.g. support-tenant-4471One business's customers never mix with another's, and a business that leaves is one workspace to delete.
A customerPeer cust-1042, your customer idThe same memory whether they write by chat or by email.
Your bot and each human agentPeers bot-support, agent-317 with observeMe: falseRecall learns about customers, not about your staff; their replies are read as context and cost less.
A ticket or conversationSession, e.g. ticket-58213, whatever the channelOne summary per ticket, even when it moves from email to chat. Put the channel in message metadata.
A customer account with several contactsScope, e.g. acct-2209, holding the account's ticketsAsk about the whole company without reading other customers.
Plan, region and other CRM fieldsPeer metadataFor your own code and UI. Recall learns from what people write, not from metadata.

Build it

Install the SDK (npm install @recall-memory/sdk or pip install recall-mem) and set RECALL_API_KEY. Create the customer, your staff and the ticket.

import { constructWebhookEvent, Recall } from "@recall-memory/sdk";

const recall = new Recall({ workspaceId: "support-tenant-4471" });

// The customer is a peer keyed by your stable customer id, whatever channel they use.
const customer = recall.peer("cust-1042", { metadata: { plan: "pro", account: "acct-2209" } });
// Recall learns about customers, not about your staff: the bot and agents are not observed.
const staff = { configuration: { observeMe: false } };
const bot = recall.peer("bot-support", staff);
const agent = recall.peer("agent-317", { ...staff, metadata: { team: "billing" } });
// One session per ticket, across every channel it moves through.
const ticket = recall.session("ticket-58213", { metadata: { subject: "Charged twice" } });

Email: each message with the time it was sent. Add every email as a message from its sender, with createdAt set to when it was sent, so what Recall learns is dated correctly. Strip quoted replies and signatures first; otherwise every email repeats the thread and you pay to read it again.

// The ticket starts by email. Add each email with the time it was sent, so what Recall learns
// is dated correctly. Strip quoted earlier emails and signatures first.
const email = { metadata: { channel: "email" } };
await ticket.addMessages([
  customer.message("You charged us twice for September, order 77120. Please refund one.", {
    ...email,
    createdAt: "2026-10-06T08:05:00Z",
  }),
  agent.message("Sorry about that. I've refunded the duplicate €49; allow 3 to 5 days.", {
    ...email,
    createdAt: "2026-10-06T09:12:00Z",
  }),
]);

Chat: one request per message. When the customer writes in the widget, turn stores the message and returns the context for the bot's reply: the whole ticket across channels, and what Recall knows about the customer from earlier tickets. Building the context uses no model and takes well under a second.

// Days later the customer opens the chat widget. One request stores the message and returns
// the context for the bot's reply: the whole ticket so far and what Recall knows about them.
const chat = { metadata: { channel: "chat" } };
const { context } = await ticket.turn(
  [customer.message("Still no refund. And can we switch to yearly billing from November?", chat)],
  { peerTarget: customer, tokens: 3000 },
);
const prompt = context.toOpenAI(bot);
// Your model writes the bot's reply from `prompt`; store it so the ticket is complete.
const reply = "The €49 refund was sent on 6 October, so it should arrive by 11 October.";
await ticket.addMessages([bot.message(reply, chat)]);

Accounts with several contacts: a scope. When several people from one company write in, put the account's tickets in a scope. A question with scope reads only those tickets. Add a ticket with addSessions, or name the scope when you create the ticket.

// cust-1042 and cust-1043 work at the same company. A scope per customer account collects
// their tickets, so you can ask about the whole account.
const account = recall.scope("acct-2209");
await account.addSessions([ticket]);
// A new ticket can join the scope when you create it.
const other = recall.session("ticket-58190", { scopes: [account] });
await other.addMessages([recall.peer("cust-1043").message("Please send invoices to finance.")]);

Handoff to a human: context, not a question. When the bot hands over, fetch the ticket's context with the customer as peerTarget: the summary, the latest messages, the customer's card and what Recall knows about them, in one call with no model. Show it in your agent's sidebar. For the account as a whole, ask with scope; that is a model call of a few seconds.

// Handing over to a human: the ticket's summary and latest messages plus the customer's card,
// in one call with no model, for your agent's sidebar.
// (waitForIdle is for this demo; real apps rarely block on learning.)
await recall.waitForIdle();
const brief = await ticket.context({ peerTarget: customer, tokens: 4000 });
const sidebar = {
  summary: brief.summary?.content ?? "",
  customer: brief.peerCard ?? [],
  latest: brief.messages.slice(-5).map((m) => `${m.peerId}: ${m.content}`),
};
// Questions about the whole account read the scope instead of one customer.
const accountNotes = await recall.chat("What has this account asked us to change?", {
  scope: account,
  reasoningLevel: "low",
});

Past tickets: search. To answer "has this happened before?", search what the customer wrote across all their tickets; each hit carries its ticket id. Search takes about 0.9 seconds and is included in the price. To include your agents' replies in those tickets, search the workspace with the peer_perspective filter set to the customer (see Search filters).

// "Has this happened before?": search what the customer wrote across all their tickets.
const earlier = await customer.search("charged twice", { limit: 5 });
const relatedTickets = [...new Set(earlier.map((m) => m.sessionId))];

Keep your CRM in step: webhooks. Recall sends a queue.empty event when it has finished learning from a ticket. Register your endpoint once with recall.webhooks.create(url), or POST /v3/workspaces/{workspace}/webhooks with {"url": "https://…"}; a workspace can have 10. In the handler, verify the X-Recall-Signature header against the raw body, skip event ids you have already handled (X-Recall-Event-Id is the same on every retry) and answer with a 2xx quickly. See Webhooks.

// Your endpoint for Recall's webhooks. queue.empty means Recall has finished learning from a
// ticket: verify the signature on the raw body, then refresh the customer's note in your CRM.
async function onRecallWebhook(rawBody: string, signature: string | null, secret: string) {
  const event = await constructWebhookEvent(rawBody, signature, secret);
  const { session_id: ticketId, observed: customerId } = event.data;
  if (event.type !== "queue.empty" || typeof customerId !== "string") return null;
  const latest = await recall
    .session(String(ticketId))
    .context({ peerTarget: customerId, tokens: 2000 });
  // Write this to the customer's record in your CRM.
  return { customerId, summary: latest.summary?.content ?? "", card: latest.peerCard ?? [] };
}

Deletion requests. customer.delete() erases the customer: their messages, every fact and card about them, the account scope's memory of them, and the summaries that quoted them. Their tickets still hold your agents' replies, so delete the tickets too, unless you must keep them.

// A deletion request: erase the customer (their messages, every fact and card about them, and
// the account scope's memory of them), then their tickets, which still hold your replies.
const tickets = await (await customer.sessions()).toArray();
const removed = await customer.delete();
for (const t of tickets) await t.delete();

Settings that matter

SettingUse
observeMe: false on bots and agentsRecall learns nothing from your staff's replies; it reads them as context for the customer's messages. Give it when you create the handle; the SDKs send it before their first message.
Message createdAtFor emails and imported tickets: when each message was written. Without it, "next Monday" is reckoned from the import.
Message metadataChannel, order number, help desk ids: for your own filters and UI.
peerTarget and tokens on turn and context3,000 to 4,000 tokens for a bot reply or a handoff; older parts of the ticket become a summary.
scope on chat, search and contextRead only one account's tickets. Scopes need a workspace-level key.
API keysScope keys to a workspace, peer or session in the dashboard or with recall.createKey. In a help desk for many businesses, give each one's workers a key for its own workspace.

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 search about 0.9 seconds, a question at minimal or low 2 to 4.5 seconds. Learning happens in the background, usually within about 20 seconds of a message, and never holds up a call.

Context, search, storage, summaries and dreams are included, so the context behind bot replies and handoffs costs nothing extra. You pay for learning, $1.75 per million message tokens with every message counting as at least 50; a message Recall reads without learning from it, such as a reply from your staff, counts only what it reads, at most 200 tokens as context. A customer who opens two tickets a month, one in chat (8 messages of about 40 tokens, 8 replies of about 100) and one by email (3 emails of about 300 tokens, 3 replies of about 250), and about whom your agents ask 2 questions at low:

ItemPer month
Customer messages: (8 × 50 + 3 × 300) tokens × $1.75 / 1M$0.0023
Replies, read but not learned from: (8 × 100 + 3 × 200) tokens × $1.75 / 1M$0.0025
Questions at low: 2 × $0.005$0.0100
Totalabout $0.015 per customer

For 10,000 such customers that is about $107 a month. Set a monthly cap and auto top-up under Billing so the bot never stops: out of credit, Recall answers 402 but still stores messages. More in Cost and speed.

Privacy and forgetting

Say in your privacy notice that support conversations are stored to help customers, and that AI model providers process them. Then:

Pitfalls

Next: Sales and CRM copilots · Privacy, consent and forgetting