Stripe PaymentIntent with confirmation method manual fails every time
Stefan Bogdanescu
Founder & Senior Architect · 2026-06-29
Stripe PaymentIntent: Why Manual Confirmation Fails and How to Fix It in Laravel
As a developer integrating payment gateways like Stripe into custom applications built with frameworks like Laravel, we often run into subtle yet frustrating API conflicts. The scenario you've encountered—where switching to manual confirmation causes an error related to using the publishable key—is a classic example of misunderstanding how Stripe manages the lifecycle of a PaymentIntent.
This post will dive deep into why this specific error occurs and provide a robust solution, ensuring your Laravel application handles payments securely and correctly.
Understanding the Conflict: Manual vs. Automatic Confirmation
The error message you are receiving is very explicit:
"This PaymentIntent cannot be confirmed using your publishable key because its
confirmation_methodis set tomanual. Please use your secret key instead, or create a PaymentIntent withconfirmation_methodset toautomatic."
This conflict stems from the fundamental difference between how Stripe handles payment flows:
- Automatic Confirmation (
automatic): In this mode, the client-side (your JavaScript) is responsible for initiating and confirming the payment using theclient_secret. Stripe handles the subsequent communication with the server to finalize the transaction status. This is the standard, modern approach for most e-commerce setups. - Manual Confirmation (
manual): When you set the confirmation method tomanual, you are telling Stripe that your server must handle the final confirmation step using the Secret Key. The publishable key (used on the frontend) is insufficient for this action because it lacks the necessary security context to finalize a payment state.
The error occurs because your frontend JavaScript, when attempting to confirm the payment after receiving the client_secret, is trying to use the publishable key in a way that Stripe rejects when the intent itself is marked as manual. The backend (where you usually manage the secret keys) must be involved for manual confirmations.
The Correct Approach: Embracing Automatic Confirmation
For most Laravel applications, especially those using the standard flow where a payment happens directly after collecting card details, the recommended approach is to use confirmation_method: 'automatic'. This simplifies your frontend logic significantly and leverages Stripe’s built-in security mechanisms.
Refactoring Your Laravel Implementation
Instead of forcing manual confirmation, let Stripe manage the process end-to-end. Your backend only needs to create the intent and pass the resulting client secret to the frontend.
Here is how you should adjust your Laravel code:
use Stripe\Stripe;
use Illuminate\Support\Facades\Config; // Useful for configuration access
// Initialize Stripe (ensure this uses your secret key)
Stripe::setApiKey(config('services.stripe.secret'));
// ... inside your controller or service method ...
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => $orderSession->order_total * 100,
'currency' => 'eur',
'description' => "Pagamento di " . price($orderSession->order_total) . "€ a " . $orderSession->user->user_name . " in data (" . now()->format('d-m-Y H:i:s') . ")",
'metadata' => [
'subtotal' => $orderSession->order_subtotal,
'user' => "{$orderSession->user_id} : {$orderSession->user->user_email}",
// ... other metadata ...
],
// *** CRITICAL CHANGE HERE ***
'confirmation_method' => 'automatic', // Use automatic for standard flows
]);
// Return the client secret to the frontend
return response()->json([
'client_secret' => $paymentIntent->client_secret,
]);
Updating Your Frontend Logic
With the automatic method, your JavaScript flow becomes cleaner and relies solely on the provided client_secret for confirmation:
cardButton.addEventListener('click', function() {
// Check validation logic here...
stripe.handleCardPayment(
clientSecret,
cardElement,
{
payment_method_data: {
billing_details: {name: cardholderName.value}
}
}
).then(function (result) {
if (result.error) {
console.log(result.error);
} else {
// Success! The payment is confirmed automatically via the client_secret interaction.
document.getElementById('myForm').submit();
}
});
});
Conclusion: Security and Simplicity in Payment Flows
The error you faced highlights a crucial distinction between server-side operations (using secret keys) and client-side interactions (using publishable keys). When dealing with Stripe, adopting the automatic confirmation method simplifies your integration, improves security by letting Stripe manage the final state transitions, and aligns perfectly with modern API design.
By ensuring your Laravel backend creates PaymentIntents with confirmation_method: 'automatic', you eliminate this conflict, resulting in a more stable and secure payment experience for your users. Always prioritize using established flows when integrating services; for further guidance on robust API integration within the Laravel ecosystem, check out resources from laravelcompany.com.