Five-step payment flow showing authorization, approval, and capture stages, drawn as a hand-sketched step-by-step card for the post.

Authorize now, capture later, while cart updates run

Managing payments in an e-commerce system requires handling the gap between authorization and capture. The pattern is simple. You authorize (hold funds), wait for approval, then capture (charge the customer). The challenge arises when the cart updates after authorization. You must update the payment intent to match the new total. We walk through that flow with working code for both frontend and backend.

Understanding the authorization and capture flow#

The flow has distinct steps. Your customer clicks "Pay." You check if a payment intent exists for this cart. If it does, update it. If not, create one. Fetch the payment intent secret. Pass it to the frontend. The frontend collects card details and authorizes. Funds get held, not charged. Now you wait. The seller approves the order. You capture the funds. If the cart changes between authorization and capture, you update the intent first.

The keyphrase here is "update before you charge." Most payment processors allow you to change the amount and metadata on an unpaid intent. Use that window to keep the payment in sync with your cart.

Frontend: collecting card details and authorizing#

Your JavaScript needs three jobs. First, fetch the payment intent secret from your backend. Second, use that secret to collect card details through the payment processor. Third, authorize the payment.

checkout.js · js
async function handlePayViaCard() {
  const response = await fetch('/api/getPaymentIntentSecret', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ cartId: cart.id }),
  });
  const { paymentIntentSecret } = await response.json();
  await handleCardDetailsSubmission(paymentIntentSecret);
}

async function handleCardDetailsSubmission(paymentIntentSecret) {
  const { cardDetails } = await collectCardDetails(paymentIntentSecret);

  const response = await fetch('/api/authorizePayment', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ paymentIntentId: cardDetails.paymentIntentId }),
  });

  const paymentResult = await response.json();
  if (paymentResult.success) {
    await waitForSellerApproval(cardDetails.paymentIntentId);
  }
}

The key pattern: if the cart changes, call getPaymentIntentSecret again. Your backend updates the existing intent to the new amount. The frontend stays simple. It calls the same endpoint whether creating or updating. For a detailed look at the server layer, read about HTTP servers in Node.js, which covers the same pattern of accepting requests and managing lifecycle.

Backend: managing payment intents and captures#

Your backend carries the real logic. When the frontend asks for a secret, check if this cart already has a payment intent. If yes, update it to the current cart total. If no, create one. Store the intent ID on the cart. Return the secret.

PaymentIntentController.php · php
$app->post('/api/getPaymentIntentSecret', function ($request, $response, $args) {
  $data = $request->getParsedBody();
  $cartId = $data['cartId'];

  $cart = getCartById($cartId);
  $paymentIntent = null;

  if ($cart['payment_intent_id']) {
    $paymentIntent = updatePaymentIntent($cart['payment_intent_id']);
  } else {
    $paymentIntent = createPaymentIntent();
    updateCart($cartId, ['payment_intent_id' => $paymentIntent->id]);
  }

  return $response->withJson(['paymentIntentSecret' => $paymentIntent->client_secret]);
});

The capture endpoint runs after seller approval. Take the payment intent ID. Call your processor to capture. Return success or error. The intent is now paid.

Keeping payment intents in sync#

Cart updates pose one risk. The customer authorizes a payment for $100. Then they add an item. The cart is now $150. You must update the payment intent before capturing. Your frontend already handles this. When updateCart runs, it calls getPaymentIntentSecret again. The backend sees the cart now has a payment intent, so it updates the intent amount. The capture will be for the correct total.

The pattern prevents silent errors. Without the update, you capture $100 and owe the customer $50. With it, the payment matches the cart.

When authorization and capture makes sense#

This pattern is most useful when you need a window between accepting payment and charging. That window might be minutes (waiting for inventory check) or hours (waiting for seller approval in a marketplace). If you charge immediately, the simpler flow of authorize-and-capture-in-one works fine.

Use the pattern when your business logic requires time between the customer's commit and the actual charge. That time lets you verify, adjust, or cancel without refunding.

Integrating with your architecture#

This flow fits into a larger system. Your backend talks to your payment processor. Your frontend talks to your backend. Your checkout page talks to your frontend code. The authorization and capture pattern sits cleanly in that stack because it works with the processor's standard APIs.

For real-time approval workflows, consider a custom backend service that watches your order status and captures automatically when conditions are met. For marketplace systems where sellers approve manually, this code gives you the structure to handle that approval step safely. Understanding whether to build on a headless platform vs a traditional system shapes how you handle this payment flow.

The security piece is critical. Never store card details. Always use the processor's SDK on the frontend. Your backend only touches payment intent IDs and secrets, never the card itself. That keeps your PCI scope minimal and your security strong.

Building checkout systems that grow#

Authorize-and-capture is the foundation for checkout systems that need flexibility. As you add features like seller approval, dynamic pricing, or inventory checks, this pattern scales cleanly. You update the intent, wait for your logic to complete, then capture.

The code above is a starting point. Your exact implementation depends on your payment processor (Stripe, PayPal, Square all have different APIs). But the pattern stays the same. Create or update the intent. Authorize. Wait. Capture.

For systems that need more complex approval workflows, the pattern handles that too. Build your approval system as a separate service. Have it update your cart status. When approval lands, call capture. The payment intent sits quietly in the meantime, holding the customer's funds without charging them.

Questions this post answers

Why authorize first instead of capturing immediately?
Authorization holds funds but does not charge the customer until capture. This pattern gives you time to verify inventory, get seller approval, and apply discounts before the final charge. You can release the authorization without charging if needed.
What happens to the payment intent if the cart changes?
You must update the payment intent to reflect the new cart total. Most payment processors allow you to update the amount and metadata on an existing payment intent. Call the update endpoint whenever cart items or prices change.
Should you use the same payment intent if a customer adds items?
Yes. Reuse the existing payment intent and update its amount. This avoids creating multiple intents for one checkout. Check if a cart has a payment_intent_id stored. If it does, update it. If not, create a new one.

Keep reading