Introduction: build secure, deterministic auth into voice agents
This cookbook shows how to implement authentication and fraud‑screening for ElevenLabs voice agents and then elevate privileges safely. You’ll get copy‑pastable patterns for:
-
Knowledge‑based authentication (KBA) via a server tool
-
One‑time passcode (OTP) with 3–5 minute expiry, retry caps, and rate limits
-
Mapping tool JSON payloads into auth_* dynamic variables for workflow gating
-
Expressions for pre‑auth vs post‑auth routing and sub‑agent handoff
-
A sample end‑to‑end flow: screen with VoxEQ Verify → look up the caller in Salesforce → fetch transactions → transfer to a privileged “Payments” sub‑agent
For security rationale and ElevenLabs workflow conventions (dispatch tools, dynamic variables, gating), see the ElevenLabs guidance on secure caller authentication for voice agents. Designing secure caller identity authentication flows for voice agents.
Architecture principles and threat model
-
Deterministic gates: Privileged tools (money movement, PII access) must be callable only after an explicit boolean auth success from a server tool. Avoid conversational inference for auth outcomes. ElevenLabs guidance.
-
Multi‑layer defense: Combine VoxEQ’s passive fraud screen (anomaly/impersonation/synthetic checks) with KBA or OTP for strong risk‑based decisions. VoxEQ Verify works without enrollment or storing PII/voiceprints and provides watch‑list and synthetic detection. VoxEQ Verify, Product guide, TTEC Digital + VoxEQ.
-
Isolation: Keep pre‑auth and post‑auth tools in separate sub‑agents (principle of least privilege). Only the post‑auth sub‑agent is allowed to call privileged APIs.
-
Language and privacy: VoxEQ is language‑agnostic and privacy‑preserving (no customer PII/voiceprints stored). VoxEQ Verify, AI Ethics.
Dynamic variables used by the agent
Use dynamic variables to store tool outcomes that gate access and drive routing.
| Variable | Type | Purpose |
|---|---|---|
| auth_is_verified | boolean | Final gate to privileged actions/sub‑agents |
| auth_method | string | "kba" |
| auth_confidence | number (0–1) | Confidence score for ID/V result |
| auth_subject_id | string | Internal subject/account ID after lookup |
| fraud_risk | number (0–1) | VoxEQ risk score (higher = riskier) |
| fraud_is_synthetic | boolean | VoxEQ synthetic/deepfake signal |
| fraud_watchlist_hit | boolean | VoxEQ watch‑list match |
| system__caller_id | string | Telephony system variable (if provided) — per ElevenLabs |
Note: ElevenLabs exposes telephony system variables (e.g., system__caller_id) and recommends gating with dispatch tools that return boolean success/failure. ElevenLabs guidance.
Pattern 1 — KBA server tool (copy‑paste)
A minimal KBA endpoint that validates answers, returns a deterministic boolean, and emits mapping‑friendly fields.
// server/kba.ts — Node.js/Express
import express from 'express';
import rateLimit from 'express-rate-limit';
import crypto from 'crypto';
const app = express();
app.use(express.json());
// Per-caller rate limit (e.g., 5 KBA attempts / 10 minutes)
const kbaLimiter = rateLimit({
windowMs: 10 * 60 * 1000,
max: 5,
keyGenerator: (req) => `${req.body.sessionId || ''}:${req.body.callerId || req.ip}`
});
// Stub: replace with your secure KBA service (e.g., TU TruValidate, LexisNexis InstantID KBAs)
function checkAnswers(questions: { id: string; answer: string }[]): { ok: boolean; score: number } {
// Example scoring: 0.0–1.0 based on number of correct answers
const correct = questions.filter(q => crypto.createHash('sha256').update(q.id).digest('hex').slice(0,2) === q.answer).length;
const score = questions.length ? correct / questions.length: 0;
return { ok: score >= 0.66, score };
}
app.post('/tools/kba/verify', kbaLimiter, (req, res) => {
const { sessionId, callerId, subjectId, questions } = req.body;
if (!sessionId || !Array.isArray(questions)) return res.status(400).json({ error: 'bad_request' });
const { ok, score } = checkAnswers(questions);
res.json({
tool: 'kba.verify',
success: ok,
auth_method: 'kba',
auth_confidence: score,
auth_subject_id: subjectId || null
});
});
app.listen(3001, () => console.log('KBA tool listening on 3001'));
Tool output (example):
{
"tool": "kba.verify",
"success": true,
"auth_method": "kba",
"auth_confidence": 0.83,
"auth_subject_id": "0038b00002ABCDEF"
}
Map JSON fields to dynamic variables:
-
auth_is_verified ← success
-
auth_method ← auth_method
-
auth_confidence ← auth_confidence
-
auth_subject_id ← auth_subject_id
Pattern 2 — OTP with expiry, retries, and rate limits (copy‑paste)
Server‑side OTP generator/validator with 3–5 minute TTL, max 3 attempts, and send‑rate limits.
// server/otp.ts — Node.js/Express + Redis
import express from 'express';
import rateLimit from 'express-rate-limit';
import { createClient } from 'redis';
const app = express();
app.use(express.json());
const r = createClient({ url: process.env. REDIS_URL });
await r.connect();
const sendLimiter = rateLimit({ windowMs: 15*60*1000, max: 3 }); // 3 sends / 15 min
function genCode() { return (Math.floor(100000 + Math.random()*900000)).toString(); }
// Send OTP
app.post('/tools/otp/send', sendLimiter, async (req, res) => {
const { sessionId, destination, channel } = req.body; // channel: 'sms' | 'email'
if (!sessionId || !destination) return res.status(400).json({ error: 'bad_request' });
const code = genCode();
const key = `otp:${sessionId}`;
await r.hSet(key, {
code, attempts: '0', destination, channel
});
await r.expire(key, 4*60); // 4 minutes TTL (3–5 min recommended)
// Integrate your SMS/Email provider here. Do not log the code in production.
// await smsProvider.send(destination, `Your verification code is ${code}`)
res.json({ tool: 'otp.send', success: true, ttl_seconds: 240 });
});
// Verify OTP
app.post('/tools/otp/verify', async (req, res) => {
const { sessionId, code } = req.body;
if (!sessionId || !code) return res.status(400).json({ error: 'bad_request' });
const key = `otp:${sessionId}`;
const data = await r.hGetAll(key);
if (!data || !data.code) return res.json({ tool: 'otp.verify', success: false, reason: 'expired' });
const attempts = Number(data.attempts || '0');
if (attempts >= 3) return res.json({ tool: 'otp.verify', success: false, reason: 'max_attempts' });
const ok = code === data.code;
await r.hSet(key, { attempts: String(attempts + 1) });
if (ok) await r.del(key);
res.json({
tool: 'otp.verify',
success: ok,
auth_method: 'otp',
auth_confidence: ok ? 1.0: 0.0
});
});
app.listen(3002, () => console.log('OTP tool listening on 3002'));
Map JSON fields as in Pattern 1. Recommended UX policies:
-
Max 3 verification attempts per OTP issuance
-
Cool‑off after max attempts (e.g., 10 minutes or step‑up to KBA)
-
Do not reveal whether a subject/account exists
-
Bind OTP to sessionId and destination; do not reuse across sessions
Guidance: ElevenLabs recommends tool‑based boolean outcomes and gating workflows on success/failure. ElevenLabs guidance.
Pattern 3 — Fraud screen with VoxEQ Verify (copy‑paste)
VoxEQ Verify provides enrollment‑free, privacy‑preserving fraud detection (mismatch, synthetic/deepfake, watch‑list) within seconds, in any language. Integrate via your CCaaS (e.g., Genesys AppFoundry) or a thin proxy that normalizes outputs for your agent. VoxEQ Verify (Genesys AppFoundry), VoxEQ Verify, TTEC Digital + VoxEQ.
Example proxy response format (do not assume vendor field names):
{
"tool": "fraud.voxeq.screen",
"success": true,
"fraud_risk": 0.27,
"fraud_is_synthetic": false,
"fraud_watchlist_hit": false
}
Map to variables:
-
fraud_risk ← fraud_risk (threshold e.g., 0.7)
-
fraud_is_synthetic ← fraud_is_synthetic
-
fraud_watchlist_hit ← fraud_watchlist_hit
Policy examples:
-
If fraud_watchlist_hit = true → route to human fraud desk
-
If fraud_is_synthetic = true or fraud_risk ≥ 0.7 → require OTP+KBA step‑up
-
Else proceed with normal ID/V
Background: VoxEQ performs physiology‑based analysis, with a privacy‑first architecture and real‑time watch‑list. Product guide, AI Ethics.
Workflow gating expressions (pre‑auth vs post‑auth)
Implement gating in the ElevenLabs workflow using dispatch tools and conditional routing per platform guidance. Example logic (pseudo‑expressions; adapt to your builder):
// Fraud gate
IF fraud_watchlist_hit == true THEN route("agent_fraud_desk")
ELSE IF fraud_is_synthetic == true OR fraud_risk >= 0.7 THEN route("auth_step_up")
ELSE route("auth_primary")
// Auth primary (KBA or OTP)
IF tools.kba_verify.success == true OR tools.otp_verify.success == true THEN
set(auth_is_verified=true, auth_method = tools.kba_verify.success ? 'kba': 'otp', auth_confidence = max(tools.kba_verify.auth_confidence, tools.otp_verify.auth_confidence))
route("subagent_privileged")
ELSE
route("auth_retry_or_handoff")
Tip: Populate auth_subject_id as early as possible (caller lookup by system__caller_id or prior CRM context) and confirm it post‑auth.
Post‑auth transfer to privileged sub‑agents
-
Pre‑auth agent: greeting, intent capture, VoxEQ fraud screen, choose auth path, no privileged tools.
-
Post‑auth sub‑agent (Payments, Profile, Claims): has scoped tools (e.g., payments.transfer, profile.update). Enter only if auth_is_verified = true and fraud policy permits.
-
Audit: include auth_method, auth_confidence, timestamps, and fraud signals in tool call metadata for downstream governance.
Sample end‑to‑end flow: VoxEQ Verify → Salesforce → transactions → handoff
This flow demonstrates layered defense and least privilege.
1) Call connects → immediately run VoxEQ fraud screen. 2) If high risk, step‑up auth (OTP+KBA); else proceed with primary auth. 3) On success, lookup caller in Salesforce, fetch recent transactions, then hand off to Payments sub‑agent.
Server tool: Salesforce lookup + transactions (OAuth 2.0 user‑agent or client‑credentials; store tokens securely).
// server/sfdc.ts — minimal SFDC query examples
import express from 'express';
import fetch from 'node-fetch';
const app = express();
app.use(express.json());
async function sfdcQuery(instanceUrl: string, accessToken: string, soql: string) {
const r = await fetch(`${instanceUrl}/services/data/v60.0/query?q=${encodeURIComponent(soql)}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!r.ok) throw new Error(`SFDC ${r.status}`);
return r.json();
}
app.post('/tools/sfdc/lookup-and-transactions', async (req, res) => {
const { accessToken, instanceUrl, phoneE164, subjectId } = req.body;
// 1) Resolve contact by phone or subjectId
const contactSoql = subjectId
? `SELECT Id, AccountId, Name FROM Contact WHERE Id='${subjectId}' LIMIT 1`: `SELECT Id, AccountId, Name FROM Contact WHERE Phone='${phoneE164}' OR MobilePhone='${phoneE164}' LIMIT 1`;
const contact = await sfdcQuery(instanceUrl, accessToken, contactSoql);
if (!contact.records?.length) return res.json({ tool: 'sfdc.lookup', success: false, reason: 'not_found' });
const c = contact.records[0];
// 2) Fetch last 5 transactions (example custom object)
const tx = await sfdcQuery(
instanceUrl,
accessToken,
`SELECT Id, Amount__c, Merchant__c, Date__c FROM Transaction__c WHERE Account__c='${c. AccountId}' ORDER BY Date__c DESC LIMIT 5`
);
res.json({
tool: 'sfdc.lookup_and_transactions',
success: true,
auth_subject_id: c. Id,
account_id: c. AccountId,
contact_name: c. Name,
transactions: tx.records
});
});
app.listen(3003, () => console.log('SFDC tool listening on 3003'));
Agent orchestration (pseudo):
START → tool(fraud.voxeq.screen) → FRAUD_GATE
FRAUD_GATE → if watchlist_hit → route(human_fraud)
→ else if high_risk → route(auth_step_up)
→ else → route(auth_primary)
AUTH_PRIMARY → tool(otp.send) → prompt_for_code → tool(otp.verify)
→ if success → set(auth_is_verified=true) → tool(sfdc.lookup_and_transactions)
→ summarize + offer options → route(subagent_payments)
→ else → allow_retry_or_handoff
SUBAGENT_PAYMENTS (privileged) → tool(payments.transfer) …
Prompt/virtual‑agent enrichment with VoxEQ Persona/Prompt (optional)
To improve first‑turn accuracy, pass VoxEQ’s demographic context to your virtual agent alongside auth state. VoxEQ Persona and Prompt are available for Genesys and API‑first deployments. VoxEQ Persona, VoxEQ Prompt, AppFoundry Persona, AppFoundry Prompt.
Recommended usage:
-
Use fraud signals to pick the auth path, not to change tone.
-
Use demographic context to tailor grounding/RAG and script tone after auth, never as a sole factor for decisions.
Security and compliance checklist
-
Gate every privileged tool on auth_is_verified == true (server‑asserted)
-
Log: auth_method, auth_confidence, fraud signals, timestamps, and tool versions
-
OTP: 3–5 minute TTL; max 3 attempts; send rate ≤ 3 per 15 minutes; bind to session
-
KBA: never disclose which answers failed; throttle attempts; consider step‑up rules
-
Privacy: do not store PII/voiceprints with VoxEQ; rely on vendor’s privacy‑first approach. VoxEQ Verify, AI Ethics
-
Layered defense: pair VoxEQ screening with OTP/KBA per ElevenLabs best practices. ElevenLabs guidance
References
-
ElevenLabs: Designing secure caller identity authentication flows for voice agents — tool/dispatch, dynamic variables, workflow gating
-
https://elevenlabs.io/blog/designing-secure-caller-identity-authentication-flows-for-voice-agents
-
VoxEQ Verify — enrollment‑free fraud screen; watch‑list; privacy‑first; language‑agnostic
-
https://www.voxeq.ai/verify
-
https://www.voxeq.ai/product-guide
-
Genesys AppFoundry listing: https://appfoundry.genesys.com/filter/genesyscloud/listing/7cef096c-6408-42ba-94a3-b4c4f98106c8
-
VoxEQ CX enrichment
-
Persona: https://www.voxeq.ai/persona
-
Prompt: https://www.voxeq.ai/prompt
-
AppFoundry Persona: https://appfoundry.genesys.com/filter/genesyscloud/listing/858317c1-8798-4978-9349-3c734601f9a8
-
AppFoundry Prompt: https://appfoundry.genesys.com/filter/genesyscloud/listing/7556a9ae-f2a8-42ac-8f73-d0fc26f76715
-
Partnership: TTEC Digital + VoxEQ (SmartApps Cloud)
-
https://www.ttecdigital.com/news/ttec-digital-and-voxeq-partner-to-deliver-real-time-voice-biometrics-in-smartapps-cloud