#paulc7053_handling-setup-intent-confirm-failures
1 messages ยท Page 1 of 1 (latest)
๐ Welcome to your new thread!
โฒ๏ธ We'll be here soon! Typically we respond in a few minutes, but sometimes we might take a bit longer if the server is busy or if you have a particularly tricky question.
โฑ๏ธ We close idle threads, which makes them read-only. Once a thread is closed it won't be reopened, but you can always start a new thread if you have another question.
๐ This thread will always be available, even after it's closed. You can find it again using Discord's search, or you can save this link: https://discord.com/channels/841573134531821608/1357108732714160148
๐ Have more to share? Add more details, code, screenshots, videos, etc. below.
Hello!
Hi there!
Could you explain in a little more detail what you want to do? Is it that you want to know about when people enter wrong card details when signing up?
Yep
On the frontend, I show the error message provided
But the above webhooks seem to behave as if the card was valid...
Do you have an example?
Like an ID?
Yes please. Subscription or Customer would be fine
sure, 1 sec please
This is a produciton subscription ID: sub_1R9UEMJ4ILijjURS12yDTdD6
Also shows up as paid (just like in the webhook)
An Invoice that doesn't require payment ($0 Invoice) will always show up as "paid"
ah, I see, when you tried to confirm the SetupIntent, it returned an error
or a decline, really
Just looking into the behavior around the SetupIntent's webhooks
I assume that you are looking to understand this soon after the decline happens, is that right?
yes
it just seems odd to me that I can't see any obvious way to tell that the subscription doesnt have a valid card attached
Well payment methods on Subscriptions have some complexity, because you don't necessarily have to define the payment method on the Subscription object itself
If the Customer has a default_payment_method set, Stripe will use that
But you should receive events to let you know the SetupIntent failed. I think in terms of a real decline, you would see a setup_intent.setup_failed event
And then at some point after that, around the 24 hour mark, you would see a setup_intent.cancelled event.
This may not have happened in this case because it isn't a real decline - you're using a Stripe test card to confirm the SetupIntent in this request: https://dashboard.stripe.com/logs/req_j38THagNXCzNM3
Which is against our terms of service. Instead, you should use our test cards, of which we have a wide variety in our documentation: https://docs.stripe.com/testing
Unless you have a customer who is signing up and trying to pay with a Stripe test card, which would be interesting. ๐
So you're saying the above problems mainly arise because I've tested with stripe cards in production, but otherwise the webhook payload/events are different?
Assuming the user uses a real card
And they introduce wrong details
I suspect that's the case. I would encourage you to test with Test Clocks and a decline-coded card in testmode
Test Clocks are a wonderful tool
Again, this is the event type I think you will want to listen to here: https://docs.corp.stripe.com/api/events/types#event_types-setup_intent.setup_failed
This seems to be broken
Hi hi! Iโm going to be taking over for my colleague here. Let me just read back.
Hello, alright!
Can you share the Setup Intent ID which failed to emit that event?
following up on your colleague's recommandation to listen to setup_intent.setup_failed, is there something in particular I should check in the payload?
What do you plan to do with the information?
I think it might have been because I was using a stripe test card in production. But I just tried again with another card and the setup_intent.setup_failed event is delivered
That Setup Intent is in a status: "requires_payment_method" state; it hasn't failed.
check that the credit card used to sign up vor the subscrition has been declined
https://dashboard.stripe.com/events/evt_1R9UENJ4ILijjURSbw0Q7f2S The setup_intent.created event includes the status telling you what it needs (requires_payment_method in this case).
This is the state machine for Setup Intents: https://docs.stripe.com/payments/setupintents/lifecycle
Can the value be requires_payment_method if a user has previously subscribed, but for the subsequent billing cycle their card has expired, or eliminated from the wallet?
Ok let's take a step back. What are you trying to accomplish here exactly?
differentiate between a user whose card has been declined at signup and a user whose card has been accepted
it's a free trial signup
So you're using Subscriptions?
yes
You're usnig the SetupIntent from the Subscription here, right?
How are you creating Subscriptions?
Sorry, that was not the correct snippet
return await stripe.subscriptions.create({ customer: customerId, items: [{ price_data: { currency: currency.toLowerCase(), product: productId, unit_amount: Math.round(amount * 100), recurring: { interval: stripeInterval } } }], payment_behavior: 'default_incomplete', payment_settings: { save_default_payment_method: 'on_subscription' }, expand: ['latest_invoice.payment_intent', 'pending_setup_intent'], metadata, trial_period_days: trialDays });
expand doesn't belong in the same object as the params, it belongs in a separate, second argument object.
(So that shouldn't work, but maybe it does ยฏ_(ใ)_/ยฏ)
It does work actually haha
yep
Ok. I don't think you want payment_behavior=default_incomplete in there, because your subscription should be active (but trialing) immediately, ya?
yes
ECE or Card element
Yes
so I'm basically using await elements.submit(); then creating the subscription and customer, then returning the secret
The customer in that example Subscription didn't have a card.
Also you definitely need to be doing all this in test mode, as my colleague said.
Yes, I know. I just realized this is a problem and was lazy to set up ngrok and all that.
Thought it would be an easy fix
Could you please check this subscription : sub_1R9ZkeJ4ILijjURS7aFknOGc. I didnt use a stripe card, but ensured it would be declined
What am I looking for?
You said previously you couldnt see a card attached
https://dashboard.stripe.com/logs/req_PMbLax8kQfRqsJ The Confirm redirect completed and you returned to Stripe for the final step (where the SetupIntent actually fails).
great, so going back to my initial question: how do I differential between a successful and a declined card attachment? listed to setup_intent.payment_failed?
listen**
That only fires when the setup intent fails, which the first one hasn't done yet.
When you called confirm on that Setup Intent (https://dashboard.stripe.com/logs/req_j38THagNXCzNM3) it responded with the error details you needed to have your user try again.
So you should check the status of that call. Otherwise, once the next Invoice is generated, it will fail to be paid, so you could deal with it then in the usual flow.
you're talking about confirmCardSetup on the frontent, right?
Maybe?
Yes, but that would add additional friction. I think lots of people will forget/defer introducing their cards again
I guess. You can contrast that with the nontrivially large set of potential users that will exit your signup flow when you require a card to sign up.
I mean yes... but Im trying to minimize both of these =))
No matter what you do here, you still will need to deal with declined cards after the trial ends. Any time Stripe attempts to pay an Invoice on one of your users' Subscriptions, it may fail, and you'll need to have a way to handle that.
I personally don't trial things that require a card; either your product speaks for itself and I will subsequently add my card so I can continue using it, or it doesn't and I don't. Saves me the hassle of figuring out how to remove my payment method from something I don't use.
This seems very odd to me. The client knows its not valid- where can I get this info in the webhooks?
But I'm not all humans, so ... fair.
The setup_intent.created event also shows that.
Webhooks is the wrong place to handle this type of failure. In the client when you get the error is the right place.
This is also only related to initial signup; future errors wouldn't be like this.
Okay. This is my handler `const handlePaymentButtonClick = async (e) => {
e.preventDefault();
setStripeError(null);
if (!stripe || !elements) return;
setIsLoading(true);
const { error: submitError } = await elements.submit();
if (submitError) {
setStripeError(submitError.message);
track('payment_error', {
payment_method: 'card',
error_type: 'submit'
});
return;
};
const formData = new FormData();
....
....
const subscriptionRes = await fetch('/api/subscriptions/create', {
method: 'POST',
body: formData
});
const subscriptionData = await subscriptionRes.json();
const clientSecret = subscriptionData.clientSecret;
const stripeCustomerId = subscriptionData.stripeCustomerId;
......
if (!clientSecret) {
console.log("No immediate payment required due to trial period.");
router.push(returnUrl);
return;
};
// confirm the payment
const cardElement = elements.getElement(CardElement);
const { error } = await stripe.confirmCardSetup(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
email: billingEmail,
},
},
});
if (error) {
setStripeError(error.message);
setIsLoading(false);
console.log({ error });
track('payment_error', {
payment_method: 'card',
error_type: 'setup'
});
} else {
router.push(returnUrl);
};
};`
As you can see I make a request to /subscriptions/create and create the stripe customer & subscription before I get the error from confirmCardSetup
because it needs the clientSecret
await elements.submit(); will include the Payment Method ID if it succeeds, but you don't seem to be collecting that?
yes, I was just following the guides
Which guide?
I was just looking it up to link it here, but I can't see the Card element implementation anywhere on the site
That's an antique at this point so ... fair.
I wrote this a few months ago
const { error } = await stripe.confirmCardSetup(clientSecret, {
That error is where you'd see that it failed and that you need to re-collect their payment details.
yes, thats true
but it happens after I create the sub and customer on the backend
because I need the client secret
I see a similar implementation (with clientSecret vor ECE) here: https://docs.stripe.com/elements/express-checkout-element/migration#one-time-update-method
https://docs.stripe.com/payments/save-and-reuse is likely a better guide, as is https://docs.stripe.com/payments/checkout/free-trials
But following this guide would yield the same problems -> create customer -> recieve customer created event ->create setup intent/subscription & get clientSecret -> receive event -> only then we see the error on the client ...
You don't need to wait for the event.
Do you mean the client side event in JavaScript? You can use await if you prefer; it doesn't really matter.
Point is, you knew that SetupIntent failed when const { error } = await stripe.confirmCardSetup() returned.
my bad, I wrote that sequence badly
Or rather, that the SetupIntent didn't succeed to be properly set up.
It didn't fail, it just didn't succeed.
But that happens after I create the customer and subscription/ payment intent. so the webhooks are sent
events*
Yes, they are.
Okay, let me put it differently: since I cannot prevent the invoice.payment_succeeded event from being sent - when the free subscription is created. How can I address on the backend the card failure to be validated
As you know, when you create a subscription and customer you handle events switch (event.type) { case 'customer.subscription.created': await handleSubscriptionCreated(event.data.object); break; case 'invoice.payment_succeeded': await handlePaymentSucceeded(event.data.object); ...... default: console.warn(Unhandled event type: ${event.type}); };. The problem is that I cannot differentiate between a valid and invalid card when signing up...
Based on your current setup and desire to not change it, there are three events your current system needs to see in order to confirm a "fully set up trialing subscription:"
[ customer.subscription.created, invoice.payment_succeeded, setup_intent.succeeded ]
If you haven't received all of those for a Subscription, it's not fully set up. More specifically, if you haven't received a setup_intent.succeeded event for a Subscription, the payment method part is still outstanding, so you would want to direct your user to try adding another payment method (which you'd then retry confirming with the Setup Intent client secret).
You could use setup_intent.succeeded to mark the Subsctription as payment_method_set=true (either in your app's data model, or in Stripe via metadata) and then if a Subscription doesn't have that, it isn't 'complete' from your perspective.
Awesome. ๐
I know I stole lots of your time, sorry for that and thanks for your patience!
No worries at all - that's what we're here for! ๐
It was also a quiet afternoon so I was able to mostly focus on this.
Of course! This thread will get closed after idling for a half hour or so, but you can always come back and access it and create another whenever you need. ๐