# Set up future card payments

Use manual server-side confirmation or present payment methods separately.

# Android


> We recommend that you follow the [Accept in-app payments](https://docs.stripe.com/payments/mobile/accept-payment.md?platform=android&type=setup) guide. Only use this guide if you need to use manual server-side confirmation or your integration requires presenting payment methods separately. If you’ve already integrated with Elements, see the [Payment Element migration guide](https://docs.stripe.com/payments/payment-element/migration.md).

The [Setup Intents API](https://docs.stripe.com/api/setup_intents.md) lets you save a customer’s card without an initial payment. This is helpful if you want to onboard customers now, set them up for payments, and charge them in the future—when they’re offline.

Use this integration to set up recurring payments or to create one-time payments with a final amount determined later, often after the customer receives your service.

> The steps in this guide are fully implemented on GitHub. Clone the [repo](https://github.com/stripe-samples/mobile-saving-card-without-payment) and follow the [instructions](https://github.com/stripe-samples/mobile-saving-card-without-payment#how-to-run) to run the demo app.

## Set up Stripe

First, you need a Stripe account. [Register now](https://dashboard.stripe.com/register).

### Server-side 

This integration requires endpoints on your server that talk to the Stripe API. Use our official libraries for access to the Stripe API from your server:

#### Ruby

```bash
# Available as a gem
sudo gem install stripe
```

```ruby
# If you use bundler, you can add this line to your Gemfile
gem 'stripe'
```

### Client-side 

The [Stripe Android SDK](https://github.com/stripe/stripe-android) is open source and [fully documented](https://stripe.dev/stripe-android/).

To install the SDK, add `stripe-android` to the `dependencies` block of your [app/build.gradle](https://developer.android.com/studio/build/dependencies) file:

#### Kotlin

```kotlin
plugins {
    id("com.android.application")
}

android { ... }

dependencies {
  // ...

  // Stripe Android SDK
  implementation("com.stripe:stripe-android:23.25.1")
  // Include the financial connections SDK to support US bank account as a payment method
  implementation("com.stripe:financial-connections:23.25.1")
}
```

> For details on the latest SDK release and past versions, see the [Releases](https://github.com/stripe/stripe-android/releases) page on GitHub. To receive notifications when a new release is published, [watch releases for the repository](https://docs.github.com/en/github/managing-subscriptions-and-notifications-on-github/configuring-notifications#configuring-your-watch-settings-for-an-individual-repository).

Configure the SDK with your Stripe [publishable key](https://dashboard.stripe.com/apikeys) so that it can make requests to the Stripe API, such as in your `Application` subclass:

#### Kotlin

```kotlin
import com.stripe.android.PaymentConfiguration

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        PaymentConfiguration.init(
            applicationContext,
            "<<YOUR_PUBLISHABLE_KEY>>"
        )
    }
}
```

> Use your [test keys](https://docs.stripe.com/keys.md#obtain-api-keys) while you test and develop, and your [live mode](https://docs.stripe.com/keys.md#test-live-modes) keys when you publish your app.

## Create a Customer before setup [Server-side]

To set up a payment method for future payments, you must attach it to an object that represents your customer. When your customer creates an account or has their first transaction with your business, create either a customer-configured [Account](https://docs.stripe.com/api/v2/core/accounts/create.md) object with the Accounts v2 API or a [Customer](https://docs.stripe.com/api/customers/create.md) object with the Customers API.

#### Accounts v2

```curl
curl -X POST https://api.stripe.com/v2/core/accounts \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-10-28.preview" \
  --json '{
    "contact_email": "jenny.rosen@example.com",
    "display_name": "Jenny Rosen",
    "configuration": {
        "customer": {}
    },
    "include": [
        "configuration.customer"
    ]
  }'
```

Successful creation returns the customer-configured [Account](https://docs.stripe.com/api/v2/core/accounts/object.md#v2_account_object-configuration-customer) object. Inspect the object for the customer’s `id` and store the value in your database for later retrieval.

#### Customers v1

```curl
curl https://api.stripe.com/v1/customers \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "name=Jenny Rosen" \
  --data-urlencode "email=jennyrosen@example.com"
```

Successful creation returns the [Customer](https://docs.stripe.com/api/customers/object.md) object. Inspect the object for the customer’s `id` and store the value in your database for later retrieval.

You can find these customers in the [Customers](https://dashboard.stripe.com/customers) page in the Dashboard.

## Create a SetupIntent [Server-side]

A [SetupIntent](https://docs.stripe.com/api/setup_intents.md) is an object that represents your intent to set up a payment method for future payments.

The SetupIntent object contains a [client secret](https://docs.stripe.com/api/setup_intents/object.md#setup_intent_object-client_secret), a unique key that you pass to your app. The client secret lets you perform certain actions on the client, such as confirming the setup and updating payment method details, while hiding sensitive information like `customer`. You can use the client secret to validate and authenticate card details using the credit card networks. The client secret is sensitive—don’t log it, embed it in URLs, or expose it to anyone but the customer.

### Server-side

On your server, make an endpoint that creates a SetupIntent and returns its client secret to your app.

#### curl

```bash
curl https://api.stripe.com/v1/setup_intents/ \
  -u <<YOUR_SECRET_KEY>>: \
  -d "customer"="{{CUSTOMER_ID}}"
```

If you only plan on using the card for future payments when your customer is present during the checkout flow, set the [usage](https://docs.stripe.com/api/setup_intents/object.md#setup_intent_object-usage) parameter to *on\_session* (A payment is described as on-session if it occurs while the customer is actively in your checkout flow and able to authenticate the payment method) to improve authorization rates.

### Client-side

On the client, request a SetupIntent from your server.

#### Kotlin

```kotlin
class CheckoutActivity : AppCompatActivity() {
   private lateinit var setupIntentClientSecret: String

   private fun loadPage() {
       // Request a SetupIntent from your server and store its client secret
       // Click View full sample to see a complete implementation
   }
}
```

## Collect card details [Client-side]

When the customer submits the payment form, collect their card details using [CardInputWidget](https://stripe.dev/stripe-android/payments-core/com.stripe.android.view/-card-input-widget/index.html), a drop-in UI component provided by the SDK that collects the card number, expiration date, CVC, and postal code.
![](https://d37ugbyn3rpeym.cloudfront.net/docs/mobile/android/android-card-input-widget-with-postal.mp4)
> When saving card details to use for future off-session payments, especially in Europe because of regulations around card reuse, [get permission to save a card](https://docs.stripe.com/strong-customer-authentication.md#sca-enforcement). Include some text in your checkout flow to inform your user how you intend to use the card.

Call the `getPaymentMethodCard` method to retrieve the card details. Pass the collected information into new [PaymentMethodCreateParams](https://stripe.dev/stripe-android/payments-core/com.stripe.android.model/-payment-method-create-params/index.html) and `PaymentMethod.BillingDetails` instances to create a `ConfirmSetupIntentParams` instance.

#### Kotlin

```kotlin
// Collect card details
val cardInputWidget =
    findViewById<CardInputWidget>(R.id.cardInputWidget)
val paymentMethodCard = cardInputWidget.paymentMethodCard

// Later, you will need to attach the PaymentMethod to the Customer it belongs to.
// This example collects the customer's email to know which customer the PaymentMethod belongs to, but your app might use an account id, session cookie, and so on.
val emailInput = findViewById<EditText>(R.id.emailInput)
val billingDetails = PaymentMethod.BillingDetails.Builder()
    .setEmail((emailInput.text ?: "").toString())
    .build()

// Create SetupIntent confirm parameters with the above
if (paymentMethodCard != null) {
    val paymentMethodParams = PaymentMethodCreateParams
        .create(paymentMethodCard, billingDetails, null)
    val confirmParams = ConfirmSetupIntentParams
        .create(paymentMethodParams, setupIntentClientSecret)
    lifecycleScope.launch {
        paymentLauncher.confirm(confirmParams)
    }
}
```

To complete the setup, pass the `SetupIntentParams` object with the current Activity to [PaymentLauncher confirm](https://stripe.dev/stripe-android/payments-core/com.stripe.android.payments.paymentlauncher/-payment-launcher/index.html#74063765%2FFunctions%2F-1622557690). The SetupIntent verifies that the card information your customer is using is valid on the network. However, automatic verification isn’t always performed. For details on this, see [Check if a card is valid without a charge](https://support.stripe.com/questions/check-if-a-card-is-valid-without-a-charge). Also, don’t maintain long-lived, unhandled SetupIntents as they might no longer be valid when you call `PaymentLauncher#confirm()`.

Some payment methods require [additional authentication steps](https://docs.stripe.com/payments/payment-intents/verifying-status.md#next-actions) to complete a payment. The SDK manages the payment confirmation and authentication flow, which might involve presenting additional screens required for authentication. For more information on *3D Secure* (3D Secure (3DS) provides an additional layer of authentication for credit card transactions that protects businesses from liability for fraudulent card payments) authentication and customizing the authentication experience, see [Supporting 3D Secure Authentication on Android](https://docs.stripe.com/payments/3d-secure.md?platform=android).

The result of the flow returns to your calling Activity through the callback provided, here `onPaymentResult`. The [PaymentResult](https://stripe.dev/stripe-android/payments-core/com.stripe.android.payments.paymentlauncher/-payment-result/index.html) returned by the [PaymentLauncher](https://stripe.dev/stripe-android/payments-core/com.stripe.android.payments.paymentlauncher/-payment-launcher/index.html) has these types:

- `Completed` - confirmation or authentication succeeded
- `Canceled` - the customer canceled required authentication
- `Failed` - the authentication attempt failed, a reason is provided by the [throwable](https://stripe.dev/stripe-android/payments-core/com.stripe.android.payments.paymentlauncher/-payment-result/-failed/index.html#257609069%2FProperties%2F-1622557690)

#### Kotlin

```kotlin
private fun onPaymentResult(paymentResult: PaymentResult) {
    var title = ""
    var message = ""
    var restartDemo = false
    when (paymentResult) {
        is PaymentResult.Completed -> {
            title = "Setup Completed"
            restartDemo = true
        }
        is PaymentResult.Canceled -> {
            title = "Setup Canceled"
        }
        is PaymentResult.Failed -> {
            title = "Setup Failed"
            message = paymentResult.throwable.message!!
        }
    }
    displayAlert(title, message, restartDemo)
}
```

You now have a flow to collect card details and handle any authentication requests. Use the test card `4000 0025 0000 3155`, along with any CVC, postal code, and future expiration date to test the authentication process.

When the SetupIntent succeeds, the resulting PaymentMethod ID (in `setupIntent.getPaymentMethodId()`) will be saved to the provided Customer.

## Charge the saved card later [Server-side]

When you’re ready to charge your customer off-session, use the Customer and PaymentMethod IDs to create a PaymentIntent. To find a card to charge, [list](https://docs.stripe.com/api/payment_methods/list.md) the PaymentMethods associated with your Customer.

#### Accounts v2

```curl
curl -G https://api.stripe.com/v1/payment_methods \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "customer_account={{CUSTOMERACCOUNT_ID}}" \
  -d type=card
```

#### Customers v1

```curl
curl -G https://api.stripe.com/v1/payment_methods \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "customer={{CUSTOMER_ID}}" \
  -d type=card
```

When you have the Customer and PaymentMethod IDs, create a PaymentIntent with the amount and currency of the payment. Set a few other parameters to make the off-session payment:

- Set [off_session](https://docs.stripe.com/api/payment_intents/confirm.md#confirm_payment_intent-off_session) to `true` to indicate that the customer isn’t in your checkout flow during this payment attempt. This causes the PaymentIntent to throw an error if authentication is required.
- Set the value of the PaymentIntent’s [confirm](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-confirm) property to `true`, which causes confirmation to occur immediately when the PaymentIntent is created.
- Set [payment_method](https://docs.stripe.com/api.md#create_payment_intent-payment_method) to the ID of the PaymentMethod and [customer](https://docs.stripe.com/api.md#create_payment_intent-customer) to the ID of the Customer.

#### curl

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

Inspect the [status property](https://docs.stripe.com/payments/paymentintents/lifecycle.md) of the PaymentIntent to confirm that the payment completed successfully. If the payment attempt succeeded, the PaymentIntent’s status is `succeeded` and the *off-session payment* (A payment is described as off-session if it occurs without the direct involvement of the customer, using previously-collected payment information) is complete.

### Start a recovery flow

If the PaymentIntent has any other status, the payment didn’t succeed and the request fails. Notify your customer to return to your application (for example, by email, text, push notification) to complete the payment. We recommend creating a recovery flow in your app that shows why the payment failed initially and lets your customer retry.

In your recovery flow, retrieve the PaymentIntent through its *client secret* (The client secret is a unique key returned from Stripe as part of a PaymentIntent. This key lets the client access important fields from the PaymentIntent (status, amount, currency) while hiding sensitive ones (metadata, customer)). Check the PaymentIntent’s [lastPaymentError](https://stripe.dev/stripe-android/payments-core/com.stripe.android.model/-payment-intent/index.html#com.stripe.android.model/PaymentIntent/lastPaymentError/#/PointingToDeclaration/) to inspect why the payment attempt failed. For card errors, you can show the user the last payment error’s [message](https://docs.stripe.com/api/payment_intents/object.md#payment_intent_object-last_payment_error-message). Otherwise, you can show a generic failure message.

#### Kotlin

```kotlin
fun startRecoveryFlow(clientSecret: String) {
    val stripe = Stripe(
        applicationContext,
        PaymentConfiguration.getInstance(applicationContext).publishableKey
    )
    lifecycleScope.launch {
        runCatching {
            stripe.retrievePaymentIntent(clientSecret)
        }.fold(
            onSuccess = { paymentIntent ->
                var failureReason =
                    "Payment failed, try again" // Default to a generic error message
                paymentIntent.lastPaymentError?.let { lastPaymentError ->
                    if (lastPaymentError.type == PaymentIntent.Error.Type.CardError) {
                        lastPaymentError.message?.let { errorMessage ->
                            failureReason = errorMessage
                            // Display the failure reason to your customer
                        }
                    }
                }
            },
            onFailure = {
                // Handle error
            }
        )
    }
}
```

### Let your customer try again

Give the customer the option to [update](https://docs.stripe.com/api/payment_methods/update.md) or [remove](https://docs.stripe.com/api/payment_methods/detach.md) their saved card and try payment again in your recovery flow. Follow the same steps you did to accept their initial payment with one difference—*confirm* (Confirming a PaymentIntent indicates that the customer intends to pay with the current or provided payment method. Upon confirmation, the PaymentIntent attempts to initiate a payment) the original, failed PaymentIntent by reusing its [client secret](https://docs.stripe.com/api/payment_intents/object.md#payment_intent_object-client_secret) instead of creating a new one.

If the payment failed because it requires authentication, try again with the existing *PaymentMethod* (PaymentMethods represent your customer's payment instruments, used with the Payment Intents or Setup Intents APIs) instead of creating a new one.

#### Kotlin

```kotlin
fun startRecoveryFlow(clientSecret: String) {
    // ...continued from previous step
    val paymentConfiguration = PaymentConfiguration.getInstance(applicationContext)
    val paymentLauncher = PaymentLauncher.Companion.create(
        this,
        paymentConfiguration.publishableKey,
        paymentConfiguration.stripeAccountId,
        ::onPaymentResult
    )

    val lastPaymentError = paymentIntent.lastPaymentError
    val lastPaymentMethodId = lastPaymentError.paymentMethod?.id
    if (lastPaymentError?.code == "authentication_required" && lastPaymentMethodId != null) {
        // Payment failed because authentication is required, reuse the PaymentMethod
        val paymentIntentParams =
            ConfirmPaymentIntentParams.createWithPaymentMethodId(
                lastPaymentMethodId,
                clientSecret // Reuse the existing PaymentIntent
            )
        // Submit the payment...
        paymentLauncher.confirm(paymentIntentParams)
    } else {
        // Collect a new PaymentMethod from the customer...
    }
}
```

## Test the integration

By this point you should have an integration that:

1. Collects and saves card details without charging the customer by using a SetupIntent
2. Charges the card off-session and has a recovery flow to handle declines and authentication requests

There are several test cards you can use to make sure this integration is ready for production. Use them with any CVC, postal code, and future expiration date.

| Number           | Description                                                                                                                                                                 |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 4242424242424242 | Succeeds and immediately processes the payment.                                                                                                                             |
| 4000002500003155 | Requires authentication for the initial purchase, but succeeds for subsequent payments (including off-session ones) as long as the card is setup with `setup_future_usage`. |
| 4000002760003184 | Requires authentication for the initial purchase, and fails for subsequent payments (including off-session ones) with an `authentication_required` decline code.            |
| 4000008260003178 | Requires authentication for the initial purchase, but fails for subsequent payments (including off-session ones) with an `insufficient_funds` decline code.                 |
| 4000000000009995 | Always fails (including the initial purchase) with a decline code of `insufficient_funds`.                                                                                  |

See the full list of [test cards](https://docs.stripe.com/testing.md).

