Bonaventure OgetoBy Bonaventure Ogeto|

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.

Sign up for the McTaba newsletter

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