@cemscale-voip/voip-sdk 1.48.0 → 1.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +296 -862
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -280,1018 +280,452 @@ await voip.blockNumber({ number: '+15559999999', reason: 'Spam', direction: 'inb
280
280
  const { blocked } = await voip.checkBlocked('+15559999999'); // true
281
281
  ```
282
282
 
283
- ### AI Agents (Voice AI)
284
-
285
- Create and manage AI phone agents powered by Gemini. Agents answer calls, speak to callers in real-time, and can transfer to humans.
286
-
287
- **No extra configuration needed.** AI Agents work through the same `voip` client — no separate `BRIDGE_URL` or `BRIDGE_API_KEY` required.
288
-
289
- ```typescript
290
- // List all AI agents
291
- const { agents } = await voip.listAiAgents();
292
- console.log(agents);
293
- // [{ agent_id: 'acme-sales', display_name: 'Sofia', voice: 'Kore', ... }]
294
-
295
- // Create a new AI agent
296
- const { agent } = await voip.createAiAgent({
297
- agent_id: 'acme-sales', // unique slug (used as route target)
298
- display_name: 'Sofia', // name shown in dashboard/logs
299
- company: 'Acme Corp', // company context for the AI
300
- role: 'Sales Representative', // role context
301
- language: 'en', // 'en', 'es', 'fr', etc.
302
- voice: 'Kore', // Gemini voice: Kore, Aoede, Charon, Fenrir, Puck
303
- speak_first: true, // agent greets the caller first
304
- thinking_level: 'minimal', // minimal, low, medium, high
305
- temperature: 0.7, // 0.0 (precise) to 1.0 (creative)
306
- system_prompt: `You are Sofia, a professional sales representative for Acme Corp.
307
- You speak English and Spanish fluently. Respond in whichever language the caller uses.
308
- Be conversational, warm, and helpful. Keep responses SHORT (1-2 sentences).
309
- If the caller asks to speak with a human, transfer them.
310
- Start with: "Thank you for calling Acme Corp, how can I help you today?"`,
311
- });
312
-
313
- // Update an existing agent
314
- await voip.updateAiAgent('acme-sales', {
315
- display_name: 'Sofia v2',
316
- system_prompt: 'Updated instructions...',
317
- voice: 'Aoede',
318
- temperature: 0.5,
319
- });
320
-
321
- // Get a single agent
322
- const { agent: detail } = await voip.getAiAgent('acme-sales');
323
- console.log(detail.system_prompt);
324
-
325
- // Delete an agent
326
- await voip.deleteAiAgent('acme-sales');
327
283
 
328
- // Health check (verify AI Bridge is connected)
329
- const health = await voip.aiAgentsHealth();
330
- console.log(health.status); // 'connected'
331
- ```
284
+ ### AI Agents (Voice AI)
332
285
 
286
+ AI agents are virtual receptionists powered by Google Gemini 3.1 Live API. They answer phone calls, hold natural conversations in English/Spanish, and perform real actions during the call (create leads, send SMS, book appointments, transfer to humans).
333
287
 
288
+ #### SDK Methods
334
289
 
335
- #### All Editable Agent Fields
290
+ | Method | Description |
291
+ |--------|-------------|
292
+ | `createAiAgent(params)` | Create a new AI agent |
293
+ | `updateAiAgent(agentId, params)` | Update any field on an agent |
294
+ | `deleteAiAgent(agentId)` | Delete an agent |
295
+ | `getAiAgent(agentId)` | Get agent details (includes system_prompt) |
296
+ | `listAiAgents()` | List all agents |
297
+ | `listAiVoices()` | List available voices (30 Chirp 3 HD voices) |
298
+ | `previewAiVoice(voice, text)` | Preview a voice with sample text |
299
+ | `moveAiAgent(agentId, newTenantId)` | Move agent to another tenant |
300
+ | `aiAgentsHealth()` | Check AI Bridge connectivity |
336
301
 
337
- Every field below can be updated via `updateAiAgent()`. Only include the fields you want to change — omitted fields keep their current values.
302
+ #### Create an Agent
338
303
 
339
304
  ```typescript
340
- await voip.updateAiAgent('my-agent', {
341
- // ─── Identity ─────────────────────────────────
342
- display_name: 'Sofia', // Agent's spoken name
343
- company: 'Acme Corp', // Company the agent represents
344
- role: 'Sales Representative', // Agent's role/title
345
- language: 'es', // BCP-47 language code
346
-
347
- // ─── Voice ────────────────────────────────────
348
- voice: 'Sulafat', // Chirp 3 HD voice (use listAiVoices())
349
-
350
- // ─── Behavior ─────────────────────────────────
351
- speak_first: true, // Agent speaks first on connect
352
- max_duration_s: 600, // Max call length (seconds)
353
- idle_timeout_s: 30, // Silence timeout (seconds)
354
- enable_transcription: false, // Real-time transcription
355
-
356
- // ─── AI Model ─────────────────────────────────
357
- temperature: 0.5, // 0-2, lower = more deterministic
358
- thinking_level: 'minimal', // 'minimal' | 'medium' | 'high'
359
- system_prompt: '...', // Full system prompt text
360
-
361
- // ─── Tools ────────────────────────────────────
362
- tools: [ // Available functions during calls
363
- 'transfer_to_human', // Transfer to human (built-in)
364
- 'end_call', // End call (built-in)
365
- 'create_lead', // Create CRM lead (webhook)
366
- 'leave_message', // Save message (webhook)
367
- 'schedule_appointment', // Book appointment (webhook)
368
- 'send_sms', // Send SMS (webhook)
369
- 'check_policy_status', // Look up policy (webhook)
305
+ const { agent } = await voip.createAiAgent({
306
+ agent_id: 'my-receptionist', // unique slug, cannot change after creation
307
+ display_name: 'Sofia', // name the AI uses
308
+ company: 'Acme Corp', // company context
309
+ role: 'Receptionist', // role context
310
+ language: 'en', // 'en', 'es', etc.
311
+ voice: 'Sulafat', // use listAiVoices() for options
312
+ speak_first: true, // AI greets caller first
313
+ temperature: 0.5, // 0-2, lower = more consistent
314
+ thinking_level: 'minimal', // 'minimal' | 'medium' | 'high'
315
+ system_prompt: 'You are Sofia...', // full AI instructions
316
+ tools: [
317
+ 'transfer_to_human', // built-in: transfer call
318
+ 'end_call', // built-in: hang up
319
+ 'create_lead', // webhook: create CRM lead
320
+ 'leave_message', // webhook: save message
321
+ 'schedule_appointment', // webhook: book appointment
322
+ 'send_sms', // webhook: send text message
323
+ 'check_policy_status', // webhook: look up policy
370
324
  ],
371
325
  tools_webhook_url: 'https://your-crm.com/api/webhooks/ai-tools',
372
-
373
- // ─── Messages ─────────────────────────────────
374
- greeting: 'Hello! How can I help?', // Override speak_first greeting
375
- farewell: 'Thanks for calling!', // Override farewell message
326
+ max_duration_s: 600, // max call length (seconds)
327
+ idle_timeout_s: 30, // silence timeout (seconds)
376
328
  });
377
329
  ```
378
330
 
379
- **Clearing fields:** Send `null` to clear string fields (`system_prompt: null`, `tools_webhook_url: null`). Send `[]` to clear tools. Number and boolean fields cannot be nulled — send the default value instead.
380
-
381
- #### Assigning an AI Agent to a Phone Number
331
+ #### Update an Agent
382
332
 
383
- After creating an agent, link it to a DID so inbound calls are answered by the AI:
333
+ Every field is editable after creation (except `agent_id`). Only send the fields you want to change:
384
334
 
385
335
  ```typescript
386
- // Assign agent to DID — calls to this number now go to the AI
387
- await voip.updateDid('did-uuid', {
388
- inboundRoute: 'ai_agent',
389
- routeTarget: 'acme-sales', // must match agent_id
390
- aiTransferType: 'queue', // where AI transfers when caller asks for human
391
- aiTransferTarget: 'support-queue-uuid',
336
+ // Change the voice and name
337
+ await voip.updateAiAgent('my-receptionist', {
338
+ display_name: 'Isabella',
339
+ voice: 'Aoede',
392
340
  });
393
341
 
394
- // Transfer types: 'extension', 'queue', 'ringgroup', 'ivr', 'external', 'voicemail'
395
- // Transfer target: the UUID/number of the destination
396
- ```
397
-
398
- #### Complete Example: Full Agent Setup
399
-
400
- ```typescript
401
- import { VoIPClient } from '@cemscale-voip/voip-sdk';
402
-
403
- const voip = new VoIPClient({
404
- apiUrl: process.env.VOIP_API_URL, // https://voip-api.cemscale.com
405
- apiKey: process.env.VOIP_API_KEY, // your API key
342
+ // Change the system prompt
343
+ await voip.updateAiAgent('my-receptionist', {
344
+ system_prompt: 'You are Isabella, a bilingual receptionist...',
406
345
  });
407
346
 
408
- // 1. Create the agent
409
- const { agent } = await voip.createAiAgent({
410
- agent_id: 'support-bot',
411
- display_name: 'Alex',
412
- company: 'My Company',
413
- language: 'en',
414
- voice: 'Kore',
415
- speak_first: true,
416
- thinking_level: 'minimal',
417
- system_prompt: 'You are Alex, a support agent for My Company. Help callers with their issues. Transfer to a human only if the caller explicitly asks.',
347
+ // Change tools
348
+ await voip.updateAiAgent('my-receptionist', {
349
+ tools: ['transfer_to_human', 'end_call', 'send_sms'],
418
350
  });
419
351
 
420
- // 2. Link agent to a phone number
421
- await voip.updateDid('did-uuid-here', {
422
- inboundRoute: 'ai_agent',
423
- routeTarget: 'support-bot',
424
- aiTransferType: 'extension',
425
- aiTransferTarget: '1001',
352
+ // Change behavior
353
+ await voip.updateAiAgent('my-receptionist', {
354
+ temperature: 0.3,
355
+ speak_first: false,
356
+ max_duration_s: 300,
357
+ idle_timeout_s: 15,
426
358
  });
427
359
 
428
- // Now when someone calls that number, Alex (the AI) answers.
429
- // If the caller says "let me talk to a person", Alex transfers to extension 1001.
430
- ```
431
-
432
- #### Available Voices
433
-
434
- | Voice | Style |
435
- |-------|-------|
436
- | `Kore` | Professional, warm (default) |
437
- | `Aoede` | Friendly, energetic |
438
- | `Charon` | Deep, authoritative |
439
- | `Fenrir` | Calm, measured |
440
- | `Puck` | Upbeat, playful |
441
-
442
- #### Agent Fields Reference
443
-
444
- | Field | Type | Required | Default | Description |
445
- |-------|------|----------|---------|-------------|
446
- | `agent_id` | string | Yes | — | Unique slug identifier (e.g. `acme-sales`) |
447
- | `display_name` | string | Yes | — | Display name in dashboard and logs |
448
- | `company` | string | No | `display_name` | Company name for AI context |
449
- | `role` | string | No | `AI Assistant` | Role context for the AI |
450
- | `language` | string | No | `en` | Language code: `en`, `es`, `fr`, etc. |
451
- | `voice` | string | No | `Kore` | Gemini voice name |
452
- | `system_prompt` | string | No | `''` | Instructions for the AI agent |
453
- | `speak_first` | boolean | No | `true` | Agent greets caller first |
454
- | `thinking_level` | string | No | `minimal` | Reasoning depth: `minimal`, `low`, `medium`, `high` |
455
- | `temperature` | number | No | `0.7` | Creativity: 0.0 (precise) to 1.0 (creative) |
456
- | `tools` | string[] | No | `[]` | List of tools the agent can use (see below) |
457
- | `tools_webhook_url` | string | No | `''` | URL where tool invocations are sent via POST |
458
-
459
- #### AI Agent Tools (Function Calling)
460
-
461
- During a phone call, the AI agent can invoke tools — actions that do something in the real world. When Gemini decides to call a tool, the platform either handles it internally (transfer, hangup) or sends a POST request to your `tools_webhook_url` and waits for your response. Gemini then uses your response to continue the conversation naturally.
462
-
463
- **There are 7 standard tools available:**
464
-
465
- | Tool Name | Type | Description | Webhook? |
466
- |-----------|------|-------------|----------|
467
- | `transfer_to_human` | Built-in | Transfers the live call to a human agent at the configured extension via PBX | No — handled internally by the platform |
468
- | `end_call` | Built-in | Ends the phone call after the AI says goodbye. Triggers after 2 seconds to let final audio play | No — handled internally by the platform |
469
- | `create_lead` | Webhook | Creates a new lead/prospect. Gemini collects first_name, last_name, phone, email, company, interest, notes from the caller | Yes — POST to your `tools_webhook_url` |
470
- | `leave_message` | Webhook | Records a message. Gemini collects caller_name, caller_phone, message content, urgency, and who it's for | Yes — POST to your `tools_webhook_url` |
471
- | `schedule_appointment` | Webhook | Books an appointment. Gemini collects caller_name, phone, preferred date, time, duration, purpose | Yes — POST to your `tools_webhook_url` |
472
- | `check_policy_status` | Webhook | Looks up a policy/account status. Gemini collects policy_number, phone, last_name, date_of_birth, lookup_type | Yes — POST to your `tools_webhook_url` |
473
- | `send_sms` | Webhook | Sends an SMS text message to the caller during the live call. Gemini collects to_phone, message content, purpose, caller_name | Yes — POST to your `tools_webhook_url` |
474
-
475
- **Complete flow — what happens when a tool is invoked:**
476
-
477
- ```
478
- EXAMPLE: Caller wants to leave a message
479
-
480
- 1. Caller says: "I'd like to leave a message for the sales team"
481
- 2. Gemini (AI) says: "Sure, I can take that for you. What's your name?"
482
- 3. Caller says: "Juan Garcia"
483
- 4. Gemini says: "Got it, Juan. And what's the best number to reach you?"
484
- 5. Caller says: "305-123-4567"
485
- 6. Gemini says: "Perfect. What's the message you'd like me to pass along?"
486
- 7. Caller says: "I'm interested in the premium plan, please have someone call me back"
487
- 8. Gemini says: "Let me confirm — your message for the sales team is that you're
488
- interested in the premium plan and you'd like a callback at 305-123-4567. Is that right?"
489
- 9. Caller says: "Yes, that's correct"
490
-
491
- 10. >>> Gemini invokes the tool: leave_message({
492
- caller_name: "Juan Garcia",
493
- caller_phone: "+13051234567",
494
- for_person: "sales team",
495
- message: "Interested in the premium plan, requesting callback",
496
- urgent: false,
497
- callback_preferred_time: "anytime"
498
- })
499
-
500
- 11. >>> Platform sends POST to YOUR tools_webhook_url:
501
- POST https://your-crm.com/api/ai-tools-webhook
502
- Content-Type: application/json
503
-
504
- {
505
- "function": "leave_message",
506
- "args": {
507
- "caller_name": "Juan Garcia",
508
- "caller_phone": "+13051234567",
509
- "for_person": "sales team",
510
- "message": "Interested in the premium plan, requesting callback",
511
- "urgent": false,
512
- "callback_preferred_time": "anytime"
513
- },
514
- "call_id": "ff6a1693-b2f8-123f-b8b1-0a7dee2809a7",
515
- "tenant_id": "6a097cb2-504f-43be-819a-b76209976818"
516
- }
517
-
518
- 12. >>> YOUR app processes it, saves to database, and responds:
519
- {
520
- "status": "saved",
521
- "message": "Message recorded successfully. The sales team will call Juan back today."
522
- }
523
-
524
- 13. >>> Gemini reads your response and tells the caller:
525
- "All set, Juan. I've passed your message to the sales team.
526
- They'll give you a call back today. Is there anything else?"
527
-
528
- 14. Caller says: "No, that's all. Thanks!"
529
-
530
- 15. >>> Gemini invokes: end_call({ reason: "conversation_complete", summary: "Took message for sales team from Juan Garcia" })
531
-
532
- 16. >>> Platform waits 2 seconds for final audio, then hangs up the call.
533
- ```
534
-
535
-
536
- **Example 2: SMS confirmation during a call**
537
-
538
- ```
539
- EXAMPLE: Caller schedules an appointment and wants a text confirmation
540
-
541
- 1. Caller says: "Can I schedule a consultation for Friday?"
542
- 2. Gemini (AI): "Of course! What time works best for you?"
543
- 3. Caller: "Around 2pm would be great"
544
- 4. Gemini: "Perfect. I have you down for Friday at 2pm. Can I get your name?"
545
- 5. Caller: "It's Ana Martinez"
546
- 6. Gemini: "Thank you Ana. And your phone number?"
547
- 7. Caller: "786-555-0198"
548
-
549
- 8. >>> Gemini invokes: schedule_appointment({...})
550
- 9. >>> Your app responds: { status: "scheduled", message: "Confirmed for Friday at 2pm." }
551
-
552
- 10. Gemini: "You're all set for Friday at 2pm, Ana. Would you like me
553
- to send you a text confirmation with the details?"
554
- 11. Caller: "Yes please, send it to this number"
555
-
556
- 12. >>> Gemini invokes: send_sms({
557
- to_phone: "+17865550198",
558
- message: "Hi Ana! Your consultation is confirmed for Friday at 2:00 PM at our office. Address: 123 Main St. Reply HELP for questions.",
559
- purpose: "appointment_confirmation",
560
- caller_name: "Ana Martinez"
561
- })
562
-
563
- 13. >>> Platform sends POST to YOUR tools_webhook_url:
564
- POST https://your-crm.com/api/ai-tools-webhook
565
- Content-Type: application/json
566
-
567
- {
568
- "function": "send_sms",
569
- "args": {
570
- "to_phone": "+17865550198",
571
- "message": "Hi Ana! Your consultation is confirmed for Friday at 2:00 PM...",
572
- "purpose": "appointment_confirmation",
573
- "caller_name": "Ana Martinez"
574
- },
575
- "call_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
576
- "tenant_id": "6a097cb2-504f-43be-819a-b76209976818"
577
- }
578
-
579
- 14. >>> YOUR app sends the SMS via Twilio/Telnyx/etc and responds:
580
- { "status": "sent", "message": "The text has been sent to your phone." }
581
-
582
- 15. >>> Gemini tells the caller:
583
- "Done! I just sent you a text with your appointment details.
584
- Is there anything else I can help you with?"
585
-
586
- 16. Caller: "No that's it, thank you!"
587
-
588
- 17. >>> Gemini invokes: end_call({ reason: "conversation_complete", summary: "Scheduled Friday 2pm consultation for Ana Martinez, sent SMS confirmation" })
589
- ```
590
-
591
- **Step 1 — Create the agent with tools enabled:**
592
-
593
- ```typescript
594
- const { agent } = await voip.createAiAgent({
595
- agent_id: 'acme-receptionist',
596
- display_name: 'Sofia',
597
- company: 'Acme Corp',
598
- language: 'en',
360
+ // Change everything at once
361
+ await voip.updateAiAgent('my-receptionist', {
362
+ display_name: 'Daniela',
363
+ company: 'New Company',
364
+ role: 'Sales Agent',
365
+ language: 'es',
599
366
  voice: 'Sulafat',
600
367
  speak_first: true,
601
- thinking_level: 'minimal',
602
368
  temperature: 0.5,
603
-
604
- // Enable the tools you want this agent to use
605
- tools: [
606
- 'transfer_to_human', // Transfer to human when caller asks
607
- 'end_call', // Hang up when conversation is done
608
- 'create_lead', // Create leads in your CRM
609
- 'leave_message', // Take messages from callers
610
- 'schedule_appointment', // Book appointments
611
- 'send_sms', // Send SMS confirmations during calls
612
- ],
613
-
614
- // YOUR endpoint that receives tool calls — you MUST build this
615
- tools_webhook_url: 'https://your-app.com/api/ai-tools-webhook',
616
-
617
- system_prompt: `You are Sofia, a professional receptionist for Acme Corp.
618
- You are warm, natural, and efficient. Keep responses to 1-2 sentences.
619
- You can help callers with:
620
- - Leaving a message for the team
621
- - Scheduling an appointment
622
- - Answering general questions about Acme Corp
623
- - Creating a lead when a new prospect calls
624
- If the caller asks to speak with a person, transfer them.
625
- When the conversation is complete, end the call politely.`,
369
+ thinking_level: 'minimal',
370
+ max_duration_s: 600,
371
+ idle_timeout_s: 30,
372
+ system_prompt: 'You are Daniela...',
373
+ tools: ['transfer_to_human', 'end_call', 'create_lead', 'send_sms'],
374
+ tools_webhook_url: 'https://your-crm.com/api/webhooks/ai-tools',
375
+ greeting: 'Hola, gracias por llamar',
376
+ farewell: 'Que tengas buen dia',
626
377
  });
627
378
  ```
628
379
 
629
- **Step 2 Build the webhook endpoint in your app:**
630
-
631
- Your app needs ONE endpoint that receives ALL tool calls. The `function` field tells you which tool was invoked. The `args` field contains the data Gemini collected from the caller. The `call_id` and `tenant_id` let you track which call and tenant triggered the action.
380
+ **Clearing fields:** Send `null` to clear string fields (`tools_webhook_url: null`). Send `[]` to clear tools.
381
+
382
+ #### All Agent Fields Reference
383
+
384
+ | Field | Type | Default | Description |
385
+ |-------|------|---------|-------------|
386
+ | `agent_id` | string | required | Unique slug (cannot change after creation) |
387
+ | `display_name` | string | required | Name the AI uses on calls |
388
+ | `company` | string | display_name | Company name for AI context |
389
+ | `role` | string | `"AI Assistant"` | Role/title for AI context |
390
+ | `language` | string | `"en"` | Language code: `en`, `es`, `fr`, etc. |
391
+ | `voice` | string | `"Kore"` | Chirp 3 HD voice name |
392
+ | `system_prompt` | string | `""` | Full instructions for the AI |
393
+ | `speak_first` | boolean | `true` | Whether AI greets caller first |
394
+ | `temperature` | number | `0.7` | Creativity: 0.0 precise, 1.0 creative |
395
+ | `thinking_level` | string | `"minimal"` | `"minimal"`, `"medium"`, `"high"` |
396
+ | `tools` | string[] | `[]` | Tools the agent can invoke |
397
+ | `tools_webhook_url` | string/null | `null` | URL for webhook tool calls |
398
+ | `greeting` | string/null | `null` | Override greeting phrase |
399
+ | `farewell` | string/null | `null` | Override farewell phrase |
400
+ | `max_duration_s` | number | `600` | Max call duration (seconds) |
401
+ | `idle_timeout_s` | number | `30` | Silence timeout (seconds) |
402
+ | `enable_transcription` | boolean | `false` | Enable real-time transcription |
403
+
404
+ #### Assign Agent to a Phone Number
405
+
406
+ After creating an agent, link it to a DID so inbound calls go to the AI:
632
407
 
633
408
  ```typescript
634
- // In your app (Express, Fastify, Next.js API route, etc.)
635
-
636
- app.post('/api/ai-tools-webhook', async (req, res) => {
637
- const { function: toolName, args, call_id, tenant_id } = req.body;
638
-
639
- console.log(`[AI Tool] ${toolName} called during call ${call_id}`);
640
- console.log(`[AI Tool] Args:`, args);
641
-
642
- try {
643
- switch (toolName) {
644
-
645
- // ─── CREATE LEAD ──────────────────────────────────────────
646
- // Gemini collected: first_name, last_name (optional), phone,
647
- // email (optional), company (optional), notes (optional),
648
- // interest (optional), source (optional)
649
- case 'create_lead': {
650
- const lead = await db.leads.create({
651
- data: {
652
- firstName: args.first_name,
653
- lastName: args.last_name || '',
654
- phone: args.phone,
655
- email: args.email || null,
656
- company: args.company || null,
657
- notes: args.notes || null,
658
- interest: args.interest || null,
659
- source: 'ai_phone_call',
660
- callId: call_id,
661
- tenantId: tenant_id,
662
- createdAt: new Date(),
663
- },
664
- });
665
-
666
- // The "message" field is what Gemini reads back to the caller
667
- return res.json({
668
- status: 'created',
669
- lead_id: lead.id,
670
- message: `I've created a record for ${args.first_name}. ` +
671
- `A team member will follow up shortly.`,
672
- });
673
- }
674
-
675
- // ─── LEAVE MESSAGE ────────────────────────────────────────
676
- // Gemini collected: caller_name, caller_phone, for_person (optional),
677
- // message, urgent (boolean, optional),
678
- // callback_preferred_time (optional)
679
- case 'leave_message': {
680
- const msg = await db.messages.create({
681
- data: {
682
- callerName: args.caller_name,
683
- callerPhone: args.caller_phone,
684
- forPerson: args.for_person || null,
685
- content: args.message,
686
- urgent: args.urgent || false,
687
- callbackTime: args.callback_preferred_time || 'anytime',
688
- callId: call_id,
689
- tenantId: tenant_id,
690
- createdAt: new Date(),
691
- },
692
- });
693
-
694
- // Optional: send a real-time notification to your team
695
- await notifyTeam({
696
- type: args.urgent ? 'URGENT_MESSAGE' : 'NEW_MESSAGE',
697
- from: args.caller_name,
698
- phone: args.caller_phone,
699
- message: args.message,
700
- });
701
-
702
- return res.json({
703
- status: 'saved',
704
- message_id: msg.id,
705
- message: args.urgent
706
- ? `I've flagged this as urgent. The team will get back to ${args.caller_name} right away.`
707
- : `Message recorded. Someone from the team will call ${args.caller_name} back soon.`,
708
- });
709
- }
710
-
711
- // ─── SCHEDULE APPOINTMENT ─────────────────────────────────
712
- // Gemini collected: caller_name, phone, date, time (optional),
713
- // duration (optional, default "30 minutes"),
714
- // purpose, notes (optional)
715
- case 'schedule_appointment': {
716
- // Check availability in your calendar system
717
- const isAvailable = await calendar.checkAvailability(
718
- args.date,
719
- args.time,
720
- );
721
-
722
- if (!isAvailable) {
723
- // Tell Gemini the slot is not available — it will offer alternatives
724
- return res.json({
725
- status: 'unavailable',
726
- message: `That time slot is not available. ` +
727
- `We have openings in the morning between 9 and 12, ` +
728
- `or in the afternoon between 2 and 4. ` +
729
- `Would any of those work?`,
730
- });
731
- }
732
-
733
- const appointment = await calendar.create({
734
- name: args.caller_name,
735
- phone: args.phone,
736
- date: args.date,
737
- time: args.time || '10:00 AM',
738
- duration: args.duration || '30 minutes',
739
- purpose: args.purpose,
740
- notes: args.notes || null,
741
- callId: call_id,
742
- tenantId: tenant_id,
743
- });
744
-
745
- return res.json({
746
- status: 'scheduled',
747
- appointment_id: appointment.id,
748
- message: `Appointment confirmed for ${args.date} at ${args.time}. ` +
749
- `${args.caller_name} will receive a confirmation shortly.`,
750
- });
751
- }
752
-
753
- // ─── CHECK POLICY STATUS ──────────────────────────────────
754
- // Gemini collected: policy_number, phone (optional),
755
- // last_name (optional), date_of_birth (optional),
756
- // lookup_type (optional: status/coverage/renewal/payment/claims)
757
- case 'check_policy_status': {
758
- const policy = await db.policies.findFirst({
759
- where: { policyNumber: args.policy_number },
760
- });
761
-
762
- if (!policy) {
763
- return res.json({
764
- status: 'not_found',
765
- message: `I couldn't find a policy with number ${args.policy_number}. ` +
766
- `Could you double-check that number?`,
767
- });
768
- }
409
+ await voip.updateDid('did-uuid', {
410
+ inboundRoute: 'ai_agent',
411
+ routeTarget: 'my-receptionist', // must match agent_id
412
+ aiTransferType: 'extension', // where transfer_to_human sends calls
413
+ aiTransferTarget: '1001', // extension/queue to transfer to
414
+ });
415
+ // Transfer types: 'extension', 'queue', 'ringgroup', 'ivr', 'external', 'voicemail'
416
+ ```
769
417
 
770
- return res.json({
771
- status: 'found',
772
- policy_number: policy.policyNumber,
773
- policy_status: policy.status, // e.g. "active", "expired", "pending"
774
- renewal_date: policy.renewalDate,
775
- balance_due: policy.balanceDue,
776
- message: `Policy ${args.policy_number} is currently ${policy.status}. ` +
777
- `${policy.renewalDate ? 'Renewal date is ' + policy.renewalDate + '.' : ''} ` +
778
- `${policy.balanceDue ? 'Balance due: $' + policy.balanceDue + '.' : 'No balance due.'}`,
779
- });
780
- }
418
+ #### Move Agent Between Tenants
781
419
 
420
+ ```typescript
421
+ const result = await voip.moveAiAgent('my-receptionist', 'new-tenant-uuid');
422
+ console.log(result.previous_tenant_id, '->', result.new_tenant_id);
423
+ ```
782
424
 
783
- // --- SEND SMS -------------------------------------------------
784
- // Gemini collected: to_phone, message, purpose (optional),
785
- // caller_name (optional)
786
- case 'send_sms': {
787
- // Send the SMS via your SMS provider (Twilio, Telnyx, etc.)
788
- const smsResult = await smsProvider.send({
789
- to: args.to_phone,
790
- body: args.message,
791
- // Use your tenant's DID or a dedicated SMS number as the sender
792
- from: tenant.smsNumber || tenant.mainDid,
793
- });
425
+ ---
794
426
 
795
- // Log it in your CRM
796
- await db.smsLogs.create({
797
- data: {
798
- toPhone: args.to_phone,
799
- message: args.message,
800
- purpose: args.purpose || 'general',
801
- callerName: args.caller_name || null,
802
- callId: call_id,
803
- tenantId: tenant_id,
804
- status: smsResult.success ? 'sent' : 'failed',
805
- createdAt: new Date(),
806
- },
807
- });
427
+ ### AI Tools & Webhook Integration
808
428
 
809
- if (!smsResult.success) {
810
- return res.json({
811
- status: 'failed',
812
- message: "I was unable to send the text message right now. " +
813
- "Let me take note of your number and we will send it to you shortly.",
814
- });
815
- }
429
+ When the AI agent invokes a webhook tool during a live call, the platform sends a POST to your `tools_webhook_url`. Your CRM processes the action and responds with JSON. The `message` field in your response is what the AI speaks to the caller.
816
430
 
817
- return res.json({
818
- status: 'sent',
819
- sms_id: smsResult.id,
820
- message: "The text message has been sent to " + args.to_phone + ". " +
821
- "Please check your messages in a moment.",
822
- });
823
- }
431
+ #### Architecture
824
432
 
825
- // ─── UNKNOWN TOOL ─────────────────────────────────────────
826
- default: {
827
- console.warn(`[AI Tool] Unknown tool: ${toolName}`);
828
- return res.json({
829
- status: 'unknown',
830
- message: `I've noted your request. A team member will follow up.`,
831
- });
832
- }
833
- }
834
- } catch (error) {
835
- console.error(`[AI Tool] Error handling ${toolName}:`, error);
836
- return res.status(500).json({
837
- status: 'error',
838
- message: `I'm having trouble processing that right now. ` +
839
- `Let me take a note and have someone follow up with you.`,
840
- });
841
- }
842
- });
433
+ ```
434
+ Caller speaks -> Gemini AI -> invokes tool (e.g. send_sms)
435
+ -> VoIP Platform -> POST to VoIP API -> POST to YOUR webhook
436
+ -> Your CRM processes (sends SMS, creates lead, etc.)
437
+ -> Your CRM responds: { status: "sent", message: "Text sent" }
438
+ -> Gemini speaks to caller: "I just sent you the text"
843
439
  ```
844
440
 
845
- **Step 3 Link the agent to a phone number (DID):**
441
+ #### The 7 Standard Tools
846
442
 
847
- ```typescript
848
- // Assign the agent to answer a specific phone number
849
- await voip.updateDid('did-uuid-here', {
850
- inboundRoute: 'ai_agent',
851
- routeTarget: 'acme-receptionist', // must match agent_id
852
- aiTransferType: 'extension', // where transfer_to_human sends the call
853
- aiTransferTarget: '1001', // extension number to transfer to
854
- });
855
- ```
443
+ | Tool | Type | What it does |
444
+ |------|------|-------------|
445
+ | `transfer_to_human` | Built-in | Transfers call to the extension/queue configured on the DID |
446
+ | `end_call` | Built-in | Hangs up after 2s delay so AI can say goodbye |
447
+ | `create_lead` | Webhook | Creates a lead/prospect in your CRM |
448
+ | `leave_message` | Webhook | Saves a message from the caller |
449
+ | `schedule_appointment` | Webhook | Books an appointment |
450
+ | `send_sms` | Webhook | Sends an SMS text message during the live call |
451
+ | `check_policy_status` | Webhook | Looks up a policy/account status |
452
+
453
+ #### Webhook Request (what your CRM receives)
856
454
 
857
- **Webhook request your app receives (POST):**
455
+ Every webhook tool call arrives as a POST with this format:
858
456
 
859
457
  ```json
860
458
  {
861
- "function": "create_lead",
459
+ "function": "send_sms",
862
460
  "args": {
863
- "first_name": "Maria",
864
- "last_name": "Rodriguez",
865
- "phone": "+17865551234",
866
- "email": "maria@gmail.com",
867
- "company": "Rodriguez LLC",
868
- "interest": "Premium shipping plan",
869
- "notes": "Called asking about bulk shipping rates for e-commerce business"
461
+ "to_phone": "+17865551234",
462
+ "message": "Your appointment is confirmed for Friday at 2pm",
463
+ "purpose": "appointment_confirmation",
464
+ "caller_name": "Ana Martinez"
870
465
  },
871
466
  "call_id": "ff6a1693-b2f8-123f-b8b1-0a7dee2809a7",
872
467
  "tenant_id": "6a097cb2-504f-43be-819a-b76209976818"
873
468
  }
874
469
  ```
875
470
 
876
- **Webhook response your app must return (JSON):**
471
+ **Headers:**
472
+
473
+ | Header | Value |
474
+ |--------|-------|
475
+ | `Content-Type` | `application/json` |
476
+ | `Authorization` | `Bearer csk_live_...` (your VOIP_TOOLS_WEBHOOK_KEY) |
477
+ | `X-Webhook-Source` | `voiceai-platform` |
478
+
479
+ #### Webhook Response (what your CRM must return)
877
480
 
878
481
  ```json
879
482
  {
880
- "status": "created",
881
- "message": "Lead created for Maria Rodriguez. A sales rep will follow up within the hour."
483
+ "status": "sent",
484
+ "message": "The text message has been sent to your phone."
882
485
  }
883
486
  ```
884
487
 
885
- **Important rules for the webhook response:**
886
- - The `message` field is what Gemini speaks to the caller. Write it naturally, as if a person is saying it on the phone.
887
- - Keep the `message` short 1-2 sentences maximum.
888
- - If something fails, still return a `message` with a graceful fallback so Gemini can tell the caller.
889
- - Your webhook has a **10 second timeout**. If it doesn't respond in time, Gemini gets: `"Action has been recorded. A team member will follow up."`
890
- - If no `tools_webhook_url` is configured on the agent, webhook tools still "work" but always return the generic fallback message.
488
+ | Field | Type | Description |
489
+ |-------|------|-------------|
490
+ | `status` | string | Result: `"sent"`, `"created"`, `"saved"`, `"scheduled"`, `"found"`, `"not_found"`, `"error"`, `"failed"` |
491
+ | `message` | string | **What the AI speaks to the caller.** Write naturally, 1-2 sentences max. |
492
+
493
+ **Rules:**
494
+ - `message` is spoken aloud to the caller. Write it as natural speech.
495
+ - Keep to 1-2 sentences max.
496
+ - If something fails, still return a `message` with a graceful fallback.
497
+ - Your webhook has a **9 second timeout**.
498
+ - Return HTTP 200 for success, 500 for errors.
891
499
 
892
- **Tool arguments reference:**
500
+ #### Tool Arguments Reference
893
501
 
894
- `create_lead` args:
502
+ **`create_lead`**
895
503
  | Arg | Type | Required | Description |
896
504
  |-----|------|----------|-------------|
897
505
  | `first_name` | string | Yes | Caller's first name |
898
- | `last_name` | string | No | Caller's last name |
506
+ | `last_name` | string | No | Last name |
899
507
  | `phone` | string | Yes | Phone number |
900
508
  | `email` | string | No | Email if provided |
901
509
  | `company` | string | No | Company name |
902
- | `interest` | string | No | What they're interested in |
903
- | `notes` | string | No | Additional notes |
904
- | `source` | string | No | Lead source (default: "phone_call") |
510
+ | `interest` | string | No | What they want |
511
+ | `notes` | string | No | Additional context |
905
512
 
906
- `leave_message` args:
513
+ **`leave_message`**
907
514
  | Arg | Type | Required | Description |
908
515
  |-----|------|----------|-------------|
909
516
  | `caller_name` | string | Yes | Who is leaving the message |
910
517
  | `caller_phone` | string | Yes | Callback number |
911
518
  | `for_person` | string | No | Who the message is for |
912
519
  | `message` | string | Yes | Message content |
913
- | `urgent` | boolean | No | Whether caller said it's urgent |
914
- | `callback_preferred_time` | string | No | When they prefer a callback |
520
+ | `urgent` | boolean | No | Marked as urgent |
521
+ | `callback_preferred_time` | string | No | When to call back |
915
522
 
916
- `schedule_appointment` args:
523
+ **`schedule_appointment`**
917
524
  | Arg | Type | Required | Description |
918
525
  |-----|------|----------|-------------|
919
- | `caller_name` | string | Yes | Who the appointment is for |
920
- | `phone` | string | Yes | Contact phone |
921
- | `date` | string | Yes | Preferred date ("tomorrow", "Thursday", "2026-04-18") |
922
- | `time` | string | No | Preferred time ("2pm", "morning", "10:30 AM") |
923
- | `duration` | string | No | Duration ("30 minutes", "1 hour") |
526
+ | `caller_name` | string | Yes | Name |
527
+ | `phone` | string | Yes | Phone |
528
+ | `date` | string | Yes | Date ("Friday", "2026-04-18") |
529
+ | `time` | string | No | Time ("2pm", "morning") |
530
+ | `duration` | string | No | Duration ("30 minutes") |
924
531
  | `purpose` | string | Yes | Reason for appointment |
925
532
  | `notes` | string | No | Additional notes |
926
533
 
927
- `check_policy_status` args:
534
+ **`send_sms`**
928
535
  | Arg | Type | Required | Description |
929
536
  |-----|------|----------|-------------|
930
- | `policy_number` | string | Yes | Policy or account number |
931
- | `phone` | string | No | Phone for verification |
932
- | `last_name` | string | No | Last name for verification |
933
- | `date_of_birth` | string | No | DOB for verification |
934
- | `lookup_type` | string | No | What to look up: "status", "coverage", "renewal", "payment", "claims" |
537
+ | `to_phone` | string | Yes | Phone with country code ("+17865551234") |
538
+ | `message` | string | Yes | Text message content |
539
+ | `purpose` | string | No | Category: "appointment_confirmation", "address_info", etc. |
540
+ | `caller_name` | string | No | For personalization |
935
541
 
936
- `send_sms` args:
542
+ **`check_policy_status`**
937
543
  | Arg | Type | Required | Description |
938
544
  |-----|------|----------|-------------|
939
- | `to_phone` | string | Yes | Phone number to send SMS to, with country code (e.g. "+17865551234") |
940
- | `message` | string | Yes | Text message content. Max 160 chars recommended for single SMS |
941
- | `purpose` | string | No | Why the SMS is being sent: "appointment_confirmation", "address_info", "reference_number", "follow_up_link", "general" |
942
- | `caller_name` | string | No | Caller name for personalization and logging |
545
+ | `policy_number` | string | Yes | Policy/account number |
546
+ | `phone` | string | No | Verification |
547
+ | `last_name` | string | No | Verification |
943
548
 
944
- `transfer_to_human` args (no webhook — handled internally):
549
+ **`transfer_to_human`** (built-in, no webhook)
945
550
  | Arg | Type | Required | Description |
946
551
  |-----|------|----------|-------------|
947
- | `reason` | string | Yes | Why the caller wants to transfer |
948
- | `department` | string | No | Specific department if mentioned |
552
+ | `reason` | string | Yes | Why caller wants transfer |
553
+ | `department` | string | No | Specific department |
949
554
 
950
- `end_call` args (no webhook — handled internally):
555
+ **`end_call`** (built-in, no webhook)
951
556
  | Arg | Type | Required | Description |
952
557
  |-----|------|----------|-------------|
953
- | `reason` | string | Yes | Why: "conversation_complete", "caller_goodbye", "no_response", "voicemail_detected" |
954
- | `summary` | string | No | Brief summary of the call |
558
+ | `reason` | string | Yes | Why: "conversation_complete", "caller_goodbye", "no_response" |
559
+ | `summary` | string | No | Brief call summary |
955
560
 
561
+ #### CRM Webhook Setup
956
562
 
563
+ **IMPORTANT:** Your webhook endpoint must NOT have your CRM's global auth middleware. It uses its own auth via the `Authorization: Bearer` header.
957
564
 
958
- ---
959
-
960
- #### CRM Webhook Setup — Receiving AI Tool Calls
961
-
962
- When an AI agent invokes a tool during a live call (e.g. `send_sms`, `create_lead`), the VoIP platform sends a POST request to your CRM. Your CRM **must** have a webhook endpoint that receives these tool calls, processes them, and returns a response that Gemini speaks to the caller.
963
-
964
- **Architecture:**
965
- ```
966
- Caller speaks -> Gemini AI -> invokes tool (e.g. send_sms)
967
- -> VoIP Platform (Go) -> POST to VoIP API
968
- -> VoIP API forwards -> POST to YOUR CRM webhook
969
- -> Your CRM processes (sends SMS, creates lead, etc.)
970
- -> Your CRM responds with JSON
971
- -> Gemini reads response to caller
972
- ```
973
-
974
- **CRITICAL: Your webhook endpoint must NOT have global auth middleware.**
975
-
976
- The VoIP platform authenticates via `Authorization: Bearer <api_key>` where `api_key` is a VoIP SDK API key (`csk_live_...`). Your webhook handler must validate this token itself — do NOT put it behind your CRM's global auth middleware (e.g. session auth, JWT, capability checks). If your CRM's router wraps routes with automatic auth, register this endpoint WITHOUT that wrapper.
977
-
978
- **Environment variable your CRM needs:**
979
-
565
+ **Step 1 — Add environment variable:**
980
566
  ```env
981
- # The VoIP SDK API key used to authenticate tool call webhooks.
982
- # This key is created in the VoIP platform and shared with your CRM.
983
- # The VoIP platform sends it as: Authorization: Bearer <this_key>
984
567
  VOIP_TOOLS_WEBHOOK_KEY=csk_live_da3d1ee6d3b51696922206dc139beab84239a7935049b5a5
985
568
  ```
986
569
 
987
- **Complete webhook handler your CRM needs (Express/Fastify/etc.):**
570
+ **Step 2 Create the endpoint (register WITHOUT global auth middleware):**
988
571
 
989
572
  ```typescript
990
573
  import crypto from 'crypto';
991
574
 
992
- // ──────────────────────────────────────────────────────────
993
- // IMPORTANT: Register this route WITHOUT global auth middleware.
994
- // This endpoint has its own auth via validateToolsWebhookAuth().
995
- // If your router uses a `wrap()` or `requireAuth()` middleware,
996
- // do NOT apply it to this route.
997
- // ──────────────────────────────────────────────────────────
998
-
999
- /**
1000
- * Validate that the request comes from the VoIP platform.
1001
- * Checks Authorization: Bearer <api_key> against VOIP_TOOLS_WEBHOOK_KEY env var.
1002
- */
1003
- function validateToolsWebhookAuth(req: { headers: Record<string, any> }): boolean {
1004
- const expectedKey = process.env.VOIP_TOOLS_WEBHOOK_KEY;
1005
- if (!expectedKey) return false;
1006
-
1007
- // Method 1: Authorization Bearer token
1008
- const authHeader = req.headers.authorization || req.headers.Authorization;
1009
- if (authHeader) {
1010
- const token = typeof authHeader === 'string' && authHeader.startsWith('Bearer ')
1011
- ? authHeader.slice(7)
1012
- : authHeader;
1013
- if (typeof token === 'string' && token.length === expectedKey.length) {
1014
- try {
1015
- return crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expectedKey));
1016
- } catch { return false; }
1017
- }
1018
- }
1019
-
1020
- // Method 2: X-API-Key header
1021
- const apiKeyHeader = req.headers['x-api-key'];
1022
- if (typeof apiKeyHeader === 'string' && apiKeyHeader.length === expectedKey.length) {
1023
- try {
1024
- return crypto.timingSafeEqual(Buffer.from(apiKeyHeader), Buffer.from(expectedKey));
1025
- } catch { return false; }
1026
- }
1027
-
1028
- return false;
575
+ // Auth validation — checks Bearer token
576
+ function validateAuth(req: any): boolean {
577
+ const expected = process.env.VOIP_TOOLS_WEBHOOK_KEY;
578
+ if (!expected) return false;
579
+ const auth = req.headers.authorization || '';
580
+ const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
581
+ if (token.length !== expected.length) return false;
582
+ try {
583
+ return crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expected));
584
+ } catch { return false; }
1029
585
  }
1030
586
 
1031
- // Register WITHOUT global auth middleware:
1032
- // WRONG: router.post('/api/webhooks/ai-tools', requireAuth, handler)
1033
- // RIGHT: router.post('/api/webhooks/ai-tools', handler)
1034
- // If using a `wrap()` function that enforces auth, do NOT use it here.
1035
-
587
+ // IMPORTANT: Register WITHOUT global auth middleware (no wrap(), no requireAuth)
1036
588
  app.post('/api/webhooks/ai-tools', async (req, res) => {
1037
- // 1. Validate auth
1038
- if (!validateToolsWebhookAuth(req)) {
589
+ if (!validateAuth(req)) {
1039
590
  return res.status(401).json({ status: 'error', message: 'Unauthorized' });
1040
591
  }
1041
592
 
1042
- // 2. Parse the tool call
1043
593
  const { function: toolName, args, call_id, tenant_id } = req.body;
1044
594
 
1045
- if (!toolName || !args) {
1046
- return res.status(400).json({ status: 'error', message: 'Missing function or args' });
1047
- }
1048
-
1049
- console.log(`[AI Tool] ${toolName} called during call ${call_id} (tenant: ${tenant_id})`);
595
+ switch (toolName) {
1050
596
 
1051
- try {
1052
- switch (toolName) {
1053
-
1054
- // ─── SEND SMS ──────────────────────────────────────────────
1055
- // Gemini collected: to_phone (required), message (required),
1056
- // purpose (optional), caller_name (optional)
1057
- //
1058
- // When to use: The caller asks "can you send me that by text?"
1059
- // or the AI decides to send a confirmation after booking, etc.
1060
- case 'send_sms': {
1061
- const toPhone = args.to_phone;
1062
- const messageText = args.message;
1063
-
1064
- if (!toPhone || !messageText) {
1065
- return res.json({
1066
- status: 'error',
1067
- message: 'I need the phone number and message to send. Could you provide those?',
1068
- });
1069
- }
1070
-
1071
- // ── Send the SMS using YOUR SMS provider ──
1072
- // Replace this with your actual SMS sending code.
1073
- // Examples: Twilio, Telnyx, Vonage, AWS SNS, etc.
1074
- //
1075
- // Twilio example:
1076
- // const twilio = require('twilio')(TWILIO_SID, TWILIO_AUTH);
1077
- // const sms = await twilio.messages.create({
1078
- // to: toPhone,
1079
- // from: YOUR_TWILIO_NUMBER,
1080
- // body: messageText,
1081
- // });
1082
- //
1083
- // Telnyx example:
1084
- // const telnyx = require('telnyx')(TELNYX_API_KEY);
1085
- // const sms = await telnyx.messages.create({
1086
- // to: toPhone,
1087
- // from: YOUR_TELNYX_NUMBER,
1088
- // text: messageText,
1089
- // });
1090
-
1091
- // YOUR SMS SENDING CODE HERE:
1092
- const smsResult = await sendSms({
1093
- to: toPhone,
1094
- message: messageText,
1095
- });
1096
-
1097
- // Log the SMS in your database
1098
- // await db.smsLogs.create({ toPhone, message: messageText,
1099
- // purpose: args.purpose, callerName: args.caller_name,
1100
- // callId: call_id, tenantId: tenant_id, status: smsResult.success ? 'sent' : 'failed' });
1101
-
1102
- if (!smsResult.success) {
1103
- return res.json({
1104
- status: 'failed',
1105
- message: 'I was unable to send the text message right now. ' +
1106
- 'Let me take note and we will send it to you shortly.',
1107
- });
1108
- }
1109
-
1110
- return res.json({
1111
- status: 'sent',
1112
- message: 'The text message has been sent to ' + toPhone + '. ' +
1113
- 'You should receive it in a moment.',
1114
- });
1115
- }
1116
-
1117
- // ─── CREATE LEAD ──────────────────────────────────────────
1118
- case 'create_lead': {
1119
- // args: first_name (required), last_name, phone (required),
1120
- // email, company, interest, notes, source
1121
- const lead = await createLeadInCrm({
597
+ case 'create_lead': {
598
+ const lead = await db.leads.create({
599
+ data: {
1122
600
  firstName: args.first_name,
1123
- lastName: args.last_name,
601
+ lastName: args.last_name || '',
1124
602
  phone: args.phone,
1125
- email: args.email,
1126
- company: args.company,
1127
- interest: args.interest,
1128
- notes: args.notes,
603
+ email: args.email || null,
604
+ company: args.company || null,
605
+ interest: args.interest || null,
606
+ notes: args.notes || null,
607
+ source: 'ai_phone_call',
1129
608
  callId: call_id,
1130
609
  tenantId: tenant_id,
1131
- });
1132
-
1133
- return res.json({
1134
- status: 'created',
1135
- lead_id: lead.id,
1136
- message: `I've created a record for ${args.first_name}. A team member will follow up shortly.`,
1137
- });
1138
- }
610
+ },
611
+ });
612
+ return res.json({
613
+ status: 'created',
614
+ message: `Record created for ${args.first_name}. A team member will follow up.`,
615
+ });
616
+ }
1139
617
 
1140
- // ─── LEAVE MESSAGE ────────────────────────────────────────
1141
- case 'leave_message': {
1142
- // args: caller_name (required), caller_phone (required),
1143
- // message (required), for_person, urgent, callback_preferred_time
1144
- const msg = await saveMessageInCrm({
618
+ case 'leave_message': {
619
+ await db.messages.create({
620
+ data: {
1145
621
  callerName: args.caller_name,
1146
622
  callerPhone: args.caller_phone,
1147
- message: args.message,
1148
- forPerson: args.for_person,
1149
- urgent: args.urgent,
1150
- callbackTime: args.callback_preferred_time,
623
+ forPerson: args.for_person || null,
624
+ content: args.message,
625
+ urgent: args.urgent || false,
626
+ callbackTime: args.callback_preferred_time || 'anytime',
1151
627
  callId: call_id,
1152
628
  tenantId: tenant_id,
1153
- });
1154
-
1155
- return res.json({
1156
- status: 'saved',
1157
- message: args.urgent
1158
- ? `I've flagged this as urgent. The team will get back to ${args.caller_name} right away.`
1159
- : `Message recorded. Someone from the team will call ${args.caller_name} back soon.`,
1160
- });
1161
- }
629
+ },
630
+ });
631
+ return res.json({
632
+ status: 'saved',
633
+ message: args.urgent
634
+ ? `Flagged as urgent. Team will call ${args.caller_name} back right away.`
635
+ : `Message saved. Someone will call ${args.caller_name} back soon.`,
636
+ });
637
+ }
1162
638
 
1163
- // ─── SCHEDULE APPOINTMENT ─────────────────────────────────
1164
- case 'schedule_appointment': {
1165
- // args: caller_name (required), phone (required), date (required),
1166
- // time, duration, purpose (required), notes
1167
- const appt = await scheduleAppointmentInCrm({
639
+ case 'schedule_appointment': {
640
+ await db.appointments.create({
641
+ data: {
1168
642
  callerName: args.caller_name,
1169
643
  phone: args.phone,
1170
644
  date: args.date,
1171
- time: args.time,
1172
- duration: args.duration,
645
+ time: args.time || null,
646
+ duration: args.duration || '30 minutes',
1173
647
  purpose: args.purpose,
1174
- notes: args.notes,
648
+ notes: args.notes || null,
1175
649
  callId: call_id,
1176
650
  tenantId: tenant_id,
1177
- });
1178
-
1179
- return res.json({
1180
- status: 'scheduled',
1181
- message: `Appointment confirmed for ${args.date}${args.time ? ' at ' + args.time : ''}. ` +
1182
- `${args.caller_name} will receive a confirmation shortly.`,
1183
- });
1184
- }
1185
-
1186
- // ─── CHECK POLICY STATUS ──────────────────────────────────
1187
- case 'check_policy_status': {
1188
- // args: policy_number (required), phone, last_name, date_of_birth, lookup_type
1189
- const policy = await lookupPolicyInCrm({ policyNumber: args.policy_number });
651
+ },
652
+ });
653
+ return res.json({
654
+ status: 'scheduled',
655
+ message: `Appointment confirmed for ${args.date}${args.time ? ' at ' + args.time : ''}.`,
656
+ });
657
+ }
1190
658
 
1191
- if (!policy) {
1192
- return res.json({
1193
- status: 'not_found',
1194
- message: `I couldn't find a policy with number ${args.policy_number}. Could you double-check that number?`,
1195
- });
1196
- }
659
+ case 'send_sms': {
660
+ // Use YOUR SMS provider (Twilio, Telnyx, etc.)
661
+ const result = await smsProvider.send({
662
+ to: args.to_phone,
663
+ body: args.message,
664
+ from: process.env.SMS_FROM_NUMBER,
665
+ });
1197
666
 
667
+ if (!result.success) {
1198
668
  return res.json({
1199
- status: 'found',
1200
- message: `Policy ${args.policy_number} is currently ${policy.status}.`,
669
+ status: 'failed',
670
+ message: 'Unable to send the text right now. Let me note your number and we will send it shortly.',
1201
671
  });
1202
672
  }
673
+ return res.json({
674
+ status: 'sent',
675
+ message: 'Text message sent to ' + args.to_phone + '. Check your messages in a moment.',
676
+ });
677
+ }
1203
678
 
1204
- // ─── UNKNOWN TOOL ─────────────────────────────────────────
1205
- default: {
1206
- console.warn(`[AI Tool] Unknown tool: ${toolName}`);
679
+ case 'check_policy_status': {
680
+ const policy = await db.policies.findFirst({
681
+ where: { policyNumber: args.policy_number },
682
+ });
683
+ if (!policy) {
1207
684
  return res.json({
1208
- status: 'noted',
1209
- message: "I've noted your request. A team member will follow up.",
685
+ status: 'not_found',
686
+ message: `No policy found with number ${args.policy_number}. Can you double-check?`,
1210
687
  });
1211
688
  }
689
+ return res.json({
690
+ status: 'found',
691
+ message: `Policy ${args.policy_number} is ${policy.status}.`,
692
+ });
1212
693
  }
1213
- } catch (error: any) {
1214
- console.error(`[AI Tool] Error handling ${toolName}:`, error);
1215
- return res.status(500).json({
1216
- status: 'error',
1217
- message: "I'm having trouble processing that right now. Let me take a note and have someone follow up.",
1218
- });
694
+
695
+ default:
696
+ return res.json({
697
+ status: 'noted',
698
+ message: 'Noted. A team member will follow up.',
699
+ });
1219
700
  }
1220
701
  });
1221
702
  ```
1222
703
 
1223
- **Webhook request format (what your CRM receives):**
1224
-
1225
- Every tool call arrives as a POST with this JSON body:
1226
-
1227
- ```json
1228
- {
1229
- "function": "send_sms",
1230
- "args": {
1231
- "to_phone": "+17865550198",
1232
- "message": "Your appointment is confirmed for Friday at 2:00 PM. Address: 123 Main St.",
1233
- "purpose": "appointment_confirmation",
1234
- "caller_name": "Ana Martinez"
1235
- },
1236
- "call_id": "ff6a1693-b2f8-123f-b8b1-0a7dee2809a7",
1237
- "tenant_id": "6a097cb2-504f-43be-819a-b76209976818"
1238
- }
1239
- ```
1240
-
1241
- **Headers your CRM receives:**
1242
-
1243
- | Header | Value | Description |
1244
- |--------|-------|-------------|
1245
- | `Content-Type` | `application/json` | Always JSON |
1246
- | `Authorization` | `Bearer csk_live_...` | VoIP SDK API key for auth |
1247
- | `X-Webhook-Source` | `voiceai-platform` | Identifies the source |
1248
-
1249
- **Webhook response format (what your CRM must return):**
1250
-
1251
- Your response MUST be JSON. The `message` field is what Gemini speaks to the caller:
1252
-
1253
- ```json
1254
- {
1255
- "status": "sent",
1256
- "message": "The text message has been sent to your phone. You should receive it in a moment."
1257
- }
1258
- ```
1259
-
1260
- | Field | Type | Required | Description |
1261
- |-------|------|----------|-------------|
1262
- | `status` | string | Yes | Result: `"sent"`, `"created"`, `"saved"`, `"scheduled"`, `"found"`, `"not_found"`, `"error"`, `"failed"` |
1263
- | `message` | string | Yes | **CRITICAL: This is what the AI says to the caller.** Write naturally, 1-2 sentences max. |
1264
-
1265
- **Response rules:**
1266
- - The `message` field is spoken by the AI to the caller. Write it as natural speech.
1267
- - Keep `message` to 1-2 sentences maximum.
1268
- - If something fails, STILL return a `message` with a graceful fallback.
1269
- - Your webhook has a **9 second timeout**. If it doesn't respond, Gemini hears: `"I'm having trouble processing that right now."`
1270
- - HTTP status should be `200` for success, `500` for errors. The AI always gets the `message` field either way.
1271
-
1272
- **Testing your webhook with curl:**
1273
-
704
+ **Step 3 Test with curl:**
1274
705
  ```bash
1275
- # Test send_sms tool call
1276
706
  curl -X POST https://your-crm.com/api/webhooks/ai-tools \
1277
707
  -H "Content-Type: application/json" \
1278
708
  -H "Authorization: Bearer YOUR_VOIP_TOOLS_WEBHOOK_KEY" \
1279
709
  -d '{
1280
710
  "function": "send_sms",
1281
- "args": {
1282
- "to_phone": "+17865551234",
1283
- "message": "Your appointment is confirmed for Friday at 2pm.",
1284
- "purpose": "appointment_confirmation",
1285
- "caller_name": "Juan Garcia"
1286
- },
711
+ "args": { "to_phone": "+17865551234", "message": "Test SMS" },
1287
712
  "call_id": "test-123",
1288
- "tenant_id": "your-tenant-id"
713
+ "tenant_id": "test"
1289
714
  }'
1290
-
1291
- # Expected response:
1292
- # {"status":"sent","message":"The text message has been sent to +17865551234."}
715
+ # Expected: {"status":"sent","message":"Text message sent to +17865551234..."}
1293
716
  ```
1294
717
 
718
+ #### Example Flow: SMS During a Call
719
+
720
+ ```
721
+ 1. Caller: "Can you send me the address by text?"
722
+ 2. AI: "Sure! Let me confirm your number — is it 786-555-1234?"
723
+ 3. Caller: "Yes"
724
+ 4. AI invokes: send_sms({ to_phone: "+17865551234", message: "Our address: 123 Main St, Miami FL 33101" })
725
+ 5. Platform POSTs to your webhook
726
+ 6. Your CRM sends the SMS via Twilio and responds: { status: "sent", message: "Text sent" }
727
+ 7. AI: "Done! I just sent you a text with our address. Anything else?"
728
+ ```
1295
729
 
1296
730
  ### CDR Export
1297
731
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cemscale-voip/voip-sdk",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "description": "VoIP SDK for CemScale multi-tenant platform — API client, WebRTC, React hooks",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",