Skip to main content
Add card-on-file payments to a Pipecat voice pipeline by call transfer. This works like a hosted web checkout, over the phone: when the order is confirmed, your pipeline creates a Checkout Session and transfers the live call to a Ringup payment line, the caller pays there, and the call optionally returns to your agent. Ringup recognizes the caller, charges their saved card, settles it against the order, and texts a receipt. Your server reconciles from a webhook. If the model is new to you, read Call transfer first. Pipecat runs the pipeline; the telephony under it is your transport (Twilio, Telnyx, Plivo, or Exotel WebSocket serializers, or Daily PSTN/SIP). Your transport owns the call leg, so the transfer happens there. This page is end to end: setup, the call, the return, and what happens after payment.

How the pieces map to Pipecat

Ringup pieceOn Pipecat
create_checkoutAn HTTP POST from a function your LLM calls when the order is confirmed
The transfer targetThe transfer_to SIP URI the Checkout Session returns
The transferA <Dial><Sip> on your telephony transport to the Ringup SIP URI
The resultA checkout.completed webhook to your server

Prerequisites

  • A running Pipecat pipeline with telephony (a Twilio/Telnyx/Plivo/Exotel WebSocket serializer, or Daily PSTN/SIP dial-in).
  • The telephony account that owns the inbound call leg.
  • A Ringup account and API key. Test mode works out of the box on a shared sandbox. See Testing.

Step 1: Create a Checkout Session when the order is confirmed

Register a function on your LLM (FunctionSchema) that the model calls once the order is confirmed, and in its handler POST to Ringup with your API key. There is nothing to register with Ringup; this is a plain HTTP call from your handler.
async def create_checkout(params: FunctionCallParams):
    r = await http.post(
        "https://api.ringup.dev/v1/checkouts",
        headers={"Authorization": f"Bearer {RINGUP_KEY}"},
        json={
            "amount_cents": 2500,
            "order_id": params.arguments.get("order_id"),          # optional
            "return_to": "sip:your-bridge@your-telephony",         # optional: bring the caller back
            "success_message": "You are all set, your order will be ready shortly.",  # optional
        },
    )
    await params.result_callback(r.json())
The response is the Checkout Session:
{
  "id": "cs_x9f2",                                   // Checkout Session id; the correlation token
  "payment_required": "required",                    // "required" | "optional" | "none"
  "transfer_to": "sip:cs_x9f2@transfer.ringup.dev"   // where to transfer; embeds the id
}
If payment_required is none, do not transfer: finish the call normally. Otherwise transfer.

Step 2: Transfer on your transport

Have the agent speak one short bridge line (“One moment, connecting you to secure payment”), then transfer the caller on your telephony. On a Twilio transport, redirect the call to TwiML that dials the Ringup SIP URI, carrying the Checkout Session id:
<Response>
  <Dial>
    <Sip>sip:cs_x9f2@transfer.ringup.dev?X-Session-Id=cs_x9f2</Sip>
  </Dial>
</Response>
On Daily PSTN/SIP, use the equivalent SIP transfer to the same URI. Because your transport controls the telephony, the header path is reliable: the URI parameter reaches the Ringup line, which matches it to the Checkout Session.

Step 3: The return, and the result (the webhook)

Ringup answers the transferred call, recognizes the Caller, charges the saved Card, and texts a receipt. A first-time caller with no saved card is texted a secure pay link instead. Then the call ends one of three ways, from what you passed in Step 1:
  • return_to set: Ringup transfers the caller back to your transport with the outcome in SIP headers (X-Payment-Status, X-Confirmation), so your agent resumes and closes the call.
  • success_message / failure_message set (no return_to): Ringup reads your line and ends.
  • Neither: Ringup reads a short default and ends. The receipt is the proof.
A return_to that cannot connect falls back to reading your success_message (or the default), so a failed return never strands the caller. Either way, your server reconciles from a webhook:
{
  "id": "evt_9f2",
  "type": "checkout.completed",         // or "checkout.failed"
  "data": {
    "checkout_id": "cs_x9f2",
    "payment_id": "pay_abc",
    "amount_cents": 2500,
    "order_id": "sq_abc",               // echoed back
    "confirmation": "2PJJZY",
    "caller": "+14155551234",
    "card": { "brand": "VISA", "last_4": "5858" }
  }
}
Match on the order_id you passed, store the payment_id, and flip your order to paid. See Webhooks for the full catalog, statuses, signature verification, and idempotency.

Validation status

Because your transport owns the telephony, the transfer is standard <Dial><Sip> (or the Daily SIP equivalent) on your provider. Ringup’s hosted transfer endpoint is rolling out; confirm access in your Ringup dashboard before relying on this in production. The recognition, charge, receipt, and webhook behavior is the same payment path Ringup runs everywhere.