Stripe Payment Intents with Stripe Elements
Stripe Payment Element is Stripe’s web UI for collecting payment details. In Stripe’s own words, it lets you accept more than 100 payment methods with a single integration. It is compatible with the Payment Intents API, and offers a customizable layout so the payment experience can match your storefront branding and flow.
Stripe Elements is a recommended way to integrate Stripe Payment Intents in Centra — Centra’s headless approach:
- Centra returns payment instructions with
clientSecret,publishableKey, andstripeParametersfor your Payment Element step. The selectionwidgetsStripe JSON is for Express checkout (wallets /expressCheckoutWidgets), not for a standard Payment Element–only checkout. - The frontend partner implements the checkout UI with Stripe Payment Element on the client side.
Supported APIs: Stripe Payment Intents with Stripe Elements works with the Storefront API and the Checkout API (Legacy).
Everything below is specific to Stripe Elements. If you need a side-by-side comparison with the legacy Payment Request Button path (UI model, payment methods, API shape), use the table on the Stripe Payment Intents overview.
Reference implementation in Storefront Accelerator
Centra’s Storefront Accelerator provides a ready-to-use Payment Element integration in src/features/checkout/components/Payment/StripePaymentIntentsWidget.tsx. Payment flow details (Stripe Elements) under Flow walks through the same behaviour in checkout order. For Storefront API–oriented pages, sequence, snippets, and checklist, see Storefront API examples below.
Prerequisites
- Stripe account connected in Centra (same as other Stripe plugins).
- Node requirement from the project: Node ≥ 18 for local doc tooling only; your storefront stack is independent.
- Familiarity with Stripe Payment Element and your Centra API (Checkout API or Storefront API).
- For client-side Payment Element, install Stripe.js in your storefront:
npm install @stripe/stripe-js @stripe/react-stripe-js.
Plugin configuration (Centra admin)
Enabling the integration
Centra uses Stripe Connect for this plugin: you do not need to copy API keys by hand when Connect is used — Stripe and Centra exchange what is required after you authorize the link.
- In the Centra admin, open the Stripe Payment Intents plugin and click Connect with Stripe (save the plugin first if the button is not available yet).
- You are redirected to Stripe to sign in. Use the Stripe account (merchant account) you want to attach to this Centra store.
- After you confirm, Stripe redirects you back to Centra. Fields such as the publishable key, secret key, webhook signing secret, and notification key are filled in automatically and the form hides them — do not edit or overwrite these values when Connect has populated them.
If the store still uses manual keys (no Stripe Connect), those fields stay visible in the plugin form and must be configured and rotated by you; webhooks must be registered in Stripe using the notification URL Centra shows. Prefer Connect for new configurations.
Direct capture
Standard capture method setting. Set to Yes if you don't plan to run separate capture request over an Integrations API. Note: Direct capture set to No limits payment methods availability on Stripe's side.
Minimum order value
The plugin enforces minimum order amounts per currency (same mechanism as other Centra payment plugins). If the selection total is below the threshold, the payment method should not appear as available.
Core settings for PaymentIntent creation
These two settings control how Stripe builds the PaymentIntent on the server:
Payment method configuration ID (dynamic payment methods)
- Purpose: Enables Stripe dynamic payment methods: Stripe decides which methods (cards, wallets, etc.) to offer based on your Stripe Dashboard configuration.
- When to use: When you want Payment Element to show dynamic methods from Stripe.
- Interaction with confirmation method: If this field is set, Centra does not send a legacy
confirmation_methodon the PaymentIntent (they are mutually exclusive in Stripe’s API). Saving a non-empty Payment method configuration ID clears Confirmation method in the admin form.
Confirmation method
- Automatic (Stripe Elements): Used for the headless Elements flow when you are not using a Payment method configuration ID, or when you use automatic confirmation without dynamic configuration as documented by Stripe.
- Manual (legacy Payment Request Button): Used for the legacy injectable button flow — not compatible with Payment method configuration ID (validation will fail if both are set incorrectly).
Rule of thumb:
- Dynamic payment methods → set Payment method configuration ID; leave confirmation method unset / cleared for that path.
- No dynamic configuration → set Confirmation method to automatic for Elements, unless your Stripe setup requires otherwise.
Validation in Centra requires either a Confirmation method or a Payment method configuration ID (at least one).
The configuration form defaults confirmation_method to manual (legacy Payment Request Button). For Stripe Elements you must switch to automatic, or use Payment method configuration ID (dynamic methods) — otherwise the plugin stays on the legacy confirmation model.
Other settings
- Merchant country should match your Stripe account business country.
- Restrictions by market, pricelist, country, language work like other payment plugins.
Flow
With Stripe Elements, the payment UI is yours: use Payment Element and Stripe.js against either the Storefront API or Checkout API (Legacy) — see the Supported APIs paragraph at the top of this page. The payment-instructions response may still include legacy formHtml — the backend generates it for compatibility — but do not use it for an Elements integration (see Payment instructions under What Centra returns below).
The sequence diagrams on the Payment Request Button (Legacy) page illustrate Centra’s injected script and DOM centra_checkout_* callbacks (shipping + payment). That is only the legacy Payment Request Button flow. Stripe Elements does not use those triggers — your app updates the selection with normal Centra API calls, calls paymentInstructions again when line items, shipping, addresses, country, or totals change (so Centra can sync the PaymentIntent — selection updates alone are not enough), and handles Stripe.js yourself. Use those diagrams only when integrating the legacy Payment Request Button.
Typical sequence (Stripe Elements)
Quick end-to-end outline for Payment Element against Centra — read Payment flow details for request shape, edge cases, and the reference widget.
- The shopper browses products and adds items to the Centra selection (Storefront API or Checkout API (Legacy)).
- The shopper continues to checkout, enters shipping and billing addresses, and selects a shipping option — persisted on the selection through your usual API calls.
- At the payment step, call
paymentInstructionswith Stripe Payment Intents aspaymentMethodand the full checkout payload your API requires (for example shipping address,paymentReturnPage/paymentFailedPage,separateBillingAddresswhere applicable). Centra returnsformFieldsto initialise Payment Element:clientSecret,publishableKey,stripeParameters. The response may also includeformHtml— ignore it for Elements (see Payment flow details). - Load Stripe.js and mount Payment Element with the
clientSecretfromformFields. - The shopper enters payment details in the Element (for example card data) and submits the payment.
- Call
stripe.confirmPayment()following Stripe’s Payment Element docs (for exampleredirect: 'if_required'and status checks — see Payment flow details). - The shopper may be redirected to your
return_url(for example after 3DS). Complete the order using your API’spaymentResult.
When selection, shipping option, or totals change before payment is complete, call paymentInstructions again so the PaymentIntent can be updated — Centra’s plugin reuses and synchronizes the intent with the selection when possible. Re-fetch whenever checkout context that affects the payment step changes; the storefront accelerator reference currently calls paymentInstructions once on mount only — see When the PaymentIntent appears and when to refresh.
Payment flow details (Stripe Elements)
The Storefront Accelerator follows this shape; your storefront may differ.
Request paymentInstructions (full checkout context)
Call payment instructions with a full checkout context — e.g. paymentMethod, shippingAddress, separateBillingAddress, paymentReturnPage, paymentFailedPage, and whatever else your Storefront API paymentInstructions or Checkout API contract requires — not only a minimal paymentInitiateOnly: true-style call (unless your flow intentionally uses that pattern). That lets Centra create or sync the PaymentIntent for the current selection with addresses attached.
When the PaymentIntent appears and when to refresh
In the accelerator, payment instructions runs once when the payment widget mounts (a ref avoids double submission), so the PaymentIntent exists after that response — not “lazily” only at pay click. Calling paymentInstructions again when line items, shipping, or totals change is recommended so the intent stays in sync. The reference widget does not refresh automatically today; treat that as a known gap for production if you need strict amount sync.
Validate the action and read formFields
Before using formFields, ensure:
action.__typename === 'FormPaymentAction'action.formType === 'stripe-payment-intents'
Otherwise the response is unsupported for the Payment Element UI — handle errors or a fallback flow.
Use formFields.publishableKey, formFields.clientSecret, and formFields.stripeParameters (parse JSON if needed). Do not render or rely on formHtml for Elements.
returnUrl for Stripe
Take the success / return URL for Stripe from parsed stripeParameters.returnUrl in the response, not by echoing paymentReturnPage from the request alone (the accelerator may fall back to a default success URL if returnUrl is missing).
confirmPayment, 3DS, and success handling
Use stripe.confirmPayment with redirect: 'if_required'. 3D Secure and next actions run inside Stripe.js. For Stripe Elements (this guide), Centra does not return an injected formHtml snippet that runs stripe.handleCardAction — that pattern exists for other integrations (e.g. legacy injected flows), not for Payment Element. After confirmPayment resolves, check paymentIntent.status === 'succeeded' before treating the payment as complete and navigating to your success / returnUrl behaviour.
Billing, shipping, and confirmParams
With automatic confirmation, addressAfterPayment is false on the plugin. Pass billing and shipping into confirmParams (e.g. payment_method_data.billing_details, shipping) from the Centra checkout selection, not from Stripe wallet merge.
Unavailable line items
If paymentInstructions returns userErrors (e.g. unavailable items, via helpers such as checkUnavailableItems), surface that in the UI (e.g. toast) and refresh the selection — same as elsewhere in checkout.
Keys from paymentInstructions only
Use publishableKey, clientSecret, and stripeParameters from payment instructions — not the widgets list. For widgets / Stripe Payment Intents JSON on selection (express-style flows), see Stripe Express wallets → Stripe Payment Intents widget JSON.
Client responsibilities
The Stripe Elements integration is fully headless. Centra does not return injectable checkout scripts.
The storefront owns checkout UI: selection mutations, follow-up paymentInstructions when checkout context changes, and Stripe.js / Payment Element on the client.
| Scenario | What to do |
|---|---|
| Shipping and billing address collection | Collect addresses in your checkout UI and persist them on the selection before the payment step — for example with mutation setAddress (Storefront API) or the equivalent Checkout API call. Centra does not drive this through an injected script; your storefront must update Centra so totals, shipping, and paymentInstructions reflect the real checkout context. |
| Line items, shipping, addresses, country, or anything that changes totals / payment context | Apply changes with the appropriate Storefront API mutations or Checkout API REST calls. Then call paymentInstructions again with a full checkout payload (see Payment flow details) so Centra can create or sync the PaymentIntent with Stripe. Updating selection alone does not refresh clientSecret or intent state for Payment Element — you need both steps. See Typical sequence (paragraph after step 7). |
| Completing payment | Use stripe.confirmPayment() on the client with clientSecret from payment instructions. Centra does not confirm the PaymentIntent server-side on this path. Finish with the payment result step per Payment flows when your return URL / callback requires it. |
The field mapping tables below show where common values live in Centra’s selection responses when you wire your UI to the API.
Fields from Selection-model (Checkout API — Legacy)
| Return Object field | Field inside Selection-response |
|---|---|
country | location.country |
currency | selection.currency |
currencyDenominator | selection.currencyFormat.denominator |
grandTotalPriceAsNumber | selection.totals.grandTotalPriceAsNumber |
shippingMethod | selection.shippingMethod |
shippingMethodsAvailable | shippingMethods |
Fields from Selection-model (Storefront API)
| Return Object field | Field inside Selection-response |
|---|---|
country | country |
currency | currency |
currencyDenominator | currencyFormat.denominator |
grandTotalPriceAsNumber | totals.grandTotalPriceAsNumber |
shippingMethod | shippingMethod |
shippingMethodsAvailable | shippingMethodsAvailable |
Storefront API examples
Use this section when you implement Payment Element against the Storefront API (GraphQL). It matches the Typical sequence (Stripe Elements) and Payment flow details above, with API links, copy-paste snippets, and a checklist.
For ExpressCheckoutElement and expressCheckoutWidgets, see Stripe Express wallets — different timing (paymentInitiateOnly) and UI than the Payment Element checkout below.
Checkout pages
Mirror the usual headless split:
- Success — order placed; render confirmation (often via
order/latestOrder). - Confirmation / return — Stripe may redirect here after 3DS; forward query params to
paymentResultwhere your API requires it. - Failure — clear errors and a path to retry.
Typical Storefront API operations
query selection— line items, addresses, shipping method, totals.mutation setPaymentMethod— when the shopper chooses Stripe, persist the payment method on the selection.mutation paymentInstructions— current checkout payload (e.g. shipping/billing,paymentReturnPage/paymentFailedPagewhere applicable). ExpectFormPaymentActionwithformType: 'stripe-payment-intents'— see Payment flow details.- Client:
loadStripe(publishableKey)→ Payment Element withclientSecret→stripe.confirmPayment— see Payment confirmation and 3D Secure. - When line items, shipping, or totals change, call
paymentInstructionsagain so the PaymentIntent can stay in sync. - After redirects or completion,
paymentResultand/ororder/latestOrder— see Payment flows.
Snippets
1) Parse paymentInstructions
const action = payload.paymentInstructions?.action;
if (
action?.__typename !== 'FormPaymentAction' ||
action.formType !== 'stripe-payment-intents'
) {
return;
}
const { publishableKey, clientSecret, stripeParameters } = action.formFields;
Use stripeParameters.returnUrl from the response with confirmPayment when the plugin supplies it.
2) Initialize Stripe.js and Payment Element
const stripe = await loadStripe(publishableKey);
// React: <Elements options={{ clientSecret }}> + <PaymentElement />
// Vanilla: stripe.elements({ clientSecret }).create('payment');
With automatic confirmation and addressAfterPayment: false, pass billing/shipping from Centra into confirmParams — see Billing, shipping, and confirmParams under Payment flow details.
3) confirmPayment (3DS via Stripe.js)
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
redirect: 'if_required',
confirmParams: {
return_url: /* use returnUrl from stripeParameters when provided */,
},
});
if (error) return;
if (paymentIntent?.status === 'succeeded') {
/* success route */
}
4) paymentResult (return URL)
mutation paymentResult($paymentMethodFields: Map!) {
paymentResult(paymentMethodFields: $paymentMethodFields) {
type
... on PaymentResultSuccessPayload {
order {
number
orderDate
status
totals {
type
price {
value
formattedValue
currency {
code
}
}
}
}
}
userErrors {
message
path
}
}
}
5) Success — latestOrder
query latestOrder {
order {
number
orderDate
status
totals {
type
price {
value
formattedValue
currency {
code
}
}
}
}
}
3D Secure and return URL (Storefront)
Stripe runs 3DS inside confirmPayment; you do not use Centra’s legacy formHtml confirm snippet for the main Elements path. After authentication, Stripe returns to your return_url; complete the order with paymentResult / selection queries per Payment flows. See also Payment confirmation and 3D Secure.
Operational hints (Storefront)
- Sync intent: When totals, shipping, or line items change, update selection as needed and request
paymentInstructionsagain before confirming — see Typical sequence (paragraph after step 7) and Client responsibilities. - Logging: In non-production, log sanitized
paymentInstructionsresponses, StripeerrorfromconfirmPayment, andpaymentIntent.status. - Keys: Use the
publishableKeythat matches Test mode vs live in the plugin.
Validation checklist (Storefront)
FormPaymentAction+stripe-payment-intentsparsing rejects unknown shapes safely.- Card and 3DS test flows complete;
redirect: 'if_required'behaves on your return route. - Shipping / address changes update totals and, where implemented, refresh payment instructions / intent amount.
- Success and failure pages work;
paymentResultmatches Stripe’s query params when used. userErrors(e.g. unavailable items) are handled without breaking the selection.
What Centra returns (backend contract)
1) Payment instructions (action: form)
Requests must include whatever your API requires for a valid checkout payment step — typically success/failure return URLs (e.g. Storefront: paymentReturnPage / paymentFailedPage; Checkout API may use a return_url field). Depending on API and plugin validation, a return_url (or equivalent) may be required; without the required fields, the plugin may return no instructions.
When you call payment instructions (Checkout API POST /payment or Storefront paymentInstructions mutation), Centra returns data for your client:
formFields.clientSecret— PaymentIntent client secret for Stripe.js / Payment Element.formFields.publishableKey— Stripe publishable key (pk_…).formFields.externalScript— Stripe.js URL (typicallyhttps://js.stripe.com/v3/).formFields.stripeParameters— JSON string withtotalAmount,currency,country(merchant country from plugin config, not the shopper country),returnUrl(use this with Stripe’sconfirmPayment/ redirects — it may differ from the success URL you sent in the request),countriesWithStates,captureMethod, andpaymentMethod(Centra payment method URI). Use it for display logic; do not assume the keycountryis the shopper’s country.
formHtml is still present in the API response — the backend always generates the legacy Payment Request Button HTML for compatibility. For Stripe Elements, ignore formHtml and build Payment Element yourself; the important fields are clientSecret, publishableKey, and stripeParameters.
Optional: 3D Secure preference on create
Who this is for: Merchants who integrate third-party risk or checkout software that needs to recommend how 3D Secure (3DS2) should be applied — for example requesting exemptions or preferring a frictionless flow. If you do not have such a signal from an external system, you typically do not need this field.
Centra lets merchants pass a 3DS2 preference through to Stripe when creating the PaymentIntent. If paymentInstructions (or the equivalent Checkout API request) includes payment_plugin_data.threeDSecurePreference (challenge or frictionless per your API), Centra maps that value onto Stripe’s payment_method_options.card.request_three_d_secure during PaymentIntent creation (exact mapping follows Centra’s rules for the plugin).
Some API setups require a secured internal request (e.g. shared secret) when this preference is used — threeDSecurePreference is only supported in that secure mode. Follow your API’s authentication rules for this flag.
Document the exact request shape for your API (payment_plugin_data, delivery address fields, etc.) and exact paymentInstructions / POST /payment request bodies for your stack (Checkout API vs Storefront API), and when to refresh the PaymentIntent (selection / line item changes, shipping changes).
Important: For the Stripe Elements product path, treat formHtml as optional/legacy — your primary integration is Stripe.js + Payment Element using clientSecret.
Dynamic payment methods (payment_method_configuration)
This is not an extra field in the payment instructions response — it describes how Centra creates the PaymentIntent when you set Payment method configuration ID in Plugin configuration.
When that plugin field is set:
- Centra passes
payment_method_configurationto Stripe when creating the PaymentIntent (instead of legacyconfirmation_method). - On the Centra side, the same JSON contract as in Payment instructions applies; which payment methods the shopper sees is determined entirely on the client by Payment Element and your Stripe Dashboard configuration.
Payment confirmation and 3D Secure
Some payment methods such as cards with 3DS2 or Pay by Bank require additional actions from the shopper. These flows are fully handled by Stripe’s Payment Element. Once the customer confirms the payment and stripe.confirmPayment is called, Stripe takes care of all remaining steps.
Link to internal examples for 3DS and return URL handling for your storefront implementation.
Webhooks
Configure Stripe to send payment_intent.succeeded and payment_intent.amount_capturable_updated to the plugin URL (Connect manages this for Connect-based setups). Centra validates the notification, resolves the selection via PaymentIntent metadata, and processes completion asynchronously (cron/async handler).
Testing
- Use Stripe test cards and enable Test mode in the plugin.
- Validate both card and 3DS flows and, if using dynamic payment methods, each method you expose in the Dashboard.
See also
- Stripe Payment Intents — overview
- Legacy — Payment Request Button
- Express checkout — Stripe integration (Express wallets vs this guide)
- Checkout API payment / Payment flows