# Twilio / Telnyx: How it works

> Read this before choosing an integration method. It explains how cardholder data moves between the four parties involved in a Twilio Pay transaction, which is w

Read this before choosing an integration method. It explains how cardholder data moves between the four parties involved in a Twilio Pay transaction, which is what you need in order to reason about PCI scope, understand what your systems can and cannot see, design around failure, and decide whether you want the IVR or the Agent Assist approach.

## The four parties

| Party | Role |
|-------|------|
| **Your platform** | Initiates the call flow and orchestrates the payment. Never touches raw card data. |
| **Twilio** | The telephony provider. Captures card digits from the caller's keypad inside its PCI DSS-compliant voice platform. |
| **Shuttle** | Twilio's payments partner. Receives card data from Twilio through the Shuttle Pay connector and routes it to one of many supported gateways. |
| **Payment gateway** | Authorises the card and returns an approval or decline. |

The relationship to keep in mind: **Twilio handles telephony, Shuttle handles payments.** Shuttle is the piece that lets a single Twilio connector fan out to whichever gateway your merchant uses.

## High-level flow

Your platform is the initiator — the call routes to Twilio because your application's voice webhook tells it to, and the session returns to your platform when Twilio finishes.

```mermaid
flowchart LR
  A[Your Platform] -->|Routes call<br/>to Twilio| B[Twilio<br/>captures DTMF]
  B -->|Card data<br/>via connector| C[Shuttle]
  C -->|Authorise| D[Gateway]
  D -->|Gateway token +<br/>reference + status| C
  C -->|Masked card +<br/>Shuttle IDs +<br/>gateway IDs| B
  B -->|Callback| A
  C -.->|Webhook async| A
```

Raw card data only ever lives inside **Twilio → Shuttle → Gateway**. Your platform receives the masked PAN, non-sensitive Shuttle and gateway identifiers, and status — never the full PAN or CVV.

## What your platform receives

Two independent response paths land on your platform, and they carry different kinds of information.

### Twilio callback

Delivered to your `action` URL (IVR) or `StatusCallback` URL (Agent Assist) the moment the session completes. This is the **detailed payload**, including:

- Masked PAN, card brand, and expiry (for display)
- **Shuttle identifiers** — Shuttle payment ID (`PaymentConfirmationCode`) and Shuttle payment method ID (`PayConnector_payment_method`). Use these with the Shuttle API for a standardised interface that works identically across every supported gateway
- **Gateway identifiers** — gateway transaction reference (`PayConnector_gateway_reference`) and gateway card token (`PayConnector_gateway_token`). Use these if you want to interact directly with the underlying gateway (native reconciliation, gateway-specific tooling)
- Status fields (`PayConnector_payment_status`, `PayConnector_gateway_status`)

Neither identifier set is sensitive — they are references, not card data. Most integrations use the Shuttle IDs so they don't need gateway-specific code, and keep the gateway IDs on file for reconciliation.

For the full list of fields, see the [Shuttle Pay connector reference](/merchants/twilio/shuttle-connector-reference/#response-fields).

### Shuttle webhook

Delivered directly from Shuttle when a payment event occurs (including async ones: pending settlements, held transactions, recurring charges). The webhook is an **event notification, not a full transaction record** — it carries the event type and references to the affected resources (account, transaction, capture, contract). Use those references to fetch the full details via the [Shuttle API](/merchants/twilio/shuttle-connector-reference/#shuttle-api) when you need them.

For event types and payload shapes, see the [webhook events reference](/merchants/reference/webhook-events/).

### Which to trust

The Twilio callback is reliable for the session itself — if the caller hangs up mid-flow, Twilio still fires the callback with `Result=caller-hung-up` rather than dropping it. Use the callback as your primary signal and the callback payload for display and immediate business logic.

Use the Shuttle webhook as the source of truth for anything that resolves *after* the call ends — pending settlements, held transactions, gateway responses that arrived late, and recurring/scheduled charges. By default Shuttle doesn't retry a webhook that fails: set retries on your webhook target, as described in [Webhooks](/merchants/reference/webhooks/).

## PCI DSS scope

Because Twilio intercepts the DTMF digits before they reach any of your systems, and suppresses the tones so agents on the call cannot hear them, card numbers stay out of your infrastructure. That greatly reduces your PCI DSS scope, but doesn't remove it. A platform is its merchants' service provider and validates its own compliance as one each year (SAQ D for service providers, or a Report on Compliance); a merchant still completes its own annual assessment. See [Your PCI DSS obligations](/platforms/getting-started/pci-dss/). Shuttle's instance and secret keys are stored in the Twilio connector configuration, not on your platform, so the card-submission leg never touches your credentials either.

Confirm your specific obligations with your QSA — this is guidance, not legal advice.

## IVR vs Agent Assist

Both integrations share the same data flow. They differ in how the session is initiated and controlled.

### IVR (`<Pay>` verb)

Your platform responds to Twilio's voice webhook with TwiML containing a `<Pay>` verb. The caller interacts only with Twilio.

```mermaid
sequenceDiagram
  participant Caller
  participant Twilio
  participant YourApp as Your Platform
  participant Shuttle
  participant Gateway

  Caller->>Twilio: Calls number
  Twilio->>YourApp: Voice webhook
  YourApp->>Twilio: TwiML with <Pay>
  Twilio->>Caller: Prompts for card
  Caller->>Twilio: DTMF digits
  Twilio->>Shuttle: Card data (connector)
  Shuttle->>Gateway: Authorise
  Gateway->>Shuttle: Approval + token
  Shuttle->>Twilio: Masked card + IDs
  Twilio->>YourApp: POST to action URL
  Shuttle-->>YourApp: Webhook (async)
```

### Agent Assist (Payments Subresource API)

Your backend drives the session programmatically, letting an agent UI trigger each field capture. The agent cannot hear the DTMF tones.

```mermaid
sequenceDiagram
  participant Caller
  participant Twilio
  participant Agent as Agent UI
  participant Backend as Your Backend
  participant Shuttle
  participant Gateway

  Agent->>Backend: Start payment
  Backend->>Twilio: POST /Payments
  loop For each field
    Agent->>Backend: Capture field
    Backend->>Twilio: POST Capture=...
    Twilio->>Caller: Prompts for field
    Caller->>Twilio: DTMF digits
    Twilio->>Backend: StatusCallback
    Backend->>Agent: Push UI update<br/>(WebSocket / poll)
  end
  Agent->>Backend: Complete
  Backend->>Twilio: Status=complete
  Twilio->>Shuttle: Card data (connector)
  Shuttle->>Gateway: Authorise
  Gateway->>Shuttle: Approval + token
  Shuttle->>Twilio: Masked card + IDs
  Twilio->>Backend: Final StatusCallback
  Shuttle-->>Backend: Webhook (async)
```

## Reusing saved cards

A card gets saved in two scenarios:

- **Tokenisation session** — you call `<Pay>` (or the Payments API) with no `chargeAmount`. The card is tokenised but not charged.
- **Charge + save** — you pass `save_card=true` alongside a charge.

In both cases, the gateway creates the durable token and Shuttle stores a reference to it against a Shuttle payment method record (`pm_...`). You get **both identifiers back**: the Shuttle payment method ID and the gateway token.

Once the card is saved, subsequent charges don't need Twilio — **Twilio is only in the path when a human is on the line entering digits**. You have two options for how to charge it:

```mermaid
flowchart LR
  A[Later charge] -->|pm_... + amount| B[Shuttle API]
  A -->|gateway_token + amount| D[Gateway directly]
  B -->|Uses stored<br/>gateway_token| D
  D -->|Approval| B
  D -->|Approval| A
  B -->|Payment result| A
```

- **Via Shuttle** — pass the Shuttle `pm_...` ID to the Shuttle API. Shuttle looks up the gateway token and submits the charge. Your code stays identical across every supported gateway.
- **Via the gateway directly** — use the gateway token with your own gateway integration. Useful if you already have gateway-native tooling or want to bypass Shuttle for specific flows.

Most integrations go through Shuttle so they don't need gateway-specific code, but the gateway-direct path stays open.

## Connector timeout and retry

There are two timeouts stacked on top of each other: Twilio's deadline on Shuttle, and Shuttle's deadline on the gateway.

### Twilio → Shuttle (10 seconds)

Twilio enforces a **10-second response deadline** on the call it makes to the Shuttle Pay Connector. If Shuttle hasn't responded within that window, Twilio automatically retries the connector call, reusing the same `transaction_id` as an idempotency key. Retries continue up to **5 times** (6 attempts total) before Twilio gives up.

Shuttle's connector uses the `transaction_id` to dedupe — if the original authorisation eventually succeeds while Twilio is retrying, the retry returns the same result rather than creating a duplicate charge.

### Shuttle → gateway (45 seconds)

Shuttle enforces its own **45-second deadline** on the underlying gateway. If the gateway hasn't returned a definitive result by then, Shuttle marks the transaction as `UNRESOLVED` — meaning the outcome cannot be determined from the original request.

For gateways that support it, Shuttle schedules background reconciliation jobs — at **+1 minute** and **+15 minutes** — to check the gateway and confirm the outcome. When a job resolves the state, Shuttle:

- Transitions the transaction to `APPROVED` or `DECLINED`
- Fires the corresponding Shuttle webhook

Reconciliation support varies by gateway. Contact Shuttle support to confirm whether your gateway is covered.

**In this edge case, the Twilio callback is not updated** — the session already completed from Twilio's perspective, so only the Shuttle webhook will carry the final state. This is unlikely to happen in production other than at scale, but worth designing for: if you see `UNRESOLVED`, trust the Shuttle webhook to deliver the final outcome rather than treating it as a terminal state.

## Failure modes

The connector timeout and retry section above covers slow-gateway and unresolved scenarios. The table below covers the remaining cases.

| Where it fails | Symptom | What to do |
|----------------|---------|------------|
| Caller hangs up mid-session | Callback fires with `Result=caller-hung-up`, no Shuttle data | Likely abandoned; no charge is typically attempted. Reconcile via the Shuttle webhook if a later `UNRESOLVED` arrives |
| Gateway decline | `PayConnector_payment_status` = `DECLINED`, reason in `PayConnector_gateway_status` | Show a reason, allow retry |
| Connector credentials wrong | Twilio returns `Result: success` but no `PayConnector_*` fields | Detect the missing `PayConnector_payment_status` and treat as failure |

## Credentials

| Credential | Held by | Purpose |
|------------|---------|---------|
| Twilio Account SID / Auth Token | Your backend (Agent Assist only) | Authenticate to the Twilio Payments API |
| Shuttle instance key / secret key | **Twilio connector config** | Authenticate Twilio → Shuttle during card submission |
| Shuttle API key | Your backend | Post-payment operations (refunds, captures, reuse) |

Rotating your Shuttle instance/secret keys requires updating the connector in the Twilio Console — they are not held on your platform during the call.

## See also

- [Twilio IVR integration guide](/merchants/twilio/integration-guide/) — implementation detail for the `<Pay>` verb path
- [Twilio Agent Assist integration guide](/merchants/twilio/agent-assist-guide/) — implementation detail for the Payments Subresource API path
- [Shuttle Pay connector reference](/merchants/twilio/shuttle-connector-reference/) — full parameter and response documentation