Handling payments with PCI compliance
Learn how to handle payments in LiveKit voice agents using a hybrid architecture that keeps sensitive payment data off the voice channel.
Last Updated:
Need to collect payments during voice calls? Don't let cardholder data touch your voice agent. Here's how to stay PCI-compliant with a hybrid architecture.
The core principle: keep payment data off the voice channel
PCI-DSS compliance becomes dramatically simpler when cardholder data never enters your voice agent's scope. Instead of trying to secure every component that handles card numbers, you isolate payment collection entirely.
Your voice agent handles:
- Conversation flow and context
- Customer qualification and intent detection
- Order details and confirmation
- Handoff orchestration
A separate PCI-compliant system handles:
- Card number collection
- Payment processing
- Tokenization
Hybrid architecture patterns
Pattern 1: Secure payment link
The most common approach—send customers a link to complete payment:
1from livekit.agents import Agent, function_tool, RunContext23class PaymentAgent(Agent):4def __init__(self):5super().__init__(6instructions="""You are a helpful sales assistant. When the customer7is ready to pay, use the send_payment_link tool. Never ask for or8accept credit card numbers directly."""9)1011@function_tool()12async def send_payment_link(13self,14amount: float,15description: str,16customer_phone: str,17) -> str:18"""Send a secure payment link to the customer's phone."""1920# Generate payment link via your PCI-compliant provider21# (Stripe, Square, PayPal, etc.)22payment_link = await create_payment_session(23amount=amount,24description=description,25metadata={"session_id": self.session.id}26)2728# Send via SMS29await send_sms(30to=customer_phone,31body=f"Complete your payment securely: {payment_link}"32)3334return "I've sent a secure payment link to your phone. Let me know once you've completed the payment."
The agent continues the conversation while the customer completes payment on their device:
1@function_tool()2async def check_payment_status(self, context: RunContext, session_id: str) -> str:3"""Check if the customer has completed their payment."""45status = await get_payment_status(session_id)67if status == "completed":8return "Payment received successfully."9elif status == "pending":10return "Payment is still pending."11else:12return f"Payment status: {status}"
Pattern 2: Transfer to PCI-compliant IVR
For phone-based payments, transfer the caller to a dedicated payment IVR using the TransferSIPParticipant API:
1from livekit import api2from livekit.agents import Agent, function_tool, RunContext, get_job_context3from livekit.rtc import ParticipantKind45class SalesAgent(Agent):6def __init__(self):7super().__init__(8instructions="""You are a sales agent. When ready to collect payment,9transfer the caller to our secure payment system. Never collect10card details yourself."""11)1213@function_tool()14async def transfer_to_payment_ivr(15self,16context: RunContext,17amount: float,18order_id: str,19) -> str:20"""Transfer caller to secure payment IVR."""2122job_ctx = get_job_context()2324# Find the SIP participant25sip_participant = None26for participant in job_ctx.room.remote_participants.values():27if participant.kind == ParticipantKind.PARTICIPANT_KIND_SIP:28sip_participant = participant29break3031if not sip_participant:32return "Unable to transfer - no phone connection found."3334# Transfer using the SIP API35# Note: Your SIP trunk must have call transfers enabled36try:37await job_ctx.api.sip.transfer_sip_participant(38api.TransferSIPParticipantRequest(39room_name=job_ctx.room.name,40participant_identity=sip_participant.identity,41transfer_to="sip:payment-ivr@your-pci-provider.com",42)43)44except Exception as e:45return f"Transfer failed: {e}"4647return "Transferring you to our secure payment line now."
Note: You must enable SIP REFER on your SIP trunk provider. For Twilio, enable "Call Transfer (SIP REFER)" and "Enable PSTN Transfer" in your trunk settings. See the Call forwarding documentation for details.
Why DTMF payment collection doesn't work in-call
You might be tempted to have callers enter card numbers via DTMF tones while staying connected to your LiveKit agent. This doesn't work for PCI compliance.
If the caller enters DTMF tones while connected to LiveKit:
- LiveKit receives and processes those tones
- The tones could appear in logs, recordings, or event streams
- This puts LiveKit (and your agent) in PCI scope
The only compliant approach for DTMF-based payment is to transfer the call entirely to a PCI-compliant IVR system (Pattern 2). The caller must leave the LiveKit room before entering any card data.
Some providers like Twilio Pay and Plivo offer PCI-compliant IVR systems that handle DTMF payment collection. Use SIP REFER to transfer the caller to these systems when payment is needed.
Preventing accidental PCI scope creep
Even with a hybrid architecture, you need guardrails to prevent card data from leaking into your agent.
1. Disable recording for payment sessions
Prevent any possibility of card numbers appearing in transcripts by passing record=False to the start method:
1await session.start(2agent=agent,3room=ctx.room,4record=False, # Disables audio, transcripts, traces, and logs upload5)
See Redacting PII from agent logs and transcripts for complete guidance on protecting sensitive data.
2. Add explicit guardrails in your prompt
1instructions="""CRITICAL COMPLIANCE RULES:21. NEVER ask for credit card numbers, CVV, or expiration dates32. NEVER repeat back any numbers that sound like card numbers43. If a customer tries to give you card details, immediately redirect:5"I can't accept card details directly. Let me send you a secure link instead."64. For payments, either send a secure payment link OR transfer to our payment line75. NEVER collect card details while the caller is connected to you8"""
3. Implement real-time filtering
Block card patterns from reaching the LLM by overriding the stt_node:
1import re2from typing import AsyncIterable, Optional3from livekit import rtc4from livekit.agents import Agent, ModelSettings, stt56class PCICompliantAgent(Agent):7CARD_PATTERN = r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b'89async def stt_node(10self,11audio: AsyncIterable[rtc.AudioFrame],12model_settings: ModelSettings13) -> Optional[AsyncIterable[stt.SpeechEvent]]:14"""Filter potential card numbers from transcription before LLM."""1516async for event in super().stt_node(audio, model_settings):17# Check if this is a final transcript event18if isinstance(event, stt.SpeechEvent) and event.type == stt.SpeechEventType.FINAL_TRANSCRIPT:19for alt in event.alternatives:20if re.search(self.CARD_PATTERN, alt.text):21# Replace with placeholder22alt.text = re.sub(23self.CARD_PATTERN,24"[CARD NUMBER BLOCKED]",25alt.text26)27# Queue a redirect message28self.session.say(29"I noticed you're trying to share card details. "30"For your security, I can't accept those directly. "31"Let me send you a secure payment link instead."32)33yield event
Example: Complete payment flow
Here's a full example combining conversation handling with secure payment handoff:
1from livekit.agents import Agent, AgentSession, AgentServer, JobContext, function_tool, RunContext2from livekit.plugins import openai, deepgram, silero3import httpx45class SecurePaymentAgent(Agent):6def __init__(self):7super().__init__(8instructions="""You are a friendly order assistant for Acme Corp.910Your job:111. Help customers with their orders122. Confirm order details (items, quantities, shipping)133. Calculate totals and explain charges144. When ready to pay, send a secure payment link1516NEVER collect card details directly. Always use send_payment_link."""17)18self.order_total = 019self.order_items = []2021@function_tool()22async def add_to_order(23self,24context: RunContext,25item: str,26quantity: int,27price: float28) -> str:29"""Add an item to the customer's order."""30self.order_items.append({31"item": item,32"quantity": quantity,33"price": price34})35self.order_total += price * quantity36return f"Added {quantity}x {item} at ${price:.2f} each. Current total: ${self.order_total:.2f}"3738@function_tool()39async def send_payment_link(self, context: RunContext, customer_phone: str) -> str:40"""Send secure payment link when customer is ready to pay."""4142if self.order_total <= 0:43return "No items in the order yet. Please add items first."4445# Create Stripe payment session46async with httpx.AsyncClient() as client:47response = await client.post(48"https://api.stripe.com/v1/payment_links",49auth=("sk_live_xxx", ""),50data={51"line_items[0][price_data][currency]": "usd",52"line_items[0][price_data][product_data][name]": "Order",53"line_items[0][price_data][unit_amount]": int(self.order_total * 100),54"line_items[0][quantity]": 1,55}56)57payment_link = response.json()["url"]5859# Send SMS (implement your SMS provider here)60await send_sms(customer_phone, f"Complete your ${self.order_total:.2f} payment: {payment_link}")6162return f"I've sent a secure payment link for ${self.order_total:.2f} to {customer_phone}. The link is valid for 24 hours. Let me know once you've completed the payment!"636465server = AgentServer()6667@server.rtc_session(agent_name="my-agent")68async def entrypoint(ctx: JobContext):69session = AgentSession(70llm=openai.LLM(model="gpt-4o"),71stt=deepgram.STT(),72tts=openai.TTS(),73vad=silero.VAD.load(),74)7576await session.start(77agent=SecurePaymentAgent(),78room=ctx.room,79record=False, # Disable recording for payment flows80)
Key takeaways
| Do | Don't |
|---|---|
| Use payment links or IVR transfers | Collect card numbers via voice |
| Disable recording during payment flows | Store or log potential card data |
| Add explicit prompt guardrails | Trust the LLM to self-censor |
| Filter transcripts in real-time | Let raw audio reach your systems |
| Use established PCI providers | Build your own payment handling |
Further reading
- Redacting PII from agent logs and transcripts — Essential guidance for protecting sensitive data
- Disabling recording at the session level — How to turn off audio, transcripts, traces, and logs
- Call forwarding (SIP REFER) — How to transfer calls to external systems
- PCI DSS Quick Reference Guide — Official PCI standards documentation
- Stripe's PCI compliance guide — How payment links reduce scope