
How to update payment intent amounts in Stripe when the cart changes after the card is entered
The card is in, then the cart moves: an extra item, a coupon, a new shipping quote. Which call still works depends on one field, and it is not the amount.
Can you update payment intent amounts after the card is entered?#
You can update payment intent amounts with one call until confirmation; after authorization Stripe documents three routes: capture less, increment on eligible cards, or charge the difference on a new PaymentIntent.
Stripe's PaymentIntents overview, read on 4 October 2026, states the first case. In its words, "If the amount changes, you can update its amount". Then the hold guide covers the second: "A partial capture automatically releases the remaining amount". Finally, the increment reference covers the third. It needs the status requires_capture.
So the first question is not the new total. Instead, ask what status the intent is in now.
// Read the status before you touch the amount.
const pi = await fetch(`https://api.stripe.com/v1/payment_intents/${id}`, {
headers: { Authorization: "Basic " + btoa(process.env.STRIPE_SECRET_KEY + ":") },
}).then((r) => r.json());
console.log(pi.status, pi.amount, pi.amount_capturable);What does a mismatched hold cost the checkout?#
An unreconciled hold either releases money the order needed or expires: Stripe's capture reference (accessed 4 October 2026) cancels uncaptured PaymentIntents 7 days after creation by default. In its words, "Uncaptured PaymentIntents are cancelled a set number of days (7 by default) after their creation."
So a hold that drifts from the final cart ends in one of three ways. The order is cancelled, the customer sees a second charge, or you send a refund. Only the last two keep the sale.
Stripe's hold guide, read on 4 October 2026, gives about a week for online cards. Some Visa payments you start yourself get less.
Show data table
| Item | Value |
|---|---|
| Visa, customer-initiated | 7 |
| Visa, merchant-initiated | 5 |
| Mastercard | 7 |
| American Express | 7 |
| Discover | 7 |
Online card holds last about a week, and some Visa payments you start yourself get less.
Then, when the hold runs out, the guide says "the funds are released and the payment status changes to canceled."
What can you change at each PaymentIntent status?#
Stripe documents a plain amount update before payment; once the status is requires_capture, the documented routes are amount_to_capture to lower it and increment_authorization to raise it.
Stripe's update reference says the call "Updates properties on a PaymentIntent object without confirming". In our October 2026 test-mode run, an update on a held intent failed with payment_intent_unexpected_state. Its message named the three statuses that take it: requires_payment_method, requires_confirmation and requires_action.
Stripe's own pages, read on 4 October 2026, fill in the rest. For example, the lifecycle page lets you cancel "before it reaches a processing or succeeded state". Then the capture reference captures only in requires_capture. Finally, the increment reference adds: "After it's captured, a PaymentIntent can no longer be incremented."
| Status | Amount up | Amount down | Cancel | Recapture |
|---|---|---|---|---|
| requires_payment_method | update | update | yes | no hold yet |
| requires_confirmation | update | update | yes | no hold yet |
| requires_action | update, then confirm again | update, then confirm again | yes | no hold yet |
| processing | no | no | only some bank debits | no |
| requires_capture | increment_authorization if eligible, or overcapture | amount_to_capture at capture | yes, releases the hold | one capture, or multicapture |
| succeeded | new PaymentIntent | refund | no | no |
| canceled | new PaymentIntent | none needed | already canceled | no |
Source: Stripe update, capture, cancel and increment_authorization references, lifecycle page, hold guide and multicapture guide, accessed 4 October 2026; requires_action row from our October 2026 test-mode run.
How do you update payment intent amounts before confirmation?#
Before confirmation, update payment intent amounts on the server, then call elements.fetchUpdates so the Stripe Payment Element shows the total the PaymentIntent will charge.
The Payment Element is Stripe's prebuilt card form. Stripe's guide to dynamic amounts, read on 30 October 2026, gives the order. Work out the total on the server, update the intent, then "call elements.fetchUpdates to sync the Payment Element with the new amount". The same guide warns: "After confirmation, you generally can't increase the amount."
So change the amount and leave the card alone, because "updating the payment_method always requires you to confirm the PaymentIntent again", in the update reference's words. As a result, a Stripe update payment intent call sends just the new total in cents.
curl https://api.stripe.com/v1/payment_intents/pi_123 \
-u "$STRIPE_SECRET_KEY:" \
-d amount=5500// In the browser, after the server has updated the amount.
const { error } = await elements.fetchUpdates();Also, keep one intent per cart, because Stripe's overview says to create it "as soon as you know the amount". But once the card is charged, the call fails. A bug log for Drupal Commerce, a shop module, quotes Stripe's reply: "You may only update the amount of a PaymentIntent with one of the following statuses".
How do you lower the amount after authorization?#
After authorization you lower the total at capture: pass amount_to_capture below the held amount, and Stripe's hold documentation says the remainder is released automatically.
Because no call shrinks a hold in place, a coupon or a removed item waits for capture. Stripe's capture reference, read on 4 October 2026, sets the bound: the amount "must be less than or equal to the original amount". If you leave it out, Stripe takes all it holds.
curl https://api.stripe.com/v1/payment_intents/pi_123/capture \
-u "$STRIPE_SECRET_KEY:" \
-d amount_to_capture=4500How do you raise the amount with Stripe incremental authorization?#
Call increment_authorization with the new total while the PaymentIntent is in requires_capture; Stripe's documentation (accessed 4 October 2026) allows 10 attempts per PaymentIntent on eligible cards.
In the words of Stripe's page, read on 4 October 2026: "You have a maximum of 10 incremental authorization attempts per PaymentIntent". But few cards and accounts qualify. For example, Stripe's increment page lists the brands. "It's only available with Visa, Mastercard, American Express, or Discover". It also offers the feature "to users on IC+ pricing", a plan other than standard Stripe pricing. So other accounts must ask support.
Then you ask for it when you create the intent, with request_incremental_authorization set to if_available. After payment, the charge marks it available or unavailable. Only available is worth a call.
curl https://api.stripe.com/v1/payment_intents/pi_123/increment_authorization \
-u "$STRIPE_SECRET_KEY:" \
-d amount=6500Also, each step has a cap. The same page, read on 4 October 2026, sets it at "either 500 USD (or local equivalent) over, or 500% over the previously authorized amount (whichever is higher)".
Show data table
| Item | Value |
|---|---|
| previous authorization 50 USD | 500 |
| previous authorization 100 USD | 500 |
| previous authorization 200 USD | 1,000 |
| previous authorization 1000 USD | 5,000 |
One increment can add the higher of 500 USD or 500% over the previously authorized amount.
What if the card cannot be incremented?#
If the increment is unavailable or declined, the original hold stays capturable; capture it, then charge the difference on a new PaymentIntent against the saved card with off_session set.
Stripe's increment reference, read on 4 October 2026, says a failure returns card_declined. The intent "remains capturable for the previously authorized amount", so a failed step never loses the sale.
For the second charge, save the card on the first intent with setup_future_usage. Stripe's save and reuse guide, read on 30 October 2026, then says to set stored_credential_usage "on top of setting off_session to true".
curl https://api.stripe.com/v1/payment_intents \
-u "$STRIPE_SECRET_KEY:" \
-d amount=1000 -d currency=usd \
-d customer=cus_123 -d payment_method=pm_saved \
-d off_session=true -d confirm=true \
-d "payment_method_options[card][stored_credential_usage]=unscheduled"Meanwhile, overcapture is the smaller way out. Stripe's overcapture page, read on 4 October 2026, lets you take a little more than the hold, up to a brand limit. It is also an IC+ feature, asked for with request_overcapture.
Show data table
| Item | Value |
|---|---|
| Visa, all other merchant categories | 15 |
| Visa, lodging and cruise lines | 15 |
| Visa, restaurants (global) | 20 |
| Visa, restaurants (US) | 30 |
Overcapture takes only a little more than the hold, up to a brand limit.
Finally, if the customer drops the order, cancel the intent. Stripe's cancel reference says that for requires_capture, "the remaining amount_capturable is automatically refunded."
What does one cart with three changes look like in code?#
This worked example, run in Stripe test mode, creates one PaymentIntent, updates it for an added item, confirms it with manual capture, then reconciles a coupon and shipping.
The script calls the REST API with fetch, so it needs Node 18 or later and nothing else.
Say the cart holds two items for 4,000 cents, a third item takes it to 5,500, and a coupon of 1,000 lands later; these amounts are illustrative.
For test cards the script uses pm_card_visa from Stripe's testing page, and the increment page's pm_card_debit_incrementalAuthAuthorized.
// cart-three-changes.mjs Run: STRIPE_SECRET_KEY=sk_test_... node cart-three-changes.mjs
// Add STRIPE_INCREMENTS=1 only if Stripe has enabled incremental authorization on the account.
const KEY = process.env.STRIPE_SECRET_KEY;
const AUTH = "Basic " + btoa(KEY + ":"); // same as curl -u "$KEY:"
const VERSION = "2026-08-26.dahlia"; // pin it: an old account default renames statuses
const INCREMENTS = process.env.STRIPE_INCREMENTS === "1";
async function stripe(path, params = {}, idem) {
const headers = { Authorization: AUTH, "Stripe-Version": VERSION, "Content-Type": "application/x-www-form-urlencoded" };
if (idem) headers["Idempotency-Key"] = idem; // a retry returns the same object
const res = await fetch(`https://api.stripe.com/v1/${path}`, { method: "POST", headers, body: new URLSearchParams(params) });
const json = await res.json();
if (!res.ok) throw Object.assign(new Error(json.error?.message), { code: json.error?.code });
return json;
}
const log = (step, pi) => console.log(step, pi.status, pi.amount, pi.amount_capturable, pi.amount_received);
async function runCart(cartId, testCard, shipping) {
// 1. Two items: create with manual capture, save the card for a later charge.
const customer = (await stripe("customers", {}, `cart-${cartId}-customer`)).id;
let pi = await stripe("payment_intents", {
amount: "4000", currency: "usd", "payment_method_types[]": "card",
customer, setup_future_usage: "off_session", capture_method: "manual",
...(INCREMENTS && { "payment_method_options[card][request_incremental_authorization]": "if_available" }),
}, `cart-${cartId}`);
log("create", pi); // requires_payment_method
// 2. Item added before confirmation: update.
pi = await stripe(`payment_intents/${pi.id}`, { amount: "5500" });
log("update", pi); // requires_payment_method
// 3. Confirm: the card is held, not charged.
pi = await stripe(`payment_intents/${pi.id}/confirm`, { payment_method: testCard });
log("confirm", pi); // requires_capture
// 4. Coupon after authorization, shipping at fulfilment: settle at capture.
const held = pi.amount_capturable;
const total = 5500 - 1000 + shipping;
if (total <= held) {
return log("capture less", await stripe(`payment_intents/${pi.id}/capture`, { amount_to_capture: String(total) }));
}
const full = await fetch(`https://api.stripe.com/v1/payment_intents/${pi.id}?expand[]=latest_charge`,
{ headers: { Authorization: AUTH, "Stripe-Version": VERSION } }).then((r) => r.json());
const inc = full.latest_charge?.payment_method_details?.card?.incremental_authorization?.status;
console.log("increment", inc); // "available" or "unavailable"
if (inc === "available") {
try {
pi = await stripe(`payment_intents/${pi.id}/increment_authorization`, { amount: String(total) });
return log("capture", await stripe(`payment_intents/${pi.id}/capture`));
} catch (e) { console.log("increment failed:", e.code); } // the hold stays as it was
}
// 5. Fallback: capture the hold, charge the difference to the saved card.
log("capture hold", await stripe(`payment_intents/${pi.id}/capture`));
log("difference", await stripe("payment_intents", {
amount: String(total - held), currency: "usd", "payment_method_types[]": "card",
customer, payment_method: pi.payment_method, off_session: "true", confirm: "true",
}, `cart-${cartId}-difference`));
}
await runCart(`a-${Date.now()}`, "pm_card_visa", 500); // total 5,000: capture less
await runCart(`b-${Date.now()}`, "pm_card_debit_incrementalAuthAuthorized", 2000); // total 6,500: increment if enabled, else a new intentIn our October 2026 test-mode run, the first cart printed confirm requires_capture 5500, then capture less succeeded with the illustrative 5,000 received. The second cart printed increment unavailable, took the whole hold, and charged the illustrative 1,000 gap as a new intent that succeeded.
However, we never saw an increment succeed. Our test account is not on IC+ pricing. When we asked for the feature, Stripe replied "This account is not eligible for the requested card features." So that branch follows Stripe's reference, not our output.
Check the sums by hand. In this illustrative cart, a 5,500 hold minus the 1,000 coupon leaves 4,500. Add 500 of shipping and the total is 5,000, so capture takes 5,000 and frees 500. But add 2,000 and it is 6,500, which is 1,000 over the hold.
Final total
5,500 held, minus 1,000 coupon, plus 2,000 shipping = 6,500Increment, then capture
incrementThe total is 1,000 over the hold and the card reports increments as available, within one step's cap of 50,000. Rules from Stripe: place a hold, capture a PaymentIntent and incremental authorization, accessed 4 October 2026. Modelled, not measured.
| Stripe call | Amount |
|---|---|
increment_authorization, amount | 6,500 |
capture | 6,500 |
| Total | 5,500 held, minus 1,000 coupon, plus 2,000 shipping = 6,500 |
|---|---|
| Call | increment_authorization amount=6500, then capture |
| Why | The total is 1,000 over the hold and the card reports increments as available, within one step's cap of 50,000. |
Which Stripe API calls does a cart integration make, in order?#
A cart integration makes its Stripe API calls in a fixed order: create, update, confirm, increment_authorization when eligible, and capture, each returning the PaymentIntent as JSON.
Each call is a plain form post. Each reply is the same object with a new status. Build it from these pages:
- PaymentIntents overview: one intent per cart, and a key per cart so a retry is safe.
- Update a PaymentIntent: the new amount, before payment.
- Place a hold: manual capture, and capture less.
- Increment an authorization: the flag and the call.
- Capture a PaymentIntent: the capture call, and Stripe's 7-day default as of 4 October 2026.
curl https://api.stripe.com/v1/payment_intents -u "$STRIPE_SECRET_KEY:" \
-d amount=4000 -d currency=usd -d "payment_method_types[]=card" \
-d capture_method=manual \
-d "payment_method_options[card][request_incremental_authorization]=if_available"
curl https://api.stripe.com/v1/payment_intents/pi_123 -u "$STRIPE_SECRET_KEY:" -d amount=5500
curl https://api.stripe.com/v1/payment_intents/pi_123/confirm -u "$STRIPE_SECRET_KEY:" -d payment_method=pm_card_visa
curl https://api.stripe.com/v1/payment_intents/pi_123/capture -u "$STRIPE_SECRET_KEY:" -d amount_to_capture=5000{
"id": "pi_123",
"object": "payment_intent",
"amount": 5500,
"amount_capturable": 5500,
"amount_received": 0,
"capture_method": "manual",
"status": "requires_capture"
}In short, the Stripe cart API flow is these few calls, and a Stripe JSON integration reads only status and amount_capturable from each reply. For the wider pattern, see how REST and other API styles compare. If the cart code in your store needs work, our API integrations team does that work.
How do Adyen and Braintree limit a changed amount?#
Adyen treats card payments as final authorizations by default, which cannot be adjusted, and Braintree marks an authorization expired after a window that varies by card brand.
So the rule is not a Stripe quirk. Adyen's adjustment page, read on 4 October 2026, says a final hold means "it doesn't allow you to adjust the authorized amount later". Instead, you ask for a pre-auth, a hold you can change, up front, with authorisationType set to PreAuth.
Meanwhile, Braintree's status page, read on 4 October 2026, marks a payment expired once it has "spent too much time in the Authorized status".
Show data table
| Item | Value |
|---|---|
| Visa, default | 8 |
| Visa, merchant-initiated recurring or unscheduled | 3 |
| Visa, lodging, vehicle rental, cruise | 28 |
| Mastercard, preauthorization | 30 |
| Mastercard, final authorization | 7 |
Braintree marks an authorization expired after a window that varies by card brand.
When is incremental authorization the wrong tool?#
Skip incremental authorization when the card or account is not eligible, when the hold is near expiry, or when the order ships in parts; use a new PaymentIntent or multicapture instead.
First, a card or account that does not qualify. The flag comes back unavailable, so build the capture and new intent path, and skip the increment code. Second, a hold close to its end. Stripe's increment page says increments "don't extend the validity period", so capture now and charge the gap. Third, an order that ships in parts. The same page says an intent "partially captured with multicapture" can no longer be raised, so multicapture fits better there.
Also, card brands bar increments where the amount "should be known at the time of authorization", in Stripe's words. Subscriptions are the usual case.
| Case | Why the increment fails | Use instead |
|---|---|---|
| Card or account does not qualify | The flag comes back unavailable | Capture and a new intent |
| Hold close to its end | Increments don't extend the validity period | Capture now and charge the gap |
| Order ships in parts | An intent partially captured with multicapture can no longer be raised | Multicapture |
What should the cart check before every change?#
Check the status before every cart change, keep one PaymentIntent per cart, and reconcile the final total once at capture.
Before payment you update payment intent amounts; after it, you settle at capture or charge the gap.
If the real question is who owns the checkout, headless vs traditional ecommerce covers when the payment logic is yours to build. For card sales at a till, the point of sale, read cloud POS systems for retail. If you want the whole checkout built, our ecommerce development team takes that on.
Still, the Stripe pages linked above are enough to build all of it yourself.