I'm currently migrating my app from using the Stripe Charges API to use the Stripe PaymentIntents API, in order to comply with SCA regulations. My application is a subscription service with a recurring billing model, so I've been generally following the "Gym Membership" examples of the migration docs, as well as looking at other relevant docs and references.
I'm using Stripe Elements at the front-end to capture payment details etc. on a custom form and then send to my backend with Stripe payment token for further processing (synchronously). The frontend updates are simple enough and I have no problems there, but I'm a bit confused with the backend updates.
All the code examples I can find in the docs (which are usually terrific) show how to convert Charge
calls to PaymentIntent
calls e.g. this old Charge call:
Map<String, Object> chargeParams = new HashMap<String, Object>();
chargeParams.put("amount", 1099);
chargeParams.put("currency", "eur");
chargeParams.put("source", request.token_id);
Charge.create(chargeParams);
...becomes this using the PaymentIntents API:
Map<String, Object> createPaymentIntentParams = new HashMap<String, Object>();
createPaymentIntentParams.put("currency", "eur");
createPaymentIntentParams.put("amount", 1099);
createPaymentIntentParams.put("confirm", true);
createPaymentIntentParams.put("confirmation_method", "manual");
createPaymentIntentParams.put("payment_method", request.paymentMethodId);
intent = PaymentIntent.create(createPaymentIntentParams);
So if additional authorization is required by the customer (as indicated by the PaymentIntent
status), the request will be kicked back to the customer and the Stripe SDK will handle the additional security measures.
But my app is not using Charge
calls in this way. It generally looks like this:
Map<String, Object> srchOpts = new HashMap<>();
srchOpts.put("email", userEmail);
List<Customer> matchingCustomers = Customer.list(srchOpts).getData();
Customer customer = null;
Subscription subscription = null;
if ( matchingCustomers.isEmpty() ){
Map<String, Object> params = new HashMap<String, Object>();
params.put("email", userEmail);
params.put("source", stripeToken);
customer = Customer.create(params); // potential SCA rejection ??
}
else if (matchingCustomers.size() == 1) {
customer = matchingCustomers.get(0);
Map<String, Object> params = new HashMap<String, Object>();
params.put("source", stripeToken);
PaymentSourceCollection paymentSources = customer.getSources();
paymentSources.create(params); // potential SCA rejection ??
}
Map<String, Object> item = new HashMap<String, Object>();
item.put("plan", planId);
Map<String, Object> items = new HashMap<String, Object>();
items.put("0", item);
Map<String, Object> params = new HashMap<String, Object>();
params.put("items", items);
params.put("customer", customer.getId());
subscription = Subscription.create(params); // potential SCA rejection ??
Are the new Customer
creation, new PaymentSource
creation and new Subscription
creation calls subject to the SCA rejection, at which point I would have to return to the customer for further authentication?
If so, how do I check if this is necessary with the Customer and PaymentSource calls and how do I get the required client secret token to send back to the frontend? The Subscription object does provide access to a SetupIntent
object that has status and client secret, so do I have to check and use these?
Any links to relevant docs with examples would be very helpful.
The only time SCA is required is when you attempt to make a payment. After you've collected your customer's payment details (and optionally saved them as a new customer) you ask Stripe to complete the payment. Stripe will then contact the customer's bank and ask if the payment can be made or if additional authentication is required.
If the bank says nothing extra is needed, the payment succeeds and all is well.
If the bank says a 3DS check is required, then you'll need to run your customer through the 3DS flow, which is essentially a 2FA step to ensure that the person requesting the payment is also the cardholder.
If your customer is still "on-session" (e.g. still on your site), you'd pass the newly created PaymentIntent's client secret to your frontend and use Stripe.js to complete the 2FA step and authenticate the payment.
If your customer is "off-session" (e.g. this is a recurring subscription and they are not on your site) then you will have to email your customer to get them back on your site to do the 3DS step (or you can use Stripe's hosted invoice page).
In your case, when you create a subscription (assuming you're not using trial periods), Stripe will create an Invoice with an automatically created PaymentIntent attached. You can access this Invoice via the latest_invoice
parameter on the Subscription. If the 3DS step is required, the PaymentIntent will have the requires_action
status, meaning that you need to get your customer back "on-session" to complete the payment.
With the hosted invoice page, Stripe will automatically email your user in this case so they can complete the payment. Without the hosted invoice page you'll have to build your own implementation to get your user back "on-session".
You won't be required to do 3DS when creating a Customer or PaymentMethod, only when you actually attempt to move funds from one place to another.