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