# IVR + Voicemail-to-Email Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Deploy a two-level IVR on 1-877-DOPE-707 (ScopeDOPE subaccount) that routes callers to voicemail, delivers the recording + transcription by email with subject tags and custom headers, and is fully documented in the Twilio Architecture Reference and Operating Runbook.

**Architecture:** TwiML Bins hosted in Twilio (no external server required) handle the IVR menu logic. Twilio's built-in `<Record>` verb with `transcribeCallback` delivers the MP3 and transcription to a SendGrid transactional email via the Twilio SendGrid API. Each menu path maps to a specific recipient address and email tags. `feedback-service` integration is explicitly deferred — this plan is self-contained within Twilio.

**Tech Stack:** Twilio TwiML Bins, Twilio Voice (TwiML `<Gather>`, `<Record>`, `<Say>`), SendGrid Transactional Email API, Twilio Functions (Node.js) for voicemail dispatch logic.

---

## Prerequisite Checklist

Before starting, confirm:
- [ ] Logged in to Twilio Console at `https://console.twilio.com`
- [ ] Switched to the **ScopeDOPE subaccount** (`AC7cc6b6571c9b0907b73b783a5422a2c4`) — account switcher is the dropdown in the top-left corner
- [ ] SendGrid is activated (CBB-102 must be complete — Console → Explore Products → SendGrid)
- [ ] A SendGrid API key with **Mail Send** permission exists. If not: SendGrid Console → Settings → API Keys → Create API Key → Restricted Access → Mail Send → Full Access → Create & View → copy the key immediately (shown only once)
- [ ] `phone@scope-dope.com` and `phone@coldboreballisticsllc.com` mailboxes exist in M365 and are receiving email

---

## Routing Map

| Caller presses | Path | Subject prefix | To address | X-CBB-Option |
|---|---|---|---|---|
| 1 | Bug report | `[Bug Report]` | `phone@scope-dope.com` | `1` |
| 2 | Feature request | `[Feature Request]` | `phone@scope-dope.com` | `2` |
| 3 | General support | `[Support]` | `phone@scope-dope.com` | `3` |
| 4 | Business inquiry | `[Business Inquiry]` | `phone@coldboreballisticsllc.com` | `4` |
| 9 → 1 (or timeout) | ScopeDOPE general | `[General — ScopeDOPE]` | `phone@scope-dope.com` | `9-1` |
| 9 → 2 | CBB general | `[General — CBB]` | `phone@coldboreballisticsllc.com` | `9-2` |

**Press 0** repeats the current menu (main or submenu).  
**Press \*** returns to main menu from any level (including main menu itself — replays greeting).  
**Voicemail limit:** 120 seconds.  
**X-CBB-Called:** Set to the Twilio `To` number from the webhook payload (e.g., `+18773673707`).

---

## IVR Script (verbatim)

### Main Menu

> *"Thank you for calling ScopeDOPE — the world's only Ballistic Computer.*
>
> *To report a technical issue, press 1.*
> *To suggest a new feature or improvement, press 2.*
> *For general support, press 3.*
> *For marketing, partnership, or business inquiries, press 4.*
> *For all other inquiries, press 9.*
> *To repeat these options, press 0."*

### Press 9 Submenu

> *"You've reached the general inquiries line.*
>
> *For matters related to ScopeDOPE, press 1, or stay on the line.*
> *For matters related to Cold Bore Ballistics LLC — the company behind ScopeDOPE — press 2.*
> *To return to the main menu, press \*.*
> *To repeat these options, press 0."*

### Voicemail Prompt (all paths)

> *"Please leave your message after the tone. You have two minutes. Press \# when you are finished."*

---

## File Structure

| File | Location | Purpose |
|---|---|---|
| `ivr-main.xml` | Twilio TwiML Bin | Main menu `<Gather>` + `<Say>` |
| `ivr-submenu-9.xml` | Twilio TwiML Bin | Press-9 submenu `<Gather>` + `<Say>` |
| `ivr-voicemail.js` | Twilio Function | Receives `<Record>` callback, dispatches email via SendGrid |
| `Twilio_Architecture_Reference.md` | OneDrive/03_-_Confidential/Infrastructure/Twilio/ | Updated with TwiML Bin SIDs, Function SID, env vars |
| `Twilio_Operating_Runbook.md` | OneDrive/03_-_Confidential/Infrastructure/Twilio/ | New procedures: update IVR script, rotate SendGrid key, test IVR |

---

## Task 1: Create the Twilio Function service

Twilio Functions is a serverless Node.js runtime hosted in Twilio. It will receive the recording webhook and send email via SendGrid.

- [ ] **Step 1: Open Twilio Functions**

  In the Console (ScopeDOPE subaccount), navigate to:
  **Explore Products → Developer Tools → Functions & Assets → Services**

  Click **Create Service**.

- [ ] **Step 2: Name the service**

  Name: `ivr-voicemail-handler`

  Click **Next**. You are now in the Functions editor.

- [ ] **Step 3: Add environment variables**

  Click **Environment Variables** (bottom panel).

  Add the following:

  | Key | Value |
  |---|---|
  | `SENDGRID_API_KEY` | Your SendGrid API key (starts with `SG.`) |
  | `FROM_EMAIL` | `noreply@scope-dope.com` |

  Click **Save**.

- [ ] **Step 4: Add the SendGrid dependency**

  Click **Dependencies** (bottom panel).

  Add:
  - Module: `@sendgrid/mail`
  - Version: `8.1.3`

  Click **Save**.

- [ ] **Step 5: Create the voicemail handler function**

  Click **Add** → **Add Function**.

  Path: `/voicemail`

  Replace the default content with:

  ```javascript
  const sgMail = require('@sendgrid/mail');

  const ROUTE_MAP = {
    '1':   { subject: '[Bug Report]',          to: 'phone@scope-dope.com' },
    '2':   { subject: '[Feature Request]',     to: 'phone@scope-dope.com' },
    '3':   { subject: '[Support]',             to: 'phone@scope-dope.com' },
    '4':   { subject: '[Business Inquiry]',    to: 'phone@coldboreballisticsllc.com' },
    '9-1': { subject: '[General — ScopeDOPE]', to: 'phone@scope-dope.com' },
    '9-2': { subject: '[General — CBB]',       to: 'phone@coldboreballisticsllc.com' },
  };

  exports.handler = async function (context, event, callback) {
    sgMail.setApiKey(context.SENDGRID_API_KEY);

    const option    = event.option || 'unknown';
    const calledNum = event.To    || 'unknown';
    const callerNum = event.From  || 'unknown';
    const recordingUrl  = event.RecordingUrl  || '';
    const transcription = event.TranscriptionText || '(transcription not yet available)';

    const route = ROUTE_MAP[option] || { subject: '[Voicemail]', to: 'phone@scope-dope.com' };

    const msg = {
      to:      route.to,
      from:    context.FROM_EMAIL,
      subject: `${route.subject} Voicemail from ${callerNum}`,
      text: [
        `Called number : ${calledNum}`,
        `Caller        : ${callerNum}`,
        `Menu option   : ${option}`,
        ``,
        `Transcription:`,
        transcription,
        ``,
        `Recording: ${recordingUrl}.mp3`,
      ].join('\n'),
      headers: {
        'X-CBB-Called': calledNum,
        'X-CBB-Option': option,
      },
    };

    try {
      await sgMail.send(msg);
    } catch (err) {
      console.error('SendGrid error:', JSON.stringify(err.response?.body || err.message));
    }

    // Always return empty TwiML — Twilio requires a response
    const twiml = new Twilio.twiml.VoiceResponse();
    return callback(null, twiml);
  };
  ```

  Click **Save**.

- [ ] **Step 6: Deploy the service**

  Click **Deploy All** (bottom of the editor).

  After deployment, note the **Function URL** — it will look like:
  `https://ivr-voicemail-handler-XXXX.twil.io/voicemail`

  You will need this URL in Tasks 2 and 3.

---

## Task 2: Create the main menu TwiML Bin

A TwiML Bin is a static XML document hosted by Twilio. No server required.

- [ ] **Step 1: Open TwiML Bins**

  Console → Explore Products → Developer Tools → TwiML Bins

  Click **Create new TwiML Bin**.

- [ ] **Step 2: Name it**

  Friendly name: `ScopeDOPE IVR — Main Menu`

- [ ] **Step 3: Paste the TwiML**

  Replace `https://ivr-voicemail-handler-XXXX.twil.io/voicemail` with the actual Function URL from Task 1 Step 6.

  Replace `SUBMENU_9_BIN_URL` with the TwiML Bin URL for the press-9 submenu — you will get this after completing Task 3. Come back and update this Bin after Task 3 is done.

  ```xml
  <?xml version="1.0" encoding="UTF-8"?>
  <Response>
    <Gather numDigits="1" action="/handle-main" timeout="10">
      <Say voice="alice">
        Thank you for calling ScopeDOPE — the world's only Ballistic Computer.
        To report a technical issue, press 1.
        To suggest a new feature or improvement, press 2.
        For general support, press 3.
        For marketing, partnership, or business inquiries, press 4.
        For all other inquiries, press 9.
        To repeat these options, press 0.
      </Say>
    </Gather>
    <!-- No input — repeat -->
    <Redirect>MAIN_MENU_BIN_URL</Redirect>
  </Response>
  ```

  > **Note:** The `<Gather action="/handle-main">` routes digit input to a second TwiML Bin (Task 2b below). The `<Redirect>` at the bottom handles the no-input timeout by looping back.

  Click **Create**.

  Note the **TwiML Bin URL** (e.g., `https://handler.twilio.com/twiml/EHXXXXXX`). This is `MAIN_MENU_BIN_URL`.

- [ ] **Step 4: Create the main menu digit handler Bin**

  Click **Create new TwiML Bin**.

  Friendly name: `ScopeDOPE IVR — Main Menu Handler`

  Replace all URLs with actual values from Tasks 1 and 3:

  ```xml
  <?xml version="1.0" encoding="UTF-8"?>
  <Response>
    <!-- Press 0: repeat main menu -->
    <Gather numDigits="1" action="/handle-main" timeout="10" input="dtmf">
      <Say voice="alice">
        <!-- This Bin is only reached after a digit is pressed via Gather action -->
      </Say>
    </Gather>
  </Response>
  ```

  > **Note:** Because TwiML Bins are static XML, digit routing logic is handled by chaining Bins. Create one Bin per voicemail destination below.

- [ ] **Step 5: Create voicemail Bins for options 1, 2, 3, 4**

  For each option, create a TwiML Bin named `ScopeDOPE IVR — Voicemail <option>`.

  Use this template, substituting `OPTION_NUMBER` and `FUNCTION_URL`:

  ```xml
  <?xml version="1.0" encoding="UTF-8"?>
  <Response>
    <Say voice="alice">
      Please leave your message after the tone. You have two minutes. Press hash when you are finished.
    </Say>
    <Record
      maxLength="120"
      transcribe="true"
      transcribeCallback="FUNCTION_URL?option=OPTION_NUMBER"
      finishOnKey="#"
      playBeep="true"
    />
    <Say voice="alice">Thank you for your message. Goodbye.</Say>
    <Hangup/>
  </Response>
  ```

  Create four Bins:
  - `ScopeDOPE IVR — Voicemail 1` → `option=1`
  - `ScopeDOPE IVR — Voicemail 2` → `option=2`
  - `ScopeDOPE IVR — Voicemail 3` → `option=3`
  - `ScopeDOPE IVR — Voicemail 4` → `option=4`

  Note all four Bin URLs.

---

## Task 3: Create the press-9 submenu TwiML Bin

- [ ] **Step 1: Create voicemail Bins for 9-1 and 9-2**

  Using the same voicemail template from Task 2 Step 5:

  - `ScopeDOPE IVR — Voicemail 9-1` → `option=9-1`
  - `ScopeDOPE IVR — Voicemail 9-2` → `option=9-2`

  Note both Bin URLs.

- [ ] **Step 2: Create the submenu Bin**

  Friendly name: `ScopeDOPE IVR — Submenu 9`

  Replace `VOICEMAIL_9_1_URL`, `VOICEMAIL_9_2_URL`, `MAIN_MENU_BIN_URL` with actual URLs:

  ```xml
  <?xml version="1.0" encoding="UTF-8"?>
  <Response>
    <Gather numDigits="1" timeout="10" action="SUBMENU_9_HANDLER_URL">
      <Say voice="alice">
        You've reached the general inquiries line.
        For matters related to ScopeDOPE, press 1, or stay on the line.
        For matters related to Cold Bore Ballistics L L C — the company behind ScopeDOPE — press 2.
        To return to the main menu, press star.
        To repeat these options, press 0.
      </Say>
    </Gather>
    <!-- No input timeout — default to ScopeDOPE (9-1) -->
    <Redirect>VOICEMAIL_9_1_URL</Redirect>
  </Response>
  ```

  Note the Bin URL. This is `SUBMENU_9_BIN_URL` — go back to Task 2 Step 3 and update the main menu Bin with this URL.

- [ ] **Step 3: Create the submenu handler Bin**

  Friendly name: `ScopeDOPE IVR — Submenu 9 Handler`

  ```xml
  <?xml version="1.0" encoding="UTF-8"?>
  <Response>
    <!-- Digit routing for submenu 9 -->
    <!-- Press 1 → ScopeDOPE voicemail -->
    <!-- Press 2 → CBB voicemail -->
    <!-- Press * → main menu -->
    <!-- Press 0 → repeat submenu -->
    <!-- Note: Static TwiML cannot branch on digits. Wire digit routing in Console:
         Phone Numbers → +18773673707 → Voice → A call comes in → TwiML Bin →
         Use Twilio Studio for dynamic branching OR use the Function below. -->
  </Response>
  ```

  > **Important:** Static TwiML Bins cannot branch on `<Gather>` digit input without a server. For the submenu handler (and main menu handler), use **Twilio Studio** (drag-and-drop flow builder) or extend the Twilio Function to serve dynamic TwiML. See Task 4.

---

## Task 4: Wire digit routing via Twilio Studio

Twilio Studio is a no-code flow builder in the Console that handles conditional branching — it replaces the static handler Bins for digit routing.

- [ ] **Step 1: Open Twilio Studio**

  Console → Explore Products → Developer Tools → Studio

  Click **Create new flow**.

  Name: `ScopeDOPE IVR`

  Select **Start from scratch** → **Next**.

- [ ] **Step 2: Build the main menu flow**

  The Studio canvas opens. You'll drag widgets from the right panel.

  Add a **Say/Play** widget:
  - Connect it from the **Incoming Call** trigger
  - Text: *(paste main menu script verbatim from IVR Script section above)*
  - Voice: `Polly.Joanna`

  Add a **Gather Input on Call** widget:
  - Connect from the Say/Play widget
  - Input type: DTMF
  - Number of digits: 1
  - Timeout: 10 seconds
  - On timeout: connect back to the Say/Play widget (creates the press-0 / repeat loop)

- [ ] **Step 3: Add digit branches**

  From the Gather widget, you'll see output transitions for each digit. Wire them:

  | Digit | Connect to |
  |---|---|
  | 1 | Voicemail widget (option 1) |
  | 2 | Voicemail widget (option 2) |
  | 3 | Voicemail widget (option 3) |
  | 4 | Voicemail widget (option 4) |
  | 9 | Submenu 9 Say/Play widget |
  | 0 | Back to main menu Say/Play widget |
  | `*` | Back to main menu Say/Play widget (CBB standard — all levels) |

- [ ] **Step 4: Add voicemail widgets for options 1–4**

  For each option, add a **Record Voicemail** widget:
  - Max recording length: 120 seconds
  - Transcribe: Yes
  - Transcription callback: `FUNCTION_URL?option=OPTION_NUMBER`
  - Finish on key: `#`
  - Play beep: Yes

  Add a **Say/Play** widget after each Record widget:
  - Text: *"Thank you for your message. Goodbye."*
  - Connect to **End Call** widget.

- [ ] **Step 5: Build the press-9 submenu**

  Add a **Say/Play** widget for the submenu:
  - Text: *(paste press-9 submenu script verbatim from IVR Script section above)*

  Add a **Gather Input on Call** widget:
  - Number of digits: 1
  - Timeout: 10 seconds
  - On timeout: connect to Voicemail widget for 9-1 (stay on line = ScopeDOPE default)

  Wire digit branches:

  | Digit | Connect to |
  |---|---|
  | 1 | Voicemail widget (option 9-1) |
  | 2 | Voicemail widget (option 9-2) |
  | `*` | Main menu Say/Play widget (all levels — standing CBB IVR standard) |
  | 0 | Back to submenu Say/Play widget |

  Add Record Voicemail widgets for 9-1 and 9-2 using the same pattern as Step 4, with `option=9-1` and `option=9-2`.

- [ ] **Step 6: Publish the flow**

  Click **Publish** (top right).

  Note the **Flow SID** (starts with `FW...`). You will need this in Task 5.

---

## Task 5: Wire the flow to 1-877-DOPE-707

- [ ] **Step 1: Navigate to the phone number**

  Console → Phone Numbers → Manage → Active numbers → click `+18773673707`

  Ensure you are in the **ScopeDOPE subaccount**.

- [ ] **Step 2: Configure Voice**

  Under **Voice & Fax** → **A call comes in**:
  - Change dropdown from **Webhook** to **Studio Flow**
  - Select `ScopeDOPE IVR` from the flow list

  Click **Save**.

- [ ] **Step 3: Test**

  Call `1-877-367-3707` from a personal cell.
  - Verify the greeting plays correctly
  - Press each digit (1, 2, 3, 4, 9, then 9→1, 9→2)
  - Leave a test voicemail on each path
  - Verify emails arrive at `phone@scope-dope.com` and `phone@coldboreballisticsllc.com` with correct subject tags and headers
  - Verify MP3 link and transcription are in the email body

---

## Task 6: Update documentation

- [ ] **Step 1: Update Twilio_Architecture_Reference.md**

  Add a new **IVR & Voicemail** section with:
  - Studio Flow SID (`FW...`)
  - Twilio Function SID and URL
  - Flow name: `ScopeDOPE IVR`
  - Number wired: `+18773673707`
  - Routing map table (copy from this plan)
  - SendGrid env var name (`SENDGRID_API_KEY`) — note location only, never the value

- [ ] **Step 2: Update Twilio_Operating_Runbook.md**

  Add three new procedures:

  **Procedure: Update IVR script**
  - Studio → open `ScopeDOPE IVR` → edit the relevant Say/Play widget text → Publish

  **Procedure: Rotate SendGrid API key for IVR**
  - SendGrid Console → Settings → API Keys → create new key with Mail Send permission → copy key
  - Twilio Console → Functions → ivr-voicemail-handler → Environment Variables → update `SENDGRID_API_KEY` → Deploy All
  - Delete old SendGrid key

  **Procedure: Test IVR end-to-end**
  - Call `+18773673707` → verify greeting → press each option → leave voicemail → confirm email received at correct address with correct subject tag and headers

- [ ] **Step 3: Update Twilio_Architecture_Reference.md cost table**

  Add:
  - Twilio Functions: $0 (free tier — 10,000 invocations/month included)
  - Twilio Studio: $0.01 per active user flow execution (negligible at current volume)
  - Transcription: $0.05/minute (120-second max = $0.10/voicemail max)

---

## Task 7: Close CBB-100

CBB-100 tracked manual call forwarding configuration. The Studio flow replaces that — the number is now wired to the IVR, not a direct forward.

- [ ] Close CBB-100 in Jira with a comment: "Superseded by Studio IVR flow — call forwarding not applicable; number wired directly to ScopeDOPE IVR Studio flow."

---

## Document Control

| Field | Value |
|---|---|
| **Document owner** | `jschwefel@coldboreballisticsllc.com` |
| **Classification** | Confidential (03_-_Confidential/) |
| **Location** | `OneDrive/CBB_Corporate/03_-_Confidential/Infrastructure/Twilio/plans/2026-06-05-ivr-voicemail.md` |
| **Cross-references** | `Twilio_Architecture_Reference.md`; `Twilio_Operating_Runbook.md` |
| **Created** | 2026-06-06 |
| **Last updated** | 2026-06-06 |
| **Next full review due** | 2027-06-06 |

---

## Revision Control Block

| Rev | Date | Changed By | Reviewed By | Approved By | Summary |
|-----|------|------------|-------------|-------------|---------|
| 0.9 | 2026-06-06 | jschwefel | — | — | Initial release |
