#paulc7053_api
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/1279134440748220550
๐ Have more to share? Add more details, code, screenshots, videos, etc. below.
Below are links to other discussions we've had with you in the past week in case you want to review that information. If your question is related to one of these previous discussions, please provide a comprehensive summary of the current state and what you need help with now. We help many users simultaneously, so a summary allows us to resolve your issue as soon as possible.
- paulc7053_webhooks, 2 hours ago, 50 messages
Hi!
This is how I handle the payment on the client
`const handlePaymentButtonClick = async (e) => {
e.preventDefault();
if (!stripe || !elements) {
return;
};
const { error: submitError } = await elements.submit();
if (submitError) {
setStripeError(submitError.message);
return;
};
// create the customer
const stripeCustomerIdRes = await fetch('/api/stripe/findOrCreateCustomer', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
billingEmail,
...
}),
});
const stripeCustomerId = (await stripeCustomerIdRes.json()).stripeCustomerId;
// create the subscription
const subscriptionRes = await fetch('/api/stripe/createSubscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
stripeCustomerId,
...
}),
});
const clientSecret = (await subscriptionRes.json()).clientSecret;
const queryParams = { ...router.query };
const queryString = getPaymentSuccessfulQueryString(queryParams);
const host = process.env.NODE_ENV === 'development' ? a : b;
// confirm the payment
const cardElement = elements.getElement(CardElement);
const { error } = await stripe.confirmCardSetup(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
email: billingEmail,
},
},
});
if (error) {
console.log({ error });
} else {
router.push(returnUrl);
console.log(returnUrl);
};
};`
And this is how I create the subscription `// /api/stripe/createSubscription
import Stripe from "stripe";
const stripe = new Stripe(
process.env.NODE_ENV === 'development'
? process.env.STRIPE_TEST_API_KEY
: process.env.STRIPE_PRODUCTION_API_KEY
);
export default async function createSubscription(req, res) {
const {
stripeCustomerId,
billingEmail,
..
} = req.body;
const metadata = {
stripeCustomerId,
billingEmail,
...
};
//create a SetupIntent for collecting payment method
const setupIntent = await stripe.setupIntents.create({
customer: stripeCustomerId,
});
console.log("Stripe setup intent created.");
//create a dynamic product
const product = await stripe.products.create({
name: `${currency.toUpperCase()}${totalPrice} Subscription`,
});
console.log("Stripe product has been created.");
//create the subscription
const subscription = await stripe.subscriptions.create({
customer: stripeCustomerId,
items: [{
price_data: {
currency: currency.toLowerCase(),
product: product.id,
unit_amount: Math.round(totalPrice * 100),
recurring: { interval: 'year' },
}
}],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
metadata: metadata,
trial_period_days: 3
});
console.log("Stripe subscription has been created.");
const clientSecret = setupIntent.client_secret || null;
res.send({
subscriptionId: subscription.id,
clientSecret: clientSecret,
});
};`
But the problem is that when the trial ends, the invoice is always getting payment failed/past due
And I don't know what I'm doing wrong
Sorry this is a wall of code, can we step back for a bit?
Can you explain in words, using a bulleted list, the steps you are taking to create the subscription?
yes
- create a customer 2.create a setupIntent with the customer id 3. create the subscription 4. return the client secret from the setup intent to the client and confirm it with stripe.confirmCardSetip
Sorry, now I'm a little confused, you create the subscription after you create the Setup Intent but before you confirm it?
yes
Okay so the customer doesn't have a saved payment method when the Subscription is created.
yes, that seems to be the problem
How long is the free trial period?
3 days
Are you currently listening to webhooks?
yes
When you confirm the SetupIntent you should get a setup_intent.succeeded event. This will include the Setup Intent which has the Customer and Payment Method IDs in it.
When you get that event, you can update the Customer record to set the newly saved Card as the default payment method for that Customer, using the invoice_settings.default_payment_method parameter
https://docs.stripe.com/api/customers/update#update_customer-invoice_settings-default_payment_method
This is pretty normal
But then how come I can easily collect the default payment method without a webhook when I have no trial period?
Well I would change your integration entirely
I would not separate the payment method collections from the Subscription flow
Yes you can configure your subscriptions to do that
In fact, when you create a Subscription with a free trial period, we generate a pending_setup_intent on the Subscription object so you can use that to collect payment method information
https://docs.stripe.com/api/subscriptions/object#subscription_object-pending_setup_intent
Okay, I'm confused
so you're saying that when I create a subscription, I can do like this const setupIntent = await stripe.setupIntents.create({ customer: stripeCustomerId, }); const subscription = await stripe.subscriptions.create({ customer: stripeCustomerId, items: [{ price_data: { currency: currency.toLowerCase(), product: product.id, unit_amount: Math.round(totalPrice * 100), recurring: { interval: 'year' }, } }], payment_behavior: 'default_incomplete', pending_setup_intent: setup_intent, expand: ['latest_invoice.payment_intent'], metadata: metadata, trial_period_days: 3 });
No
Actually, since you are setting trial_period_days it doesn't make sense to expand the latest_invoice.payment_intent
There isn't a payment intent
I am saying that the sub object you get back from your request will include a property called pending_setup_intent and you can use this instead of creating your own Setup Intent to collect the payment method information
You mean like this const subscription = await stripe.subscriptions.create({ customer: stripeCustomerId, items: [{ }], payment_behavior: 'default_incomplete', metadata: metadata, trial_period_days: 3 }); const setupIntent = subscription.pending_setup_intent;?
Yes
And you use the client Secret when you confirmCardSetup
Honestly I recommend coding your way through our canonical Subscription integration here
https://docs.stripe.com/billing/subscriptions/build-subscriptions?platform=web&ui=elements
Verifying that it works, and then tweaking it for your purposes
I think you would want to create your Subscription with payment_settings.save_default_payment_method set to on_subscription
https://docs.stripe.com/api/subscriptions/object#subscription_object-payment_settings-save_default_payment_method
indeed that makes more sense
Okay yeah, it's a bit more streamlined
Basically, according to this example I ought to use 'on_subscription', return the client secret that now comes from the subscription itself & confirm the payment on the client
1 sec I'll try right now
In the docs, they do `const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{
price: priceId,
}],
payment_behavior: 'default_incomplete',
payment_settings: { save_default_payment_method: 'on_subscription' },
expand: ['latest_invoice.payment_intent'],
});
res.send({
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client_secret,
});`. I do exactly like that but add the trial period param & I get no payment_intent .....
okay, so what do I do?
You need to use the Setup Intent
okay so I use subscription.setup_intent
pending_setup_intent
alright, 1 sec please
Okay, it also fails to collect it after the trial period
this is what I have rn `const subscription = await stripe.subscriptions.create({
customer: stripeCustomerId,
items: [{
price_data: {
}],
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method : 'on_subscription'
},
expand: ['latest_invoice.payment_intent'],
metadata: metadata,
trial_period_days: 3
});
const setupIntent = subscription.pending_setup_intent;
const clientSecret = subscription.client_secret || null;
res.send({
subscriptionId: subscription.id,
clientSecret: clientSecret,
});`
and on the client const { error } = await stripe.confirmPayment(clientSecret, { payment_method: { card: cardElement, billing_details: { email: billingEmail, }, }, });
Can you share a request ID for the Subscription creation?
hello
Hi, stepping in and catching up.
basically you can just read my last code blocks comments
the payment fails after the trial period
Why do you think it would succeed? I do not see any default payment methods set on that customer, cus_Ql5sthORenOAuX: https://docs.stripe.com/api/customers/object#customer_object-invoice_settings-default_payment_method. You need to collect the payment method details.
because the exact same setup succeeds on a regular sub with no trial
okay, and so that implies something like await stripe.customers.update(customerId, { invoice_settings: { default_payment_method: paymentMethodId, }, });. but where do I get the paymentMrthodId?
You would need to collect that using the subscription.pending_setup_intent.client_secret . You can also use the Cucstomer Portal, https://docs.stripe.com/payments/checkout/free-trials#customer-portal to collect payment details from the customer.
On your code, you would need to add another logic to handle subscription on trial
these subscriptions return a pending setup intent, and you would need to use that to collect the payment method on the client side
so const setupIntent = subscription.pending_setup_intent; and const clientSecret = subscription.client_secret; . now what exactly do I need to do on the client?
This whole flow is just so damn blurry to me. I get that I need to associate a default payment with the customer, but not how
can you tell me step by step what I have to do: ex. 1. create customer 2.create subscription 3.get the pending payment intent from the subscription etc. ?
After you create the subscription with trials, if you want to collect payment methods then, you can use pending setup intent: https://dashboard.stripe.com/test/logs/req_xlP0bGzgTalMMv and use that client secret to collect the payment details on the client side. That piece would work similarly to how you collect the payment method details that you do not have trials.
Sign in to the Stripe Dashboard to manage business payments and operations in your account. Manage payments and refunds, respond to disputes and more.
but I am already doing that and it doesnt work
You would change this step: https://docs.stripe.com/billing/subscriptions/build-subscriptions?platform=web&ui=elements#create-subscription
And the client secret would be under pending setup intent
yes, instead ov clientSecret: subscription.latest_invoice.payment_intent.client_secret, , I get the client secret vrom the pending intent
and then I do const { error } = await stripe.confirmPayment(clientSecret, { payment_method: { card: cardElement, billing_details: { email: billingEmail, }, }, }); on the client
yes
I was doing the exact same thing here....
Can you share the request where you confirmed it? Here's how you can find a request ID: https://support.stripe.com/questions/finding-the-id-for-an-api-request
what event to look out for?
I need the request id for when you create teh subscription that attempted this function confirm on the subscription that was on trial and you tried using the client secret from the pending intent.
or what endpoint
just so you know, this is what I see
How am I supposed to know which one is the confirmPayment
Can you share a subscription id where you attempted to use the pending intent's client secret?
it's sub_1PtaBuJ4ILijjURSIxu3vYGm
Looking ..
Alright, thanks
any updates? @cinder rover
Look, I know you're busy an have other chats too, but I've been here for 2 HOURS, running in circles
I'm still looking, and trying to reproduce the issue on my end.
It looks like you're using Test Clocks here as well, can you share the exact steps you're taking here so I can test fully?
I just set the test clock 3 hours after the subscription trial end date, as your colleague suggested...
After you rencer the Payment Element to collect the payment details, are you passing test card details and clicking on 'Pay'? Are you saying that after you add those details and click on 'Pay' nothing happens?
after that I am being redirected to the stated redirect URl
Url
that works as expected
What does that mean? You're rending the UI successfully using the pending setup intent's client secret?
Hey again ๐ catching up on what you've been chatting with my teammates, just a bit more for me to review.
Yes....
Alright, let me try to lay this out.
The flow:
- Create a Customer
- Create a Subscription (with a trial period)
- Use the client secret from
pending_setup_intentthat creates to render your payment form and confirm the Setup Intent
hi!
The key things that need to change based on what I'm seeing are:
- adjust the
expandline when creating the Subscription so that you also expandpending_setup_intent(where you're currently already expandinglatest_invoice.payment_intent)
That way you'll get the Setup Intent's client secret directly.
Now let me check your frontend code again.
Looks like you already adjusted the frontend code to use confirmCardSetup
I just keep changing these, right now I have confirmPayment
so you're saying to expand like this expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],?
That won't work with a Setup Intent. confirmPayment only accepts Payment Intent client secrets:
https://docs.stripe.com/js/payment_intents/confirm_payment
You would use confirmSetup if you're using the Payment Element, but since you're using the Card Element you'd use confirmCardSetup.
Yup.
One clarifying question, are you trying to build a flow that only handles Subscriptions with trials, or that can handle Subscriptions that don't start with a trial period as well?
okay, so now I have const { error } = await stripe.confirmCardSetup(clientSecret, { payment_method: { card: cardElement, billing_details: { email: billingEmail, }, }, }); then
at some point i will try to A/B test these, so will probably need one without trial as well
Conversely, what method do I have to use here when it comes to the Express Checkout Component (Apple Pay, GPay)?
This is what I currently use const { error } = await stripe.confirmSetup({ elements, clientSecret, confirmParams: { return_url: returnUrl }, });
Gotcha, when you do that you may want to add some logic to your flow that looks at the client secret value provided, or pass enough information to your frontend so that it knows whether it's handling a Setup Intent or Payment Intent. You can look at the prefix on the client secret to see what it's for. Setup Intent ones will start with seti_ and Payment Intent ones start with pi_
You will want to use confirmSetup
Alright, great
testing now
With the ECE, I get :{ "type": "invalid_request_error", "message": "Payment details were collected through Stripe Elements using automatic payment methods and cannot be confirmed with a Setup Intent configured with payment_method_types.", "request_log_url": "https://dashboard.stripe.com/test/logs/req_3X3GUjAPhHjaIu?t=1725048505", "setup_intent": { "id": "seti_1Ptb7MJ4ILijjURSICZzJt9l", "object": "setup_intent", "automatic_payment_methods": null, "cancellation_reason": null, "client_secret": "seti_1Ptb7MJ4ILijjURSICZzJt9l_secret_Ql7MJDCvPsvkiFE6uaKmn5tedHiXS25", "created": 1725048504, "description": null, "last_setup_error": null, "livemode": false, "next_action": null, "payment_method": null, "payment_method_configuration_details": null, "payment_method_types": [ "card", "link", "paypal" ], "status": "requires_payment_method", "usage": "off_session" }, "shouldRetry": false }
That error indicates that you used conflicting parameters when creating the Setup Intent (so in this case when creating the Subscription) versus how Elements was initialized.
Can you share your ECE related frontend code again? I know it's in here somewhere, but the thread is getting pretty lengthy.
Yes
``
And `export function InlineExecutiveCheckout({
props
}) {
const options = {
mode: 'setup', // Use setup mode to indicate no immediate charge
amount: 0, // Set the initial amount to 0 for the free trial
currency,
appearance: {
variables: {
borderRadius: '36px',
fontSizeBase: '16px', // Adjusted appearance to make trial info clearer
}
},
setupFutureUsage: "off_session", // Prepare for future payment
};
return (
<Elements stripe={stripePromise} options={options}>
<ElementsConsumer>
{({ stripe }) => {
if (!stripe) {
return <Spinner />;
};
return (
<>
<ExpressCheckout
{...props}
/>
</>
);
}}
</ElementsConsumer>
</Elements>
);
};`
Huh, weird, you aren't suppressing automatic payment methods though
yeah
Going to do some testing on my end
Ah, think I see the cause. Can you try swapping mode from setup to subscription?
For the Express Checkout Element, you're essentially trying to build this flow but using ECE instead of the Payment Element:
https://docs.stripe.com/payments/accept-a-payment-deferred?platform=web&type=subscription
It's for if you were using Setup Intents directly, but here you're relying on the Setup Intent being created by a Subscription instead. (Which is the better path so the Setup Intent pulls in the payment method types for your Subscriptions, rather than running the risk of pulling in types that you don't have enabled for Subscription payments)
OMG!
You're the MVP Toby
I would have been here for God knows how many hours staring at this chat
if it werent for you
finally works!
Woohoo, so glad to hear that did the trick!
Thanks a lot!
Any time!
Thank you, hope you do the same!