# Twilio Integration Guide
**Cold Bore Ballistics LLC — Confidential**
Last updated: 2026-06-06

> This guide covers the repeatable pattern for wiring a new CBB use case into the Twilio account structure. It is written for an engineer who has never used Twilio before. For current resource SIDs, phone numbers, and account structure, see `Twilio_Architecture_Reference.md`. For day-to-day operations, see `Twilio_Operating_Runbook.md`.

---

## When to Use This Guide

Use this guide when you need to add:
- A new inbound voice flow (IVR menu, call routing, voicemail)
- A new outbound or inbound SMS use case
- A new Twilio Function (serverless logic triggered by voice or SMS)
- A new Studio flow (visual call/message flow)

---

## Account Structure — Where New Work Lives

CBB uses a two-level account structure. New work always goes to the correct account level:

| Account | SID | Use for |
|---|---|---|
| CBB Corporate | `AC26cc215c2e2f0425984bffbe7050a0e0` | 1-855-COLD-390, CBB-branded SMS/voice |
| ScopeDOPE subaccount | `AC7cc6b6571c9b0907b73b783a5422a2c4` | 1-877-DOPE-707, all ScopeDOPE IVR/SMS |

**Rule:** If the use case is ScopeDOPE product-facing (user support, voicemail, in-app SMS), it goes on the ScopeDOPE subaccount. If it is CBB Corporate-facing (business line, vendor comms), it goes on CBB Corporate.

All credentials are in Azure Key Vault — see `Twilio_Architecture_Reference.md` → Credentials section for the secret names.

---

## Integration Patterns

### Pattern 1 — New Inbound Voice Flow (IVR / Studio)

A Studio flow handles the call from Twilio's side and optionally calls a Function for backend logic.

**Step 1 — Create a Studio flow**

```bash
# Set account credentials (ScopeDOPE example)
SUBACCOUNT_SID="AC7cc6b6571c9b0907b73b783a5422a2c4"
TWILIO_AUTH=$(az keyvault secret show \
  --vault-name kv-scope-dope-prod \
  --name twilio-scopedope-auth-token \
  --query value -o tsv)

curl -s -X POST "https://studio.twilio.com/v2/Flows" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" \
  --data-urlencode "FriendlyName=<Flow Name>" \
  --data-urlencode "Status=published" \
  --data-urlencode "Definition=$(cat /tmp/flow.json)"
```

Note the returned flow `sid` (`FW...`).

The flow JSON definition is the source of truth for the IVR logic. Store a copy in `plans/` in this directory. See the existing `plans/2026-06-05-ivr-voicemail.md` as a reference for the plan format and the `Twilio_Architecture_Reference.md` for the live flow definition JSON structure.

**IVR design standards (from CLAUDE.md — mandatory for every CBB IVR):**
- **Press 0** always repeats the current menu at any level
- **Press \*** always returns to the main menu at any level, including from the main menu itself
- Default voicemail max: 120 seconds
- All voicemail notifications include both a subject-line tag and custom headers (`X-CBB-Source`, `X-CBB-Called`, `X-CBB-Category`, `X-CBB-Option`)
- Sender for ScopeDOPE IVR voicemail: `noreply@scope-dope.com` — for CBB Corporate IVR: `noreply@coldboreballisticsllc.com`

**Step 2 — Wire the phone number to the flow**

Get the PN SID for the target number from `Twilio_Architecture_Reference.md`, then:

```bash
curl -s -X POST \
  "https://api.twilio.com/2010-04-01/Accounts/$SUBACCOUNT_SID/IncomingPhoneNumbers/<PN_SID>.json" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" \
  --data-urlencode "VoiceUrl=https://webhooks.twilio.com/v1/Accounts/$SUBACCOUNT_SID/Flows/<FLOW_SID>"
```

---

### Pattern 2 — New Twilio Function (Serverless Logic)

Functions are used when Studio's built-in widgets are insufficient — e.g., sending email via SendGrid, calling an external API, or complex conditional logic. Functions run as Node.js on Twilio's serverless platform.

**Step 1 — Create the Function Service**

Each logical group of functions gets one service. Check `Twilio_Architecture_Reference.md` first — if a suitable service already exists, add the function to it rather than creating a new one.

```bash
curl -s -X POST "https://serverless.twilio.com/v1/Services" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" \
  --data-urlencode "UniqueName=<service-unique-name>" \
  --data-urlencode "FriendlyName=<service-friendly-name>" \
  --data-urlencode "IncludeCredentials=false"
```

Note the returned `sid` (`ZS...`).

**Step 2 — Create the Production Environment**

```bash
curl -s -X POST "https://serverless.twilio.com/v1/Services/<SERVICE_SID>/Environments" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" \
  --data-urlencode "UniqueName=production" \
  --data-urlencode "DomainSuffix=prod"
```

Note the environment `sid` (`ZE...`). The deployed Function URL will be:
`https://<service-unique-name>-<account-suffix>-prod.twil.io/<function-path>`

**Step 3 — Set Environment Variables**

Store secrets in Azure Key Vault and inject them as Function environment variables. Never hardcode secrets in function code.

```bash
# Get the secret from vault first
SECRET=$(az keyvault secret show \
  --vault-name kv-cbb-shared-prod \
  --name <secret-name> \
  --query value -o tsv)

curl -s -X POST \
  "https://serverless.twilio.com/v1/Services/<SERVICE_SID>/Environments/<ENV_SID>/Variables" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" \
  --data-urlencode "Key=<VARIABLE_NAME>" \
  --data-urlencode "Value=$SECRET"
```

Note each variable's `sid` (`ZV...`) — required for rotation without redeploy.

**Step 4 — Upload and Deploy the Function**

```bash
# Create the function entry
curl -s -X POST "https://serverless.twilio.com/v1/Services/<SERVICE_SID>/Functions" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" \
  --data-urlencode "FriendlyName=<function-name>"
# Note the function SID (ZH...)

# Upload code (use serverless-upload.twilio.com — NOT the standard API host)
curl -s -X POST \
  "https://serverless-upload.twilio.com/v1/Services/<SERVICE_SID>/Functions/<FUNCTION_SID>/Versions" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" \
  -F "Path=/<url-path>" \
  -F "Visibility=private" \
  -F "Content=@/tmp/<filename>.js;type=application/javascript"
# Note the version SID (ZN...)

# Create a build (include npm dependencies as a JSON array)
curl -s -X POST "https://serverless.twilio.com/v1/Services/<SERVICE_SID>/Builds" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" \
  --data-urlencode 'Dependencies=[{"name":"<pkg>","version":"<ver>"}]' \
  --data-urlencode "FunctionVersions=<FUNCTION_VERSION_SID>"
# Note the build SID (ZB...)

# Poll until Status = completed (~30s)
curl -s "https://serverless.twilio.com/v1/Services/<SERVICE_SID>/Builds/<BUILD_SID>/Status" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" | python3 -m json.tool | grep '"status"'

# Deploy
curl -s -X POST \
  "https://serverless.twilio.com/v1/Services/<SERVICE_SID>/Environments/<ENV_SID>/Deployments" \
  -u "$SUBACCOUNT_SID:$TWILIO_AUTH" \
  --data-urlencode "BuildSid=<BUILD_SID>"
```

> **Important:** Function code uploads must use `serverless-upload.twilio.com`, not `api.twilio.com`. The standard API host returns 405 for function version uploads.

**Step 5 — Wire the Function to Studio**

In the Studio flow, use an **HTTP Request** widget or a **Run Function** widget to call the Function URL. Pass context values (caller number, recording URL, selected option, etc.) as POST parameters.

---

### Pattern 3 — New SMS Use Case (A2P Messaging)

All outbound A2P SMS must route through a Messaging Service. The CBB Primary Messaging Service (`MG...` — see Architecture Reference) is already provisioned.

**Prerequisites before sending production SMS:**
1. A2P 10DLC brand registration must be complete (CBB-103) — this registers CBB LLC with The Campaign Registry
2. A campaign must be registered for the specific use case (support notifications, marketing, etc.) — $15/month per campaign
3. The phone number must be added to the Messaging Service

**Send an SMS via the Messaging Service:**

```bash
ACCT_SID="AC26cc215c2e2f0425984bffbe7050a0e0"
AUTH=$(az keyvault secret show \
  --vault-name kv-cbb-shared-prod \
  --name twilio-cbb-auth-token \
  --query value -o tsv)

curl -s -X POST \
  "https://api.twilio.com/2010-04-01/Accounts/$ACCT_SID/Messages.json" \
  -u "$ACCT_SID:$AUTH" \
  --data-urlencode "MessagingServiceSid=<MG_SID>" \
  --data-urlencode "To=+1XXXXXXXXXX" \
  --data-urlencode "Body=Your message here."
```

Do not send SMS directly from a phone number — always use the Messaging Service SID. This ensures proper carrier routing and A2P compliance.

---

## Credentials Pattern

Every integration follows this credential pattern — no exceptions:

| Secret type | Where stored | How accessed at runtime |
|---|---|---|
| Account SID | `Twilio_Architecture_Reference.md` (non-secret) | Hardcoded in scripts (not sensitive) |
| Auth Token | Azure Key Vault | `az keyvault secret show` |
| Function env vars | Twilio Function environment (set from vault) | `context.<VAR_NAME>` in function code |

Never store auth tokens in:
- Code (`.js`, `.py`, `.ps1`, `.sh`)
- Git-tracked config files
- Terraform variables (use Key Vault references)
- Environment variables exported to the shell session long-term

---

## Documentation Updates After Any Integration

1. **`Twilio_Architecture_Reference.md`** — add all new SIDs (service, environment, function, flow, variables, phone number assignments)
2. **Azure Key Vault Architecture Reference** — if new secrets were added to either vault
3. **`Twilio_Rebuild_Guide.md`** — add the new component to the rebuild steps so it is not lost in a rebuild
4. **`Twilio_Operating_Runbook.md`** — add any new day-to-day procedures (rotation, testing, updating scripts)

---

## Document Control

| Field | Value |
|---|---|
| **Document owner** | `jschwefel@coldboreballisticsllc.com` |
| **Classification** | Confidential (03_-_Confidential/) |
| **Location** | `OneDrive/CBB_Corporate/03_-_Confidential/Infrastructure/Twilio/Twilio_Integration_Guide.md` |
| **Cross-references** | `Twilio_Architecture_Reference.md`; `Twilio_Operating_Runbook.md`; `Twilio_Rebuild_Guide.md`; `Azure_Key_Vault_Architecture_Reference.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 |
