#xenoliss_checkout-setup-subscriptions-3ds
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/1395140788681310250
๐ Have more to share? Add more details, code, screenshots, videos, etc. below.
Hello there
In our newer API version you want the latest_invoice.confirmation_secret: https://docs.stripe.com/api/invoices/object?api-version=2025-06-30.basil#invoice_object-confirmation_secret
Trying now.
Just for context we discuss this in our changelog here: https://docs.stripe.com/changelog/basil/2025-03-31/add-support-for-multiple-partial-payments-on-invoices
It is to enable multiple PaymentIntents to be associated to an Invoice
Also most generally I'm wondering if I'm doing things right. I am VERY new to Strip (first time using it actually) I have a maybe unusual flow (well I'm sure other people might have already faced it) which is:
- User registers payment but is not charged anything yet
- User can purchase things which trigger an immediate bill for a new subscription (at prorata) with a billing anchor set to 1st day next month
- User can buy more things that will be billed immediately at prorata again and update the existing subscription quantity.
I am using
stripe.checkout.sessions.createin setup mode for 1.- TBD for 2. but ideally something simple to implement that support 3DS but without affecting too much UX (e.g if no 3DS is needed the payment should just be processed in the background without any redirection)
- For 3. I think the only option (to update a subscription quantity) is via a
stripe.subscriptionItems.update. Is that right?
Thanks a lot for the help ๐
Yeah that is mostly correct... for step 3 do you expect them to pay immediately for those new items or have it be a proration item to be picked up with the next Invoice?
If you update the quantity then the default will be to just have it create a proration item since the billing period isn't actually changing here
Quick additional context: the things user buy are sort of "memberships" so they can buy 1, 2, 3 etc memberships. Each membership has the same cost X. When buying the very first membership it bills the user and creates the suibscription. When buying new memberships it update the existing subscription quantity and bill immediately.
for step 3 do you expect them to pay immediately for those new items or have it be a proration item to be picked up with the next Invoice
I think the most straightforward is to have them pay at purchase time? Like when they buy something a new membership they immediately pay for it (at prorata).
If you update the quantity then the default will be to just have it create a proration item since the billing period isn't actually changing here
The code for this is just that currently (though everything has been designed without 3DS so it kind of breaks now which is why I oepned this ticket):
await stripe.subscriptionItems.update(existingItem.id, {
quantity: existingItem.quantity + 1,
proration_behavior: "always_invoice",
});
PS: It looks like this code is vulnerable to a race condition but I don't feel like going into a full payment queue right now...
Hi hi! Iโm going to be taking over for my colleague here. Just give me a sec to grok all this.
(appreciate the responsiveness, really impressive)
Thanks - we try. ๐
Out of curiosity do you offer paid support? Where I could chat live/share screen with one member of your team ?
Not that I'm aware of? but I'll pass that along.
Ok so:
That's sorted. From there it's basically this, right?
if isNew(user):
createSubscription(user, items)
else:
updateSubscription(user, items)
Yes for the setup I just relly on your checkout sessions and assume it will handle 3DS properly if needed.
And yes for the pseudo code
It will, yup.
Ok awesome. Now we just need to figure out createSubscritption() and updateSubscritption() for your usecase. ๐
(will have to drop for about 30mins but will be back right after just in case)
Ok, will keep this open since I'll still be here then. ๐
Alright I'm back sorry for the delay!
No worries. ๐
Here is my current code (that does not work well with 3DS):
async function chargeForStudy(service: {
id: string;
stripeCustomerId: string | null;
stripeSubscriptionId: string | null;
}) {
if (!service.stripeCustomerId) {
return {
success: false,
message: "Service not registered as a customer",
} as const;
}
try {
// Create new subscription with billing cycle anchor set to 1st of next month
if (!service.stripeSubscriptionId) {
// Calculate billing cycle anchor for 1st of next month
const now = new Date();
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
const billingCycleAnchor = Math.floor(nextMonth.getTime() / 1000);
const subscription = await stripe.subscriptions.create({
customer: service.stripeCustomerId,
items: [{ price: PRICE_ID }],
billing_cycle_anchor: billingCycleAnchor,
metadata: {
serviceId: service.id,
},
});
return {
success: true,
message: "New subscription created",
subscriptionId: subscription.id,
} as const;
}
// Get existing subscription to check for existing items with this price
else {
const subscription = await stripe.subscriptions.retrieve(
service.stripeSubscriptionId,
{ expand: ["items"] }
);
// Find existing subscription item with the same price
const existingItem = subscription.items.data.find(
(item) => item.price.id === PRICE_ID
);
// If the item does not exist yet, there is an issue with the subscription
if (!existingItem || existingItem.quantity === undefined) {
return {
success: false,
message: "Subscription not configured correctly",
} as const;
}
await stripe.subscriptionItems.update(existingItem.id, {
quantity: existingItem.quantity + 1,
proration_behavior: "always_invoice",
});
return {
success: true,
message: "Subscription quantity updated",
} as const;
}
} catch (error) {
console.error(`Error charging for study - ${error}`);
return {
success: false,
message: "Internal server error",
} as const;
}
}
Why does it not work well with 3DS?
Because the user is never prompted to complete the 3DS verification process and the payment is just left pending. He has to manually go to his billing portal to pay the open invoice...
It switches the subscription status to incomplete
Right right, ok. And the user is always 'on session' when this runs, right? Because they've clicked something to trigger this, right?
Yes I assume renewal should not trigger a 3DS since this happens off-session
Wait, does this happen on-session or off-session?
The code you see above should always happen on-session because it is triggered by a user click to purchase an item
Right, ok, that's whta I thought. ๐ Ok one sec while I absorb the codez
a few open questions I have as well:
- What are the mandatory subscription status I need to track? I thought of
ACTIVE,PAST_DUEbut now with 3DS seems likeINCOMPLETEmight be needed as well. - Is it good practice to store the subscription status & quantity in my DB or should I rather alway query it from stripe API?
- what are the strictly necessary webhooks I need to properly handle all cases. More specifically do I even need to listen for the
invoice.paid/invoice.payment_failedevents if all I care about is the subscription status?
I think those might get answered as part of solving parts 2 and 3. ๐
Also I am fine with a subscription being PAST_DUE but still giving access to the user. I assume after a week Stripe will switch it to cancel (configured from the dahsboard) if it has not been paid for at which point I will remove access to the user.
Not sure how that changes if the subscription can now be INCOMPLETE as well
define chargeForStudy(service):
if !service.customer:
return "No Customer"
if !service.subscription:
createSubscription(service)
else:
updateSubscription(service)
Makes sense so far, ya?
Ooo
if !service.subscription:
createSubscription(service, onNeeds3DS)
else:
updateSubscription(service, onNeeds3DS)
Hm.
Oh. I'm silly. Sorry. This pretty much covers it: https://docs.stripe.com/billing/subscriptions/overview#requires-action
I'm batting 1000 today. ๐คฆโโ๏ธ https://docs.stripe.com/billing/subscriptions/overview#requiring-3ds-payment
Ok, so the last link I sent is how you can get Stripe to automatically follow up for you. The first link is what you need to build out.
I see, gotta do some lecture
In your 'on-session' code, you'll want to expand and look at latest_invoice.payments.data.payment.payment_intent and its status.
Where do you see this from?
How do this relate to [this](#1395140788681310250 message)?
Also it's not extremely clear to me what are Payment and Setup intents. When are they used and what for? I've never created any in my existing code.
Yup, got it. I'm not sure how Confirmation Tokens came into this, but it's probably that I'm not entirely up to date on something.
I believe this is what you would want to do with the information from the related Payment Intent: https://docs.stripe.com/payments/3d-secure/authentication-flow?platform=web#when-to-use-3d-secure
So the TLDR is to mostly keep the code as is but check if there is a required action and if so open the 3DS popup from the client secret right?
Basically, ya. ๐
And then also listen for invoice.payment_action_required, and if you get that event, contact the associated user to let them know the payment didn't complete and you need them to try again.
Does that all make sense?
invoice.payment_action_required
This sould only ever happen on-session no?
If so I wonder if listening to the event is necessary given that I would have already redirected the user to complete the 3DS thing
Yes, but if someone doesn't complete the bank redirect, or leaves your page, or times out etc, your code wouldn't be able to redirect them, so listening to this event would be one way of dealing with that.
You also likely want to listen to invoice.paid events for once it's successful - and you can 'provision' the stuff, and to invoice.payment_failed events for if it fails so you can ask them to try a different payment method.
I see. I need to read more but the chat has been really helpfull
I'm glad! ๐ There are a lot of moving parts, but once you understand how they fit together, you can build a heck of a machine out of 'em. ๐
Yeah pretty intimidating at first ๐
It's super-hard to balance powerful complexity with easy simplicity ๐ but we do our best. ๐
I have to โ๏ธ but snufkin can help if anything else comes up. Good luck - and have fun! ๐
I should be good for some time thanks for the help. Keep the feed open though as I might come back to it time to time
FYI: We only keep threads open for ~30-45m of inactivity before we close them. However, this thread will always be available for you to review and if you have a new question you can always ask it using the form in #help
I do have one question on setup session. Can I have the session automatically set the customer payment method the default one? And can I also pre-set the customer name and metadata?
Sorry I don't know what you mean by setup session. Are you using Checkout Sessions here? Or are you referring to Setup Intents?
I mean when creating a checkout session in setup mode
Okay. So you cannot pre-set the Customer name but you can specify the Customer email using the customer_email parameter
Unfortunately, if you want to set the saved Payment Method as default, you will need to do that in a separate API call after the Checkout Session completes
Yeah that's what I was experiencing.
I am using the session.complete web hook to register the default payment (and change the customer name)
I think that is the best practice, given your approach here.