Add PCI-compliant voice payments to a voice application you’ve built directly on Twilio. This guide is for developers who orchestrate their own call flow using TwiML or the Twilio REST API — if you’re using a voice agent platform like VAPI, Retell, or Outbox, see the platform-specific guides or Custom Platform guide instead.
When your call flow needs to collect a payment, your server calls the Maven API with the amount and caller’s phone number.
2
Your app transfers the caller
Maven returns a phone number. Your server responds with TwiML that dials that number, preserving the original caller ID.
3
Maven collects the payment
Maven handles the entire card collection conversation — card number, expiry, CVV, and ZIP code. Your app is never exposed to card data (PCI compliant).
4
Caller returns to your app
After payment, Maven transfers the caller back to your callback number. You receive a webhook with the payment result.
Respond to the Twilio webhook with a <Dial> that transfers the caller to Maven’s payment line. The critical detail: set callerId to the customer’s phone number, not your Twilio number.
from twilio.twiml.voice_response import VoiceResponsedef build_transfer_twiml(maven_phone: str, customer_phone: str, action_url: str): response = VoiceResponse() response.say("I'm transferring you to our secure payment line now.") dial = response.dial( caller_id=customer_phone, # preserve the original caller ID action=action_url, # called when the dial completes timeout=30, ) dial.number(maven_phone) return str(response)
This generates:
<?xml version="1.0" encoding="UTF-8"?><Response> <Say>I'm transferring you to our secure payment line now.</Say> <Dial callerId="+14155551234" action="https://your-server.com/payment-complete" timeout="30"> <Number>+18338...</Number> </Dial></Response>
callerId must be the customer’s phone number. Maven matches sessions by the caller ID on the inbound leg. If you use your Twilio number as the caller ID, the session won’t connect.
After the payment completes and the caller is transferred back, Twilio hits your action URL. You can look up the session result:
async def handle_payment_complete(caller: str): async with httpx.AsyncClient() as client: resp = await client.get( "https://api.trymaven.com/v1/sessions", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"caller": caller}, ) session = resp.json() if session["status"] == "payment-success": # payment went through return build_twiml_say("Your payment was successful. Thank you!") elif session["status"] == "payment-failed": return build_twiml_say("The payment didn't go through. Would you like to try again?") else: return build_twiml_say("It looks like the payment session expired.")
You’ll also receive a webhook with the full payment details (card brand, last 4, gateway transaction IDs).