@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.
- package/README.md +299 -817
- 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
|
-
|
|
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
|
-
|
|
288
|
+
#### SDK Methods
|
|
288
289
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
await voip.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
####
|
|
331
|
+
#### Update an Agent
|
|
334
332
|
|
|
335
|
-
|
|
333
|
+
Every field is editable after creation (except `agent_id`). Only send the fields you want to change:
|
|
336
334
|
|
|
337
335
|
```typescript
|
|
338
|
-
//
|
|
339
|
-
await voip.
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
//
|
|
347
|
-
|
|
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
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
//
|
|
373
|
-
await voip.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
**
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
441
|
+
#### The 7 Standard Tools
|
|
798
442
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
455
|
+
Every webhook tool call arrives as a POST with this format:
|
|
810
456
|
|
|
811
457
|
```json
|
|
812
458
|
{
|
|
813
|
-
"function": "
|
|
459
|
+
"function": "send_sms",
|
|
814
460
|
"args": {
|
|
815
|
-
"
|
|
816
|
-
"
|
|
817
|
-
"
|
|
818
|
-
"
|
|
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
|
-
**
|
|
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": "
|
|
833
|
-
"message": "
|
|
483
|
+
"status": "sent",
|
|
484
|
+
"message": "The text message has been sent to your phone."
|
|
834
485
|
}
|
|
835
486
|
```
|
|
836
487
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
|
|
500
|
+
#### Tool Arguments Reference
|
|
845
501
|
|
|
846
|
-
|
|
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 |
|
|
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
|
|
855
|
-
| `notes` | string | No | Additional
|
|
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
|
-
|
|
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 |
|
|
866
|
-
| `callback_preferred_time` | string | No | When
|
|
520
|
+
| `urgent` | boolean | No | Marked as urgent |
|
|
521
|
+
| `callback_preferred_time` | string | No | When to call back |
|
|
867
522
|
|
|
868
|
-
|
|
523
|
+
**`schedule_appointment`**
|
|
869
524
|
| Arg | Type | Required | Description |
|
|
870
525
|
|-----|------|----------|-------------|
|
|
871
|
-
| `caller_name` | string | Yes |
|
|
872
|
-
| `phone` | string | Yes |
|
|
873
|
-
| `date` | string | Yes |
|
|
874
|
-
| `time` | string | No |
|
|
875
|
-
| `duration` | string | No | Duration ("30 minutes"
|
|
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
|
-
|
|
534
|
+
**`send_sms`**
|
|
880
535
|
| Arg | Type | Required | Description |
|
|
881
536
|
|-----|------|----------|-------------|
|
|
882
|
-
| `
|
|
883
|
-
| `
|
|
884
|
-
| `
|
|
885
|
-
| `
|
|
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
|
-
|
|
542
|
+
**`check_policy_status`**
|
|
889
543
|
| Arg | Type | Required | Description |
|
|
890
544
|
|-----|------|----------|-------------|
|
|
891
|
-
| `
|
|
892
|
-
| `
|
|
893
|
-
| `
|
|
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
|
-
|
|
549
|
+
**`transfer_to_human`** (built-in, no webhook)
|
|
897
550
|
| Arg | Type | Required | Description |
|
|
898
551
|
|-----|------|----------|-------------|
|
|
899
|
-
| `reason` | string | Yes | Why
|
|
900
|
-
| `department` | string | No | Specific department
|
|
552
|
+
| `reason` | string | Yes | Why caller wants transfer |
|
|
553
|
+
| `department` | string | No | Specific department |
|
|
901
554
|
|
|
902
|
-
|
|
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"
|
|
906
|
-
| `summary` | string | No | Brief summary
|
|
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
|
-
|
|
561
|
+
#### CRM Webhook Setup
|
|
929
562
|
|
|
930
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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: '
|
|
1152
|
-
message:
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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: '
|
|
1161
|
-
message:
|
|
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
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
**
|
|
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": "
|
|
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
|
|