# Stripe PaymentIntent on Next.js App Router
Never create a PaymentIntent in a client component: the secret key would ship to the browser. The route handler creates the intent with the secret key and returns only the client secret; the client component confirms the payment with Elements.
## Server: route handler
```
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_API_KEY_VALUE);
export async function POST(request: Request) {
const { amount, currency } = await request.json();
// validate amount and currency against your own pricing here,
// never trust them blindly from the client
const intent = await stripe.paymentIntents.create({
amount,
currency,
automatic_payment_methods: { enabled: true },
});
return Response.json({ 'clientSecret': intent.client_secret });
}
```
## Client: Elements plus confirm
```
'use client';
import { useEffect, useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_VALUE);
function Form() {
const stripe = useStripe();
const elements = useElements();
const pay = async () => {
const { error } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: 'https://example.com/order/complete' },
});
if (error) alert(error.message);
};
return (
[fragment]
[PaymentElement /]
[button onClick={pay} disabled={!stripe}]Pay[/button]
[/fragment]
);
}
export default function Page() {
const [secret, setSecret] = useState(null);
useEffect(() => {
fetch('/api/payment-intent', {
method: 'POST',
body: JSON.stringify({ amount: 2000, currency: 'usd' }),
}).then((r) => r.json()).then((d) => setSecret(d.clientSecret));
}, []);
if (!secret) return [p]loading[/p];
return (
[Elements stripe={stripePromise} options={{ clientSecret: secret }}]
[Form /]
[/Elements]
);
}
```
## Notes
- Amounts are in the smallest currency unit (cents for USD). Compute them server-side from your catalog, not from client input.
- `automatic_payment_methods` lets Stripe choose the right method per customer without you enumerating them.
- Handle the `return_url`: after redirect-based methods (3D Secure, iDEAL) Stripe sends the customer back there.