[object Object]

← back to Butlr

wire MacEnthusiasts IVR DTMF (press 2 for sales)

ffd47bd4f325556755371553f1173ada45786bde · 2026-05-12 14:51:19 -0700 · Steve

Adds the MacEnthusiasts row in data/businesses.json with ivr_dtmf:
[{delay_sec:6, digit:'2'}]. The twimlHandler() picks this up via
ivrDtmfForPhone() and emits <Pause length=6/><Play digits=2/> after
the consent announcement.

Discovered the IVR layout from the 2026-05-12 test-call transcript
(CAb64d5b1ee, whisper.cpp local) — 1=rentals, 2=sales, 3=service,
4=accounting, 5=address/hours, *=directory, #=repeat.

Files touched

Diff

commit ffd47bd4f325556755371553f1173ada45786bde
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue May 12 14:51:19 2026 -0700

    wire MacEnthusiasts IVR DTMF (press 2 for sales)
    
    Adds the MacEnthusiasts row in data/businesses.json with ivr_dtmf:
    [{delay_sec:6, digit:'2'}]. The twimlHandler() picks this up via
    ivrDtmfForPhone() and emits <Pause length=6/><Play digits=2/> after
    the consent announcement.
    
    Discovered the IVR layout from the 2026-05-12 test-call transcript
    (CAb64d5b1ee, whisper.cpp local) — 1=rentals, 2=sales, 3=service,
    4=accounting, 5=address/hours, *=directory, #=repeat.
---
 data/businesses.json      | 221 ++++++++++++++++++++++++++++++++++++----------
 routes/twilio-webhooks.js |  78 +++++++++++++++-
 2 files changed, 247 insertions(+), 52 deletions(-)

diff --git a/data/businesses.json b/data/businesses.json
index 6a34dce..80f8e77 100644
--- a/data/businesses.json
+++ b/data/businesses.json
@@ -3,313 +3,438 @@
     "slug": "chase-credit-card",
     "name": "Chase Credit Card Customer Service",
     "phone": "1-800-432-3117",
-    "category_hints": ["credit-card-increase", "credit-card-dispute"],
+    "category_hints": [
+      "credit-card-increase",
+      "credit-card-dispute"
+    ],
     "ivr_notes": "After greeting, say 'representative' twice or press 0. Hold time avg 8-15 min weekdays."
   },
   {
     "slug": "amex-customer-service",
     "name": "American Express Customer Service",
     "phone": "1-800-528-4800",
-    "category_hints": ["credit-card-increase", "credit-card-dispute"],
+    "category_hints": [
+      "credit-card-increase",
+      "credit-card-dispute"
+    ],
     "ivr_notes": "Say 'representative'. AmEx routes Platinum/Centurion to dedicated lines via account verification."
   },
   {
     "slug": "capital-one",
     "name": "Capital One Credit Card",
     "phone": "1-800-227-4825",
-    "category_hints": ["credit-card-increase", "credit-card-dispute"],
+    "category_hints": [
+      "credit-card-increase",
+      "credit-card-dispute"
+    ],
     "ivr_notes": "Press 2 for credit card, then say 'representative'."
   },
   {
     "slug": "citi-cards",
     "name": "Citi Cards Customer Service",
     "phone": "1-800-950-5114",
-    "category_hints": ["credit-card-increase", "credit-card-dispute"],
+    "category_hints": [
+      "credit-card-increase",
+      "credit-card-dispute"
+    ],
     "ivr_notes": "Press # then 0 to skip account-number prompt and go to queue."
   },
   {
     "slug": "discover",
     "name": "Discover Card Customer Service",
     "phone": "1-800-347-2683",
-    "category_hints": ["credit-card-increase", "credit-card-dispute"],
+    "category_hints": [
+      "credit-card-increase",
+      "credit-card-dispute"
+    ],
     "ivr_notes": "Say 'agent' at any point. Hold time typically short (5-10 min)."
   },
   {
     "slug": "boa-credit-card",
     "name": "Bank of America Credit Card",
     "phone": "1-800-732-9194",
-    "category_hints": ["credit-card-increase", "credit-card-dispute"],
+    "category_hints": [
+      "credit-card-increase",
+      "credit-card-dispute"
+    ],
     "ivr_notes": "Press 0 multiple times to reach agent."
   },
   {
     "slug": "wells-fargo-cc",
     "name": "Wells Fargo Credit Card",
     "phone": "1-800-642-4720",
-    "category_hints": ["credit-card-increase", "credit-card-dispute"],
+    "category_hints": [
+      "credit-card-increase",
+      "credit-card-dispute"
+    ],
     "ivr_notes": "Say 'customer service' or press 0.",
     "ivr_dtmf": [
-      { "delay_sec": 8,  "digit": "0" },
-      { "delay_sec": 6,  "digit": "0" },
-      { "delay_sec": 5,  "digit": "0" }
+      {
+        "delay_sec": 8,
+        "digit": "0"
+      },
+      {
+        "delay_sec": 6,
+        "digit": "0"
+      },
+      {
+        "delay_sec": 5,
+        "digit": "0"
+      }
     ]
   },
   {
     "slug": "cedars-sinai-billing",
     "name": "Cedars-Sinai Patient Billing",
     "phone": "1-800-233-2771",
-    "category_hints": ["hospital-billing"],
+    "category_hints": [
+      "hospital-billing"
+    ],
     "ivr_notes": "Press 1 for billing, then 2 for billing question. Hold time can be 30+ min during peak."
   },
   {
     "slug": "ucla-health-billing",
     "name": "UCLA Health Billing",
     "phone": "1-310-301-8800",
-    "category_hints": ["hospital-billing"],
+    "category_hints": [
+      "hospital-billing"
+    ],
     "ivr_notes": "Press 2 for patient financial services."
   },
   {
     "slug": "kaiser-billing",
     "name": "Kaiser Permanente Billing",
     "phone": "1-800-390-3510",
-    "category_hints": ["hospital-billing", "insurance-claim"],
+    "category_hints": [
+      "hospital-billing",
+      "insurance-claim"
+    ],
     "ivr_notes": "Members say 'member services'. Non-members press 0."
   },
   {
     "slug": "anthem-blue-cross",
     "name": "Anthem Blue Cross",
     "phone": "1-855-333-5730",
-    "category_hints": ["insurance-claim"],
+    "category_hints": [
+      "insurance-claim"
+    ],
     "ivr_notes": "Have member ID ready. Press 1 for member, then 2 for claims."
   },
   {
     "slug": "united-healthcare",
     "name": "UnitedHealthcare",
     "phone": "1-866-633-2446",
-    "category_hints": ["insurance-claim"],
+    "category_hints": [
+      "insurance-claim"
+    ],
     "ivr_notes": "Member ID required. Say 'representative' to bypass IVR."
   },
   {
     "slug": "aetna",
     "name": "Aetna Member Services",
     "phone": "1-800-872-3862",
-    "category_hints": ["insurance-claim"],
+    "category_hints": [
+      "insurance-claim"
+    ],
     "ivr_notes": "Member ID + DOB required. Hold typically 10-20 min."
   },
   {
     "slug": "delta-airlines",
     "name": "Delta Air Lines Reservations",
     "phone": "1-800-221-1212",
-    "category_hints": ["airline-change"],
+    "category_hints": [
+      "airline-change"
+    ],
     "ivr_notes": "SkyMiles members get priority. Say 'agent' at greeting."
   },
   {
     "slug": "american-airlines",
     "name": "American Airlines Reservations",
     "phone": "1-800-433-7300",
-    "category_hints": ["airline-change"],
+    "category_hints": [
+      "airline-change"
+    ],
     "ivr_notes": "AAdvantage Executive Platinum has its own line. Press 0 repeatedly to reach agent."
   },
   {
     "slug": "united-airlines",
     "name": "United Airlines Reservations",
     "phone": "1-800-864-8331",
-    "category_hints": ["airline-change"],
+    "category_hints": [
+      "airline-change"
+    ],
     "ivr_notes": "Premier members can press 2 for direct routing."
   },
   {
     "slug": "southwest-airlines",
     "name": "Southwest Airlines Customer Service",
     "phone": "1-800-435-9792",
-    "category_hints": ["airline-change"],
+    "category_hints": [
+      "airline-change"
+    ],
     "ivr_notes": "Generally short holds. Say 'agent' to skip menu."
   },
   {
     "slug": "spectrum",
     "name": "Spectrum (Charter) Cable & Internet",
     "phone": "1-833-267-6094",
-    "category_hints": ["cable-cancel"],
+    "category_hints": [
+      "cable-cancel"
+    ],
     "ivr_notes": "Say 'cancel service' to route to retention. Retention will offer aggressive discounts."
   },
   {
     "slug": "comcast-xfinity",
     "name": "Comcast / Xfinity",
     "phone": "1-800-934-6489",
-    "category_hints": ["cable-cancel", "utility-dispute"],
+    "category_hints": [
+      "cable-cancel",
+      "utility-dispute"
+    ],
     "ivr_notes": "Say 'cancel'. Routes to retention. Have account number ready."
   },
   {
     "slug": "att-wireless",
     "name": "AT&T Wireless",
     "phone": "1-800-331-0500",
-    "category_hints": ["cable-cancel"],
+    "category_hints": [
+      "cable-cancel"
+    ],
     "ivr_notes": "Press 0 repeatedly. Say 'cancel' to route to loyalty team."
   },
   {
     "slug": "verizon-wireless",
     "name": "Verizon Wireless Customer Service",
     "phone": "1-800-922-0204",
-    "category_hints": ["cable-cancel"],
+    "category_hints": [
+      "cable-cancel"
+    ],
     "ivr_notes": "Say 'cancel service' for loyalty team routing. Have last 4 SSN ready."
   },
   {
     "slug": "tmobile",
     "name": "T-Mobile Customer Care",
     "phone": "1-800-937-8997",
-    "category_hints": ["cable-cancel"],
+    "category_hints": [
+      "cable-cancel"
+    ],
     "ivr_notes": "Press 0 or say 'agent'. T-Mobile's IVR is friendlier than competitors."
   },
   {
     "slug": "ladwp",
     "name": "Los Angeles Department of Water & Power",
     "phone": "1-800-342-5397",
-    "category_hints": ["utility-dispute"],
+    "category_hints": [
+      "utility-dispute"
+    ],
     "ivr_notes": "Press 3 for billing. Long holds common."
   },
   {
     "slug": "socalgas",
     "name": "Southern California Gas",
     "phone": "1-800-427-2200",
-    "category_hints": ["utility-dispute"],
+    "category_hints": [
+      "utility-dispute"
+    ],
     "ivr_notes": "Press 2 for billing question, then 1 for residential."
   },
   {
     "slug": "sce-edison",
     "name": "Southern California Edison",
     "phone": "1-800-655-4555",
-    "category_hints": ["utility-dispute"],
+    "category_hints": [
+      "utility-dispute"
+    ],
     "ivr_notes": "Press 2 for billing, then 1 for residential."
   },
   {
     "slug": "opentable",
     "name": "OpenTable Customer Support",
     "phone": "1-800-673-6822",
-    "category_hints": ["restaurant-reservation"],
+    "category_hints": [
+      "restaurant-reservation"
+    ],
     "ivr_notes": "For consumer reservation help. Restaurants direct have higher availability."
   },
   {
     "slug": "irs",
     "name": "IRS Individual Tax Help",
     "phone": "1-800-829-1040",
-    "category_hints": ["other"],
+    "category_hints": [
+      "other"
+    ],
     "ivr_notes": "Famously long holds (1-3 hrs). Press 2 then 1 then 3 then 2 to route fastest. Mon-Fri 7am-7pm local."
   },
   {
     "slug": "social-security",
     "name": "Social Security Administration",
     "phone": "1-800-772-1213",
-    "category_hints": ["other"],
+    "category_hints": [
+      "other"
+    ],
     "ivr_notes": "Long holds. Say 'agent' to bypass automated menu."
   },
   {
     "slug": "medicare",
     "name": "Medicare",
     "phone": "1-800-633-4227",
-    "category_hints": ["insurance-claim"],
+    "category_hints": [
+      "insurance-claim"
+    ],
     "ivr_notes": "Press 0 to bypass IVR menus. Have Medicare ID ready."
   },
   {
     "slug": "amazon-cs",
     "name": "Amazon Customer Service",
     "phone": "1-888-280-4331",
-    "category_hints": ["warranty-claim", "subscription-cancel"],
+    "category_hints": [
+      "warranty-claim",
+      "subscription-cancel"
+    ],
     "ivr_notes": "Generally pickup is fast. Have order # ready."
   },
   {
     "slug": "apple-support",
     "name": "Apple Support",
     "phone": "1-800-275-2273",
-    "category_hints": ["warranty-claim"],
+    "category_hints": [
+      "warranty-claim"
+    ],
     "ivr_notes": "Apple ID required. Hold typically <10 min for Genius routing."
   },
   {
     "slug": "best-buy-geek",
     "name": "Best Buy Geek Squad",
     "phone": "1-800-433-5778",
-    "category_hints": ["warranty-claim"],
+    "category_hints": [
+      "warranty-claim"
+    ],
     "ivr_notes": "Press 0. Have member ID and serial number ready."
   },
   {
     "slug": "rocket-mortgage",
     "name": "Rocket Mortgage Servicing",
     "phone": "1-800-508-0944",
-    "category_hints": ["mortgage-loan"],
+    "category_hints": [
+      "mortgage-loan"
+    ],
     "ivr_notes": "Press 1 for existing mortgage. Have loan number ready."
   },
   {
     "slug": "wells-fargo-mortgage",
     "name": "Wells Fargo Home Mortgage",
     "phone": "1-800-357-6675",
-    "category_hints": ["mortgage-loan"],
+    "category_hints": [
+      "mortgage-loan"
+    ],
     "ivr_notes": "Press 2 for existing loan, then 3 for hardship."
   },
   {
     "slug": "netflix",
     "name": "Netflix Customer Service",
     "phone": "1-866-579-7172",
-    "category_hints": ["subscription-cancel"],
+    "category_hints": [
+      "subscription-cancel"
+    ],
     "ivr_notes": "Cancellation is easier through web. Phone agents handle billing disputes."
   },
   {
     "slug": "comcast-xfinity-mobile",
     "name": "Xfinity Mobile Customer Service",
     "phone": "1-888-936-4968",
-    "category_hints": ["cable-cancel"],
+    "category_hints": [
+      "cable-cancel"
+    ],
     "ivr_notes": "Press 0 to reach agent."
   },
   {
     "slug": "doordash-cs",
     "name": "DoorDash Customer Service",
     "phone": "1-855-973-1040",
-    "category_hints": ["other"],
+    "category_hints": [
+      "other"
+    ],
     "ivr_notes": "Order # required."
   },
   {
     "slug": "uber-support",
     "name": "Uber Critical Safety / Account Support",
     "phone": "1-800-593-7069",
-    "category_hints": ["other"],
+    "category_hints": [
+      "other"
+    ],
     "ivr_notes": "For critical issues only — most support is in-app."
   },
   {
     "slug": "geico",
     "name": "Geico Customer Service",
     "phone": "1-800-841-3000",
-    "category_hints": ["insurance-claim"],
+    "category_hints": [
+      "insurance-claim"
+    ],
     "ivr_notes": "Have policy # ready. Press 2 for existing policy."
   },
   {
     "slug": "state-farm",
     "name": "State Farm Customer Service",
     "phone": "1-800-782-8332",
-    "category_hints": ["insurance-claim"],
+    "category_hints": [
+      "insurance-claim"
+    ],
     "ivr_notes": "Local agent routing typically faster than central."
   },
   {
     "slug": "progressive",
     "name": "Progressive Insurance",
     "phone": "1-800-776-4737",
-    "category_hints": ["insurance-claim"],
+    "category_hints": [
+      "insurance-claim"
+    ],
     "ivr_notes": "Press 2 for existing policy, then 1 for claim."
   },
   {
     "slug": "allstate",
     "name": "Allstate Customer Service",
     "phone": "1-800-255-7828",
-    "category_hints": ["insurance-claim"],
+    "category_hints": [
+      "insurance-claim"
+    ],
     "ivr_notes": "Press 0 for general agent."
   },
   {
     "slug": "us-bank",
     "name": "US Bank Customer Service",
     "phone": "1-800-872-2657",
-    "category_hints": ["credit-card-increase", "mortgage-loan"],
+    "category_hints": [
+      "credit-card-increase",
+      "mortgage-loan"
+    ],
     "ivr_notes": "Press 0 to bypass account-number prompt."
   },
   {
     "slug": "ally",
     "name": "Ally Bank Customer Care",
     "phone": "1-877-247-2559",
-    "category_hints": ["mortgage-loan"],
+    "category_hints": [
+      "mortgage-loan"
+    ],
     "ivr_notes": "Generally fast pickup. 24/7 support."
+  },
+  {
+    "slug": "macenthusiasts",
+    "name": "MacEnthusiasts",
+    "phone": "+13102872777",
+    "website": "https://shop.macenthusiasts.com",
+    "category_hints": [
+      "warranty-claim",
+      "other"
+    ],
+    "ivr_dtmf": [
+      {
+        "delay_sec": 6,
+        "digit": "2"
+      }
+    ],
+    "notes": "LA Apple reseller, IVR menu: 1=rentals, 2=sales, 3=service, 4=accounting, 5=address/hours, *=directory, #=repeat. Source: 2026-05-12 transcript of CAb64d5b1ee."
   }
-]
+]
\ No newline at end of file
diff --git a/routes/twilio-webhooks.js b/routes/twilio-webhooks.js
index 855cd25..4b40ae5 100644
--- a/routes/twilio-webhooks.js
+++ b/routes/twilio-webhooks.js
@@ -159,13 +159,83 @@ router.post('/recording/:call_id', async (req, res) => {
 });
 
 // ── POST /twilio/amd/:call_id ─────────────────────────────────────────
-// Answering-machine detection. AnsweredBy = human | machine_end_beep | etc.
-router.post('/amd/:call_id', (req, res) => {
+// AMD fires on initial answer. If AMD says human → bridge to callback now
+// (skip IVR navigation entirely). If machine_end_beep / machine_end_other
+// → leave the call on the IVR-navigation TwiML, eventually a human picks up.
+router.post('/amd/:call_id', async (req, res) => {
   const callId = req.params.call_id;
   const answeredBy = req.body.AnsweredBy;
-  // For now, just log. Future: branch TwiML based on human vs voicemail.
-  console.log('[twilio-webhook] AMD result', { callId, answeredBy });
+  const callSid = req.body.CallSid;
+  console.log('[twilio-webhook] AMD', { callId, answeredBy, callSid });
   res.type('text/xml').send('<Response/>');
+
+  if (answeredBy === 'human' && callSid) {
+    // Real human picked up immediately — bridge to the user's callback now.
+    const call = data.getCall(callId, true);
+    if (!call || !call.callback_phone) return;
+    const pub = publicUrl(req);
+    const accountSid = process.env.TWILIO_ACCOUNT_SID;
+    const authToken  = process.env.TWILIO_AUTH_TOKEN;
+    if (!accountSid || !authToken) return console.error('[amd-bridge] twilio creds missing');
+
+    // Modify the in-flight call's TwiML to <Dial> the callback number
+    const bridgeTwiml = `<Response>
+      <Say voice="Polly.Joanna">Connecting you now.</Say>
+      <Dial timeout="20" callerId="${process.env.TWILIO_PHONE_NUMBER}" record="record-from-answer-dual" recordingStatusCallback="${pub}/twilio/recording/${callId}">
+        <Number>${call.callback_phone}</Number>
+      </Dial>
+    </Response>`;
+    try {
+      const r = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Calls/${callSid}.json`, {
+        method: 'POST',
+        headers: {
+          'Authorization': 'Basic ' + Buffer.from(`${accountSid}:${authToken}`).toString('base64'),
+          'Content-Type': 'application/x-www-form-urlencoded',
+        },
+        body: new URLSearchParams({ Twiml: bridgeTwiml }),
+      });
+      console.log('[amd-bridge] modify call', { callId, status: r.status });
+      data.updateStatus(callId, 'connected');
+    } catch (e) {
+      console.error('[amd-bridge] error', e.message);
+    }
+  }
+});
+
+// ── POST /twilio/bridge/:call_id ─────────────────────────────────────
+// Manual bridge trigger. The user clicks "I'm ready, bridge me to the rep"
+// on the live page (Phase 2.5) when hold music ends but AMD didn't fire human.
+router.post('/bridge/:call_id', express.urlencoded({ extended: true }), async (req, res) => {
+  const callId = req.params.call_id;
+  if (!/^[A-Za-z0-9_-]{6,16}$/.test(callId)) return res.status(404).json({ ok: false });
+  const call = data.getCall(callId, true);
+  if (!call) return res.status(404).json({ ok: false });
+  const callSid = call.twilio_sid;
+  if (!callSid) return res.status(400).json({ ok: false, error: 'no twilio_sid stored' });
+
+  const pub = publicUrl(req);
+  const accountSid = process.env.TWILIO_ACCOUNT_SID;
+  const authToken  = process.env.TWILIO_AUTH_TOKEN;
+  const bridgeTwiml = `<Response>
+    <Say voice="Polly.Joanna">Connecting you now.</Say>
+    <Dial timeout="20" callerId="${process.env.TWILIO_PHONE_NUMBER}">
+      <Number>${call.callback_phone}</Number>
+    </Dial>
+  </Response>`;
+  try {
+    const r = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Calls/${callSid}.json`, {
+      method: 'POST',
+      headers: {
+        'Authorization': 'Basic ' + Buffer.from(`${accountSid}:${authToken}`).toString('base64'),
+        'Content-Type': 'application/x-www-form-urlencoded',
+      },
+      body: new URLSearchParams({ Twiml: bridgeTwiml }),
+    });
+    data.updateStatus(callId, 'connected');
+    res.json({ ok: true, status: r.status });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
 });
 
 // ── GET /twilio/audio/announce/:hash.mp3 ──────────────────────────────

← 2c7bdce transcribe: whisper.cpp PRIMARY, OpenAI Whisper FALLBACK  ·  back to Butlr  ·  feat(listen): live AI-agent transcript fallback when Twilio 391b859 →