> ## Documentation Index
> Fetch the complete documentation index at: https://docs.poly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Recipe: Caller ID validation

> Verify a caller's identity before allowing access to sensitive account information.

export const LessonMeta = ({level, difficulty, time}) => {
  const levelConfig = {
    1: {
      badge: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
      label: 'Level 1'
    },
    2: {
      badge: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
      label: 'Level 2'
    },
    3: {
      badge: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
      label: 'Level 3'
    }
  };
  const difficultyConfig = {
    Beginner: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
    Intermediate: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
    Advanced: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
  };
  const lvl = levelConfig[level] || levelConfig[1];
  const diffColor = difficultyConfig[difficulty] || difficultyConfig['Beginner'];
  return <div className="flex flex-wrap items-center gap-2 my-4 not-prose">
      <span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${lvl.badge}`}>
        {lvl.label}
      </span>
      <span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${diffColor}`}>
        {difficulty}
      </span>
      {time && <span className="inline-flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
          <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
          </svg>
          {time}
        </span>}
    </div>;
};

export const Quiz = ({questions = []}) => {
  const [selected, setSelected] = useState({});
  const [resetCount, setResetCount] = useState(0);
  const letters = ['A', 'B', 'C', 'D'];
  const handleSelect = (qIdx, optIdx) => {
    if (selected[qIdx] !== undefined) return;
    setSelected(prev => ({
      ...prev,
      [qIdx]: optIdx
    }));
  };
  const handleReset = () => {
    setSelected({});
    setResetCount(c => c + 1);
  };
  if (!questions?.length) return null;
  const getOptionClasses = ({hasAnswered, isThisCorrect, isThisSelected}) => {
    if (!hasAnswered) {
      return {
        btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-pointer border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50 hover:shadow-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:border-gray-500 dark:hover:bg-gray-700',
        badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-300',
        icon: null
      };
    }
    if (isThisCorrect) {
      return {
        btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-default border-green-400 bg-green-50 text-green-900 font-medium dark:border-green-500 dark:bg-green-950 dark:text-green-100',
        badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-green-500 text-white dark:bg-green-500',
        icon: <svg className="shrink-0 w-4 h-4 text-green-500 dark:text-green-400 ml-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
          </svg>
      };
    }
    if (isThisSelected) {
      return {
        btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-default border-red-400 bg-red-50 text-red-900 dark:border-red-500 dark:bg-red-950 dark:text-red-100',
        badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-red-500 text-white dark:bg-red-500',
        icon: <svg className="shrink-0 w-4 h-4 text-red-400 dark:text-red-400 ml-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
      };
    }
    return {
      btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-default border-gray-100 bg-white text-gray-400 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-500',
      badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-500',
      icon: null
    };
  };
  return <div key={resetCount} className="my-6">
      {questions.map((q, qIdx) => {
    const answer = selected[qIdx];
    const hasAnswered = answer !== undefined;
    const isCorrect = answer === q.correct;
    return <div key={String(qIdx)} className="mb-8">
            <p className="flex items-start gap-2.5 font-semibold text-sm mb-3 mt-0 leading-relaxed text-gray-900 dark:text-gray-100">
              <span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-800 dark:bg-gray-200 text-white dark:text-gray-900 text-xs font-bold shrink-0 mt-px leading-none">
                {qIdx + 1}
              </span>
              {q.q}
            </p>

            <div className="flex flex-col gap-2">
              {q.options.map((opt, i) => {
      const isThisCorrect = i === q.correct;
      const isThisSelected = i === answer;
      const {btn, badge, icon} = getOptionClasses({
        hasAnswered,
        isThisCorrect,
        isThisSelected
      });
      return <button key={String(i)} type="button" onClick={() => handleSelect(qIdx, i)} className={btn}>
                    <span className={badge}>{letters[i]}</span>
                    <span className="flex-1">{opt}</span>
                    {icon}
                  </button>;
    })}
            </div>

            {hasAnswered ? <div className={`mt-3 py-3 pl-4 pr-3.5 rounded-r-xl text-sm leading-relaxed border-l-4 ${isCorrect ? 'border-green-500 bg-green-50 dark:bg-green-950 dark:border-green-500' : 'border-red-500 bg-red-50 dark:bg-red-950 dark:border-red-500'}`}>
                <span className={`font-semibold ${isCorrect ? '!text-green-800 dark:!text-green-200' : '!text-red-800 dark:!text-red-200'}`}>
                  {isCorrect ? 'Correct.' : 'Not quite.'}
                </span>{' '}
                <span className="!text-gray-700 dark:!text-gray-300">{q.explanation}</span>
              </div> : null}
          </div>;
  })}

      <button type="button" onClick={handleReset} className="mt-1 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 underline underline-offset-2 cursor-pointer transition-colors duration-150">
        Reset quiz
      </button>
    </div>;
};

<LessonMeta level={2} difficulty="Intermediate" time="10 min" />

Many voice agents need to confirm who they're speaking to before sharing account data, processing requests, or completing transactions. This recipe shows how to collect an identifier, verify it against your backend, and gate the rest of the conversation on the result.

## When to use this

Use this pattern when:

* The agent handles personal account data (balances, orders, medical records)
* Compliance or security policy requires identity verification before proceeding
* You want to personalize the conversation based on confirmed identity

## The complete pattern

### Step 1 – Collect the identifier

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def collect_identifier(conv, flow, date_of_birth: str) -> str:
    # Lightweight format check before hitting the API
    import re
    if not re.match(r"^\d{2}/\d{2}/\d{4}$", date_of_birth):
        return "That doesn't look like a valid date. Ask the caller for their date of birth in DD/MM/YYYY format."

    conv.state["dob_input"] = date_of_birth
    flow.goto_step("verify_identity")
    return f"Date of birth received: {date_of_birth}."
```

**Step prompt:** "Ask the caller for their date of birth in DD/MM/YYYY format. Once they provide it, call `collect_identifier`."

### Step 2 – Verify against the backend

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def verify_identity(conv, flow) -> dict:
    dob = conv.state.get("dob_input")
    caller_number = conv.state.get("caller_ani")  # Set by telephony integration if available

    result = lookup_account(caller_number=caller_number, dob=dob)

    if result.get("verified"):
        # Store account data for the rest of the conversation
        conv.state["account_id"] = result["account_id"]
        conv.state["caller_name"] = result.get("name", "")
        conv.state["identity_verified"] = True
        flow.exit_flow()
        return {"content": f"Identity verified. Account holder: {result.get('name')}. Proceed with account queries."}

    attempts = conv.state.get("identity_attempts", 0) + 1
    conv.state["identity_attempts"] = attempts

    if attempts >= 3:
        return {
            "utterance": "I wasn't able to verify your identity. For your security, I'll connect you with our team directly.",
            "handoff": True,
        }

    flow.goto_step("collect_identifier")
    return {"content": f"Verification failed (attempt {attempts} of 3). Ask the caller to try again."}
```

## Full flow

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart TD
    A[Collect date of birth] --> B{Format valid?}
    B -->|No| A
    B -->|Yes| C[Verify against backend]
    C -->|Verified| D[Store identity, exit flow]
    C -->|Failed, attempts < 3| A
    C -->|Failed, attempts >= 3| E["Handoff to live agent"]
    D --> F[Agent continues with verified context]
```

## Using verified identity downstream

After verification, other functions can check `conv.state.get("identity_verified")` before returning sensitive data:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_account_balance(conv) -> str:
    if not conv.state.get("identity_verified"):
        return "Identity has not been verified. Do not share account information. Ask the caller to verify their identity first."

    account_id = conv.state.get("account_id")
    balance = fetch_balance(account_id)
    return f"Account balance: £{balance:.2f}."
```

<Tip>
  Use a consistent `identity_verified` flag pattern across all functions that touch sensitive data. This makes it easy to audit which functions are gated and which are not.
</Tip>

## Key decisions

<AccordionGroup>
  <Accordion title="Why format-check before hitting the API?" icon="shield-halved">
    A format check in `collect_identifier` catches obvious transcription errors (missing digits, wrong separator) before you make an API call that will fail anyway. It also gives the caller a more specific error message — "That doesn't look like a valid date" rather than a generic "verification failed."
  </Accordion>

  <Accordion title="Why store caller_name after verification?" icon="user">
    Storing the verified caller's name in `conv.state` lets the agent personalise subsequent responses — "Great, I can see your account, Aaron" — without making a second API call. Only store what you'll use.
  </Accordion>

  <Accordion title="Why not use utterance for the failure messages?" icon="comment">
    For retry messages, returning `content` lets the LLM rephrase the request naturally ("Sorry, that didn't match — could you try your date of birth again?"). Using a hard-coded utterance would make every retry sound identical.
  </Accordion>
</AccordionGroup>

## Check your understanding

<Quiz
  questions={[
{
q: "A function that returns account balance checks `conv.state.get('identity_verified')` before proceeding. Why not rely on the flow to guarantee this?",
options: [
  "Flows don't have access to conv.state",
  "It's a defensive guard — functions can be called outside of flows, and explicit checks prevent accidental data exposure",
  "The LLM can call any function at any time, ignoring flow step order",
  "It makes the function faster by skipping the API call",
],
correct: 1,
explanation: "Functions can be referenced in the Behavior field or topics, not just flows. A function that returns sensitive data should verify the precondition itself rather than trusting that it will only ever be called from a verified flow step.",
}
]}
/>

***

<CardGroup cols={2}>
  <Card title="← Retry with handoff" icon="arrow-left" href="/learn/recipes/retry-with-handoff">
    Previous recipe
  </Card>

  <Card title="Intent-based routing →" icon="arrow-right" href="/learn/recipes/smart-routing">
    Next recipe
  </Card>
</CardGroup>
