# Migrate from Payment Intents to the Off-Session Payments API

Update your recurring and unscheduled off-session payment integration to use the Off-Session Payments API.

The Off-Session Payments API replaces the pattern of creating off-session `PaymentIntent` objects with `off_session=true` for recurring and unscheduled business-initiated transactions. It provides built-in smart retries, asynchronous authorization, and a dedicated payment lifecycle with granular event notifications.

Use this guide if you have a direct Payments integration that currently creates off-session `PaymentIntent` objects with `off_session=true`. If you use Stripe Billing subscriptions and invoices, this guide doesn’t apply.

> The Off-Session Payments API is a v2 API. Requests require a `Stripe-Version: 2026-04-22.preview` header and follow the v2 request and response format. See the [v2 API overview](https://docs.stripe.com/api-v2-overview.md) for details about v2 conventions.

## What stays the same

On-session flows remain unchanged. You still use the Payment Intents API and Setup Intents API to collect payment method details and authenticate your customers while they’re active in your UI.

- Use the [Setup Intents API](https://docs.stripe.com/api/setup_intents.md) with `usage=off_session` to save a payment method without an initial charge. For example, use it to collect a payment method for future use without charging the customer.

- Use the [Payment Intents API](https://docs.stripe.com/api/payment_intents.md) with `setup_future_usage=off_session` to charge a customer and save their payment method at the same time.

In both cases, the on-session step can perform required customer authentication up front, which helps later off-session charges proceed without customer interaction. However, issuers can still decline or require additional authentication for a later off-session payment. Your on-session payment method collection and authentication flow doesn’t change.

Charges API usage remains unchanged. The Off-Session Payments API still creates a [Charge](https://docs.stripe.com/api/charges.md) for each payment attempt, so your existing refund, dispute, reconciliation, and balance reporting workflows that depend on `Charge` objects can continue to work. However, for payment lifecycle handling, move from `payment_intent.*` events to the new `v2.payments.off_session_payment.*` events described in [Update your webhook handling](https://docs.stripe.com/payments/off-session-payments/migrate-from-payment-intents.md#update-your-webhook-handling).

- `charge.refunded` and `charge.disputed` events for handling refunds and disputes remain the same.

- Balance Transactions linked to the `Charge` object continue to work for payout and balance reporting.

## What changes

The biggest integration change is that the API response no longer returns authorization results synchronously. The off-session charge moves from `POST /v1/payment_intents` with `off_session=true` to `POST /v2/payments/off_session_payments`. The key differences are:

- **Asynchronous authorization**: The v2 API returns immediately with a `pending` status. The authorization result arrives in a webhook event, not in the API response.

- **Built-in smart retries**: Failed authorizations are automatically retried according to your configured retry strategy. You don’t need to implement retry logic yourself.

- **Dedicated event type**: A new set of `v2.payments.off_session_payment.*` events replaces `payment_intent.payment_failed` for off-session retry workflows.

- **API version header**: Requests require an explicit `Stripe-Version: 2026-04-22.preview` header. You must set this header directly—see [Private preview release channel](https://docs.stripe.com/sdks/versioning.md#private-preview-release-channel) for how to configure the preview version with a Stripe SDK. Requests also require `Content-Type: application/json`, which Stripe SDKs set automatically.

- **Amount format**: The amount is a JSON object with `value` and `currency` fields instead of separate `amount` and `currency` parameters.

## Set up event destinations

The Off-Session Payments API uses v2 events, which always use thin payloads. This requires a separate event destination from your existing v1 webhooks. Each event destination has its own signing secret, so update your webhook signature verification to use the signing secret for your v2 destination. See [Migrate from snapshot to thin events](https://docs.stripe.com/webhooks/migrate-snapshot-to-thin-events.md) for details.

Set up a v2 event destination before you migrate:

```curl
curl -X POST https://api.stripe.com/v2/core/event_destinations \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-08-26.preview" \
  --json '{
    "name": "Off-Session Payments Events",
    "enabled_events": [
        "v2.payments.off_session_payment.created",
        "v2.payments.off_session_payment.attempt_failed",
        "v2.payments.off_session_payment.attempt_started",
        "v2.payments.off_session_payment.succeeded",
        "v2.payments.off_session_payment.canceled",
        "v2.payments.off_session_payment.failed",
        "v2.payments.off_session_payment.paused",
        "v2.payments.off_session_payment.resumed"
    ],
    "type": "webhook_endpoint",
    "event_payload": "thin",
    "webhook_endpoint": {
        "url": "https://example.com/webhook"
    }
  }'
```

See [Register event destinations](https://docs.stripe.com/event-destinations.md) for more details.

## Replace the off-session Payment Intent API with the Off-Session Payments API

Replace your off-session `POST /v1/payment_intents` request, which used `off_session=true`, with a `POST /v2/payments/off_session_payments` request.

### Payment Intents with off_session=true

```curl
curl https://api.stripe.com/v1/payment_intents \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d amount=10000 \
  -d currency=usd \
  -d "customer={{CUSTOMER_ID}}" \
  -d "payment_method={{PAYMENTMETHOD_ID}}" \
  -d confirm=true \
  -d off_session=true \
  -d description=Example
```

### Off-Session Payments API

```curl
curl -X POST https://api.stripe.com/v2/payments/off_session_payments \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-04-22.preview" \
  --json '{
    "amount": {
        "value": 10000,
        "currency": "usd"
    },
    "customer": "{{CUSTOMER_ID}}",
    "payment_method": "{{PAYMENTMETHOD_ID}}",
    "description": "Example",
    "cadence": "recurring"
  }'
```

The v2 API returns a `pending` status immediately. The authorization result arrives asynchronously as a webhook event.

- (Required) `cadence`: Set to `recurring` for payments on a regular schedule, or `unscheduled` for merchant-initiated transactions that don’t follow a fixed schedule. This value is used for network compliance classification.

\** (Optional) `retry_details`: Configures the retry strategy for failed authorizations. If omitted, the default strategy is `none`, meaning no automatic retries are enabled. Set `retry_strategy` to `best_available` to let Stripe retry using the optimal schedule for the payment method.

As with Payment Intents, send an idempotency key when creating an off-session payment to avoid creating duplicate payment objects during retries or network failures.

## Update your webhook handling

For off-session payment lifecycle handling, replace your `payment_intent.succeeded` and `payment_intent.payment_failed` handlers with the corresponding `v2.payments.off_session_payment.*` events. The v2 API also introduces a terminal failure state and built-in automatic retries that have no equivalent in the Payment Intents API.

### Payment Intents events

```
payment_intent.succeeded
payment_intent.payment_failed
```

### Off-Session Payments events

```
v2.payments.off_session_payment.succeeded
v2.payments.off_session_payment.attempt_failed
v2.payments.off_session_payment.failed
```

The new events have distinct meanings:

| Payment Intents event | Off-Session Payments event | Meaning |
| --- | --- | --- |
| `payment_intent.succeeded` | `v2.payments.off_session_payment.succeeded` | The payment completed successfully. |
| `payment_intent.payment_failed`. The PaymentIntent moves to `requires_payment_method` status. Automatic retries are not available. You must implement retry logic yourself. | `v2.payments.off_session_payment.attempt_failed` | A single authorization attempt failed. Stripe might retry automatically based on your retry strategy. |
| Not available. Payment Intents has no terminal failure state for off-session payments. A failed PaymentIntent remains in `requires_payment_method` indefinitely. | `v2.payments.off_session_payment.failed` | The off-session payment reached a terminal failure state. Retries are exhausted or no longer possible. |

Because v2 events always use thin payloads, the event contains a `related_object` with the off-session payment ID. Fetch the full object to get payment details:

```curl
curl https://api.stripe.com/v2/payments/off_session_payments/osp_test_12345abc \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-04-22.preview"
```

The `payment_record` field on the off-session payment object contains payment outcome data. See [Payment Records](https://docs.stripe.com/payments/payment-records.md) for details.

To find the Charge ID associated with a payment attempt, fetch the Payment Attempt Record from the off-session payment object. The Charge ID is available in `processor_details.stripe.charge` on the Payment Attempt Record.

## Parameter mapping

Use this table to translate your existing Payment Intents parameters to the Off-Session Payments API.

| Payment Intents parameter | Off-Session Payments parameter | Notes |
| --- | --- | --- |
| `amount` | `amount.value` | Amount is now a nested object with `value` and `currency` |
| `currency` | `amount.currency` | Moved inside the `amount` object |
| `customer` | `customer` | Unchanged |
| `payment_method` | `payment_method` | Unchanged |
| `description` | `description` | Unchanged |
| `metadata` | `metadata` | Unchanged |
| `statement_descriptor_suffix` | `statement_descriptor_suffix` | Unchanged |
| `on_behalf_of` | `on_behalf_of` | Unchanged |
| `transfer_data` | `transfer_data` | Unchanged |
| `off_session=true` | Replaced by the v2 endpoint itself | The endpoint is inherently off-session; no parameter needed |
| `confirm=true` | Not applicable | Authorization is always asynchronous; no confirmation step |
| `payment_method_types` | Not applicable | Payment method type is inferred from the saved payment method |
| `confirmation_method` | Not applicable | All authorizations are asynchronous |
| Not applicable | `cadence` | Required. Set to `recurring` or `unscheduled` to classify the payment for network compliance. |
| Not applicable | `retry_details.retry_strategy` | Optional. Defaults to `none` (no retries). Set to `best_available` to enable automatic retries. |

## Test before migrating

Before migrating production traffic, validate your integration in the Stripe sandbox:

1. Set up your v2 event destination and verify webhook signature verification using the destination’s signing secret.
2. Create off-session payments in the sandbox and confirm your webhook handler receives and processes `v2.payments.off_session_payment.*` events.
3. Test both the `attempt_failed` and terminal `failed` paths to verify your retry and failure handling.

Once your sandbox integration is working, migrate your production integration.

## Next steps

- [How Off-Session Payments work](https://docs.stripe.com/payments/off-session-payments/integrate-with-off-session-payments.md) — full integration guide including event handling and terminal states
- [Smart Retries on Off-Session Payments](https://docs.stripe.com/payments/off-session-payments/smart-retries.md) — configure retry strategies and schedules
