Страница Checkout удобна, но иногда форма оплаты должна быть частью вашего сайта: те же шрифты и цвета, без перехода на чужой адрес. Для этого есть Payment Element — готовые защищённые поля Stripe, которые вы встраиваете в свою страницу. Подключим его к магазину: создадим на сервере PaymentIntent, отдадим браузеру его client_secret, подтвердим платёж через stripe.confirmPayment, разберёмся со статусами, 3-D Secure и ошибками карты и снова доверим итог вебхуку.
Checkout или своя форма
Checkout (урок 039) — готовая страница Stripe: проще и быстрее всего. Payment Element — форма в вашей странице: макет и текст вокруг поля ваши, а номер карты вводится в защищённом iframe Stripe и не попадает на сервер (SAQ A, урок 038). Цена свободы — код: кнопку, ошибки и возврат после 3-D Secure пишете вы.
| Способ | Где вводят карту | Кода |
|---|---|---|
Checkout (hosted_page) | страница Stripe | меньше всего |
Embedded Checkout (embedded_page) | iframe Stripe в вашей странице | мало |
Payment Element + Checkout Sessions (ui_mode: 'elements') | поля Stripe в вашем макете | средне |
| Payment Element + PaymentIntent | поля Stripe в вашем макете | больше всего |
💡 Честно о рекомендации. В документации (октябрь 2026) Stripe советует для большинства новых интеграций Checkout Sessions с Payment Element:
ui_mode: 'elements', в React —CheckoutElementsProviderиз@stripe/react-stripe-js/checkoutиcheckout.confirm. Кода меньше, налоги и скидки готовы. Мы разбираем PaymentIntent: он лежит и под Checkout, поэтому статусы,client_secretи вебхуки в обоих путях одинаковы.
PaymentIntent: «папка» одного платежа
PaymentIntent — объект Stripe, который ведёт один платёж от намерения до результата: сумма, валюта, способ оплаты, статус, ошибки. Аналогия: папка с делом. Вы заводите её, когда известна сумма, клиент кладёт в неё карту, Stripe записывает, что произошло. Не вышло — папка остаётся, и клиент пробует снова.
С папкой Stripe выдаёт client_secret — ключ именно от неё. С ним браузер подтвердит этот платёж, но не изменит сумму и не увидит чужие платежи. Это не секретный ключ sk_…, который не покидает сервер; но и client_secret видит только покупатель этого заказа: его не логируют и не кладут в URL.
Шаги 1–3: сервер создаёт PaymentIntent и возвращает браузеру только client_secret. Шаги 4–5: stripe.confirmPayment отправляет данные карты из iframe прямо в Stripe, при необходимости Stripe.js открывает окно 3-D Secure. Шаги 6–7: браузер узнаёт итог, а настоящее подтверждение приходит вебхуком.
Шаг 1. Сервер создаёт PaymentIntent
Добавим в Order поле для связи (npx prisma migrate dev --name add_payment_intent_id):
model Order {
// id, sku, quantity, total, status, stripeSessionId: as before
stripePaymentIntentId String? @unique
}Маршрут POST /orders/:id/payment-intent берёт заказ из базы, создаёт PaymentIntent и возвращает clientSecret: