Stack: Twilio (phone call reception), Deepgram or Whisper (speech-to-text), Claude with function calling (intent detection + orchestration), Paystack payment links (POST /paymentrequest), Twilio SMS (link delivery). Flow: 1) Customer calls Twilio number. 2) Audio streamed to STT. 3) Transcript sent to Claude with a create_payment_link tool. 4) Claude identifies product and amount from conversation, calls tool. 5) Tool creates Paystack payment link. 6) Claude calls send_sms tool — link texted to caller's number. 7) Customer pays on phone. Critical: Claude picks amount from a product catalog, not from what the caller says — callers must not be able to specify arbitrary amounts.
// Express endpoint for Twilio voice webhook
app.post('/voice', async (req, res) => {
// Twilio sends speech input via SpeechResult when using
var speechResult = req.body.SpeechResult;
var callerPhone = req.body.From;
if (!speechResult) {
// Initial greeting — prompt caller
res.type('text/xml').send(`
<Response>
<Gather input="speech" action="/voice" timeout="5" speechTimeout="auto">
<Say>Welcome. What would you like to pay for today?</Say>
</Gather>
</Response>
`);
return;
}
// Send transcript to Claude with tools
var result = await runVoiceAgent(speechResult, callerPhone);
res.type('text/xml').send(`
<Response>
<Say>${result.voiceResponse}</Say>
</Response>
`);
});
// Product catalog — Claude maps intent here, does not accept caller amounts
var PRODUCTS = {
'consulting': { name: '1-hour consulting session', amount: 15000_00 }, // NGN 15,000 in kobo
'basic plan': { name: 'Basic monthly plan', amount: 5000_00 },
'premium plan': { name: 'Premium monthly plan', amount: 25000_00 },
};
var voiceTools = [
{
name: 'create_payment_link',
description: 'Create a Paystack payment link for a product and send it to the customer via SMS.',
input_schema: {
type: 'object',
properties: {
product_key: { type: 'string', enum: Object.keys(PRODUCTS) },
customer_phone: { type: 'string' },
},
required: ['product_key', 'customer_phone'],
},
},
];
async function runVoiceAgent(transcript, callerPhone) {
var messages = [
{
role: 'user',
content: 'Customer said: "' + transcript + '". Customer phone: ' + callerPhone +
'. Available products: ' + JSON.stringify(Object.keys(PRODUCTS)) +
'. If you can identify the product, create a payment link and confirm what you're creating before sending.',
},
];
var response = await anthropic.messages.create({
model: 'claude-opus-4-6',
max_tokens: 512,
tools: voiceTools,
messages,
system: 'You are a voice payment assistant. Only create payment links for products in the catalog. Never accept amounts from the caller.',
});
// Handle tool call
for (var block of response.content) {
if (block.type === 'tool_use' && block.name === 'create_payment_link') {
var product = PRODUCTS[block.input.product_key];
// Create Paystack link
var linkRes = await fetch('https://api.paystack.co/paymentrequest', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ description: product.name, amount: product.amount, currency: 'NGN' }),
});
var link = (await linkRes.json()).data?.offline_reference;
// Send SMS via Twilio
await twilioClient.messages.create({ to: callerPhone, from: process.env.TWILIO_NUMBER, body: 'Your payment link: https://paystack.com/pay/' + link });
return { voiceResponse: 'I have created a payment link for ' + product.name + ' and sent it to your phone.' };
}
}
return { voiceResponse: response.content.find(b => b.type === 'text')?.text || 'I could not identify the product. Please try again.' };
}
See building a WhatsApp AI sales agent for a similar pattern over chat instead of voice.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course