diff --git a/backend/src/voice/voice.controller.ts b/backend/src/voice/voice.controller.ts
index 1133779..09d0835 100644
--- a/backend/src/voice/voice.controller.ts
+++ b/backend/src/voice/voice.controller.ts
@@ -98,20 +98,55 @@ export class VoiceController {
async outboundTwiml(@Req() req: FastifyRequest, @Res() res: FastifyReply) {
const body = req.body as any;
const to = body.To;
+ const from = body.From;
+ const callSid = body.CallSid;
- this.logger.log(`Outbound call to: ${to}`);
+ this.logger.log(`=== TwiML OUTBOUND REQUEST RECEIVED ===`);
+ this.logger.log(`CallSid: ${callSid}, Body From: ${from}, Body To: ${to}`);
+ this.logger.log(`Full body: ${JSON.stringify(body)}`);
- // TwiML to dial the number and setup media stream for OpenAI
- const twiml = `
+ try {
+ // Extract tenant domain from Host header
+ const host = req.headers.host || '';
+ const tenantDomain = host.split('.')[0]; // e.g., "tenant1" from "tenant1.routebox.co"
+
+ this.logger.log(`Extracted tenant domain: ${tenantDomain}`);
+
+ // Look up tenant's Twilio phone number from config
+ let callerId = to; // Fallback (will cause error if not found)
+ try {
+ // Get Twilio config to find the phone number
+ const { config } = await this.voiceService['getTwilioClient'](tenantDomain);
+ callerId = config.phoneNumber;
+ this.logger.log(`Retrieved Twilio phone number for tenant: ${callerId}`);
+ } catch (error: any) {
+ this.logger.error(`Failed to get Twilio config: ${error.message}`);
+ }
+
+ const dialNumber = to;
+
+ this.logger.log(`Using callerId: ${callerId}, dialNumber: ${dialNumber}`);
+
+ // Return TwiML to DIAL the phone number with proper callerId
+ const twiml = `
-
-
-
- Connecting your call
- ${to}
+
+ ${dialNumber}
+
`;
- res.type('text/xml').send(twiml);
+ this.logger.log(`Returning TwiML with Dial verb - callerId: ${callerId}, to: ${dialNumber}`);
+ res.type('text/xml').send(twiml);
+ } catch (error: any) {
+ this.logger.error(`=== ERROR GENERATING TWIML ===`);
+ this.logger.error(`Error: ${error.message}`);
+ this.logger.error(`Stack: ${error.stack}`);
+ const errorTwiml = `
+
+ An error occurred while processing your call.
+`;
+ res.type('text/xml').send(errorTwiml);
+ }
}
/**
@@ -154,30 +189,8 @@ export class VoiceController {
const status = body.CallStatus;
const duration = body.CallDuration ? parseInt(body.CallDuration) : undefined;
- this.logger.log(`Call status update: ${callSid} -> ${status}`);
-
- // TODO: Extract tenant ID from call record
- // For now, we'll need to lookup the call to get tenant ID
- // This is a limitation - we should store tenantId in call metadata
-
- try {
- // Update call status
- // await this.voiceService.updateCallStatus({
- // callSid,
- // tenantId: 'LOOKUP_NEEDED',
- // status,
- // duration,
- // });
-
- // Notify user via WebSocket
- // await this.voiceGateway.notifyCallUpdate(userId, {
- // callSid,
- // status,
- // duration,
- // });
- } catch (error) {
- this.logger.error('Failed to process status webhook', error);
- }
+ this.logger.log(`Call status webhook - CallSid: ${callSid}, Status: ${status}, Duration: ${duration}`);
+ this.logger.log(`Full status webhook body:`, JSON.stringify(body));
return { success: true };
}
@@ -189,30 +202,11 @@ export class VoiceController {
async recordingWebhook(@Req() req: FastifyRequest) {
const body = req.body as any;
const callSid = body.CallSid;
- const recordingUrl = body.RecordingUrl;
+ const recordingSid = body.RecordingSid;
+ const recordingStatus = body.RecordingStatus;
- this.logger.log(`Recording available for call ${callSid}: ${recordingUrl}`);
-
- // TODO: Update call record with recording URL
- // TODO: Trigger transcription if needed
+ this.logger.log(`Recording webhook - CallSid: ${callSid}, RecordingSid: ${recordingSid}, Status: ${recordingStatus}`);
return { success: true };
}
-
- /**
- * WebSocket endpoint for Twilio Media Streams
- */
- @Post('stream')
- async mediaStream(@Req() req: FastifyRequest, @Res() res: FastifyReply) {
- // Twilio Media Streams use WebSocket protocol
- // This would need to be handled by the WebSocket server
- // In Fastify, we need to upgrade the connection
-
- this.logger.log('Media stream connection requested');
-
- // TODO: Implement WebSocket upgrade for media streams
- // This will handle bidirectional audio streaming between Twilio and OpenAI
-
- res.send({ message: 'WebSocket upgrade required' });
- }
}
diff --git a/backend/src/voice/voice.service.ts b/backend/src/voice/voice.service.ts
index 17d9c47..60bc5da 100644
--- a/backend/src/voice/voice.service.ts
+++ b/backend/src/voice/voice.service.ts
@@ -137,37 +137,60 @@ export class VoiceService {
const { tenantId: tenantDomain, userId, toNumber } = params;
try {
+ this.logger.log(`=== INITIATING CALL ===`);
+ this.logger.log(`Domain: ${tenantDomain}, To: ${toNumber}, User: ${userId}`);
+
+ // Validate phone number
+ if (!toNumber.match(/^\+?[1-9]\d{1,14}$/)) {
+ throw new Error(`Invalid phone number format: ${toNumber}. Use E.164 format (e.g., +1234567890)`);
+ }
+
const { client, config, tenantId } = await this.getTwilioClient(tenantDomain);
+ this.logger.log(`Twilio client obtained for tenant: ${tenantId}`);
+
+ // Get from number
+ const fromNumber = config.phoneNumber;
+ if (!fromNumber) {
+ throw new Error('Twilio phone number not configured');
+ }
+ this.logger.log(`From number: ${fromNumber}`);
+
+ // Construct tenant-specific webhook URLs using HTTPS (for Traefik)
+ const backendUrl = `https://${tenantDomain}`;
+ const twimlUrl = `${backendUrl}/api/voice/twiml/outbound?phoneNumber=${encodeURIComponent(fromNumber)}&toNumber=${encodeURIComponent(toNumber)}`;
+ const statusUrl = `${backendUrl}/api/voice/webhook/status`;
+
+ this.logger.log(`TwiML URL: ${twimlUrl}`);
+ this.logger.log(`Status URL: ${statusUrl}`);
// Create call record in database
const tenantKnex = await this.tenantDbService.getTenantKnexById(tenantId);
const callId = uuidv4();
- // Construct tenant-specific webhook URLs
- // The tenantDomain is the subdomain (e.g., "tenant1")
- const backendPort = process.env.PORT || '3000';
- const backendUrl = `http://${tenantDomain}.routebox.co:${backendPort}`;
-
- const twimlUrl = `${backendUrl}/api/voice/twiml/outbound`;
-
// Initiate call via Twilio
+ this.logger.log(`Calling Twilio API...`);
+
+ // For Device-to-Number calls, we need to use a TwiML App SID
+ // The Twilio SDK will handle the Device connection, and we return TwiML with Dial
const call = await client.calls.create({
to: toNumber,
- from: config.phoneNumber,
+ from: fromNumber, // Your Twilio phone number
url: twimlUrl,
- statusCallback: `${backendUrl}/api/voice/webhook/status`,
+ statusCallback: statusUrl,
statusCallbackEvent: ['initiated', 'ringing', 'answered', 'completed'],
statusCallbackMethod: 'POST',
- record: true,
- recordingStatusCallback: `${backendUrl}/api/voice/webhook/recording`,
+ record: false,
+ machineDetection: 'Enable', // Optional: detect answering machines
});
+ this.logger.log(`Call created successfully: ${call.sid}, Status: ${call.status}`);
+
// Store call in database
await tenantKnex('calls').insert({
id: callId,
call_sid: call.sid,
direction: 'outbound',
- from_number: config.phoneNumber,
+ from_number: fromNumber,
to_number: toNumber,
status: 'queued',
user_id: userId,