# Accept card payments without webhooks

Learn how to confirm a card payment on your server and handle card authentication requests.

# Android


For a wider range of support and future proofing, use the [standard integration](https://docs.stripe.com/payments/accept-a-payment.md) for asynchronous payments.

This integration uses a single client-to-server flow to take payments, without using *webhooks* (A webhook is a real-time push notification sent to your application as a JSON payload through HTTPS requests) or processing offline events. While it might seem simpler, this integration is difficult to scale as your business grows and has several limitations:

- **Only supports cards**—You’ll have to write more code to support ACH and popular regional payment methods separately.
- **Double-charge risk**—By synchronously creating a new PaymentIntent each time your customer attempts to pay, you risk accidentally double-charging your customer. Be sure to follow [best practices](https://docs.stripe.com/error-low-level.md#should-retry).
- **Manual authentication handling**—Cards with 3D Secure or those that are subject to regulations such as *Strong Customer Authentication* (Strong Customer Authentication (SCA) is a regulatory requirement in effect as of September 14, 2019, that impacts many European online payments. It requires customers to use two-factor authentication like 3D Secure to verify their purchase) require extra steps on the client.

Keep these limitations in mind if you decide to use this integration. Otherwise, use the [standard integration](https://docs.stripe.com/payments/accept-a-payment.md).

## Set up Stripe [Client-side] [Server-side]

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.

Our code samples also use [OkHttp](https://github.com/square/okhttp) and [GSON](https://github.com/google/gson) to make HTTP requests to a server.

## Create your checkout screen [Client-side]

Securely collect card information on the client with [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)
Create an instance of the card component and a “Pay” button by adding the following to your checkout page’s layout:

```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:showIn="@layout/activity_checkout"
        tools:context=".CheckoutActivity">
    <!--  ...  -->
    <com.stripe.android.view.CardInputWidget
            android:id="@+id/cardInputWidget"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"/>
    <Button
            android:text="Pay"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/payButton"
            android:layout_marginTop="20dp"
            app:layout_constraintTop_toBottomOf="@+id/cardInputWidget"
            app:layout_constraintStart_toStartOf="@+id/cardInputWidget"
            app:layout_constraintEnd_toEndOf="@+id/cardInputWidget"/>
      <!--  ...  -->
</androidx.constraintlayout.widget.ConstraintLayout>
```

Run your app, and make sure your checkout screen shows the card component and pay button.

## Collect card details [Client-side]

When your customer is ready to check out, create a *PaymentMethod* (PaymentMethods represent your customer's payment instruments, used with the Payment Intents or Setup Intents APIs) with the details collected by the card element.

#### Kotlin

```kotlin
class CheckoutActivity : AppCompatActivity() {
    private lateinit var stripe: Stripe
    // ...
    private fun pay() {
        val weakActivity = WeakReference<Activity>(this)
        // Collect card details on the client
        val cardInputWidget =
            findViewById<CardInputWidget>(R.id.cardInputWidget)
        val params = cardInputWidget.paymentMethodCreateParams
        if (params == null) {
            return
        }
        // Configure the SDK with your Stripe publishable key so that it can make requests to the Stripe API
        stripe = Stripe(applicationContext, PaymentConfiguration.getInstance(applicationContext).publishableKey)
        lifecycleScope.launch {
            runCatching {
                stripe.createPaymentMethod(params)
            }.fold(
                onSuccess = { result ->
                    // Create a PaymentIntent on the server with a PaymentMethod
                    print("Created PaymentMethod")
                    pay(result.id, null)
                },
                onFailure = {
                    displayAlert(weakActivity.get(), "Payment failed", "Error: $it")
                }
            )
        }
    }
    private fun pay(paymentMethod: String?, paymentIntent: String?) {
        // ...
    }
}
```

## Create a PaymentIntent [Client-side] [Server-side]

Stripe uses a `PaymentIntent` object to represent your intent to collect payment from a customer, tracking your charge attempts and payment state changes throughout the process.

### On the server

Add an endpoint that creates the PaymentIntent with the following parameters:

- [payment_method_id](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-payment_method) - the id of the PaymentMethod from the previous step
- [use_stripe_sdk](https://docs.stripe.com/api/payment_intents/confirm.md#confirm_payment_intent-use_stripe_sdk)
  - For apps that integrate with [Stripe Android SDK v10.0.0](https://github.com/stripe/stripe-android/blob/master/CHANGELOG.md#1000---2019-07-19) and above, the value of this parameter must be set to `true`
  - For apps that integrate with older SDK versions, **don’t pass this parameter**
- [confirm](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-confirm) - Set this to true to *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 PaymentIntent

If you want to save the card to reuse later, create a [Customer](https://docs.stripe.com/api/customers/create.md) to store the PaymentMethod and pass the following additional parameters when creating the PaymentIntent:

- [customer](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-customer) - set to the ID of the *Customer* (Customer objects represent customers of your business. They let you reuse payment methods and give you the ability to track multiple payments).
- [setup_future_usage](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-setup_future_usage) - set to `off_session` to tell Stripe that you plan to reuse this PaymentMethod for *off-session payments* (A payment is described as off-session if it occurs without the direct involvement of the customer, using previously-collected payment information) when your customer isn’t present. Setting this property saves the PaymentMethod to the Customer after the PaymentIntent is confirmed and any required actions from the user are complete.

After the PaymentIntent is created, return the following to the client:

- the PaymentIntent’s [client secret](https://docs.stripe.com/api/payment_intents/object.md#payment_intent_object-client_secret)
- `requiresAction: true` if the PaymentIntent [status](https://docs.stripe.com/api/payment_intents/object.md#payment_intent_object-status) is `requires_action`

> See an [example server implementation](https://github.com/stripe-samples/accept-a-payment/tree/master/custom-payment-flow/server).

### On the client

Request a `PaymentIntent` from your server. This example passes the server a list of items to determine the price. Always decide how much to charge on the server side, a trusted environment, as opposed to the client. This prevents malicious customers from being able to choose their own prices.

#### Kotlin

```kotlin
class CheckoutActivity : AppCompatActivity() {
    private val backendUrl = "http://10.0.2.2:4242/"
    private val httpClient = OkHttpClient()
    private fun displayAlert(activity: Activity?, title: String, message: String, restartDemo: Boolean = false) {
        // omitted for brevity
    }
    private fun pay(paymentMethod: String?, paymentIntent: String?) {
        val weakActivity = WeakReference<Activity>(this)
        var json = ""
        if (!paymentMethod.isNullOrEmpty()) {
            json = """
                {
                    "useStripeSdk":true,
                    "paymentMethodId":"$paymentMethod",
                    "currency":"usd",
                    "items": [
                        {"id":"photo_subscription"}
                    ]
                }
                """
        }
        else if (!paymentIntent.isNullOrEmpty()) {
            json = """
                {
                    "paymentIntentId":"$paymentIntent"
                }
                """
        }
        // Create a PaymentIntent on the server
        val mediaType = "application/json; charset=utf-8".toMediaType()
        val body = json.toRequestBody(mediaType)
        val request = Request.Builder()
            .url(backendUrl + "pay")
            .post(body)
            .build()
        httpClient.newCall(request)
            .enqueue(object: Callback {
                override fun onFailure(call: Call, e: IOException) {
                    displayAlert(weakActivity.get(), "Payment failed", "Error: $e")
                }
                override fun onResponse(call: Call, response: Response) {
                    // Request failed
                    if (!response.isSuccessful) {
                        displayAlert(weakActivity.get(), "Payment failed", "Error: $response")
                    } else {
                        val responseData = response.body?.string()
                        var responseJson = JSONObject(responseData)
                        val payError: String? = responseJson.optString("error")
                        val clientSecret: String? = responseJson.optString("clientSecret")
                        val requiresAction: Boolean? = responseJson.optBoolean("requiresAction")
                        // Payment failed
                        if (payError != null && payError.isNotEmpty()) {
                            displayAlert(weakActivity.get(), "Payment failed", "Error: $payError")
                        }
                        // Payment succeeded
                        else if ((clientSecret != null && clientSecret.isNotEmpty())
                            && (requiresAction == null || requiresAction == false)) {
                            displayAlert(weakActivity.get(), "Payment succeeded", "$clientSecret", restartDemo = true)
                        }
                        // Payment requires additional actions
                        else if ((clientSecret != null && clientSecret.isNotEmpty())
                            && requiresAction == true) {
                            runOnUiThread {
                                if (weakActivity.get() != null) {
                                    // ...continued in the next step
                                }
                            }
                        }
                    }
                }
            })
    }
}
```

## Handle any next actions [Client-side]

If the payment requires additional actions like *3D Secure authentication* (3D Secure (3DS) provides an additional layer of authentication for credit card transactions that protects businesses from liability for fraudulent card payments), the PaymentIntent’s status is set to `requires_action`. On the client, pass the `PaymentIntent` ID to [handleNextActionForPayment](https://stripe.dev/stripe-android/payments-core/com.stripe.android/-stripe/handle-next-action-for-payment.html). This SDK presents additional activities and walks the customer through authentication. See [Supporting 3D Secure Authentication on Android](https://docs.stripe.com/payments/3d-secure.md?platform=android) to learn more.

After handling a required action on the client, the status of the PaymentIntent is `requires_confirmation`. This step enables your integration to synchronously *fulfill* (Fulfillment is the process of providing the goods or services purchased by a customer, typically after payment is collected) the order on your backend and return the fulfillment result to your client.

Send the PaymentIntent ID to your backend and confirm it again within one hour to finalize the payment. Otherwise, the payment attempt fails and transitions back to `requires_payment_method`.

#### Kotlin

```kotlin
class CheckoutActivity : AppCompatActivity() {
    // ...
    private fun pay(paymentMethod: String?, paymentIntent: String?) {
        // ...
        val activity = weakActivity.get()!!
        stripe.handleNextActionForPayment(activity, clientSecret)
        // ...
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        val weakActivity = WeakReference<Activity>(this)
        // Handle the result of stripe.handleNextActionForPayment
        if (stripe.isPaymentResult(requestCode, data)) {
            lifecycleScope.launch {
                runCatching {
                    stripe.getPaymentIntentResult(requestCode, data!!)
                }.fold(
                    onSuccess = { result ->
                        val paymentIntent = result.intent
                        val status = paymentIntent.status
                        if (status == StripeIntent.Status.Succeeded) {
                            val gson = GsonBuilder().setPrettyPrinting().create()
                            displayAlert(
                                weakActivity.get(),
                                "Payment succeeded",
                                gson.toJson(paymentIntent),
                                restartDemo = true
                            )
                        } else if (status == StripeIntent.Status.RequiresPaymentMethod) {
                            // Payment failed – allow retrying using a different payment method
                            displayAlert(
                                weakActivity.get(),
                                "Payment failed",
                                paymentIntent.lastPaymentError!!.message ?: ""
                            )
                        } else if (status == StripeIntent.Status.RequiresConfirmation) {
                            print("Re-confirming PaymentIntent after handling a required action")
                            pay(null, paymentIntent.id)
                        } else {
                            displayAlert(
                                weakActivity.get(),
                                "Payment status unknown",
                                "unhandled status: $status",
                                restartDemo = true
                            )
                        }
                    }, onFailure = {
                        // Payment request failed – allow retrying using the same payment method
                        displayAlert(weakActivity.get(), "Payment failed", it.toString())
                    }
                )
            }
        }
    }
}
```

The result of payment authentication returns to your calling Activity through [Activity#onActivityResult()](https://developer.android.com/reference/android/app/Activity.html#onActivityResult\(int,%20int,%20android.content.Intent\)). Handle the result by calling [Stripe#onPaymentResult()](https://stripe.dev/stripe-android/payments-core/com.stripe.android/-stripe/on-payment-result.html) within `Activity#onActivityResult()`. The [PaymentIntentResult](https://stripe.dev/stripe-android/payments-core/com.stripe.android/-payment-intent-result/index.html) returned in [ApiResultCallback#onSuccess()](https://stripe.dev/stripe-android/payments-core/com.stripe.android/-api-result-callback/on-success.html) has two getters:

- `getIntent()` - a [PaymentIntent](https://stripe.dev/stripe-android/payments-core/com.stripe.android/-stripe-intent-result/index.html#com.stripe.android/StripeIntentResult/intent/#/PointingToDeclaration/) object retrieved after confirmation/authentication
- `getOutcome()` - a [StripeIntentResult.Status](https://stripe.dev/stripe-android/payments-core/com.stripe.android/-stripe-intent-result/index.html#com.stripe.android/StripeIntentResult/outcome/#/PointingToDeclaration/) value that indicates the outcome of payment authentication
  - `SUCCEEDED` - payment authentication succeeded
  - `FAILED` - payment authentication failed
  - `CANCELED` - the customer canceled required payment authentication
  - `TIMEDOUT` - the payment authentication attempt timed-out

## Test the integration

​​Several test cards are available for you to use in a sandbox to make sure this integration is ready. Use them with any CVC and an expiration date in the future.

| Number           | Description                                                                               |
| ---------------- | ----------------------------------------------------------------------------------------- |
| 4242424242424242 | Succeeds and immediately processes the payment.                                           |
| 4000002500003155 | Requires authentication. Stripe triggers a modal asking for the customer to authenticate. |
| 4000000000009995 | Always fails with a decline code of `insufficient_funds`.                                 |

For the full list of test cards see our guide on [testing](https://docs.stripe.com/testing.md).

## Optional: Recollect a CVC

When creating subsequent payments on a saved card, you might want to re-collect the CVC of the card as an additional fraud measure to verify the user.

Start by [listing](https://docs.stripe.com/api/payment_methods/list.md) the payment methods associated with your *Customer* (Customer objects represent customers of your business. They let you reuse payment methods and give you the ability to track multiple payments) to determine which of them to show for CVC re-collection. After re-collecting the customer’s CVC information, tokenize the CVC data using [Stripe#createCvcUpdateToken()](https://stripe.dev/stripe-android/payments-core/com.stripe.android/-stripe/create-cvc-update-token.html).

#### Kotlin

```kotlin
class CheckoutActivity : AppCompatActivity() {
    private val stripe: Stripe by lazy {
        Stripe(this, "<<YOUR_PUBLISHABLE_KEY>>")
    }
    private fun tokenizeCvc(cvc: String) {
        stripe.createCvcUpdateToken(
            cvc,
            callback = CvcUpdateResultCallback()
        )
    }
    private class CvcUpdateResultCallback : ApiResultCallback<Token> {
        override fun onSuccess(result: Token) {
            val tokenId = result.id
            // pass token ID to your backend
        }
        override fun onError(e: Exception) {
            // handle error
        }
    }
}
```

After sending the CVC token to your server, create a PaymentIntent on your server with the amount, currency, and the CVC token in the `payment_method_options[card][cvc_token]` parameter.

```curl
curl https://api.stripe.com/v1/payment_intents \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d payment_method={{PAYMENT_METHOD_ID}} \
  -d customer={{CUSTOMER_ID}} \
  -d amount=1099 \
  -d currency=usd \
  -d confirmation_method=manual \
  -d confirm=true \
  -d "payment_method_options[card][cvc_token]={{CVC_TOKEN_ID}}"
```

A payment might succeed even with a failed CVC check. To prevent this, configure your [Radar rules](https://docs.stripe.com/radar/rules.md#traditional-bank-checks) to block payments when CVC verification fails.

## See also

- [Accept Google Pay](https://docs.stripe.com/google-pay.md)

