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
- Dialer places an outbound call via Twilio
- Lead answers → enters a Twilio Conference room (room name:
callview-{callId}) - The qualifying AI assistant (Vapi) joins the same conference via SIP
- 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
- Agent selects a disposition code → lead action fires (return to pool / schedule callback / mark complete)
Tech stack
| Layer | Service | What it does |
|---|---|---|
| Telephony | Twilio | Calls, conferences, phone numbers, AMD, recordings |
| AI voice | Vapi | Conversational AI assistants, connected via SIP |
| Voice cloning | ElevenLabs | Clone a human voice for the AI to speak with |
| LLM | OpenAI / Anthropic | GPT-4o, Claude 3.5 Sonnet, etc. |
| Transcription | Deepgram | Real-time speech-to-text (Nova 3 General) |
| App | CallView (Node/TS + React) | Web app: dialer UI, lead management, campaigns |
| Database | Supabase (Postgres) | Leads, calls, orgs, users, RLS per org |
Roles & Permissions
6 roles, from least to most access. Permissions are configurable per-org by Superadmin.
| Role | Can access |
|---|---|
| SDR | Dialer, 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 |
| Admin | Same as Business Owner (legacy alias) |
| Superadmin | Everything + Platform Admin panel, user impersonation, platform costs |
Who needs which role
| Task | Minimum role |
|---|---|
| Make calls, use the dialer | SDR |
| View dashboard and team members | Team Lead |
| Toggle AMD / voicemail drop on a campaign | Team Lead |
| Create campaigns, import leads, manage lead lists | Ops Manager |
| Configure Inbound AI, Org Settings | Ops Manager |
| Invite new users | Ops Manager |
| Manage billing and workflows | Business Owner |
| Onboard a new org, access platform admin | Superadmin |
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 for | Why | Example |
|---|---|---|---|
| 1 | Company name | Creates the org in the system | "ABC Solar" |
| 2 | Admin email | First user account — gets Business Owner role | "sarah@abcsolar.com" |
| 3 | Preferred area code(s) | Buy local Twilio numbers that match their market. Leads trust local caller IDs. | "518, 845" (Hudson Valley) |
| 4 | Who will be the AI voice? | Need to clone someone's voice. Usually the client's top sales rep or the owner. | "Roger, our founder" |
| 5 | Voice recordings | 1–2 min for Instant Clone, 30–180 min for Professional Clone. See Voice Cloning for recording specs. | MP3/WAV files |
| 6 | Signed voice consent form | Legal requirement for cloning. Written consent from the voice owner. | Signed PDF/doc |
| 7 | Lead list (CSV) | Their prospects. Minimum columns: phone, first name, last name. More is better. | "leads-august.csv" (18,000 rows) |
| 8 | What do they sell? | Needed to write the AI's qualifying script and voicemail message. | "Residential solar panels in upstate NY" |
| 9 | Qualifying criteria | What makes a lead "interested"? What questions should the AI ask? | "Homeowner, pays >$150/mo electric, hasn't gone solar yet" |
| 10 | Voicemail script direction | What should the VM say? Key selling point, call to action, tone. | "Mention the solar assessment, quote savings, leave callback number" |
| 11 | Callback phone number | The number the voicemail tells leads to call back. Usually their office line. | "518-900-1431" |
| 12 | Campaign hours | When to dial. Must be 8am–9pm in the lead's timezone (TCPA). | "9am–5pm Eastern, Monday–Friday" |
| 13 | Closer phone numbers | Who receives warm transfers when a lead is interested? | "+1-518-555-1234 (Roger), +1-518-555-5678 (Ben)" |
| 14 | Disposition categories | Or use defaults: Interested / Not Interested / Callback / Wrong Number / Left VM / DNC | "Use defaults" |
| 15 | Team members to invite | Emails + 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:
| Step | What | Where | Time |
|---|---|---|---|
| 1 | Create the org | Supabase Dashboard or SQL | 2 min |
| 2 | Run onboarding wizard (Twilio + first campaign) | CallView UI — /onboarding | 5 min |
| 3 | Buy additional phone numbers (matching their area codes) | Twilio Console | 5 min |
| 4 | Clone their voice | ElevenLabs Voice Lab | 5 min (IVC) / 6 hrs (PVC) |
| 5 | Connect ElevenLabs to Vapi (API key) | Vapi Dashboard → Integrations | 2 min |
| 6 | Enter Vapi API key in CallView | CallView UI — Settings → Organization | 1 min |
| 7 | Create qualifying AI assistant | Vapi Dashboard | 10 min |
| 8 | Create voicemail AI assistant | Vapi Dashboard | 10 min |
| 9 | Write & paste the system prompts | Vapi Dashboard → each assistant | 30–60 min |
| 10 | Configure the campaign (hours, dispositions, roster, closers) | CallView UI — /campaigns | 10 min |
| 11 | Set up SIP (add to library, assign to users/leads, whitelist) | CallView UI — Settings + Campaign | 10 min |
| 12 | Import leads | CallView UI — /leads/import | 5 min |
| 13 | Invite team members | CallView UI — /users | 5 min |
| 14 | Test call — dial your own phone, verify qualifying AI + voicemail | CallView Dialer | 10 min |
| 15 | Go live — set campaign to active | CallView UI — Campaigns → General | 1 min |
Total setup time: ~1.5–2 hours (excluding PVC voice clone processing).
1. Create the Org
/login exists. Orgs are created through one of two paths below.
Which Supabase project?
| Environment | Project Ref | Dashboard URL |
|---|---|---|
| Production | jujndzvyauamkqrxgayf | supabase.com/dashboard/project/jujndzvyauamkqrxgayf |
| Staging | ekkkgejoiesodegzhhdp | supabase.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.
- Go to Supabase Dashboard → Production → Authentication → Users
- Click "Add User" → "Create New User"
- 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)
- Email: the client's admin email (e.g.
- In the User Metadata JSON field, enter:
If you leave this blank, the org will be named "Default Organization".{"organization_name": "ABC Solar"} - Click "Create User"
- The
handle_new_usertrigger fires automatically and:- Creates an
organizationsrow with the name you provided - Creates a
usersrow with role BUSINESS_OWNER, status active - User name defaults to the part before
@in the email
- Creates an
- Give the client: their email + the temporary password
- They log in at your CallView URL →
/login→ the onboarding wizard appears automatically
_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 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
| Field | Type | Notes |
|---|---|---|
id | UUID | Primary key, auto-generated |
name | TEXT | Required. Shown in the app header |
timezone | TEXT | Default UTC. IANA format |
vapi_api_key | TEXT | Encrypted. Set in Org Settings |
status | ENUM | active | suspended | deleted |
setup_step | INTEGER | Onboarding progress: 0=company, 1=twilio, 2=campaign, 4=complete |
settings | JSONB | Org-level settings (call hours, etc.) |
created_by | UUID | NULL 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
If Twilio is NOT configured yet:
- Click "Activate Phone Service"
- The system automatically creates a Twilio sub-account under the master account, generates API keys, creates a TwiML app, and purchases a phone number
- Has exponential retry: 1s, 2s, 4s delays, 4 attempts max
- On success, advances
setup_stepto 2
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
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.
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
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"
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
| Item | What |
|---|---|
| Sub-account | A Twilio sub-account under the master, scoped to this org |
| API keys | For WebRTC (browser-based calling for the agent) |
| TwiML App | For WebRTC token generation |
| Phone number | One local number (auto-purchased) |
Buying more phone numbers human
- Go to Twilio Console → Phone Numbers
- Click "Buy a Number"
- Filter by area code (match the client's local market for caller ID trust)
- Select capabilities: Voice (required), SMS (optional)
- Click "Buy"
SIP trunking (connecting Vapi) human
- Go to Twilio Console → Elastic SIP Trunking
- Click "Create New Trunk"
- Origination tab: Add Vapi's SIP URI (from Vapi dashboard → Settings → SIP). Use
transport=tcp. - Termination tab: Set the termination SIP URI for outbound calls
- IP ACL: Add Vapi's server IPs for passwordless inbound auth (reduces SIP handshake latency)
- Credential Lists: Create a username/password pair for Vapi's outbound auth
- 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)StatusCallbackEventon conferences — participant join/leave/mute/holdAsyncAmdStatusCallback— AMD result (human vs machine_end_beep)RecordingStatusCallback— recording finished
Key console pages
| Page | URL |
|---|---|
| Dashboard | twilio.com/console |
| Account SID / Auth Token | Console sidebar |
| Phone Numbers | twilio.com/console/phone-numbers |
| SIP Trunking | twilio.com/console/sip-trunking |
| Voice Logs | twilio.com/console/voice/logs |
| API Keys | twilio.com/console/project/api-keys |
| Usage & Billing | twilio.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 needed | 1–2 minutes | 30–180 minutes |
| Processing | Seconds | 3–6 hours |
| Quality | Good for common voices, flat on emotional range | Virtually indistinguishable from real person |
| Min tier | Starter ($5/mo) | Creator ($22/mo) |
| Use for | Quick tests, proof of concept | Production voicemail drops, qualifying AI |
Recording the voice samples human
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
- Dead room — walk-in closet, blanket-draped space, or vocal booth
- No background noise, no room reverb, no fans/AC running
- Sample rate: 44.1kHz or 48kHz
- Bit depth: 24-bit
- Peaks: -6 to -3 dB
- Average loudness: -18 dB
- Mic distance: ~20cm (~8 inches)
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
Account tiers
| Tier | Price/mo | ~Minutes TTS | IVC | PVC | Commercial use |
|---|---|---|---|---|---|
| Free | $0 | ~10 | No | No | No (must credit ElevenLabs) |
| Starter | $5 | ~30 | Yes | No | Yes |
| Creator | $22 | ~100 | Yes | Yes | Yes |
| Pro | $99 | ~500 | Yes | Yes | Yes (192kbps+) |
| Scale | $330 | ~2,000 | Yes | Yes | Yes |
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
- Go to elevenlabs.io/app/voice-lab
- Click "Add Generative or Cloned Voice"
- Choose "Instant Voice Cloning" or "Professional Voice Cloning"
- Upload audio file(s) — drag and drop or click browse
- Check the consent box: "I have the right to clone this voice"
- Name the voice (e.g. "RogerVM - Mister Solar Guy", "Sarah - ABC Solar Qualifying")
- Click "Add Voice"
- 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:
| Setting | Range | Recommended | Why |
|---|---|---|---|
| Stability | 0–1 | 0.65–0.75 | Warm and consistent without sounding robotic. Too high = flat, too low = jittery |
| Similarity Boost | 0–1 | 0.82–0.88 | Sounds like the person. Too low = generic, too high = artifacts |
| Style Exaggeration | 0–1 | 0 | Any value >0 adds artifacts and instability. Leave at zero. |
| Speaker Boost | on/off | On | Adds ~50ms latency but increases similarity. Irrelevant for voicemail (not real-time) |
TTS model selection
| Model | Latency | Quality | Use for |
|---|---|---|---|
| Flash v2 | ~75ms | Good | Qualifying AI (real-time conversation, latency matters) |
| Flash v2.5 | <75ms | Good+ | Multilingual qualifying AI |
| Multilingual v2 | ~300ms | Best | Voicemail drops (not real-time, quality > latency) |
Connect ElevenLabs to Vapi human
- In ElevenLabs: Profile → API Keys → copy your API key
- In Vapi: dashboard.vapi.ai → Org Settings → Integrations → ElevenLabs
- Paste the API key → Save
- 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
| Page | URL |
|---|---|
| Voice Lab (create/manage clones) | elevenlabs.io/app/voice-lab |
| API Keys | elevenlabs.io/app/settings/api-keys |
| Usage tracking | elevenlabs.io/app/usage |
| Speech Synthesis (test voices) | elevenlabs.io/app/speech-synthesis |
5. Vapi Assistants
Each client needs two Vapi assistants:
| Assistant | Purpose | When it joins the call |
|---|---|---|
| Qualifying AI | Talks to the lead, qualifies buying intent, handles objections | When the lead answers the phone |
| Voicemail AI | Drops a personalised voicemail message | After AMD detects an answering machine |
Enter Vapi API key in CallView human
- In Vapi: dashboard.vapi.ai → Org Settings → API Keys → copy your API key
- In CallView:
/settings/organization→ Vapi API Key field → paste → Save
cost-sync.worker.ts logs "No Vapi API key found for org" and cost tracking won't work.
Creating an assistant human
- Go to dashboard.vapi.ai → Assistants → Create
- Name: descriptive (e.g. "RogerVM - Mister Solar Guy", "SarahQual - ABC Solar")
- Model: GPT-4o (qualifying) or GPT-4o-mini (voicemail — cheaper, sufficient for scripted speech)
- Voice: Select the ElevenLabs cloned voice from the dropdown (it syncs automatically once the API key is linked)
- Transcriber: Deepgram Nova 3 General (recommended)
- First Message Mode: See table below
- System Prompt: The instructions the AI follows (see writing guide below)
{assistant-id}@sip.vapi.ai
First message modes
| Mode | What happens | Use for |
|---|---|---|
assistant-speaks-first | Pre-synthesizes the greeting, plays immediately on connect. Fastest. | Qualifying AI with a fixed "Hi, this is Roger from..." greeting |
assistant-waits-for-user | Stays silent until the caller speaks | Inbound call handlers |
assistant-speaks-first-with-model-generated-message | The LLM generates the first message using context from the system prompt + variables | Voicemail drops — the message contains per-lead data from {{lead_context}} |
Writing a system prompt for the qualifying AI human
- Identity & Personality — who the AI is, company name, tone (warm, professional, energetic)
- 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")
- Guardrails — what NOT to discuss (pricing commitments, technical specs beyond scope, competitor bashing)
- Context — what info the AI has via
{{lead_context}}(custom fields),{{name}},{{phone}} - Workflow — step-by-step call flow: greeting → verify identity → pitch → qualify → handle objections → transfer or wrap up. 7–9 turn budget.
- Examples — 3 few-shot examples: happy path, edge case (e.g. "I'm busy"), error recovery (e.g. misheard name)
- 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
Required settings:
firstMessageMode:assistant-speaks-first-with-model-generated-messagefirstMessage: blank (leave empty)voicemailDetection: OFF
- Tell the AI to extract specific fields from
{{lead_context}}(e.g.Vm1:,Vm2:,Vm3:) - Give the exact script template with placeholders
- Instruct it to speak the script exactly, substituting the extracted values
- Say the phone number in spoken form: "five one eight ... nine zero zero ... one four three one"
- After the last line ("Again, this is Roger, Mister Solar Guy"), end the call
- Rules: no improvising, no extra sentences, no greetings, no "sorry I missed you"
Variables available in prompts
CallView sends SIP headers that Vapi exposes as {{variableName}}:
| Variable | Content |
|---|---|
{{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
- Copy the assistant ID from the Vapi dashboard URL
- The SIP address is:
{assistant-id}@sip.vapi.ai - In CallView: Settings → Organization → Sales Rep SIPs → add a new entry with this SIP URI
- 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
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
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
- Paste the prompt into the Vapi assistant's System Prompt field
- Set the Model (GPT-4o for qualifying, GPT-4o-mini for voicemail)
- Select the cloned Voice from the dropdown
- Set Transcriber to Deepgram Nova 3 General
- For voicemail: set First Message Mode to
assistant-speaks-first-with-model-generated-message, leave First Message blank, set Voicemail Detection to OFF - 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?") - Click Save
- Copy the assistant ID from the URL — that's the SIP address:
{id}@sip.vapi.ai
7. Invite Team Members
Sending an invite human
/users):Fields:
- Email — required
- Name — optional
- Role — dropdown: SDR | Team Lead | Operations Manager
Accepting an invite (what the invitee does) human
- Invitee clicks the link in the email → lands on
/accept-invite?token=xxx - Page shows: the org name + their assigned role
- 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
- Click "Create Account"
- This creates the Supabase auth user + the
usersrow in the inviter's org - Click "Go to Login" → sign in with email + password
Invitation states
| Status | Meaning |
|---|---|
pending | Sent, not yet accepted |
accepted | User created their account |
expired | 7 days passed without acceptance |
cancelled | Inviter 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
Fields:
- Campaign Name — text, required
- Phone Numbers — multi-select from your Twilio number pool (shows area codes, marks numbers already assigned to other campaigns)
status: draft.
Campaign status flow
draft → active → paused ↔ active → completed → archived
Tab: General
| Field | Type | Notes |
|---|---|---|
| Campaign Name | Text | Editable |
| Description | Text | Optional |
| Status | Dropdown | draft / active / paused / completed / archived |
| Max Calls/Day | Number | Daily dial limit |
| Call Hours Start | Time | e.g. 09:00 |
| Call Hours End | Time | e.g. 17:00 |
| Timezone | Dropdown | 9 US timezone options |
| Caller ID Number | Multi-select | From org's Twilio numbers, shows area codes |
| AI Summary Model | Dropdown | GPT-4o / GPT-4o-mini / GPT-4 Turbo / Claude 3.5 Sonnet / Claude 3.5 Haiku |
| Summary Prompt | Textarea | Custom prompt for AI call summaries |
| Fallback Model | Dropdown | Used 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
- 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
Step 3: Mapping human
- 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)
Step 4: Review human
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 leadsFor updating existing leads' custom fields, use SQL with jsonb
|| (merge operator) instead.
Lead lifecycle
pool_status | Meaning | Dialer picks it up? |
|---|---|---|
available | Ready to be dialled | Yes |
locked | Currently being called or reserved by an agent | No |
callback | Scheduled for a callback at a specific time | Only at the scheduled time |
completed | Final disposition set | No — 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:
- 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. - 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. - 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
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
Assigning SIPs to users human
/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
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:
- Override — user SIP wins (no whitelist check)
- Lead SIP — lead's
sales_rep_sip_idused (whitelist-gated) - Blocked — lead has a SIP but it's not in the campaign whitelist
- Fallback — no SIP found, voicemail drop fails
The Dialer
/dialer — the main screen SDRs use. 4-column layout (desktop). All roles can access.
Layout
| Column | Width | What's in it |
|---|---|---|
| Lead Queue | 280px | Next leads to call (auto-loaded from campaign pool) + My Callbacks section (scheduled callbacks for this SDR) |
| Lead Details | 360px | Scrollable info about the current lead: name, phone, email, all custom fields, inline call history |
| Live Transcript | flex | Real-time conversation bubbles (speaker-labelled: AI / Customer). Streams via SSE. |
| Controls | 240px | Conference 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:
| State | Visual | Meaning |
|---|---|---|
IDLE | Grey | No active call |
PASSIVE | Green | AI is handling the call, operator is listening |
MUTE-AI | Yellow, pulsing | AI is muted, operator can speak to the lead without AI hearing |
TAKEOVER | Red, pulsing | AI removed from call, operator is in full control |
Every button in the dialer
| Button | What it does | Notes |
|---|---|---|
| Initiate Call | Starts an outbound call to the next lead in the queue | Creates conference, dials lead, adds AI |
| End Call | Ends the entire conference | All participants disconnected |
| Takeover | Human takes over from AI | Double-confirm required. Removes AI participant, unmutes operator. Has minimum duration + cooldown checks. |
| Mute AI | Mutes the AI without removing it | Operator can talk; AI still listens but can't speak |
| Voicemail Drop | Manually triggers voicemail AI swap | Double-tap activation: first click arms (1.5s window, visual pulse), second click fires. Disabled if no SIP configured (tooltip explains why). |
| Closer Invite | Invites a closer into the conference | Round Robin (sequential, 15s timeout) or Dial All (simultaneous) |
| Dial Pad | DTMF tone pad | For navigating IVR menus during calls |
| Quick Add Lead | Add a new lead without leaving the dialer | Modal with lead fields |
| Schedule Callback | Set a callback for the current lead | Date + 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:
- Validate: Check conference is configured for org (
isConferenceConfiguredForOrg()) - Resolve voice: Find the SIP for this user: user SIP assignment → default SIP → any active SIP → error if none
- Check campaign: Status must be
active, current time must be within call hours (8am–9pm in campaign timezone) - DNC check:
checkDnc()on the lead's phone - Create call record: Status
initiatedin the database - Lock the lead:
pool_status: 'locked',locked_by: userId - Create conference: Twilio REST API — outbound call to customer with conference TwiML. Room name:
callview-{callId} - Cache voice config: SIP URI, webhook base URL, lead data, sales rep SIP for AMD — all stored in
ui_state.voiceConfig - Customer answers: Twilio status webhook fires
- Add AI:
addAiToConference()— dials the qualifying AI via SIP with all x- headers (see SIP Headers) - Conference state → PASSIVE: AI is talking, SDR is listening via live transcript
- 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
- 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)
- AMD detected (Twilio async webhook or Vapi detection or manual button) →
handleVoicemailDetected() - Record
amd_detected_attimestamp - Check
campaign.amd_enabled— if disabled, returnfallback, qualifying AI keeps talking - SIP resolution: SDR
voicemail_sip_id(override, no whitelist) → leadsales_rep_sip_id(whitelist-gated) → if neither: returnfallback: no_rep_assigned - Query
sales_rep_sipstable for the actual SIP URI - Fire-and-forget audit (
auditVmDropAssistant()) — checks the Vapi assistant for problems:waits_for_user,first_message_empty,model_generated_first_message,voicemail_detection_enabled - Broadcast
swappingstatus to SDR via SSE - Record
amd_swap_started_at - Remove qualifying AI — Twilio REST:
DELETE /Conferences/{sid}/Participants/{aiCallSid} - Add voicemail AI —
addAiToConference()with the voicemail SIP URI + same lead data/headers - Record
amd_swap_completed_at - Broadcast
activestatus withautoHangupAttimestamp to SDR - Start 40-second hangup timer (
VOICEMAIL_AUTO_HANGUP_MS) - 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
| Status | Meaning |
|---|---|
active | Voicemail AI is speaking |
failed | Swap failed (SIP dial error, etc.) |
fallback | No SIP found — qualifying AI kept talking |
pending | Swap in progress |
manual | SDR manually triggered via button |
ignored_stale | AMD fired too late, call already ended |
ignored_transcript | Transcript suggests a human answered |
ignored_state | Conference in wrong state for swap |
Three trigger paths
| Trigger | How |
|---|---|
| Twilio async AMD | Automatic. AMD detects machine_end_beep → webhook fires → swap starts |
| Vapi AMD | The qualifying AI's own detection recognises it's talking to a machine |
| Manual button | SDR clicks Voicemail Drop in the dialer (double-tap activation) |
Timing budget
| Phase | Time |
|---|---|
| Swap handshake | 0.43s median, 0.69s p90 |
| Usable speech | ~39 seconds |
| Typical message | 25–32s |
| Spare | 8–14s |
Configuration checklist (new client voicemail setup) human
- Create the voicemail Vapi assistant (see Vapi section). Set first message mode to
model-generated, voicemail detection OFF. - Copy the assistant ID → SIP URI is
{id}@sip.vapi.ai - Add the SIP to the org's SIP library: Settings → Organization → Sales Rep SIPs
- Assign SIP to users (Team Members → Voicemail SIP dropdown) OR to leads (
sales_rep_sip_idvia SQL for bulk) - Add SIP to campaign whitelist: Campaign → Outbound tab → AMD section
- Enable AMD on the campaign: Campaign → Outbound tab → AMD toggle ON
- 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
| Mode | How it works |
|---|---|
| Round Robin | Dials 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 All | Dials all closers simultaneously. First to answer wins, rest are cancelled. |
What the closer experiences
- Phone rings
- On answer: hears a whisper announcement with the lead's name and the SDR's name
- Bridged into the conference
- 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
Dispositions
After every call, the SDR selects a disposition code. The code determines what happens to the lead next.
Setting up disposition codes human
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 toavailable, will be dialled againschedule_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 tocompleted, never called again
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.tspublishes,sse.service.tssubscribes,sse-manager.service.tsmanages client connections - UI:
TranscriptColumn.tsx→TranscriptBubble.tsxper 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.
| Service | Internal port | Purpose |
|---|---|---|
| client | 3000 | React/Vite frontend — app.callview.ai |
| gateway | 3000 | Express API gateway — api.callview.ai |
| auth-service | 3001 | Authentication, invitations, RBAC |
| calling-service | 3002 | Twilio calls, Vapi integration, SSE streaming, AMD/voicemail |
| campaign-service | 3003 | Campaign CRUD, settings |
| lead-service | 3004 | Lead management, CSV import, assignments |
| dashboard-service | 3005 | Dashboard stats, analytics |
| workflow-service | 3006 | Vapi webhooks, inbound config, cost telemetry, friendly callbacks |
| Redis | 6379 | Railway 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
| Service | Purpose | Dashboard |
|---|---|---|
| Supabase | PostgreSQL database + auth | supabase.com/dashboard |
| Vapi | AI voice assistants | dashboard.vapi.ai |
| Twilio | Telephony (SIP, phone numbers, conferences) | twilio.com/console |
| ElevenLabs | Voice cloning + TTS | elevenlabs.io/app |
| GrowthBook | Feature flags | app.growthbook.io |
CI/CD pipeline
Deployments use GitHub Actions → Dagger → Railway:
- Push to
mastertriggers the CI workflow (.github/workflows/ci.yml) - CI runs: lint → build all services (parallel) → unit tests
- If CI passes and it’s a push to
master, the deploy job runs - Dagger installs
@railway/cli, links to the project, and runsrailway up --detachfor each service sequentially - Push to
stagingbranch 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
| Environment | Project ref | Dashboard |
|---|---|---|
| Production | jujndzvyauamkqrxgayf | Open |
| Staging | ekkkgejoiesodegzhhdp | Open |
Viewing Logs
Railway Dashboard (recommended)
- Go to railway.app/dashboard
- Open the CallView-AI-MS project
- Click on the service you want to inspect (e.g.
calling-service) - Click the Logs tab — shows live stdout/stderr in real time
- 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
How to detect:
- Check dashboard.vapi.ai/billing — shows current balance and usage
- Look for errors in
calling-servicelogs: search for “Vapi” or “vapi” — you’ll see 402/403 errors from Vapi’s API - The
cost-sync.worker.tswill log:"No Vapi API key found for org"or"Vapi auth failed for cost sync"
How to fix:
- Add credits in Vapi billing (top up immediately)
- Once topped up, new calls will work automatically — no restart needed
- Consider setting up Vapi’s low-balance email alert in the billing page
Twilio errors
How to detect:
- Check Twilio Voice Logs — shows every call with status and error codes
- Check Twilio Debugger — aggregated errors with explanations
- Search
calling-servicelogs for “twilio” or “Twilio”
Common Twilio errors:
| Error | Code | Cause | Fix |
|---|---|---|---|
| Invalid phone number | 21211 | Lead’s phone number is malformed or disconnected | Clean phone data before import; disposition as “Bad Number” |
| Account suspended | 20003 | Twilio account suspended (billing, fraud, or compliance) | Contact Twilio support immediately |
| Insufficient funds | 20002 | Twilio balance too low | Add funds at Twilio Billing |
| Rate limit exceeded | 20429 | Too many concurrent API calls | Reduce dialer speed; contact Twilio to raise limits |
| Call failed | 31005 | Network issue or carrier rejection | Retry; if persistent, check number reputation |
| Geographic permission | 21215 | Calling a country not enabled on the account | Enable the country in Geo Permissions |
ElevenLabs quota exhausted
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:
| Plan | Monthly chars | Approx. 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
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:
| Error | Cause | Fix |
|---|---|---|
Dynamic require of "node:events" is not supported | esbuild outputting ESM instead of CJS | Already fixed in code — services use --format=cjs. If it recurs, check the build script in the service’s package.json |
pnpm: executable not found | Build Command or Start Command set in Railway | Clear both in Railway Settings — the Dockerfile handles everything |
| 502/503 after deploy | Gateway can’t reach internal services | Check env vars use ${{service.RAILWAY_PRIVATE_DOMAIN}} syntax, not hardcoded hostnames |
| Health check timeout | Service starts but /health doesn’t respond in time | Increase 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
- In Railway Dashboard, click the service
- Go to Deployments tab
- Click Redeploy on the latest deployment, or click a previous one and select Rollback
Updating environment variables
- Railway Dashboard → click the service → Variables tab
- Add or edit the variable
- 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:
| Header | Content | Always sent |
|---|---|---|
x-callview_call_id | Call UUID | Yes |
x-callview_lead_id | Lead UUID | If lead exists |
x-callback_number | Twilio phone number (E.164) | If configured |
x-timezone | Campaign IANA timezone | Yes |
x-now | Formatted local time ("Monday, August 14, 2026, 2:30 PM") | Yes |
x-first_name | Lead first name | If present |
x-last_name | Lead last name | If present |
x-company | Lead company | If present |
x-email | Lead email | If present |
x-phone | Lead phone | If present |
x-lead_status | Lead status | If present |
x-lead_context | All custom fields: Title Cased Key: value; ... | If custom fields exist |
Size limits
| Limit | Value | Enforced by |
|---|---|---|
| Total SIP URI | 1,024 chars | Twilio (error 21227 if exceeded) |
| Individual field value | 100 chars | sanitizeForSip() slices |
lead_context | 300 chars | Formatted, joined, then sliced |
Budget trimming
If the total URI exceeds 1,024 chars, headers are dropped in this priority (lowest dropped first):
lead_context(first to go)lead_statusemailnowcompany
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:
- Read the lead's
custom_fields(jsonb) - For each key-value pair: format as
Title Cased Key: value - Join all with
;(semicolon + space) - 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
| Item | Cost |
|---|---|
| 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
| Component | Cost/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
| Tier | Price/mo | ~Minutes TTS |
|---|---|---|
| Starter | $5 | ~30 |
| Creator | $22 | ~100 |
| Pro | $99 | ~500 |
| Scale | $330 | ~2,000 |
Per-call cost estimates
| Call type | Duration | Estimated 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
| Rule | Requirement |
|---|---|
| Calling hours | 8am–9pm in the recipient's local timezone only |
| Abandonment rate | Max 3% over a rolling 30-day window per campaign. An "abandoned" call = human answers but no agent connects within ~2 seconds. |
| Ring time | Minimum 15 seconds before abandoning an unanswered call |
| DNC | Check the National Do Not Call Registry + internal DNC list before every dial |
| Consent | Prior 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.
- 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
- During onboarding (Step 1), instead of clicking "Activate Phone Service", the system calls
registerByotAccount()with the credentials above - The client's Twilio account is linked — no sub-account is created
- Their existing numbers appear in CallView for campaign assignment
- Webhooks must be pointed to CallView's API URL for status callbacks, AMD callbacks, and recording callbacks
- SIP trunking must still be configured on their Twilio account (see Twilio Setup → SIP Trunking) for Vapi to work
Client wants to port an existing phone number to Twilio
- Submit a port request in Twilio Console → Porting Requests
- Takes 2–4 weeks for US numbers
- Requires a Letter of Authorization (LOA) from the number's current carrier
- During the wait, buy a temporary local number to start dialing immediately
- 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
- Get new voice recordings from the new person (see Voice Cloning for requirements)
- Get written consent from the new voice owner
- Create a new clone in ElevenLabs Voice Lab
- In each Vapi assistant, change the Voice dropdown to the new clone
- No changes needed in CallView — the SIP address stays the same
- Collect 30–180 minutes of recordings from the same person
- Submit as Professional Voice Clone in ElevenLabs (processing: 3–6 hours)
- Once approved, swap it in Vapi assistant settings → Voice
- Delete or archive the old IVC to avoid confusion
Client wants to pause dialing temporarily
- Go to CallView →
/campaigns→ select the campaign - In General tab, change Status from
activetopaused(ordraft) - No calls will be placed. In-progress calls complete normally.
- To resume: set Status back to
active
A lead says "take me off your list" (DNC request)
- The SDR dispositions the call as "DNC" or "Do Not Call"
- CallView sets
pool_statustodnc— the lead is never dialled again - This is per-org — the lead is blocked across all campaigns in that org
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:
- Lead calls the Twilio number back
- Twilio routes it to the inbound AI assistant (configured in CallView → Settings → Inbound AI)
- 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
- Go to CallView login page → "Forgot password?"
- Enter email → Supabase sends a password reset email
- Link expires in 1 hour
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
- Go to Vapi Dashboard → Assistants → select the assistant
- Edit the System Prompt
- Click Save
- Changes take effect on the next call — no restart needed, no CallView changes
Client wants to export their data
CallView has no built-in export UI. Options:
- Leads: Supabase Dashboard → Table Editor →
leads→ filter byorganization_id→ Export CSV - Calls: Same process on the
callstable - Recordings: Recording URLs are in the
callstable (recording_urlcolumn). 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 type | Concurrent calls |
|---|---|
| Trial | 1 |
| 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:
- They create their Vapi account at dashboard.vapi.ai
- Copy their API key from Vapi → Org Settings → API Keys
- Paste it in CallView → Settings → Organization → Vapi API Key
- Create assistants in their Vapi account (not yours)
- 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:
pool_statusmust beavailable- Lead must be in a lead list assigned to the campaign
- Campaign must be
active - Current time must be within campaign call hours (in recipient's timezone)
is_deletedmust befalse- 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
| Service | Page | URL |
|---|---|---|
| Twilio | Console | twilio.com/console |
| Twilio | Phone Numbers | twilio.com/console/phone-numbers |
| Twilio | SIP Trunking | twilio.com/console/sip-trunking |
| Twilio | Voice Logs | twilio.com/console/voice/logs |
| Vapi | Dashboard | dashboard.vapi.ai |
| ElevenLabs | Voice Lab | elevenlabs.io/app/voice-lab |
| ElevenLabs | API Keys | elevenlabs.io/app/settings/api-keys |
| ElevenLabs | Usage | elevenlabs.io/app/usage |
| Supabase | Dashboard | supabase.com/dashboard |