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