> ## 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: SMS confirmation

> Send an SMS after collecting explicit consent — the full collect → consent → send → confirm loop.

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" />

This recipe covers the complete SMS consent-and-send pattern: confirm the number, ask for consent, send the message, and confirm delivery — all without the agent making promises it can't keep.

## When to use this

Use this pattern when:

* You need to send a follow-up (link, confirmation, summary) after a call interaction
* Compliance or brand requirements mean you must collect explicit consent before sending
* You want a hard confirmation step so the agent never sends SMS without the user agreeing

## The complete pattern

### Step 1 – Collect the phone number

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def collect_phone_number(conv, flow, phone_number: str) -> str:
    # Store the number; validation happens in the next step
    conv.state["sms_phone"] = phone_number
    flow.goto_step("confirm_consent")
    return f"Phone number {phone_number} noted."
```

**Step prompt:** "Ask the caller for the phone number they'd like the message sent to. Once they provide it, call `collect_phone_number`."

### Step 2 – Ask for consent

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def record_consent(conv, flow, consented: bool) -> str:
    if consented:
        conv.state["sms_consent"] = True
        flow.goto_step("send_sms")
        return "Consent given."
    else:
        # Respect the refusal and exit the flow gracefully
        flow.exit_flow()
        return "The caller declined SMS. Do not attempt to send."
```

**Step prompt:** "Ask the caller to confirm they consent to receiving an SMS at the number they provided. Call `record_consent` with `consented=True` if they agree, `consented=False` if they decline."

<Warning>
  Never send an SMS if `consented` is `False`. If the model calls `record_consent(consented=False)`, exit the flow — do not loop back to ask again.
</Warning>

### Step 3 – Send and confirm

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests

def send_confirmation_sms(conv, flow) -> dict:
    phone = conv.state.get("sms_phone")
    consent = conv.state.get("sms_consent")

    if not consent:
        # Defensive check — should not reach here without consent
        flow.exit_flow()
        return {"content": "No consent on record. SMS not sent."}

    # Replace with your real SMS provider call
    response = requests.post(
        "https://your-sms-provider.com/send",
        json={"to": phone, "body": "Here's the information you requested: ..."},
        timeout=5,
    )

    if response.status_code == 200:
        flow.exit_flow()
        return {"content": f"SMS sent successfully to {phone}."}
    else:
        return {"content": f"SMS delivery failed (status {response.status_code}). Tell the caller there was a problem and offer to try again or provide the information verbally."}
```

**Step prompt:** "The caller has consented. Call `send_confirmation_sms` to send the message and confirm delivery."

## Flow structure

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart TD
    A[Caller requests SMS] --> B[Collect phone number]
    B --> C[Ask for consent]
    C -->|Consented| D[Send SMS]
    C -->|Declined| E[Exit flow gracefully]
    D -->|Success| F[Confirm to caller]
    D -->|Failure| G[Offer alternative]
```

## Key decisions

<AccordionGroup>
  <Accordion title="Why store state between steps?" icon="database">
    Each flow step runs in a separate LLM request. The phone number collected in Step 1 must be stored in `conv.state` to be accessible in Step 3 — the LLM does not carry it forward automatically.
  </Accordion>

  <Accordion title="Why exit the flow on decline?" icon="door-open">
    If the caller declines consent, `flow.exit_flow()` returns control to the LLM, which can continue the conversation naturally. Do not loop back to the consent step — that would feel coercive.
  </Accordion>

  <Accordion title="Why check consent again in Step 3?" icon="shield-check">
    The defensive consent check in `send_confirmation_sms` ensures you never send an SMS even if the flow logic has a bug or is called out of order. Treat it as a safety net.
  </Accordion>
</AccordionGroup>

## Check your understanding

<Quiz
  questions={[
{
q: "Why does `send_confirmation_sms` check `conv.state.get('sms_consent')` even though consent was already checked in `record_consent`?",
options: [
  "It's required by the SMS provider API",
  "It's a defensive check — if the flow is ever called out of order, no SMS is sent without consent",
  "The LLM forgets state between steps, so it must be re-checked",
  "It triggers a second consent request for compliance logging",
],
correct: 1,
explanation: "The check in `send_confirmation_sms` is a defensive guard — it protects against the function being called out of sequence or by a bug in the flow. `conv.state` persists correctly between steps, but checking critical flags before taking irreversible actions is good practice.",
}
]}
/>

***

<CardGroup cols={2}>
  <Card title="← Back to Recipes" icon="arrow-left" href="/learn/recipes/introduction">
    All recipes
  </Card>

  <Card title="Retry with handoff →" icon="arrow-right" href="/learn/recipes/retry-with-handoff">
    Next recipe
  </Card>
</CardGroup>
