Building a Voice Agent That Collects Payment
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.
Voice Agent Flow and Code
// 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.' };
}
Learn More
See building a WhatsApp AI sales agent for a similar pattern over chat instead of voice.
Key Takeaways
- ✓Use Twilio for call reception, STT (Deepgram/Whisper) for transcription, Claude for intent + orchestration.
- ✓Claude identifies what the customer wants to pay for and maps it to a product catalog — it does not accept caller-dictated amounts.
- ✓The payment link is texted to the caller's Twilio number after creation, not read aloud.
- ✓Confirm the product and amount with the caller before creating the payment link.
- ✓Log every voice session: caller number, transcript, product identified, link created, payment status.
Frequently Asked Questions
- What if the caller says a product that is not in the catalog?
- The LLM should not guess or map ambiguous input to the nearest product — it should ask for clarification: "I didn't recognize that product. We have consulting, basic plan, and premium plan. Which did you mean?" Your system prompt should make this explicit. Never let the LLM create a payment link for a product it cannot confidently identify from the catalog.
- How do I handle poor audio quality or heavy accents?
- Use a STT service with strong African English support. Deepgram Nova-2 and Whisper large-v3 both handle Kenyan and Nigerian English well. Add a confirmation step before creating the link — the agent reads back what it understood before executing. Give the caller a chance to correct misunderstandings before money changes hands.
- Can Paystack payment links be created via API?
- Yes — POST /paymentrequest creates a payment link programmatically. The response includes offline_reference which gives you the short link at paystack.com/pay/{reference}. You can also set customer email, amount, description, and due date. For one-time vs reusable links, set allow_multiple_use accordingly. See the Paystack API docs for full parameters.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon