The call ends at the payment
- AgentYour total is $149.99.
- CallerGreat, how do I pay?
- AgentI will send you a payment link.
- Callerleaves the call, finds the email, opens the link, types the card
Your voice agent asks for the card and keeps talking. The caller types it on the keypad, a certified partner vaults it, and the digits never reach your agent, your servers, or ours.
$149.99
Captured mid-call
Voice agents book appointments, answer questions and close deals. Then the caller needs to pay, and the conversation falls apart.
Call Voicepay.collect() at any point in the conversation with an amount and the session id. The agent keeps talking while it runs.
One call, two arguments.
The caller types their card on the keypad. Those tones are pulled out at the carrier and the card is vaulted by our payment partner, so what reaches your agent is a token and there is nothing sensitive in the transcript or the logs.
Digits never enter your stack, or ours.
The charge settles and your agent gets a callback with the outcome. It can confirm the booking in the same breath.
The conversation never pauses.
A scripted run of a real booking call, with the API events it produces alongside it.
AI Dental Assistant
Press Start to begin
Press Start to begin the demo
import { VoicePay } from '@voicepay/sdk';
const voicepay = new VoicePay(process.env.VOICEPAY_TEST_KEY);
// Your voice agent decides it's time to charge.
agent.on('intent:payment', async (call) => {
const session = await voicepay.payments.create({
amount: call.cart.total, // cents
currency: 'usd',
call_id: call.id,
merchant_id: 'mch_bs_main',
capture_method: 'dtmf_secure', // keypad, masked from the agent
});
// One await per field. The agent stays on the line and keeps talking;
// the digits go to the carrier, never to your process.
for (const field of session.required) {
await session.capture(field, {
prompt: PROMPTS[field],
timeout: 30_000,
});
}
const payment = await session.complete();
await call.say(
payment.succeeded
? `Payment confirmed — $${payment.amount / 100} on your ${payment.card.brand} ending ${payment.card.last4}.`
: `That card was declined. Want to try a different one?`
);
});Card digits never appear in this stream. They are captured by the carrier and tokenized before Voicepay or the AI agent sees anything but a mask.
Voicepay runs server side, so it does not care how your agent was built.
import { VoicePay } from '@voicepay/sdk';
const voicepay = new VoicePay('vp_live_your_api_key');
// Called by your voice agent when the conversation reaches checkout.
export async function handlePayment(call) {
const session = await voicepay.payments.create({
amount: 14999, // cents
currency: 'usd',
call_id: call.id, // your provider's call id
merchant_id: 'mch_bs_main', // which gateway account settles this
description: 'Teeth cleaning appointment',
metadata: {
customer_name: call.caller.name,
appointment_date: '2026-08-06',
},
});
// Capture runs field by field on the caller's own leg.
// Your agent is muted from the keypad tones for the whole sequence.
for (const field of session.required) {
// -> 'card_number' | 'expiry' | 'cvc' | 'postal_code'
await session.capture(field);
}
const payment = await session.complete({
idempotency_key: `${call.id}:charge:1`,
});
if (payment.status === 'succeeded') {
await call.say(
`Payment of $${payment.amount / 100} confirmed. You're all set!`
);
}
return payment;
}from voicepay import VoicePay
voicepay = VoicePay("vp_live_your_api_key")
async def handle_payment(call):
session = await voicepay.payments.create(
amount=14999, # cents
currency="usd",
call_id=call.id, # your provider's call id
merchant_id="mch_bs_main", # which gateway account settles this
description="Teeth cleaning appointment",
metadata={
"customer_name": call.caller.name,
"appointment_date": "2026-08-06",
},
)
# 'card_number', 'expiry', 'cvc', 'postal_code' — the connector
# decides which fields are required; iterate whatever it asks for.
for field in session.required:
await session.capture(field, timeout=30)
payment = await session.complete(
idempotency_key=f"{call.id}:charge:1",
)
if payment.status == "succeeded":
await call.say(
f"Payment of ${payment.amount / 100:.2f} confirmed. You're all set!"
)
return payment# 1. Open a capture session on the live call
curl https://api.voicepay.dev/v1/payments/sessions \
-H "Authorization: Bearer vp_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"amount": 14999,
"currency": "usd",
"call_id": "call_a1b2c3d4",
"merchant_id": "mch_bs_main",
"description": "Teeth cleaning appointment",
"capture_method": "dtmf_secure"
}'
# -> { "id": "pay_3M2n9Rx4", "status": "requires_capture",
# "required": ["card_number", "expiry", "cvc", "postal_code"] }
# 2. Capture one field. Blocks until the caller finishes it.
curl https://api.voicepay.dev/v1/payments/pay_3M2n9Rx4/capture \
-H "Authorization: Bearer vp_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "field": "card_number", "timeout": 30 }'
# -> { "field": "card_number", "status": "captured", "masked": "•••• 4242" }
# 3. Charge. This is the only call that reaches the gateway.
curl https://api.voicepay.dev/v1/payments/pay_3M2n9Rx4/complete \
-H "Authorization: Bearer vp_live_your_api_key" \
-H "Idempotency-Key: call_a1b2c3d4:charge:1"
# -> { "id": "pay_3M2n9Rx4", "status": "succeeded", "amount": 14999,
# "card": { "brand": "visa", "last4": "4242" },
# "receipt_url": "https://pay.voicepay.dev/r/3M2n9Rx4" }{
"tools": [
{
"name": "start_payment",
"description": "Open a secure card capture on the caller's line. Call this once the customer has agreed to pay.",
"url": "https://api.voicepay.dev/v1/retell/payments/start",
"parameters": {
"amount": { "type": "number", "description": "Amount in cents" },
"description": { "type": "string" }
}
},
{
"name": "capture_field",
"description": "Collect one field from the keypad. Call in order: card_number, expiry, cvc, postal_code. Returns when the customer has finished entering it.",
"url": "https://api.voicepay.dev/v1/retell/payments/capture",
"parameters": {
"field": {
"type": "string",
"enum": ["card_number", "expiry", "cvc", "postal_code"]
}
}
},
{
"name": "complete_payment",
"description": "Submit the card for authorization and return the result.",
"url": "https://api.voicepay.dev/v1/retell/payments/complete"
}
],
"auth": { "type": "bearer", "token": "vp_live_your_api_key" }
}No backend required. Paste three tools into your Retell or VAPI agent, point your number at VoicePay, and your agent can take money.
Works with
Bland AIThe hard part is the payment boundary, and it is not specific to voice. Every channel below reuses it.
Take a card during a live phone call, without the agent ever handling the digits. This is what the rest of the page describes, and it works today.
Collect payment inside a chat thread, with the same tokenisation boundary.
Agents that raise an invoice and settle it without a human in the loop.
Machine-to-machine payments for workflows that run with nobody watching.
Isolation is structural, not a policy. There is no point in the path where your agent could read a card number, and no point where Voicepay could either.
The caller types their card on the keypad and Twilio lifts those tones out of the audio. Shuttle Global vaults the card, and everything downstream works from a token. Voicepay does not store the card, and has no way to read it back.
Twilio operates the capture path and Shuttle Global holds the vault. Between them they carry the certifications below, audited independently of us.
256-bit AES encryption on every transaction. Keypad tone capture means the digits are separated from the call before your AI agent processes any of the audio.
Your AI agent and Voicepay both sit outside this path. Each receives only a token.
If something here is still unclear, write to us and we will answer it properly.
Still deciding?Ask us directly
Voice payments are live and access is opening gradually. Tell us where to reach you and we will email you when your account is ready.
No card, no call. One email when your access opens, and one click to leave.
Optional, and it won't change your place in the queue — it just helps us decide who to onboard first.
We sent a confirmation link to . Click it to confirm your spot and see your place in line.
Nothing there? Give it a minute, check your spam folder, then submit your email again.