Complete setup guide for integrating Vapi voice calling with OpenClaw, including the "ask human" workflow where the AI can request information via Telegram during a call.
This integration enables your OpenClaw agent to:
- Make and receive phone calls via Vapi
- Ask you for information during calls via Telegram when it doesn't know something
- Relay your responses back to the caller seamlessly
- Handle complex phone tasks (restaurant reservations, customer service, etc.)
- OpenClaw instance with Telegram configured
- Vapi account (vapi.ai)
- A domain with SSL (for webhooks)
- Node.js (for the webhook proxy server)
Phone Call ↔ Vapi ↔ Assistant ↔ ask_human tool ↔ Webhook Proxy ↔ Telegram ↔ You
When the AI doesn't know something (your insurance number, preferences, etc.), it:
- Says "one sec, let me check on that" to the caller
- Calls the
ask_humantool with the question - Webhook proxy sends you a Telegram message
- Polls for your reply (up to 2 minutes)
- Relays your answer back to the caller
First, create a strong webhook secret for security:
# Generate a secure random secret
openssl rand -hex 32
# Save this - you'll need it for VAPI_WEBHOOK_SECRET and Vapi configurationcurl -X POST "https://api.vapi.ai/assistant" \
-H "Authorization: Bearer YOUR_VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "OpenClaw Voice Assistant",
"model": {
"provider": "openai",
"model": "gpt-4o-mini",
"temperature": 0.7
},
"voice": {
"provider": "vapi",
"voiceId": "elliot"
},
"transcriber": {
"provider": "deepgram",
"model": "nova-3"
},
"firstMessage": "Hi, this is your AI assistant calling. How can I help you today?",
"systemPrompt": "You are a helpful voice assistant on a phone call. Keep responses brief and conversational (1-2 sentences max). Be natural and friendly.\n\nIMPORTANT RULE: If you are asked something you don'\''t know (personal details, insurance info, preferences, etc.), say something natural like '\''one sec, let me check on that'\'' or '\''hold on, let me get that info for you.'\'' Then use the ask_human tool to get the answer. Wait for the response and relay it back. NEVER guess or make something up — always ask when you don'\''t know.\n\nPhone numbers:\n- Primary: YOUR_PRIMARY_PHONE\n- Backup: YOUR_BACKUP_PHONE",
"tools": [
{
"type": "function",
"function": {
"name": "ask_human",
"description": "Ask the human for information you don'\''t have. Use when you need personal details, preferences, or any info not in your knowledge.",
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question to ask the human"
},
"context": {
"type": "string",
"description": "Brief context about why you need this info"
}
},
"required": ["question"]
}
}
}
]
}'Save the returned assistantId.
# Import your existing number (if you have one with Twilio/etc)
curl -X POST "https://api.vapi.ai/phone-number" \
-H "Authorization: Bearer YOUR_VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "twilio",
"twilioAccountSid": "YOUR_TWILIO_SID",
"twilioAuthToken": "YOUR_TWILIO_TOKEN",
"number": "+1YOURNUMBER"
}'
# OR get a new Vapi number
curl -X POST "https://api.vapi.ai/phone-number" \
-H "Authorization: Bearer YOUR_VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "vapi",
"areaCode": "415"
}'Save the returned phoneNumberId.
Create the webhook proxy that handles the ask_human tool calls.
import express from 'express';
import fetch from 'node-fetch';
const app = express();
app.use(express.json());
const CONFIG = {
VAPI_API_KEY: process.env.VAPI_API_KEY,
TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN,
TELEGRAM_CHAT_ID: process.env.TELEGRAM_CHAT_ID, // Your Telegram chat ID
PORT: process.env.PORT || 3334
};
// Store pending questions
const pendingQuestions = new Map();
// Webhook endpoint for Vapi tool calls
app.post('/vapi', async (req, res) => {
try {
const { call, tool } = req.body;
if (tool?.function?.name === 'ask_human') {
const { question, context } = tool.function.arguments;
const questionId = `q_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
console.log(`[${questionId}] Received question: ${question}`);
// Send question to Telegram
const telegramText = `📞 Question from call:\\n\\n**${question}**\\n\\n${context ? `Context: ${context}\\n\\n` : ''}Reply to this message to answer.`;
const telegramResponse = await fetch(`https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: CONFIG.TELEGRAM_CHAT_ID,
text: telegramText,
parse_mode: 'Markdown'
})
});
const telegramResult = await telegramResponse.json();
if (!telegramResult.ok) {
console.error('Failed to send Telegram message:', telegramResult);
return res.json({ result: "Sorry, I couldn't get that information right now." });
}
const messageId = telegramResult.result.message_id;
// Store the question and start polling
pendingQuestions.set(questionId, {
telegramMessageId: messageId,
question,
timestamp: Date.now()
});
// Poll for response
const answer = await pollForAnswer(questionId, messageId);
pendingQuestions.delete(questionId);
return res.json({ result: answer });
}
res.json({ result: "Tool not recognized" });
} catch (error) {
console.error('Webhook error:', error);
res.json({ result: "Sorry, there was an error getting that information." });
}
});
async function pollForAnswer(questionId, telegramMessageId) {
const maxWaitTime = 120000; // 2 minutes
const pollInterval = 2000; // 2 seconds
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
try {
// Check for replies to our message
const updatesResponse = await fetch(`https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/getUpdates?offset=-10&limit=10`);
const updates = await updatesResponse.json();
if (updates.ok && updates.result) {
for (const update of updates.result) {
const message = update.message;
if (message?.reply_to_message?.message_id === telegramMessageId) {
console.log(`[${questionId}] Got answer: ${message.text}`);
return message.text;
}
}
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
} catch (error) {
console.error('Polling error:', error);
}
}
console.log(`[${questionId}] Timeout waiting for answer`);
return "I didn't get a response in time. Let me continue without that information.";
}
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: Date.now() });
});
app.listen(CONFIG.PORT, () => {
console.log(`Vapi webhook proxy listening on port ${CONFIG.PORT}`);
});{
"name": "vapi-webhook-proxy",
"version": "1.0.0",
"type": "module",
"dependencies": {
"express": "^4.18.0",
"node-fetch": "^3.3.0"
},
"scripts": {
"start": "node proxy.mjs"
}
}Create .env file:
VAPI_API_KEY=your_vapi_api_key_here
VAPI_WEBHOOK_SECRET=your_32_char_hex_secret_here
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
TELEGRAM_CHAT_ID=your_telegram_chat_id
PORT=3334
DEBUG=falseVAPI_WEBHOOK_SECRET is critical - it prevents unauthorized access to your webhook.
# Install cloudflared
# macOS: brew install cloudflare/cloudflare/cloudflared
# Linux: Follow https://pkg.cloudflare.com/
# Login and create tunnel
cloudflared tunnel login
cloudflared tunnel create vapi-webhook
# Start proxy server
npm install && npm start &
# Expose via tunnel
cloudflared tunnel --url http://localhost:3334
# Note the public URL (e.g., https://xxx.trycloudflare.com)npm install && npm start &
ngrok http 3334
# Note the public URLUpdate your assistant with the webhook URL:
curl -X PATCH "https://api.vapi.ai/assistant/YOUR_ASSISTANT_ID" \
-H "Authorization: Bearer YOUR_VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tools": [
{
"type": "function",
"function": {
"name": "ask_human",
"description": "Ask the human for information you don'\''t have",
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question to ask"
},
"context": {
"type": "string",
"description": "Why you need this info"
}
},
"required": ["question"]
}
},
"server": {
"url": "https://YOUR_WEBHOOK_URL/vapi",
"secret": "your_32_char_hex_secret_here"
}
}
]
}'curl -X POST "https://api.vapi.ai/call" \
-H "Authorization: Bearer YOUR_VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"assistantId": "YOUR_ASSISTANT_ID",
"phoneNumberId": "YOUR_PHONE_NUMBER_ID",
"customer": { "number": "+1YOUR_TEST_NUMBER" }
}'- Make a call to your test number
- When connected, ask the AI something it doesn't know: "What's my insurance number?"
- AI should say "let me check on that"
- You should receive a Telegram message
- Reply to the Telegram message
- AI should relay your answer back on the call
If you want to trigger calls from OpenClaw:
Create skills/vapi/SKILL.md:
---
name: vapi
description: Make voice calls via Vapi for restaurant reservations, customer service, etc.
---
# Vapi Voice Calling
Use Vapi to make outbound voice calls when the user needs to call businesses, make reservations, or handle tasks requiring human conversation.
## Usage
Always confirm with the user before making calls. Include the purpose and what you plan to say.
Create skills/vapi/vapi.mjs:
#!/usr/bin/env node
import fetch from 'node-fetch';
const VAPI_API_KEY = process.env.VAPI_API_KEY;
const ASSISTANT_ID = "YOUR_ASSISTANT_ID";
const PHONE_NUMBER_ID = "YOUR_PHONE_NUMBER_ID";
async function makeCall(targetNumber, purpose) {
const response = await fetch('https://api.vapi.ai/call', {
method: 'POST',
headers: {
'Authorization': `Bearer ${VAPI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
assistantId: ASSISTANT_ID,
phoneNumberId: PHONE_NUMBER_ID,
customer: { number: targetNumber },
assistantOverrides: {
firstMessage: `Hi, this is calling about ${purpose}. How can I help you today?`
}
})
});
const result = await response.json();
console.log(JSON.stringify(result, null, 2));
}
const [targetNumber, ...purposeParts] = process.argv.slice(2);
const purpose = purposeParts.join(' ');
if (!targetNumber || !purpose) {
console.error('Usage: node vapi.mjs <phone-number> <purpose>');
process.exit(1);
}
makeCall(targetNumber, purpose);- Webhook Authentication: All requests must include valid HMAC-SHA256 signature using
VAPI_WEBHOOK_SECRET - Sensitive Data Logging: Questions and answers are not logged in production (only with
DEBUG=true) - Environment Variables: All secrets stored in environment variables, never committed to git
- Use Private Telegram Chat: Never use group chats for sensitive responses
- Monitor Webhook Logs: Watch for unauthorized access attempts
- Rotate Secrets: Periodically change
VAPI_WEBHOOK_SECRET - Consider IP Allowlisting: Restrict webhook access to Vapi's IP ranges if possible
- Use HTTPS: Always expose webhooks over HTTPS (Cloudflare Tunnel/ngrok provide this)
- Webhook returning 401 Unauthorized: Check
VAPI_WEBHOOK_SECRETmatches in both.envand Vapi assistant configuration - Webhook not receiving calls: Verify webhook URL is publicly accessible and uses HTTPS
- Telegram messages not sending: Verify bot token and chat ID are correct
- Call quality issues: Check Vapi voice settings and internet connection
- Tool calls failing silently: Enable
DEBUG=trueto see detailed logs
Enable verbose logging:
// Add to proxy.mjs
const DEBUG = process.env.DEBUG === 'true';
if (DEBUG) console.log('Request body:', JSON.stringify(req.body, null, 2));- Vapi calls: Dashboard at vapi.ai
- Webhook: Check your server logs
- Telegram: Bot API responses
- Use GPT-4o-mini instead of GPT-4 for cost savings
- Set call duration limits
- Implement usage monitoring
- Set up monitoring and alerts
- Add call recording/transcription
- Integrate with CRM or ticketing system
- Add support for conference calls
- Implement voicemail detection and handling
- Vapi Documentation: docs.vapi.ai
- OpenClaw Documentation: docs.openclaw.ai
- Telegram Bot API: core.telegram.org/bots/api