CallView Operations Wiki

Overview

CallView is a predictive dialer where AI qualifies and humans only close. The system auto-dials leads. When someone answers, a Vapi AI assistant (using a cloned human voice) qualifies the lead. If buying intent is detected, a live human agent is bridged into the same call seamlessly. If an answering machine picks up, a second AI drops a personalised voicemail. Uninterested leads never consume human time.

How a call flows

  1. Dialer places an outbound call via Twilio
  2. Lead answers → enters a Twilio Conference room (room name: callview-{callId})
  3. The qualifying AI assistant (Vapi) joins the same conference via SIP
  4. AI qualifies the lead:
    • Interested → agent clicks Takeover → AI is removed from conference, agent is unmuted, talks to the lead with full context
    • Not interested → AI wraps up, call ends normally
    • Answering machine (AMD) → qualifying AI is removed, voicemail AI is dialled in, speaks a personalised message, then hangs up
  5. Agent selects a disposition code → lead action fires (return to pool / schedule callback / mark complete)

Tech stack

LayerServiceWhat it does
TelephonyTwilioCalls, conferences, phone numbers, AMD, recordings
AI voiceVapiConversational AI assistants, connected via SIP
Voice cloningElevenLabsClone a human voice for the AI to speak with
LLMOpenAI / AnthropicGPT-4o, Claude 3.5 Sonnet, etc.
TranscriptionDeepgramReal-time speech-to-text (Nova 3 General)
AppCallView (Node/TS + React)Web app: dialer UI, lead management, campaigns
DatabaseSupabase (Postgres)Leads, calls, orgs, users, RLS per org

Roles & Permissions

6 roles, from least to most access. Permissions are configurable per-org by Superadmin.

RoleCan access
SDRDialer, Call History, Profile
Team Lead+ Dashboard, Team Members, AMD toggle
Ops Manager+ Campaigns, All Leads, Lead Lists, Import, Callbacks, Voicemail AI, Org Settings, Inbound AI, Friendly Callback
Business Owner+ Workflows, Billing
AdminSame as Business Owner (legacy alias)
SuperadminEverything + Platform Admin panel, user impersonation, platform costs

Who needs which role

TaskMinimum role
Make calls, use the dialerSDR
View dashboard and team membersTeam Lead
Toggle AMD / voicemail drop on a campaignTeam Lead
Create campaigns, import leads, manage lead listsOps Manager
Configure Inbound AI, Org SettingsOps Manager
Invite new usersOps Manager
Manage billing and workflowsBusiness Owner
Onboard a new org, access platform adminSuperadmin

When inviting team members, only these roles can be assigned: SDR, Team Lead, Operations Manager.

Contract Signed — Now What?

A client just signed. Here's everything you need to collect from them and everything you need to set up, in order.

What to ask the client human

#Ask forWhyExample
1Company nameCreates the org in the system"ABC Solar"
2Admin emailFirst user account — gets Business Owner role"sarah@abcsolar.com"
3Preferred area code(s)Buy local Twilio numbers that match their market. Leads trust local caller IDs."518, 845" (Hudson Valley)
4Who will be the AI voice?Need to clone someone's voice. Usually the client's top sales rep or the owner."Roger, our founder"
5Voice recordings1–2 min for Instant Clone, 30–180 min for Professional Clone. See Voice Cloning for recording specs.MP3/WAV files
6Signed voice consent formLegal requirement for cloning. Written consent from the voice owner.Signed PDF/doc
7Lead list (CSV)Their prospects. Minimum columns: phone, first name, last name. More is better."leads-august.csv" (18,000 rows)
8What do they sell?Needed to write the AI's qualifying script and voicemail message."Residential solar panels in upstate NY"
9Qualifying criteriaWhat makes a lead "interested"? What questions should the AI ask?"Homeowner, pays >$150/mo electric, hasn't gone solar yet"
10Voicemail script directionWhat should the VM say? Key selling point, call to action, tone."Mention the solar assessment, quote savings, leave callback number"
11Callback phone numberThe number the voicemail tells leads to call back. Usually their office line."518-900-1431"
12Campaign hoursWhen to dial. Must be 8am–9pm in the lead's timezone (TCPA)."9am–5pm Eastern, Monday–Friday"
13Closer phone numbersWho receives warm transfers when a lead is interested?"+1-518-555-1234 (Roger), +1-518-555-5678 (Ben)"
14Disposition categoriesOr use defaults: Interested / Not Interested / Callback / Wrong Number / Left VM / DNC"Use defaults"
15Team members to inviteEmails + roles for anyone else who needs access (SDRs, team leads, managers)."ben@abcsolar.com (SDR), lovely@abcsolar.com (Team Lead)"

Internal setup sequence

After you've collected everything above:

StepWhatWhereTime
1Create the orgSupabase Dashboard or SQL2 min
2Run onboarding wizard (Twilio + first campaign)CallView UI — /onboarding5 min
3Buy additional phone numbers (matching their area codes)Twilio Console5 min
4Clone their voiceElevenLabs Voice Lab5 min (IVC) / 6 hrs (PVC)
5Connect ElevenLabs to Vapi (API key)Vapi Dashboard → Integrations2 min
6Enter Vapi API key in CallViewCallView UI — Settings → Organization1 min
7Create qualifying AI assistantVapi Dashboard10 min
8Create voicemail AI assistantVapi Dashboard10 min
9Write & paste the system promptsVapi Dashboard → each assistant30–60 min
10Configure the campaign (hours, dispositions, roster, closers)CallView UI — /campaigns10 min
11Set up SIP (add to library, assign to users/leads, whitelist)CallView UI — Settings + Campaign10 min
12Import leadsCallView UI — /leads/import5 min
13Invite team membersCallView UI — /users5 min
14Test call — dial your own phone, verify qualifying AI + voicemailCallView Dialer10 min
15Go live — set campaign to activeCallView UI — Campaigns → General1 min

Total setup time: ~1.5–2 hours (excluding PVC voice clone processing).

1. Create the Org

There is no "Create Org" button in the UI. There is no public signup page either — only /login exists. Orgs are created through one of two paths below.

Which Supabase project?

V1 has three Supabase projects. You almost certainly want Production.

EnvironmentProject RefDashboard URL
Productionjujndzvyauamkqrxgayfsupabase.com/dashboard/project/jujndzvyauamkqrxgayf
Stagingekkkgejoiesodegzhhdpsupabase.com/dashboard/project/ekkkgejoiesodegzhhdp

Path A: Supabase Dashboard (recommended) human

When a user is created in Supabase Auth, a database trigger (handle_new_user) fires and automatically creates the org + user. You don't write any SQL — the trigger does it.

Step by step:
  1. Go to Supabase Dashboard → Production → Authentication → Users
  2. Click "Add User""Create New User"
  3. Fill in:
    • Email: the client's admin email (e.g. sarah@abcsolar.com)
    • Password: a temporary password (tell them to change it). Must be 8+ chars, uppercase, lowercase, digit, special char.
    • Auto Confirm User: check this box (skips email verification)
  4. In the User Metadata JSON field, enter:
    {"organization_name": "ABC Solar"}
    If you leave this blank, the org will be named "Default Organization".
  5. Click "Create User"
  6. The handle_new_user trigger fires automatically and:
    • Creates an organizations row with the name you provided
    • Creates a users row with role BUSINESS_OWNER, status active
    • User name defaults to the part before @ in the email
  7. Give the client: their email + the temporary password
  8. They log in at your CallView URL → /login → the onboarding wizard appears automatically
Note about invitations: The trigger has a _skip_trigger check. When users are created through the invitation flow (Team Members → Invite), the trigger is skipped — those users join the existing org instead of getting a new one. This only applies to the first user (the admin who creates the org).

Path B: Direct SQL SQL

For more control (custom org ID, specific timezone, specific settings). Run this in Supabase → SQL Editor.

Step 1: Create the auth user in Supabase Dashboard first (same as Path A, but you can leave metadata blank).
Step 2: Copy the user's UUID from the Users list.
Step 3: Run this SQL in SQL Editor:
-- Replace the values below with the actual client details
DO $$
DECLARE
  v_org_id  UUID := gen_random_uuid();
  v_user_id UUID := '<paste-the-auth-user-uuid-here>';
BEGIN
  -- Create the organization
  INSERT INTO organizations (id, name, timezone, status, setup_step)
  VALUES (
    v_org_id,
    'ABC Solar',              -- client company name
    'America/New_York',       -- their timezone (IANA format)
    'active',
    0                         -- triggers onboarding wizard on first login
  );

  -- Create the admin user linked to the org
  INSERT INTO users (id, organization_id, email, name, role, status)
  VALUES (
    v_user_id,
    v_org_id,
    'sarah@abcsolar.com',     -- must match the auth user email
    'Sarah Johnson',          -- display name
    'BUSINESS_OWNER',         -- gives them full access (except platform admin)
    'active'
  );
END $$;

After running this, give the client their email + password → they log in → onboarding wizard appears.

Org database fields

FieldTypeNotes
idUUIDPrimary key, auto-generated
nameTEXTRequired. Shown in the app header
timezoneTEXTDefault UTC. IANA format
vapi_api_keyTEXTEncrypted. Set in Org Settings
statusENUMactive | suspended | deleted
setup_stepINTEGEROnboarding progress: 0=company, 1=twilio, 2=campaign, 4=complete
settingsJSONBOrg-level settings (call hours, etc.)
created_byUUIDNULL for self-signup

Multi-tenancy uses Postgres Row-Level Security (RLS) on organization_id — every table is scoped to the org.

2. Onboarding Wizard

After the org is created and the user logs in for the first time, the onboarding wizard appears at /onboarding. It has 3 steps. The wizard doesn't create the org — it configures an existing one (Twilio, first campaign, team).

Required role: Business Owner, Admin, or Superadmin.

Step 1: Phone (Twilio provisioning) automatic

What the user sees: A card titled "Activate Phone Service".

If Twilio is NOT configured yet:
  1. Click "Activate Phone Service"
  2. The system automatically creates a Twilio sub-account under the master account, generates API keys, creates a TwiML app, and purchases a phone number
  3. Has exponential retry: 1s, 2s, 4s delays, 4 attempts max
  4. On success, advances setup_step to 2
If Twilio IS already configured: Shows existing phone numbers. Button: "Skip — Phone Service Already Active".

Alternative (BYOT): If the client brings their own Twilio, they register it via registerByotAccount() with: Account SID, Auth Token, API Key SID, API Key Secret, TwiML App SID, and their phone numbers.

Step 2: Campaign human

Blocked until Step 1 completes. Shows "Complete the Phone step first" if setup_step < 2.

Fields:
  • Campaign Name — text input, required. Placeholder: "e.g., Q1 Outreach"
  • Phone Numbers — multi-select dropdown showing all org phone numbers with area codes. Numbers already assigned to other campaigns show "(in use — CampaignName)". First unassigned number is pre-selected.
Creates: Campaign with status: 'draft'. Advances setup_step to 4 (complete).

If the org already has campaigns: Shows "Skip — I already have a campaign".

Step 3: Invite Team human

Not gated by previous steps — can always invite.

Per invite, fill in:
  • Email — text input, required. Placeholder: "colleague@company.com"
  • Role — dropdown: SDR | Team Lead | Operations Manager
  • Name — text input, optional. Placeholder: "John Doe"
Click "Send Invitation" to send. Can invite multiple. Sent invites appear below with a trash button to cancel.

Finish: Click "Go to Dashboard" or "Skip for Now".

Self-heal

If the org already has Twilio + a campaign but setup_step is stale, tryAutoComplete() idempotently advances both steps and redirects to /campaigns.

3. Twilio Setup

Most Twilio setup happens automatically during onboarding (Step 1). This section covers manual setup and what each piece does.

What gets provisioned automatically

ItemWhat
Sub-accountA Twilio sub-account under the master, scoped to this org
API keysFor WebRTC (browser-based calling for the agent)
TwiML AppFor WebRTC token generation
Phone numberOne local number (auto-purchased)

Buying more phone numbers human

  1. Go to Twilio Console → Phone Numbers
  2. Click "Buy a Number"
  3. Filter by area code (match the client's local market for caller ID trust)
  4. Select capabilities: Voice (required), SMS (optional)
  5. Click "Buy"
Pricing: US local ~$1.15/mo, US toll-free ~$2.15/mo.

SIP trunking (connecting Vapi) human

  1. Go to Twilio Console → Elastic SIP Trunking
  2. Click "Create New Trunk"
  3. Origination tab: Add Vapi's SIP URI (from Vapi dashboard → Settings → SIP). Use transport=tcp.
  4. Termination tab: Set the termination SIP URI for outbound calls
  5. IP ACL: Add Vapi's server IPs for passwordless inbound auth (reduces SIP handshake latency)
  6. Credential Lists: Create a username/password pair for Vapi's outbound auth
  7. Phone number routing: Phone Numbers → your number → Voice Configuration → "A Call Comes In" → SIP Trunk → select your trunk

AMD (Answering Machine Detection)

AMD is toggled per-campaign in CallView's UI (not in Twilio Console). Under the hood it uses Twilio's async AMD with DetectMessageEnd mode — waits for the beep, then fires the voicemail swap. Cost: $0.0075 per call.

Webhooks

CallView receives Twilio events via webhooks. These URLs must be publicly reachable (use Cloudflare Tunnel for dev). Key callbacks:

  • StatusCallback — call state changes (initiated → ringing → answered → completed)
  • StatusCallbackEvent on conferences — participant join/leave/mute/hold
  • AsyncAmdStatusCallback — AMD result (human vs machine_end_beep)
  • RecordingStatusCallback — recording finished

Key console pages

PageURL
Dashboardtwilio.com/console
Account SID / Auth TokenConsole sidebar
Phone Numberstwilio.com/console/phone-numbers
SIP Trunkingtwilio.com/console/sip-trunking
Voice Logstwilio.com/console/voice/logs
API Keystwilio.com/console/project/api-keys
Usage & Billingtwilio.com/console/usage

4. Voice Cloning (ElevenLabs)

Every client needs a cloned voice for their AI assistant. This is the most human-intensive part of onboarding.

Clone types

Instant Clone (IVC)Professional Clone (PVC)
Audio needed1–2 minutes30–180 minutes
ProcessingSeconds3–6 hours
QualityGood for common voices, flat on emotional rangeVirtually indistinguishable from real person
Min tierStarter ($5/mo)Creator ($22/mo)
Use forQuick tests, proof of conceptProduction voicemail drops, qualifying AI

Recording the voice samples human

Someone (the client, or the person whose voice will be cloned) records audio samples.

Equipment needed:
  • Microphone: XLR condenser (AT2020, Rode NT1, ~$150) + audio interface (Focusrite Scarlett, ~$150). A USB condenser mic works for IVC.
  • Pop filter and mic stand
  • DAW: Audacity (free) or REAPER
Environment:
  • Dead room — walk-in closet, blanket-draped space, or vocal booth
  • No background noise, no room reverb, no fans/AC running
Recording settings:
  • Sample rate: 44.1kHz or 48kHz
  • Bit depth: 24-bit
  • Peaks: -6 to -3 dB
  • Average loudness: -18 dB
  • Mic distance: ~20cm (~8 inches)
Output format: MP3 at 192kbps+ (recommended) or WAV.

What to read: Scripts matching the tone the clone will use. For a voicemail/sales voice: warm, confident, conversational cadence. Not dramatic, not monotone. Consistency across the full recording session matters more than variety.

Duration:
  • IVC: 1–2 minutes total
  • PVC: At least 30 minutes, ideally 60–180 minutes
Do NOT: Run denoisers, compressors, or EQ on the recordings. Upload raw audio.

Account tiers

TierPrice/mo~Minutes TTSIVCPVCCommercial use
Free$0~10NoNoNo (must credit ElevenLabs)
Starter$5~30YesNoYes
Creator$22~100YesYesYes
Pro$99~500YesYesYes (192kbps+)
Scale$330~2,000YesYesYes

Creator ($22/mo) is the minimum for production. It's the cheapest tier with Professional Voice Cloning + commercial rights.

Creating the clone in the dashboard human

  1. Go to elevenlabs.io/app/voice-lab
  2. Click "Add Generative or Cloned Voice"
  3. Choose "Instant Voice Cloning" or "Professional Voice Cloning"
  4. Upload audio file(s) — drag and drop or click browse
  5. Check the consent box: "I have the right to clone this voice"
  6. Name the voice (e.g. "RogerVM - Mister Solar Guy", "Sarah - ABC Solar Qualifying")
  7. Click "Add Voice"
  8. PVC only: Additional identity verification step, then wait 3–6 hours for processing

Voice settings (tune after cloning) human

In the Voice Lab, click the voice → adjust these sliders:

SettingRangeRecommendedWhy
Stability0–10.65–0.75Warm and consistent without sounding robotic. Too high = flat, too low = jittery
Similarity Boost0–10.82–0.88Sounds like the person. Too low = generic, too high = artifacts
Style Exaggeration0–10Any value >0 adds artifacts and instability. Leave at zero.
Speaker Booston/offOnAdds ~50ms latency but increases similarity. Irrelevant for voicemail (not real-time)

TTS model selection

ModelLatencyQualityUse for
Flash v2~75msGoodQualifying AI (real-time conversation, latency matters)
Flash v2.5<75msGood+Multilingual qualifying AI
Multilingual v2~300msBestVoicemail drops (not real-time, quality > latency)

Connect ElevenLabs to Vapi human

  1. In ElevenLabs: Profile → API Keys → copy your API key
  2. In Vapi: dashboard.vapi.aiOrg SettingsIntegrationsElevenLabs
  3. Paste the API key → Save
  4. Your cloned voices now appear in the voice picker when creating/editing assistants

Legal requirements

  • Written consent from the voice owner is required — keep documentation on file
  • IVC: checkbox confirmation during creation. PVC: additional verification step
  • 12+ US states have voice cloning laws (CA, NY, TN ELVIS Act)
  • Unauthorized impersonation or cloning public figures without consent violates ElevenLabs TOS and potentially law
  • Free tier: no commercial use, must attribute ElevenLabs

Key dashboard pages

PageURL
Voice Lab (create/manage clones)elevenlabs.io/app/voice-lab
API Keyselevenlabs.io/app/settings/api-keys
Usage trackingelevenlabs.io/app/usage
Speech Synthesis (test voices)elevenlabs.io/app/speech-synthesis

5. Vapi Assistants

Each client needs two Vapi assistants:

AssistantPurposeWhen it joins the call
Qualifying AITalks to the lead, qualifies buying intent, handles objectionsWhen the lead answers the phone
Voicemail AIDrops a personalised voicemail messageAfter AMD detects an answering machine

Enter Vapi API key in CallView human

Required before any Vapi assistant will work. CallView uses this key to sync call costs, check assistant status, and authenticate with Vapi's API.

  1. In Vapi: dashboard.vapi.aiOrg SettingsAPI Keys → copy your API key
  2. In CallView: /settings/organizationVapi API Key field → paste → Save
If you skip this: Calls will fail silently. The cost-sync.worker.ts logs "No Vapi API key found for org" and cost tracking won't work.

Creating an assistant human

  1. Go to dashboard.vapi.aiAssistantsCreate
  2. Name: descriptive (e.g. "RogerVM - Mister Solar Guy", "SarahQual - ABC Solar")
  3. Model: GPT-4o (qualifying) or GPT-4o-mini (voicemail — cheaper, sufficient for scripted speech)
  4. Voice: Select the ElevenLabs cloned voice from the dropdown (it syncs automatically once the API key is linked)
  5. Transcriber: Deepgram Nova 3 General (recommended)
  6. First Message Mode: See table below
  7. System Prompt: The instructions the AI follows (see writing guide below)
The assistant ID is in the dashboard URL when you open it — this is the SIP address: {assistant-id}@sip.vapi.ai

First message modes

ModeWhat happensUse for
assistant-speaks-firstPre-synthesizes the greeting, plays immediately on connect. Fastest.Qualifying AI with a fixed "Hi, this is Roger from..." greeting
assistant-waits-for-userStays silent until the caller speaksInbound call handlers
assistant-speaks-first-with-model-generated-messageThe LLM generates the first message using context from the system prompt + variablesVoicemail drops — the message contains per-lead data from {{lead_context}}

Writing a system prompt for the qualifying AI human

Structure the prompt in 6 sections:
  1. Identity & Personality — who the AI is, company name, tone (warm, professional, energetic)
  2. Response Guidelines — 1–2 sentences per turn, one question at a time, spoken-form numbers ("five one eight" not "518"), no markdown/lists/bullets, use contractions ("I'm", "we've")
  3. Guardrails — what NOT to discuss (pricing commitments, technical specs beyond scope, competitor bashing)
  4. Context — what info the AI has via {{lead_context}} (custom fields), {{name}}, {{phone}}
  5. Workflow — step-by-step call flow: greeting → verify identity → pitch → qualify → handle objections → transfer or wrap up. 7–9 turn budget.
  6. Examples — 3 few-shot examples: happy path, edge case (e.g. "I'm busy"), error recovery (e.g. misheard name)
Voice-specific tips:
  • Add 2–4 natural disfluencies per turn ("um", "you know", "so basically")
  • Define turn-taking rules (when to yield, when to interrupt)
  • Keep in character — don't break the persona

Writing a system prompt for the voicemail AI human

This is simpler — it's a scripted monologue, not a conversation.

Required settings:
  • firstMessageMode: assistant-speaks-first-with-model-generated-message
  • firstMessage: blank (leave empty)
  • voicemailDetection: OFF
Prompt structure:
  1. Tell the AI to extract specific fields from {{lead_context}} (e.g. Vm1:, Vm2:, Vm3:)
  2. Give the exact script template with placeholders
  3. Instruct it to speak the script exactly, substituting the extracted values
  4. Say the phone number in spoken form: "five one eight ... nine zero zero ... one four three one"
  5. After the last line ("Again, this is Roger, Mister Solar Guy"), end the call
  6. Rules: no improvising, no extra sentences, no greetings, no "sorry I missed you"
Voicemail detection must be OFF on the voicemail assistant. It's dialled in AFTER AMD already fired and the beep already played. If voicemail detection is on, the AI waits for a second beep that will never come, burning budget on dead air.

Variables available in prompts

CallView sends SIP headers that Vapi exposes as {{variableName}}:

VariableContent
{{lead_context}}All custom fields: Vm1: ...; Vm2: ...; Vm3: ...; Zip: 12457; City: Mount Tremper; ... (max 300 chars)
{{name}}Lead's first name
{{phone}}Lead's phone number
{{campaign_name}}Campaign name
{{now}}Current date/time (formatted, in campaign timezone)
{{customer.number}}Caller phone (Vapi built-in)
{{call.id}}Call UUID (Vapi built-in)

Connecting the assistant to CallView human

  1. Copy the assistant ID from the Vapi dashboard URL
  2. The SIP address is: {assistant-id}@sip.vapi.ai
  3. In CallView: Settings → Organization → Sales Rep SIPs → add a new entry with this SIP URI
  4. Assign it to users or leads (see SIP & Voicemail Config)

6. Write the Prompts

This is the most time-consuming step and the most important. You're writing the words the AI will say to leads. Both prompts go into the Vapi assistant's System Prompt field.

Qualifying AI prompt template human

Copy this template, fill in the bracketed sections for the client.
You are [NAME], a friendly and professional sales representative at [COMPANY].
You are calling potential customers to discuss [PRODUCT/SERVICE].

Your personality: [warm/energetic/professional/casual] and conversational. You speak
like a real person — use contractions (I'm, we've, that's), occasional filler words
(um, you know, so basically), and keep responses to 1-2 sentences.

## CONTEXT
You have this information about the lead:
- Name: {{name}}
- Phone: {{phone}}
- Additional context: {{lead_context}}

## CALL FLOW
1. Greet the lead by name: "Hi [first name], this is [NAME] from [COMPANY]."
2. Brief pitch: [ONE SENTENCE about the product/service — what's in it for them]
3. Qualify with these questions (one at a time, conversational):
   - [Question 1, e.g. "Are you the homeowner at this address?"]
   - [Question 2, e.g. "What are you currently paying for electricity each month?"]
   - [Question 3, e.g. "Have you ever looked into solar for your home?"]
4. If interested: "That's great! I'd love to connect you with one of our specialists
   who can walk you through the specifics. Can I transfer you now?"
5. If not interested: Thank them for their time, wish them a good day, end the call.

## RULES
- One question at a time. Wait for their answer before asking the next.
- Never read out field names, colons, or technical data.
- Say phone numbers digit by digit with pauses: "five one eight ... nine zero zero".
- Do not discuss pricing, contracts, or make financial commitments.
- Do not bash competitors.
- If they say "not interested", "take me off your list", or "do not call": respect it
  immediately, apologise for the interruption, and end the call.
- If they ask "is this AI?": be honest. "Yes, I'm an AI assistant for [COMPANY].
  Would you like me to connect you with a human team member?"
- Stay in character at all times.
- Maximum 8 turns before wrapping up.

## EXAMPLES

### Example 1: Happy path
AI: "Hi Salvatore, this is Roger from Mister Solar Guy. How are you doing today?"
Customer: "Good, what's this about?"
AI: "We've been running solar assessments in your area and your home at 100 Acampora
Drive came back with some really strong numbers. Are you the homeowner there?"
Customer: "Yeah, that's me."
AI: "Great. What are you paying for electric these days, roughly?"
Customer: "About three fifty a month."
AI: "Oh wow, yeah, at that level you'd see some serious savings. Have you ever
looked into solar before?"
Customer: "I've thought about it but never pulled the trigger."
AI: "Totally understand. I'd love to connect you with one of our specialists who can
walk you through what the numbers look like for your specific home. Can I transfer
you now, or would a callback work better?"

### Example 2: Not interested
AI: "Hi Maria, this is Roger from Mister Solar Guy —"
Customer: "Not interested, thanks."
AI: "No problem at all, Maria. Sorry to bother you. Have a great day!"

### Example 3: Busy
AI: "Hi James, this is Roger from Mister Solar Guy. How's it going?"
Customer: "I'm actually in the middle of something."
AI: "Oh, I'm sorry about that. Would it be alright if we gave you a call back
tomorrow, maybe in the afternoon?"
Customer: "Sure, after 2 works."
AI: "Perfect, we'll reach out after 2 tomorrow. Thanks James, talk soon!"

Voicemail AI prompt template human

Copy this template, fill in the bracketed sections.
Settings: First Message Mode = assistant-speaks-first-with-model-generated-message, First Message = blank, Voicemail Detection = OFF.
You are [NAME] from [COMPANY]. You are leaving a voicemail message.

You will receive context about this lead in {{lead_context}}. Extract the values
from these fields:
- After "Vm1:" — this is your first sentence (mentions their address)
- After "Vm2:" — this is your second sentence (mentions their ranking)
- After "Vm3:" — this is your third sentence (mentions savings)

If you find those fields, speak this script exactly, substituting the values:

---
Hi {{name}}, this is [NAME], [TITLE/COMPANY] here in [REGION].
[Vm1 value]. [Vm2 value]. [Vm3 value].
Give me a call when you get a chance — [CALLBACK NUMBER SPOKEN DIGIT BY DIGIT].
Again, this is [NAME], [TITLE/COMPANY].
---

RULES:
- Speak the script exactly. Do not add extra sentences, greetings, or "sorry I
  missed you" filler.
- Do not read field names (Vm1:, Vm2:, Zip:, City:, etc.) — speak only the values.
- Say the phone number once, digit by digit with natural pauses between groups:
  "five one eight ... nine zero zero ... one four three one"
- Warm, confident, conversational tone. Not rushed, not dramatic.
- After "Again, this is [NAME], [TITLE/COMPANY]" — end the call. Do not continue.
- If Vm1/Vm2/Vm3 fields are missing from the context, speak this fallback instead:
  "Hi {{name}}, this is [NAME] from [COMPANY]. I was calling about [BRIEF PITCH].
  Give me a call back at [CALLBACK NUMBER]. Again, this is [NAME], [COMPANY]."
- Total message must be under 35 seconds spoken time.

Example: filled-in voicemail prompt (solar)

You are Roger from Mister Solar Guy. You are leaving a voicemail message.

You will receive context about this lead in {{lead_context}}. Extract the values
from these fields:
- After "Vm1:" — this is your first sentence (mentions their address)
- After "Vm2:" — this is your second sentence (mentions their ranking)
- After "Vm3:" — this is your third sentence (mentions savings)

If you find those fields, speak this script exactly, substituting the values:

---
Hi {{name}}, this is Roger, Mister Solar Guy here in the Hudson Valley.
[Vm1 value]. [Vm2 value]. [Vm3 value].
Give me a call when you get a chance — five one eight ... nine zero zero ...
one four three one.
Again, this is Roger, Mister Solar Guy.
---

RULES:
- Speak the script exactly. Do not add extra sentences, greetings, or "sorry I
  missed you" filler.
- Do not read field names (Vm1:, Vm2:, Zip:, City:, etc.) — speak only the values.
- Say the phone number once, digit by digit with natural pauses between groups.
- Warm, confident, conversational tone. Not rushed, not dramatic.
- After "Again, this is Roger, Mister Solar Guy" — end the call. Do not continue.
- If Vm1/Vm2/Vm3 fields are missing from the context, speak this fallback instead:
  "Hi {{name}}, this is Roger, Mister Solar Guy. I was calling because we ran a
  solar assessment on your home and I'd love to share the results with you.
  Give me a call back at five one eight ... nine zero zero ... one four three one.
  Again, this is Roger, Mister Solar Guy."
- Total message must be under 35 seconds spoken time.

After pasting the prompt human

  1. Paste the prompt into the Vapi assistant's System Prompt field
  2. Set the Model (GPT-4o for qualifying, GPT-4o-mini for voicemail)
  3. Select the cloned Voice from the dropdown
  4. Set Transcriber to Deepgram Nova 3 General
  5. For voicemail: set First Message Mode to assistant-speaks-first-with-model-generated-message, leave First Message blank, set Voicemail Detection to OFF
  6. For qualifying: set First Message Mode to assistant-speaks-first, type the greeting as the First Message (e.g. "Hi, this is Roger from Mister Solar Guy. How are you doing today?")
  7. Click Save
  8. Copy the assistant ID from the URL — that's the SIP address: {id}@sip.vapi.ai

7. Invite Team Members

Sending an invite human

From the onboarding wizard (Step 3) or from Team Members (/users):

Fields:
  • Email — required
  • Name — optional
  • Role — dropdown: SDR | Team Lead | Operations Manager
Click "Send Invitation". The invitee gets an email with a unique link. Invitation expires in 7 days.

Accepting an invite (what the invitee does) human

  1. Invitee clicks the link in the email → lands on /accept-invite?token=xxx
  2. Page shows: the org name + their assigned role
  3. Fill in:
    • Name — text, required, max 100 chars, pre-filled from invitation
    • Password — required, must have: 8+ chars, uppercase, lowercase, digit, special character
    • Confirm Password
  4. Click "Create Account"
  5. This creates the Supabase auth user + the users row in the inviter's org
  6. Click "Go to Login" → sign in with email + password

Invitation states

StatusMeaning
pendingSent, not yet accepted
acceptedUser created their account
expired7 days passed without acceptance
cancelledInviter deleted the invitation

Campaign Setup

/campaigns — master-detail layout. Campaign list (with search) on the left, tabbed config on the right. Ops Manager or above.

Creating a campaign human

Click the "+" button → modal appears.
Fields:
  • Campaign Name — text, required
  • Phone Numbers — multi-select from your Twilio number pool (shows area codes, marks numbers already assigned to other campaigns)
Campaign is created with status: draft.

Campaign status flow

draftactivepausedactivecompletedarchived

Tab: General

FieldTypeNotes
Campaign NameTextEditable
DescriptionTextOptional
StatusDropdowndraft / active / paused / completed / archived
Max Calls/DayNumberDaily dial limit
Call Hours StartTimee.g. 09:00
Call Hours EndTimee.g. 17:00
TimezoneDropdown9 US timezone options
Caller ID NumberMulti-selectFrom org's Twilio numbers, shows area codes
AI Summary ModelDropdownGPT-4o / GPT-4o-mini / GPT-4 Turbo / Claude 3.5 Sonnet / Claude 3.5 Haiku
Summary PromptTextareaCustom prompt for AI call summaries
Fallback ModelDropdownUsed if primary fails

Tab: Outbound

  • Voicemail Drop toggle — enables/disables AMD for this campaign. Team Lead+ required. When enabled, shows the campaign's SIP whitelist.
  • Disposition Codes — drag-to-reorder list. Each code has:
    • Name (e.g. "Left VM", "Interested", "Wrong Number")
    • Emoji (visual icon)
    • Color
    • Lead action: return_to_pool | schedule_callback | mark_complete
    Can copy/paste disposition sets between campaigns.
  • Quick Action Buttons — configurable whisper/say/interrupt actions for the dialer. 4 categories: Objection Handling, Call Flow, Closing, General.

Tab: Inbound

3-step configuration for inbound AI: readiness check → greeting/context/closer routing config → audit log. Gated to Business Owner / Ops Manager / Superadmin. URL: /campaigns/:id/inbound-ai.

Tab: Roster

  • Team assignments: which SDRs work this campaign
  • Lead Lists: which lead lists are assigned
  • Closers: who can receive warm transfers (used by Closer Invite)

Tab: Manager Tools

Bulk reassign leads between agents. Ops Manager / Business Owner only.

Lead Import

/leads/import — 4-step wizard. Ops Manager or above.

Step 1: Settings human

  • Campaign — dropdown, or "None" for org-level leads
  • Assign To — user dropdown (which SDR owns these leads)
  • Update Existing — toggle. If on, matches by phone number and updates existing leads. Warning: replaces custom_fields wholesale, nulls assigned_to, rewrites lead_list_id.
  • Lead List — create new (type a name) or select existing

Step 2: Upload human

Drag & drop a CSV file, or click Browse. File is parsed client-side.

Step 3: Mapping human

Table showing each CSV column with:
  • Sample data preview
  • Dropdown to map to:
    • System fields: Phone, First Name, Last Name, Email
    • Custom fields: any existing key, or click "Add Custom Field" to create a new one on the fly
    • Skip / Do Not Import
  • Default country code selector for phone numbers (US +1 default)
Phone is the match key — V1 deduplicates on phone number.

Step 4: Review human

Sales Rep SIP assignment step (optional), then summary of what will be imported. Click "Import" to execute.
Known bug: The "Update Existing" mode is destructive. The update payload (leads.ts:2164-2177) does:
custom_fields: replaced wholesale, not merged — wipes existing keys
assigned_to: set to null — unassigns the lead
lead_list_id: rewritten to the import target
deleted_at: set to null — un-deletes soft-deleted leads

For updating existing leads' custom fields, use SQL with jsonb || (merge operator) instead.

Lead lifecycle

pool_statusMeaningDialer picks it up?
availableReady to be dialledYes
lockedCurrently being called or reserved by an agentNo
callbackScheduled for a callback at a specific timeOnly at the scheduled time
completedFinal disposition setNo — never called again

Lead detail page

Click any lead in the list → detail panel shows: all custom fields (jsonb), call history, dispositions, tags, pool status, assigned SDR. Click "Edit" to modify fields, including "Sales Rep SIP" (dropdown of org SIPs).

Bulk operations

Select multiple leads → bulk actions: Tag, Assign (SDR + lead list), Delete. There is no bulk SIP assignment.

SIP & Voicemail Configuration

SIP URIs connect Twilio conferences to Vapi voicemail assistants. There are multiple layers.

SIP resolution order (when voicemail triggers)

CallView checks these in order when AMD fires:

  1. User-level override (users.voicemail_sip_id) — if the SDR making the call has a personal voicemail SIP assigned, it wins. No whitelist gate — always used if present.
  2. Lead-level assignment (leads.sales_rep_sip_id) — checked against the campaign's SIP whitelist (campaign_sales_rep_sips). Only used if the SIP is in the whitelist.
  3. If neither is set: voicemail drop fails silently with no_sales_rep_sip. The qualifying AI keeps talking into the answering machine instead of the voicemail AI. This is the root cause of voicemail failures.

Both are resolved and cached in ui_state.voiceConfig at call creation time, not at swap time.

Sales Rep SIP library human

Location: Settings → Organization → Sales Rep SIPs

This is the org's library of Vapi assistant SIP URIs. Each entry has:
  • Name — descriptive (e.g. "RogerVM - Mister Solar Guy")
  • SIP URI — the Vapi assistant SIP address (e.g. bce2f259-c8f3-40f4-9874-86107544b42d@sip.vapi.ai)
  • Description — optional
Add, edit, and delete entries here. These SIPs are what appear in dropdowns elsewhere.

Assigning SIPs to users human

Option A: Team Members (/users) → select a user → Voicemail SIP dropdown → pick from the SIP library.

Option B: Voicemail AI Dashboard (/voicemail-ai) → inline edit per SDR (dropdown).

Assigning SIPs to leads

Per lead: Leads → click a lead → Edit → "Sales Rep SIP" dropdown.
Bulk: There is no bulk SIP assignment UI or API. For bulk writes, use SQL:

UPDATE leads SET sales_rep_sip_id = '<sip-library-id>'
WHERE organization_id = '<org-id>' AND id IN (...);

Campaign SIP whitelist human

Campaigns → select campaign → Outbound tab → AMD section.
When AMD is enabled, the campaign shows its SIP whitelist (campaign_sales_rep_sips). Only SIPs on this list can be used for voicemail drops in this campaign. Add SIPs here.

Voicemail AI dashboard (/voicemail-ai)

Read-only overview showing:

  • SDR voicemail SIP overrides (inline editable via dropdown)
  • Campaign AMD whitelist summaries
  • Lead SIP distribution across the org
  • SIP library summary
  • The 4 voicemail scenarios:
    1. Override — user SIP wins (no whitelist check)
    2. Lead SIP — lead's sales_rep_sip_id used (whitelist-gated)
    3. Blocked — lead has a SIP but it's not in the campaign whitelist
    4. Fallback — no SIP found, voicemail drop fails

The Dialer

/dialer — the main screen SDRs use. 4-column layout (desktop). All roles can access.

Layout

ColumnWidthWhat's in it
Lead Queue280pxNext leads to call (auto-loaded from campaign pool) + My Callbacks section (scheduled callbacks for this SDR)
Lead Details360pxScrollable info about the current lead: name, phone, email, all custom fields, inline call history
Live TranscriptflexReal-time conversation bubbles (speaker-labelled: AI / Customer). Streams via SSE.
Controls240pxConference state banner + call controls + quick actions + closer invite + dial pad

Mobile: collapses to single-column (MobileDialerLayout).

Conference state machine

The banner at the top of the Controls column shows the current state:

StateVisualMeaning
IDLEGreyNo active call
PASSIVEGreenAI is handling the call, operator is listening
MUTE-AIYellow, pulsingAI is muted, operator can speak to the lead without AI hearing
TAKEOVERRed, pulsingAI removed from call, operator is in full control

Every button in the dialer

ButtonWhat it doesNotes
Initiate CallStarts an outbound call to the next lead in the queueCreates conference, dials lead, adds AI
End CallEnds the entire conferenceAll participants disconnected
TakeoverHuman takes over from AIDouble-confirm required. Removes AI participant, unmutes operator. Has minimum duration + cooldown checks.
Mute AIMutes the AI without removing itOperator can talk; AI still listens but can't speak
Voicemail DropManually triggers voicemail AI swapDouble-tap activation: first click arms (1.5s window, visual pulse), second click fires. Disabled if no SIP configured (tooltip explains why).
Closer InviteInvites a closer into the conferenceRound Robin (sequential, 15s timeout) or Dial All (simultaneous)
Dial PadDTMF tone padFor navigating IVR menus during calls
Quick Add LeadAdd a new lead without leaving the dialerModal with lead fields
Schedule CallbackSet a callback for the current leadDate + time + notes (see dispositions)

Quick Actions panel

Configured per-campaign (Campaign → Outbound tab). 4 categories:

  • Objection Handling — whisper rebuttals to the AI
  • Call Flow — direct the AI's next action
  • Closing — transition commands
  • General — misc

Each action sends a command to the AI: whisper (AI hears, lead doesn't), say (AI says it to the lead), or interrupt (AI stops talking).

Call State Trail

Horizontal timeline below the controls showing every state transition: dialing → ringing → connected → passive → mute-ai / takeover / voicemail → ended. Each step timestamped. Labels show voice source: "Conversation voice" / "Your voicemail voice" / "Lead's voicemail voice".

Speed to Lead

Banner/strip for inbound lead notifications. Card stack, newest on top. Timer colour: green <2min, yellow 2–5min, red 5min+. Button: "Call Now" (immediate dial) or dismiss.

Call Flow (Step by Step)

What happens internally when an SDR clicks Initiate Call:

  1. Validate: Check conference is configured for org (isConferenceConfiguredForOrg())
  2. Resolve voice: Find the SIP for this user: user SIP assignment → default SIP → any active SIP → error if none
  3. Check campaign: Status must be active, current time must be within call hours (8am–9pm in campaign timezone)
  4. DNC check: checkDnc() on the lead's phone
  5. Create call record: Status initiated in the database
  6. Lock the lead: pool_status: 'locked', locked_by: userId
  7. Create conference: Twilio REST API — outbound call to customer with conference TwiML. Room name: callview-{callId}
  8. Cache voice config: SIP URI, webhook base URL, lead data, sales rep SIP for AMD — all stored in ui_state.voiceConfig
  9. Customer answers: Twilio status webhook fires
  10. Add AI: addAiToConference() — dials the qualifying AI via SIP with all x- headers (see SIP Headers)
  11. Conference state → PASSIVE: AI is talking, SDR is listening via live transcript
  12. From here:
    • SDR clicks Takeover → AI removed, SDR bridged in → state: TAKEOVER
    • AMD fires → qualifying AI swapped for voicemail AI (see Voicemail Drop)
    • AI finishes qualifying → call ends normally
  13. After call: Disposition panel appears. SDR selects a code → lead action fires.

Voicemail Drop

The voicemail drop is not a recording. It's a second Vapi AI assistant dialled into the same Twilio conference the qualifying AI was just removed from.

Step-by-step swap (amd-swap.service.ts)

  1. AMD detected (Twilio async webhook or Vapi detection or manual button) → handleVoicemailDetected()
  2. Record amd_detected_at timestamp
  3. Check campaign.amd_enabled — if disabled, return fallback, qualifying AI keeps talking
  4. SIP resolution: SDR voicemail_sip_id (override, no whitelist) → lead sales_rep_sip_id (whitelist-gated) → if neither: return fallback: no_rep_assigned
  5. Query sales_rep_sips table for the actual SIP URI
  6. Fire-and-forget audit (auditVmDropAssistant()) — checks the Vapi assistant for problems: waits_for_user, first_message_empty, model_generated_first_message, voicemail_detection_enabled
  7. Broadcast swapping status to SDR via SSE
  8. Record amd_swap_started_at
  9. Remove qualifying AI — Twilio REST: DELETE /Conferences/{sid}/Participants/{aiCallSid}
  10. Add voicemail AIaddAiToConference() with the voicemail SIP URI + same lead data/headers
  11. Record amd_swap_completed_at
  12. Broadcast active status with autoHangupAt timestamp to SDR
  13. Start 40-second hangup timer (VOICEMAIL_AUTO_HANGUP_MS)
  14. Timer fires → executeVoicemailAutoHangup(): check guards (already ended? SDR took over?), endConference(), update call (status: completed, ended_reason: voicemail_timeout), release lead lock, broadcast SSE

Swap status values

StatusMeaning
activeVoicemail AI is speaking
failedSwap failed (SIP dial error, etc.)
fallbackNo SIP found — qualifying AI kept talking
pendingSwap in progress
manualSDR manually triggered via button
ignored_staleAMD fired too late, call already ended
ignored_transcriptTranscript suggests a human answered
ignored_stateConference in wrong state for swap

Three trigger paths

TriggerHow
Twilio async AMDAutomatic. AMD detects machine_end_beep → webhook fires → swap starts
Vapi AMDThe qualifying AI's own detection recognises it's talking to a machine
Manual buttonSDR clicks Voicemail Drop in the dialer (double-tap activation)

Timing budget

PhaseTime
Swap handshake0.43s median, 0.69s p90
Usable speech~39 seconds
Typical message25–32s
Spare8–14s
Hard kill at 40 seconds. If the message runs over, the call is killed mid-sentence. The truncated voicemail is still logged as "delivered" — it fails invisibly. Keep messages under 35 seconds.

Configuration checklist (new client voicemail setup) human

  1. Create the voicemail Vapi assistant (see Vapi section). Set first message mode to model-generated, voicemail detection OFF.
  2. Copy the assistant ID → SIP URI is {id}@sip.vapi.ai
  3. Add the SIP to the org's SIP library: Settings → Organization → Sales Rep SIPs
  4. Assign SIP to users (Team Members → Voicemail SIP dropdown) OR to leads (sales_rep_sip_id via SQL for bulk)
  5. Add SIP to campaign whitelist: Campaign → Outbound tab → AMD section
  6. Enable AMD on the campaign: Campaign → Outbound tab → AMD toggle ON
  7. Test: dial your own phone, let it go to voicemail, verify the personalised message plays

Closer Invite / Warm Transfer

When the AI qualifies a lead and the SDR takes over, they can invite a closer into the conference for a warm handoff.

Two modes

ModeHow it works
Round RobinDials closers one at a time, sequentially. 15-second timeout per closer, then auto-advances to the next. State tracked in Redis with 5-minute TTL.
Dial AllDials all closers simultaneously. First to answer wins, rest are cancelled.

What the closer experiences

  1. Phone rings
  2. On answer: hears a whisper announcement with the lead's name and the SDR's name
  3. Bridged into the conference
  4. AI is removed automatically when the closer joins (remove-vapi-on-bridge.service.ts)

If no closer answers

A callback task is created (no-closer-callback-task.service.ts) so the lead doesn't fall through the cracks.

Setting up closers human

Campaigns → select campaign → Roster tab → Closers section. Add phone numbers for closers who can receive warm transfers for this campaign.

Dispositions

After every call, the SDR selects a disposition code. The code determines what happens to the lead next.

Setting up disposition codes human

Campaigns → select campaign → Outbound tab → Disposition Codes section.

Each code has:
  • Name (e.g. "Interested", "Left VM", "Not Interested", "Wrong Number", "Callback Requested")
  • Emoji (visual icon in the dialer)
  • Color
  • Lead action:
    • return_to_pool — lead goes back to available, will be dialled again
    • schedule_callback — opens the callback scheduler modal (date picker + time + notes). Quick options: Tomorrow / In 2 Days / Next Week. Times: Morning 9:00 / Midday 12:00 / Afternoon 14:00 / Evening 17:00.
    • mark_complete — lead moves to completed, never called again
Drag to reorder. Can copy/paste entire disposition sets between campaigns.

After a call

Disposition panel appears in the dialer. SDR picks a code → lead action fires automatically → publishCallDispositionedEvent() broadcasts to other services → the next lead loads.

Transcript & Recordings

Live transcript

  • Transport: SSE (Server-Sent Events) over HTTP
  • Backend: Redis pub/sub — broadcast.service.ts publishes, sse.service.ts subscribes, sse-manager.service.ts manages client connections
  • UI: TranscriptColumn.tsxTranscriptBubble.tsx per utterance. Each bubble shows: speaker (AI / Customer), text, timestamp
  • Events include: transcript updates, call status changes, voicemail detection, closer status, conference state changes

Recordings

  • Calls are recorded via Twilio (per-conference or per-participant)
  • Recordings are downloaded from Twilio and uploaded to Supabase Storage (bucket: call-recordings)
  • Format: MP3
  • Access: via signed URLs with 1-hour expiry
  • Dual-channel recording available (separates caller/callee)
  • Accessible in: Call History → click a call → recording player

Call History

/call-history — all calls for the org.

Layout

Split view: call list + dossier panel side-by-side (desktop), slide-over sheet (tablet), full-screen toggle (mobile). KPI bar at the top.

Filters

  • Date range
  • Campaign
  • User (which SDR)
  • Disposition
  • Search (phone, name, etc.)

Per-call detail

  • Status, duration, disposition
  • Recording player
  • AI-generated call summary
  • Lead info
  • Full transcript
  • Call events timeline (state transitions with timestamps)

Export

CSV download of filtered call data.

Deployment Architecture

Where V1 runs

CallView V1 is a microservices architecture hosted on Railway. The GitHub repo is BenLorenzoDev/CallView-AI-MS.

ServiceInternal portPurpose
client3000React/Vite frontend — app.callview.ai
gateway3000Express API gateway — api.callview.ai
auth-service3001Authentication, invitations, RBAC
calling-service3002Twilio calls, Vapi integration, SSE streaming, AMD/voicemail
campaign-service3003Campaign CRUD, settings
lead-service3004Lead management, CSV import, assignments
dashboard-service3005Dashboard stats, analytics
workflow-service3006Vapi webhooks, inbound config, cost telemetry, friendly callbacks
Redis6379Railway Redis plugin — session state, queues, pub/sub

Only client and gateway have public domains. All other services communicate internally via Railway’s private network (RAILWAY_PRIVATE_DOMAIN).

External services

ServicePurposeDashboard
SupabasePostgreSQL database + authsupabase.com/dashboard
VapiAI voice assistantsdashboard.vapi.ai
TwilioTelephony (SIP, phone numbers, conferences)twilio.com/console
ElevenLabsVoice cloning + TTSelevenlabs.io/app
GrowthBookFeature flagsapp.growthbook.io

CI/CD pipeline

Deployments use GitHub Actions → Dagger → Railway:

  1. Push to master triggers the CI workflow (.github/workflows/ci.yml)
  2. CI runs: lint → build all services (parallel) → unit tests
  3. If CI passes and it’s a push to master, the deploy job runs
  4. Dagger installs @railway/cli, links to the project, and runs railway up --detach for each service sequentially
  5. Push to staging branch deploys to the staging environment instead

Railway also has auto-deploy configured: any push to master that changes files under callviewai2/** triggers a rebuild.

Supabase projects

EnvironmentProject refDashboard
ProductionjujndzvyauamkqrxgayfOpen
StagingekkkgejoiesodegzhhdpOpen

Viewing Logs

Railway Dashboard (recommended)

  1. Go to railway.app/dashboard
  2. Open the CallView-AI-MS project
  3. Click on the service you want to inspect (e.g. calling-service)
  4. Click the Logs tab — shows live stdout/stderr in real time
  5. Use the search bar to filter by keywords (e.g. “error”, a callId UUID, “vapi”)

Railway CLI

# View live logs for a specific service
railway logs --service gateway
railway logs --service calling-service
railway logs --service workflow-service

# View all services (noisy)
railway logs

Log format

All services use Pino (structured JSON logging). Each log line is a JSON object with:

{
  "level": 30,          // 10=trace 20=debug 30=info 40=warn 50=error 60=fatal
  "time": 1723456789,   // Unix epoch ms
  "service": "calling-service",
  "env": "production",
  "msg": "POST /api/calls/start 200",
  "requestId": "abc-123",
  ...context fields
}

Search for "level":50 to find errors, or "level":40 for warnings.

No external error tracking

V1 does not use Sentry, Logtail, BetterStack, or any external error tracking service. All error visibility is through Railway logs. If you need alerts, set up Railway’s built-in notification integrations (Slack, email) or add a log drain.

Health Checks

Endpoints

Every backend service exposes GET /health returning HTTP 200 when healthy. Railway pings these automatically (configured timeout: 300 seconds).

# Quick check from your machine
curl https://api.callview.ai/health
curl https://app.callview.ai

What health checks cover

  • Service process is running and accepting HTTP
  • Redis connection is alive (gateway checks this)
  • If a health check fails, Railway marks the service as unhealthy and will restart it automatically

Checking Vapi connectivity

The calling service has an internal function checkVapiConnection() that returns 'connected', 'error', or 'not-configured'. This is used at startup — check calling-service logs for “Vapi connection check” messages.

Error Scenarios & Recovery

Vapi runs out of credits

What happens: Vapi API calls fail. New calls cannot connect to the AI assistant. Leads who answer hear silence or a Twilio hold tone, then eventually disconnect. In-progress calls where the AI is already connected will continue until they end naturally.

How to detect:

  • Check dashboard.vapi.ai/billing — shows current balance and usage
  • Look for errors in calling-service logs: search for “Vapi” or “vapi” — you’ll see 402/403 errors from Vapi’s API
  • The cost-sync.worker.ts will log: "No Vapi API key found for org" or "Vapi auth failed for cost sync"

How to fix:

  1. Add credits in Vapi billing (top up immediately)
  2. Once topped up, new calls will work automatically — no restart needed
  3. Consider setting up Vapi’s low-balance email alert in the billing page

Twilio errors

What happens: Outbound calls fail to place, or conferences fail to create. Depending on the error, leads may hear nothing, get a busy signal, or hear an error recording.

How to detect:

  • Check Twilio Voice Logs — shows every call with status and error codes
  • Check Twilio Debugger — aggregated errors with explanations
  • Search calling-service logs for “twilio” or “Twilio”

Common Twilio errors:

ErrorCodeCauseFix
Invalid phone number21211Lead’s phone number is malformed or disconnectedClean phone data before import; disposition as “Bad Number”
Account suspended20003Twilio account suspended (billing, fraud, or compliance)Contact Twilio support immediately
Insufficient funds20002Twilio balance too lowAdd funds at Twilio Billing
Rate limit exceeded20429Too many concurrent API callsReduce dialer speed; contact Twilio to raise limits
Call failed31005Network issue or carrier rejectionRetry; if persistent, check number reputation
Geographic permission21215Calling a country not enabled on the accountEnable the country in Geo Permissions

ElevenLabs quota exhausted

What happens: Vapi calls still connect, but the AI voice falls back to a default (non-cloned) voice or returns an error during TTS.

How to detect:

  • Check ElevenLabs Usage — shows character count vs plan limit
  • Calls sound “different” — the fallback voice is generic
  • Vapi end-of-call-report may show TTS errors in cost breakdown

How to fix: Upgrade ElevenLabs plan or wait for monthly reset. Each plan’s character limits:

PlanMonthly charsApprox. minutes of speech
Starter ($5)30,000~15 min
Creator ($22)100,000~50 min
Pro ($99)500,000~250 min
Scale ($330)2,000,000~1,000 min

Railway deployment fails

What happens: New code doesn’t deploy. The previous version keeps running (Railway doesn’t replace a healthy deployment with a failed one).

How to detect:

  • Check GitHub Actions — the CI/CD workflow will show red
  • Railway Dashboard → click the service → Deployments tab shows build logs

Common causes:

ErrorCauseFix
Dynamic require of "node:events" is not supportedesbuild outputting ESM instead of CJSAlready fixed in code — services use --format=cjs. If it recurs, check the build script in the service’s package.json
pnpm: executable not foundBuild Command or Start Command set in RailwayClear both in Railway Settings — the Dockerfile handles everything
502/503 after deployGateway can’t reach internal servicesCheck env vars use ${{service.RAILWAY_PRIVATE_DOMAIN}} syntax, not hardcoded hostnames
Health check timeoutService starts but /health doesn’t respond in timeIncrease timeout to 300s in Railway Settings → Deploy; check logs for startup errors

Supabase issues

Connection limit reached: Supabase free tier allows ~60 direct connections. With 7 microservices, each holding a connection pool, this can be hit under load. Check the Database Settings page. Consider upgrading or using Supabase’s connection pooler (Supavisor).

RLS policy blocks a query: All tables have Row Level Security. If a query returns empty unexpectedly, check that the JWT includes the correct organization_id claim. Look for “RLS” or “permission denied” in service logs.

Redis connection lost

What happens: Services lose shared state. SSE client tracking, recording queues, and cost-sync workers will fail.

How to detect: Look for “redis” errors in any service’s logs. Gateway health check will fail (it checks Redis connectivity).

How to fix: Check the Redis plugin in Railway Dashboard. If it’s crashed, Railway auto-restarts it. If it’s persistently down, delete and re-provision the Redis plugin (data loss is acceptable — Redis is used for ephemeral state only, not persistent data).

Railway Operations

Quick command reference

# Authenticate
railway login

# Link to the CallView project
railway link

# Deploy all services
railway up

# Deploy a single service
railway up --service gateway

# View live logs
railway logs --service calling-service

# Check deployment status
railway status

# List environment variables
railway variables

# Rollback to previous deployment
railway rollback

Redeploying a single service

  1. In Railway Dashboard, click the service
  2. Go to Deployments tab
  3. Click Redeploy on the latest deployment, or click a previous one and select Rollback

Updating environment variables

  1. Railway Dashboard → click the service → Variables tab
  2. Add or edit the variable
  3. Railway automatically redeploys the service when a variable changes

Variables that reference other services use Railway’s template syntax:

AUTH_SERVICE_URL=http://${{auth-service.RAILWAY_PRIVATE_DOMAIN}}:3001

Scaling

Railway doesn’t support horizontal scaling (multiple instances of the same service) on the Hobby plan. On the Pro plan ($20/mo), you can configure replicas per service. For V1, a single instance of each service is sufficient for current load.

Cost monitoring

Railway bills per usage (CPU, memory, network). Check railway.app/account/billing for current spend. Typical V1 cost: $5–20/mo depending on call volume.

SIP Headers (Technical Reference)

When CallView dials a Vapi assistant into a conference, it sends these x- headers in the SIP URI:

HeaderContentAlways sent
x-callview_call_idCall UUIDYes
x-callview_lead_idLead UUIDIf lead exists
x-callback_numberTwilio phone number (E.164)If configured
x-timezoneCampaign IANA timezoneYes
x-nowFormatted local time ("Monday, August 14, 2026, 2:30 PM")Yes
x-first_nameLead first nameIf present
x-last_nameLead last nameIf present
x-companyLead companyIf present
x-emailLead emailIf present
x-phoneLead phoneIf present
x-lead_statusLead statusIf present
x-lead_contextAll custom fields: Title Cased Key: value; ...If custom fields exist

Size limits

LimitValueEnforced by
Total SIP URI1,024 charsTwilio (error 21227 if exceeded)
Individual field value100 charssanitizeForSip() slices
lead_context300 charsFormatted, joined, then sliced

Budget trimming

If the total URI exceeds 1,024 chars, headers are dropped in this priority (lowest dropped first):

  1. lead_context (first to go)
  2. lead_status
  3. email
  4. now
  5. company

Never dropped: callview_call_id, callview_lead_id, first_name, last_name, phone, callback_number, timezone.

How lead_context is built

Code at twilio-conference.service.ts:560-577:

  1. Read the lead's custom_fields (jsonb)
  2. For each key-value pair: format as Title Cased Key: value
  3. Join all with ; (semicolon + space)
  4. Slice to 300 characters

Postgres sorts jsonb keys by length first, then bytewise. Short keys (like vm1, 3 chars) always sort before longer keys (like homevalue, 9 chars), guaranteeing they survive the 300-char slice.

Pricing Reference

Twilio

ItemCost
Outbound voice (US)$0.013–0.014/min
Inbound voice (local)$0.0085/min
Inbound voice (toll-free)$0.022/min
Local number$1.15/mo
Toll-free number$2.15/mo
AMD$0.0075/call
Recording$0.005/min (capture + storage)

Vapi

ComponentCost/min
Platform fee$0.05
STT (Deepgram Nova)~$0.004
LLM (GPT-4o)~$0.05–0.10
TTS (ElevenLabs)~$0.04
Telephony~$0.01
All-in (typical)$0.12–0.22

ElevenLabs

TierPrice/mo~Minutes TTS
Starter$5~30
Creator$22~100
Pro$99~500
Scale$330~2,000

Per-call cost estimates

Call typeDurationEstimated cost
Qualifying call (AI + human takeover)~2 min$0.25–0.50
Voicemail drop only~30 sec$0.06–0.12
No answer (ring + hang up)~15 sec$0.01–0.02

Compliance

TCPA / FCC rules

RuleRequirement
Calling hours8am–9pm in the recipient's local timezone only
Abandonment rateMax 3% over a rolling 30-day window per campaign. An "abandoned" call = human answers but no agent connects within ~2 seconds.
Ring timeMinimum 15 seconds before abandoning an unanswered call
DNCCheck the National Do Not Call Registry + internal DNC list before every dial
ConsentPrior express consent required for prerecorded/AI messages to cell phones. Revocable at any time.
Penalties$500–$1,500 per violation

Voice cloning consent

Written consent from the voice owner required. Keep on file. 12+ US states have specific laws (CA, NY, TN ELVIS Act).

FAQ

Client wants to use their own Twilio number

This is the BYOT (Bring Your Own Twilio) flow. The client keeps their existing Twilio account and phone numbers.

What they need to provide:
  • Twilio Account SID
  • Twilio Auth Token
  • API Key SID + API Key Secret (create in Twilio Console → API Keys)
  • TwiML App SID (create in Twilio Console → TwiML Apps)
  • Their existing phone numbers
Setup:
  1. During onboarding (Step 1), instead of clicking "Activate Phone Service", the system calls registerByotAccount() with the credentials above
  2. The client's Twilio account is linked — no sub-account is created
  3. Their existing numbers appear in CallView for campaign assignment
  4. Webhooks must be pointed to CallView's API URL for status callbacks, AMD callbacks, and recording callbacks
  5. SIP trunking must still be configured on their Twilio account (see Twilio Setup → SIP Trunking) for Vapi to work
Important: The client is billed directly by Twilio on their own account. CallView does not see or manage their Twilio billing.

Client wants to port an existing phone number to Twilio

  1. Submit a port request in Twilio Console → Porting Requests
  2. Takes 2–4 weeks for US numbers
  3. Requires a Letter of Authorization (LOA) from the number's current carrier
  4. During the wait, buy a temporary local number to start dialing immediately
  5. Once ported, swap the number in the campaign settings — no other config changes needed

Client wants to add more phone numbers later

No problem. Follow Twilio Setup → Buying More Numbers. Once purchased, the number appears in CallView automatically (it reads from the Twilio sub-account). Assign it to a campaign in Campaigns → General → Phone Numbers.

Client wants to change the AI voice

Switching to a different person's voice:
  1. Get new voice recordings from the new person (see Voice Cloning for requirements)
  2. Get written consent from the new voice owner
  3. Create a new clone in ElevenLabs Voice Lab
  4. In each Vapi assistant, change the Voice dropdown to the new clone
  5. No changes needed in CallView — the SIP address stays the same
Upgrading from IVC to PVC (same person):
  1. Collect 30–180 minutes of recordings from the same person
  2. Submit as Professional Voice Clone in ElevenLabs (processing: 3–6 hours)
  3. Once approved, swap it in Vapi assistant settings → Voice
  4. Delete or archive the old IVC to avoid confusion

Client wants to pause dialing temporarily

  1. Go to CallView → /campaigns → select the campaign
  2. In General tab, change Status from active to paused (or draft)
  3. No calls will be placed. In-progress calls complete normally.
  4. To resume: set Status back to active
No cost while paused — Twilio charges per call, Vapi charges per minute. No calls = no charges (except the ~$1.15/mo per phone number held).

A lead says "take me off your list" (DNC request)

You must comply immediately. TCPA requires honoring DNC requests.
  1. The SDR dispositions the call as "DNC" or "Do Not Call"
  2. CallView sets pool_status to dnc — the lead is never dialled again
  3. This is per-org — the lead is blocked across all campaigns in that org
For bulk DNC: Use SQL: UPDATE leads SET pool_status = 'dnc' WHERE phone IN ('...', '...') filtered by organization_id.

A lead calls back

V1 supports Friendly Callback (inbound AI) for orgs that have it configured. The flow:

  1. Lead calls the Twilio number back
  2. Twilio routes it to the inbound AI assistant (configured in CallView → Settings → Inbound AI)
  3. The AI greets the caller, identifies the lead by phone number, and can transfer to a closer

If inbound AI is NOT configured, the call goes to Twilio's default voicemail or plays an error. Make sure inbound routing is set up if the client expects callbacks.

Client forgot their password

  1. Go to CallView login page → "Forgot password?"
  2. Enter email → Supabase sends a password reset email
  3. Link expires in 1 hour
If the reset email doesn't arrive: Check spam. Supabase sends from noreply@mail.app.supabase.io. If still stuck, reset manually in Supabase Dashboard → Authentication → Users → find the user → Send Password Recovery.

Client wants a second campaign

Go to CallView → /campaigns"New Campaign". Each campaign is independent: its own phone numbers, lead lists, call hours, roster, dispositions, and closer list. A phone number can only belong to one campaign at a time — buy additional numbers if needed.

Client wants different dispositions

Dispositions are per-campaign. Go to Campaigns → select campaign → Dispositions tab. Add, rename, or remove. Defaults: Interested / Not Interested / Callback / Wrong Number / Left VM / DNC. Dispositions already used on calls cannot be deleted (they'd break call history), but they can be renamed.

Client wants to change the AI script

  1. Go to Vapi Dashboard → Assistants → select the assistant
  2. Edit the System Prompt
  3. Click Save
  4. Changes take effect on the next call — no restart needed, no CallView changes
Test first: Use Vapi's built-in test call (top right) to verify the new prompt before going live.

Client wants to export their data

CallView has no built-in export UI. Options:

  • Leads: Supabase Dashboard → Table Editor → leads → filter by organization_id → Export CSV
  • Calls: Same process on the calls table
  • Recordings: Recording URLs are in the calls table (recording_url column). These are Twilio-hosted and expire after the account's retention period (default: no expiry)

How many concurrent calls can we run?

Depends on the Twilio account tier:

Account typeConcurrent calls
Trial1
Upgraded (default)Soft limit varies — typically starts at 1 CPS (call per second). Request increase via Twilio support.
Production (with increase)100+ concurrent

If you hit the limit, Twilio returns error 20429 (rate limit). The dialer will see failed calls. Request a CPS increase in Twilio Console → Voice → CPS or contact Twilio support.

Can we use a different AI provider instead of Vapi?

V1: No. The calling service is tightly coupled to Vapi's API (assistant creation, SIP dialling, end-of-call reports, cost sync). Switching would require rewriting the calling service.

V2: Being built with an open framework (LiveKit/Pipecat) specifically to avoid vendor lock-in. See the V2 architecture docs.

Client wants their own Vapi account

Each org stores its own vapi_api_key. If a client has their own Vapi account:

  1. They create their Vapi account at dashboard.vapi.ai
  2. Copy their API key from Vapi → Org Settings → API Keys
  3. Paste it in CallView → Settings → Organization → Vapi API Key
  4. Create assistants in their Vapi account (not yours)
  5. They manage their own Vapi billing

What happens if a service crashes?

Railway automatically restarts crashed services. The previous healthy deployment keeps running until the restart succeeds. In-progress calls may drop if calling-service crashes (conference state is lost). Check Viewing Logs for crash details and Error Scenarios for recovery steps.

Troubleshooting

Voicemail drops failing (no_sales_rep_sip)

Symptom: AMD fires but the qualifying AI keeps talking into the answering machine.

Cause: Lead's sales_rep_sip_id is NULL and the SDR has no voicemail SIP override.

Fix: Assign sales_rep_sip_id to leads (bulk via SQL) and/or set user-level voicemail SIP (Team Members → Voicemail SIP dropdown).

Voicemail cut off mid-sentence

Symptom: Lead receives a truncated voicemail.

Cause: Message exceeded 40s (the hard kill timer, VOICEMAIL_AUTO_HANGUP_MS).

Fix: Shorten the script to under 35s spoken time.

AI reads raw field names ("Vm1:", "Zip:")

Symptom: Voicemail AI says "Vm1 colon We ran a solar assessment..." instead of just the content.

Cause: firstMessageMode is wrong, or prompt doesn't instruct AI to extract field values.

Fix: Set firstMessageMode to assistant-speaks-first-with-model-generated-message. Prompt must explicitly say "extract the value after 'Vm1:' and speak only the value".

Custom fields gone after CSV import

Symptom: Existing custom fields (homevalue, zip, etc.) disappeared from leads.

Cause: Import wizard's "Update Existing" mode replaces custom_fields wholesale.

Fix: Use SQL with jsonb || to merge. Only use CSV import for new leads.

Leads not being dialled

Check these in order:

  1. pool_status must be available
  2. Lead must be in a lead list assigned to the campaign
  3. Campaign must be active
  4. Current time must be within campaign call hours (in recipient's timezone)
  5. is_deleted must be false
  6. Lead must not be on DNC list

Cloned voice sounds robotic

Check voice settings in ElevenLabs Voice Lab:

  • Stability: 0.65–0.75 (too high = flat, too low = jittery)
  • Similarity Boost: 0.82–0.88
  • Style Exaggeration: must be 0
  • Speaker Boost: On

If settings are correct, the recordings were likely low quality. Re-record in a treated room with proper equipment.

SDR override vs lead SIP error

Symptom: SDR's override lookup fails silently, so it falls through to lead SIP (which may also be NULL).

Cause: call.service.ts:968-973 discards the query error. A failed database lookup is indistinguishable from "no SIP set".

Workaround: Always set both user-level and lead-level SIPs as defence in depth.

Useful console links

ServicePageURL
TwilioConsoletwilio.com/console
TwilioPhone Numberstwilio.com/console/phone-numbers
TwilioSIP Trunkingtwilio.com/console/sip-trunking
TwilioVoice Logstwilio.com/console/voice/logs
VapiDashboarddashboard.vapi.ai
ElevenLabsVoice Labelevenlabs.io/app/voice-lab
ElevenLabsAPI Keyselevenlabs.io/app/settings/api-keys
ElevenLabsUsageelevenlabs.io/app/usage
SupabaseDashboardsupabase.com/dashboard