Accept an iDEAL payment

    Learn how to accept iDEAL, a common payment method in the Netherlands.

    iDEAL is a single use payment method where customers are required to authenticate their payment. Customers pay with iDEAL by redirecting from your website, authorizing the payment, then returning to your website where you get immediate notification on whether the payment succeeded or failed.

    1 Create a PaymentIntent Server-side

    A PaymentIntent is an object that represents your intent to collect payment from a customer and tracks the lifecycle of the payment process through each stage. First, create a PaymentIntent on your server and specify the amount to collect and the eur currency (iDEAL does not support other currencies). If you already have an integration using the Payment Intents API, add ideal to the list of payment method types for your PaymentIntent.

    curl https://api.stripe.com/v1/payment_intents \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d amount=1099 \ -d currency=eur \ -d "payment_method_types[]"=ideal
    # Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' intent = Stripe::PaymentIntent.create({ amount: 1099, currency: 'eur', payment_method_types: ['ideal'], })
    # Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' stripe.PaymentIntent.create( amount=1099, currency='eur', payment_method_types=['ideal'] )
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'eur', 'payment_method_types' => ['ideal'], ]);
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("eur") .addPaymentMethodType("ideal") .build(); PaymentIntent paymentIntent = PaymentIntent.create(paymentIntentParams);
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); const paymentIntent = await stripe.paymentIntents.create({ amount: 1099, currency: 'eur', payment_method_types: ['ideal'], });
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" params := &stripe.PaymentIntentParams{ Amount: stripe.Int64(1099), Currency: stripe.String(string(stripe.CurrencyEUR)), PaymentMethodTypes: stripe.StringSlice([]string{ "ideal", }), } pi, _ := paymentintent.New(params)
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new PaymentIntentCreateOptions { Amount = 1099, Currency = "eur", PaymentMethodTypes = new List<string> { "ideal", }, }; var service = new PaymentIntentService(); var intent = service.Create(options);

    2 Collect payment method details Client-side

    You’re ready to collect payment information on the client with Stripe Elements. Elements is a set of prebuilt UI components for collecting payment details.

    A Stripe Element contains an iframe that securely sends the payment information to Stripe over a HTTPS connection. The checkout page address must also start with https:// rather than http:// for your integration to work.

    You can test your integration without using HTTPS. Enable it when you’re ready to accept live payments.

    Set up Stripe Elements

    Stripe Elements is automatically available as a feature of Stripe.js. Include the Stripe.js script on your payment page by adding it to the head of your HTML file. Always load Stripe.js directly from js.stripe.com to remain PCI compliant. Do not include the script in a bundle or host a copy of it yourself.

    <head> <title>Checkout</title> <script src="https://js.stripe.com/v3/"></script> </head>

    Create an instance of Elements with the following JavaScript on your checkout page:

    var stripe = Stripe('pk_test_TYooMQauvdEDq54NiTphI7jx'); var elements = stripe.elements();
    const stripe = Stripe('pk_test_TYooMQauvdEDq54NiTphI7jx'); const elements = stripe.elements();

    Add and configure an idealBank Element

    Elements needs a place to live in your payment form. Create empty DOM nodes (containers) with unique IDs in your payment form and then pass those IDs to Elements.

    <form id="payment-form"> <div class="form-row"> <label for="accountholder-name"> Name </label> <input id="accountholder-name" name="accountholder-name"> </div> <div class="form-row"> <!-- Using a label with a for attribute that matches the ID of the Element container enables the Element to automatically gain focus when the customer clicks on the label. --> <label for="ideal-bank-element"> iDEAL Bank </label> <div id="ideal-bank-element"> <!-- A Stripe Element will be inserted here. --> </div> </div> <button>Submit Payment</button> <!-- Used to display form errors. --> <div id="error-message" role="alert"></div> </form>
    /** * Shows how you can use CSS to style your Element's container. * These classes are added to your Stripe Element by default. * You can override these classNames by using the options passed * to the `idealBank` element. * https://stripe.com/docs/js/elements_object/create_element?type=idealBank#elements_create-options-classes */ input, .StripeElement { height: 40px; color: #32325d; background-color: white; border: 1px solid transparent; border-radius: 4px; box-shadow: 0 1px 3px 0 #e6ebf1; -webkit-transition: box-shadow 150ms ease; transition: box-shadow 150ms ease; } input { padding: 10px 12px; } input:focus, .StripeElement--focus { box-shadow: 0 1px 3px 0 #cfd7df; }

    When the form above has loaded, create an instance of an idealBank Element and mount it to the Element container created above:

    var options = { // Custom styling can be passed to options when creating an Element style: { base: { padding: '10px 12px', color: '#32325d', fontSize: '16px', '::placeholder': { color: '#aab7c4' }, }, }, }; // Create an instance of the idealBank Element var idealBank = elements.create('idealBank', options); // Add an instance of the idealBank Element into // the `ideal-bank-element` <div> idealBank.mount('#ideal-bank-element');
    const options = { // Custom styling can be passed to options when creating an Element style: { base: { padding: '10px 12px', color: '#32325d', fontSize: '16px', '::placeholder': { color: '#aab7c4' }, }, }, }; // Create an instance of the idealBank Element const idealBank = elements.create('idealBank', options); // Add an instance of the idealBank Element into // the `ideal-bank-element` <div> idealBank.mount('#ideal-bank-element');

    Elements are completely customizable. You can style Elements to match the look and feel of your site, providing a seamless checkout experience for your customers. It’s also possible to style various input states, for example when the Element has focus.

    Install @stripe/react-stripe-js and @stripe/stripe-js:

    npm install --save @stripe/react-stripe-js @stripe/stripe-js

    Add Stripe.js and Elements to your page

    To use Element components, wrap the root of your React app in an Elements provider. Call loadStripe with your publishable key and pass the returned Promise to the Elements provider.

    import React from 'react'; import ReactDOM from 'react-dom'; import {Elements} from '@stripe/react-stripe-js'; import {loadStripe} from '@stripe/stripe-js'; import CheckoutForm from './CheckoutForm'; // Make sure to call `loadStripe` outside of a component’s render to avoid // recreating the `Stripe` object on every render. const stripePromise = loadStripe("pk_test_TYooMQauvdEDq54NiTphI7jx"); function App() { return ( <Elements stripe={stripePromise}> <CheckoutForm /> </Elements> ); }; ReactDOM.render(<App />, document.getElementById('root'));

    Add and configure an IdealBankElement component

    Use the IdealBankElement to allow your customer to select their preferred bank.

    /** * Use the CSS tab above to style your Element's container. */ import React from 'react'; import {IdealBankElement} from '@stripe/react-stripe-js'; import './IdealBankSectionStyles.css' const IDEAL_ELEMENT_OPTIONS = { // Custom styling can be passed to options when creating an Element style: { base: { padding: '10px 12px', color: '#32325d', fontSize: '16px', '::placeholder': { color: '#aab7c4' }, }, }, }; function IdealBankSection() { return ( <label> iDEAL Bank <IdealBankElement options={IDEAL_ELEMENT_OPTIONS} /> </label> ); }; export default IdealBankSection;
    /** * Shows how you can use CSS to style your Element's container. * These classes are added to your Stripe Element by default. * You can override these classNames by using the options passed * to the IdealBankElement component. * https://stripe.com/docs/js/elements_object/create_element?type=idealBank#elements_create-options-classes */ input, .StripeElement { height: 40px; color: #32325d; background-color: white; border: 1px solid transparent; border-radius: 4px; box-shadow: 0 1px 3px 0 #e6ebf1; -webkit-transition: box-shadow 150ms ease; transition: box-shadow 150ms ease; } input { padding: 10px 12px; } input:focus, .StripeElement--focus { box-shadow: 0 1px 3px 0 #cfd7df; }

    Elements are completely customizable. You can style Elements to match the look and feel of your site, providing a seamless checkout experience for your customers. It’s also possible to style various input states, for example when the Element has focus.

    3 Submit the payment to Stripe Client-side

    Rather than sending the entire PaymentIntent object to the client, use its client secret from step 1. This is different from your API keys that authenticate Stripe API requests.

    The client secret should still be handled carefully because it can complete the charge. Do not log it, embed it in URLs, or expose it to anyone but the customer.

    Use stripe.confirmIdealPayment to handle the redirect away from your page and to complete the payment. Add a return_url to this function to indicate where Stripe should redirect the user after they complete the payment on their bank’s website or mobile application.

    var form = document.getElementById('payment-form'); var accountholderName = document.getElementById('accountholder-name'); form.addEventListener('submit', function(event) { event.preventDefault(); // Redirects away from the client stripe.confirmIdealPayment( '{{PAYMENT_INTENT_CLIENT_SECRET}}', { payment_method: { ideal: idealBank, billing_details: { name: accountholderName.value, }, }, return_url: 'https://your-website.com/checkout/complete', } ); });
    const form = document.getElementById('payment-form'); const accountholderName = document.getElementById('accountholder-name'); form.addEventListener('submit', (event) => { event.preventDefault(); // Redirects away from the client const {error} = await stripe.confirmIdealPayment( '{{PAYMENT_INTENT_CLIENT_SECRET}}', { payment_method: { ideal: idealBank, billing_details: { name: accountholderName.value, }, }, return_url: 'https://your-website.com/checkout/complete', } ); });

    Use stripe.confirmIdealPayment to handle the redirect away from your page and to complete the payment. Add a return_url to this function to indicate where Stripe should redirect the user after they complete the payment on their bank’s website or mobile application.

    To call stripe.confirmIdealPayment from your payment form component, use the useStripe and useElements hooks.

    If you prefer traditional class components over hooks, you can instead use an ElementsConsumer.

    import React from 'react'; import {useStripe, useElements, IdealBankElement} from '@stripe/react-stripe-js'; import IdealBankSection from './IdealBankSection'; export default function CheckoutForm() { const stripe = useStripe(); const elements = useElements(); const handleSubmit = async (event) => { // We don't want to let default form submission happen here, // which would refresh the page. event.preventDefault(); if (!stripe || !elements) { // Stripe.js has not yet loaded. // Make sure to disable form submission until Stripe.js has loaded. return; } const idealBank = elements.getElement(IdealBankElement); // For brevity, this example is using uncontrolled components for // the accountholder's name. In a real world app you will // probably want to use controlled components. // https://reactjs.org/docs/uncontrolled-components.html // https://reactjs.org/docs/forms.html#controlled-components const accountholderName = event.target['accountholder-name']; const {error} = await stripe.confirmIdealPayment('{CLIENT_SECRET}', { payment_method: { ideal: idealBank, billing_details: { name: accountholderName.value, }, }, return_url: 'https://your-website.com/checkout/complete', }); if (error) { // Show error to your customer. console.log(error.message); } // Otherwise the customer will be redirected away from your // page to complete the payment with their bank. }; return ( <form onSubmit={handleSubmit}> <div class="form-row"> <label> Name <input name="accountholder-name" placeholder="Jenny Rosen" required /> </label> </div> <div class="form-row"> <IdealBankSection /> </div> <button type="submit" disabled={!stripe}> Submit Payment </button> </form> ); }
    import React from 'react'; import {ElementsConsumer, IdealBankElement} from '@stripe/react-stripe-js'; import IdealBankSection from './IdealBankSection'; class CheckoutForm extends React.Component { handleSubmit = async (event) => { // We don't want to let default form submission happen here, // which would refresh the page. event.preventDefault(); const {stripe, elements} = this.props if (!stripe || !elements) { // Stripe.js has not yet loaded. // Make sure to disable form submission until Stripe.js has loaded. return; } const idealBank = elements.getElement(IdealBankElement); // For brevity, this example is using uncontrolled components for // the accountholder's name. In a real world app you will // probably want to use controlled components. // https://reactjs.org/docs/uncontrolled-components.html // https://reactjs.org/docs/forms.html#controlled-components const accountholderName = event.target['accountholder-name']; const {error} = await stripe.confirmIdealPayment('{CLIENT_SECRET}', { payment_method: { ideal: idealBank, billing_details: { name: accountholderName.value, }, }, return_url: 'https://your-website.com/checkout/complete', }); if (error) { // Show error to your customer. console.log(error.message); } // Otherwise the customer will be redirected away from your // page to complete the payment with their bank. }; render() { const {stripe} = this.props; return ( <form onSubmit={this.handleSubmit}> <div class="form-row"> <label> Name <input value={name} name="accountholder-name" placeholder="Jenny Rosen" required /> </label> </div> <div class="form-row"> <IdealBankSection /> </div> <button type="submit" disabled={!stripe}> Submit Payment </button> </form> ); } } export default function InjectedCheckoutForm() { return ( <ElementsConsumer> {({stripe, elements}) => ( <CheckoutForm stripe={stripe} elements={elements} /> )} </ElementsConsumer> ); }

    The return_url should correspond to a page on your website that provides the status of the payment, by verifying the status of the PaymentIntent when rendering the return page. When Stripe redirects the customer to the return_url, the following URL query parameters are provided to verify status. You may also append your own query parameters when providing the return_url. They will persist through the redirect process.

    Parameter Description
    payment_intent The unique identifier for the PaymentIntent
    payment_intent_client_secret The client secret of the PaymentIntent object

    You can find details about the bank account the customer used to complete the payment on the resulting Charge under the payment_method_details property.

    { "charges": { "data": [ { "payment_method_details": { "ideal": { "bank": "ing", "bic": "INGBNL2A", "iban_last4": "****", "verified_name": "JENNY ROSEN" }, "type": "ideal" }, "id": "src_16xhynE8WzK49JbAs9M21jaR", "object": "source",
    See all 47 lines "amount": 1099, "client_secret": "src_client_secret_UfwvW2WHpZ0s3QEn9g5x7waU", "created": 1445277809, "currency": "eur", "flow": "redirect", "livemode": true, "statement_descriptor": null, "status": "pending", "type": "ideal", "usage": "single_use" } ], "object": "list", "has_more": false, "url": "/v1/charges?payment_intent=pi_1G1sgdKi6xqXeNtkldRRE6HT" }, "payment_method_options": { "ideal": {} }, "payment_method_types": [ "ideal" ], "id": "pi_1G1sgdKi6xqXeNtkldRRE6HT", "object": "payment_intent", "amount": 1099, "client_secret": "pi_1G1sgdKi6xqXeNtkldRRE6HT_secret_h9B56ObhTN72fQiBAuzcVPb2E", "confirmation_method": "automatic", "created": 1579259303, "currency": "eur", "livemode": true, "next_action": null }

    Optional Handle post-payment events

    Stripe sends a payment_intent.succeeded event when the payment completes. Use the Dashboard, a custom webhook, or a partner solution to receive these events and run actions, like sending an order confirmation email to your customer, logging the sale in a database, or starting a shipping workflow.

    Listen for these events rather than waiting on a callback from the client. On the client, the customer could close the browser window or quit the app before the callback executes. Setting up your integration to listen for asynchronous events also makes it easier to accept more payment methods in the future. Check out our guide to payment methods to see the differences between all supported payment methods.

    Receive events and run business actions

    Manually

    Use the Stripe Dashboard to view all your Stripe payments, send email receipts, handle payouts, or retry failed payments.

    Custom code

    Build a webhook handler to listen for events and build custom asynchronous payment flows. Test and debug your webhook integration locally with the Stripe CLI.

    Prebuilt apps

    Handle common business events, like shipping and inventory management, by integrating a partner application.

    Optional Handle iDEAL Bank Element changes

    The iDEAL Bank Element outputs the customer’s selected bank as it changes. To perform additional logic with the bank value (e.g., requiring the field for form validation), you can listen to the change event:

    idealBank.on('change', function(event) { var bank = event.value; // Perform any additional logic here... });
    idealBank.on('change', function(event) { const bank = event.value; // Perform any additional logic here... });
    <IdealBankElement onChange={(event) => { const bank = event.value; // Perform any additional logic here... }}>

    The change event contains other parameters that can help to build a richer user experience. Refer to the Stripe.js reference for more detail.

    Optional Handle the iDEAL redirect manually

    We recommend relying on Stripe.js to handle iDEAL redirects and payments with confirmIdealPayment. However, you can also manually redirect your customers by:

    1. Providing the URL where your customers will be redirected after they complete their payment.
    curl https://api.stripe.com/v1/payment_intents/{{PAYMENT_INTENT_ID}}/confirm \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d return_url="https://your-website.com/checkout/complete"
    # Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' intent = Stripe::PaymentIntent.confirm( '{{PAYMENT_INTENT_ID}}', { return_url: 'https://your-website.com/checkout/complete', } )
    # Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' stripe.PaymentIntent.confirm( '{{PAYMENT_INTENT_ID}}', return_url='https://your-website.com/checkout/complete', )
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); $payment_intent = \Stripe\PaymentIntent::retrieve('{{PAYMENT_INTENT_ID}}'); $payment_intent->confirm([ 'return_url' => 'https://your-website.com/checkout/complete', ]);
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; PaymentIntent paymentIntent = PaymentIntent.retrieve("{{PAYMENT_INTENT_ID}}"); PaymentIntentConfirmParams params = PaymentIntentConfirmParams.builder() .setReturnUrl("https://your-website.com/checkout/complete") .build(); paymentIntent = paymentIntent.confirm(params);
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); const paymentIntent = await stripe.paymentIntents.confirm( '{{PAYMENT_INTENT_ID}}', { return_url: 'https://your-website.com/checkout/complete', } );
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" params := &stripe.PaymentIntentConfirmParams{ ReturnURL: stripe.String("https://your-website.com/checkout/complete"), } pi, _ := paymentintent.Confirm("{{PAYMENT_INTENT_ID}}", params)
    // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new PaymentIntentConfirmOptions { ReturnUrl = "https://your-website.com/checkout/complete", }; var service = new PaymentIntentService(); var intent = service.Confirm("{{PAYMENT_INTENT_ID}}", options);
    1. Confirming the PaymentIntent has a status of requires_action. The type for the next_action will be redirect_to_url.
    { "next_action": { "type": "redirect_to_url", "redirect_to_url": { "url": "https://hooks.stripe.com/...", "return_url": "https://your-website.com/checkout/complete" } }, "charges": { "data": [ { "payment_method_details": { "ideal": { "bank": "ing", "bic": "INGBNL2A",
    See all 53 lines "iban_last4": "****", "verified_name": "JENNY ROSEN" }, "type": "ideal" }, "id": "src_16xhynE8WzK49JbAs9M21jaR", "object": "source", "amount": 1099, "client_secret": "src_client_secret_UfwvW2WHpZ0s3QEn9g5x7waU", "created": 1445277809, "currency": "eur", "flow": "redirect", "livemode": true, "statement_descriptor": null, "status": "pending", "type": "ideal", "usage": "single_use" } ], "object": "list", "has_more": false, "url": "/v1/charges?payment_intent=pi_1G1sgdKi6xqXeNtkldRRE6HT" }, "payment_method_options": { "ideal": {} }, "payment_method_types": [ "ideal" ], "id": "pi_1G1sgdKi6xqXeNtkldRRE6HT", "object": "payment_intent", "amount": 1099, "client_secret": "pi_1G1sgdKi6xqXeNtkldRRE6HT_secret_h9B56ObhTN72fQiBAuzcVPb2E", "confirmation_method": "automatic", "created": 1579259303, "currency": "eur", "livemode": true }
    1. Redirecting the customer to the URL provided in the next_action property.
    var action = intent.next_action; if (action && action.type === 'redirect_to_url') { window.location = action.redirect_to_url.url; }
    const action = intent.next_action; if (action && action.type === 'redirect_to_url') { window.location = action.redirect_to_url.url; }

    When the customer finishes the payment process, they are sent to the return_url destination. The payment_intent and payment_intent_client_secret URL query parameters are included and you may pass through your own query parameters, as described above.

    Optional Storing customer bank preferences

    You cannot reuse iDEAL PaymentMethods or save them to customers. You will need to create a new iDEAL PaymentMethod each time your customer selects this method of payment in your checkout, using the iDEAL Bank Element. To track your customer’s bank preference, we recommend storing bank values in your own database or using the metadata field on the Customer object.

    You can prefill the iDEAL Bank Element with the customer’s bank preference when creating the Element:

    var options = { // Include the bank name along with any custom styling value: "rabobank", style: { base: { padding: '10px 12px', color: '#32325d', fontSize: '16px', '::placeholder': { color: '#aab7c4' }, }, invalid: { color: '#fa755a', }, }, } // Create an instance of the Element var idealBank = elements.create('idealBank', options); // Mount the Element idealBank.mount('#ideal-bank-element');
    const options = { // Include the bank name along with any custom styling value: "rabobank", style: { base: { padding: '10px 12px', color: '#32325d', fontSize: '16px', '::placeholder': { color: '#aab7c4' }, }, }, } // Create an instance of the Element const idealBank = elements.create('idealBank', options); // Mount the Element idealBank.mount('#ideal-bank-element');
    const IDEAL_ELEMENT_OPTIONS = { // Include the bank name along with any custom styling value: "rabobank", style: { base: { padding: '10px 12px', color: '#32325d', fontSize: '16px', '::placeholder': { color: '#aab7c4' }, }, }, } <IdealBankElement options={IDEAL_ELEMENT_OPTIONS} />

    Was this page helpful?

    Feedback about this page?

    Thank you for helping improve Stripe's documentation. If you need help or have any questions, please consider contacting support.

    On this page