Handoff
Generate a cross-channel briefing for a human agent taking over a conversation.
A handoff briefing is a structured snapshot of everything ConvoMem knows about a customer: their full conversation journey, key memories, commitments already made, sentiment trend, open-issue detection, and a narrative summary. Designed to be injected directly into a human agent's context the moment they take over.
Get a briefing
const handoff = await client.getHandoff({ email: '[email protected]' })
if (handoff.found) {
console.log('Narrative:', handoff.narrative)
console.log('Key memories:', handoff.keyMemories)
console.log('Sentiment:', handoff.sentimentTrend.direction)
console.log('Open issue:', handoff.openIssue.isOpen)
}Options
| Option | Type | Default | Description |
|---|---|---|---|
fresh | boolean | false | Rebuild the brief instead of serving the cached copy |
narrative | boolean | true | Set to false to skip the narrative — faster, structured data only |
Briefs are cached for up to 24 hours by default and rebuilt automatically whenever a
conversation ends or is escalated, or the open issue is triaged. Turns captured in a
conversation that is still active do not refresh the cache, so pass fresh: true when
you need them in the brief.
const handoff = await client.getHandoff(
{ customerId: 'f3b39d1b-2522-4a00-98df-8cdde783ba12' },
{ fresh: true, narrative: true },
)Response fields
| Field | Type | Description |
|---|---|---|
found | boolean | false only when no customer matches the identity; the response is then { found: false, customer: null } |
customer | object | Profile summary: id, name, email, phone, externalId, lastContactAt, lastSentiment |
journey | object[] | Every conversation, oldest first: conversationId, channel, status, startedAt, endedAt, summary, outcome, sentiment, messagesCount |
keyMemories | object[] | Customer-stated facts ranked by relevance to the open issue: id, content, category, memoryType, sentiment, conversationId, channelSource, relevantToIssue |
agentCommitments | object[] | What your side already committed to (same shape as keyMemories), kept separate from the customer's own statements; empty unless agent capture is enabled |
sentimentTrend | object | { direction, current, points } — see below |
openIssue | object | { isOpen, reason, conversationId, summary, priority, tags, snoozedUntil, resolvedAt, insights, effectivePriority }; reason is ESCALATED, ACTIVE, OUTCOME_UNRESOLVED, or null |
recentInsights | object[] | Open insights for the customer: id, insightType, content, status, confidence, conversationId, createdAt |
notes | object[] | Internal agent notes, newest first: id, body, authorId, conversationId, createdAt |
narrative | string | null | Narrative summary of the customer's history; null when narrative=false |
narrativeSource | string | How the narrative was produced — see below |
generatedAt | string | ISO-8601 time the brief was built |
cached | boolean | true when served from the cache |
sentimentTrend — direction is improving, declining, stable, or unknown
(no conversation has a sentiment score yet). current is the latest sentiment score (or the
customer's last known score, or null). points lists each scored conversation as
{ at, channel, sentiment }, oldest first.
narrativeSource
| Value | Meaning |
|---|---|
llm | Written by the model |
pending | A deterministic summary served now while the model-written version is generated in the background; a later request returns llm |
fallback | The model was unavailable, so a deterministic summary is used |
skipped | You passed narrative=false |
Injecting into a human agent
const handoff = await client.getHandoff(
{ customerId: escalatedCustomerId },
{ fresh: true },
)
const briefing = `
## Customer briefing
${handoff.narrative ?? 'No prior history.'}
### Key facts
${handoff.keyMemories.map((m) => `- ${m.content}`).join('\n')}
### Sentiment
${handoff.sentimentTrend.direction} (score: ${handoff.sentimentTrend.current ?? 'n/a'})
### Open issue
${handoff.openIssue.isOpen ? handoff.openIssue.summary : 'None'}
`.trim()When a call enters the queue, call getHandoff() with fresh: false (cheap, cached).
Call it again with fresh: true when the agent picks up to include anything captured
while the customer waited.
By customer ID (path-based route)
For direct ID access, the SDK routes to the path-based endpoint:
// With customerId → GET /api/v1/customers/:id/handoff
const byId = await client.getHandoff({ customerId: 'f3b39d1b-2522-4a00-98df-8cdde783ba12' })
// Without customerId → GET /api/v1/customers/handoff?email=…
const byEmail = await client.getHandoff({ email: '[email protected]' })Route a live transfer
POST /handoff/route answers the question a voice platform asks mid-call: which human
destination should take this customer? The decision is made ahead of time, when the
customer's last conversation ends, so the call is a single fast read. It is designed to
always return 200: an unknown customer or missing decision falls back to the agent's
default destination. Read source (memory, default, none, or invalid_request)
to see which tier answered.
Handoff destinations are configured per agent in the dashboard; they cannot be created or edited with an API key.
const { destination, source, reasoning } = await client.routeHandoff({
phone: '+14155550101',
})
if (destination) {
await telephony.transfer(destination.value, destination.transferMessage)
}The response is { destination, source, reasoning, customerId }, where destination
is { id, name, type, value, transferMessage } (type is PHONE or SIP) or null
when no destination is configured. Add ?format=dograh to receive Dograh's
transfer_context shape instead.
What's next
- Customers — manage the profiles behind briefings
- Conversations — manage and escalate sessions
- Embed — the hosted agent panel for browser-based handoff
SDK: TypeScript · Python · Rust