Rootline offers multiple flavours of the Checkout Components. Depending on your needs, you may opt for the Standard or Deferred flow. Continue reading this page if you have read Choose your integration and decided that the Deferred option suits your needs best.
Overview
The Deferred implementation lets you change payment details before sending the payment request to Rootline's Payments API. This is useful when the payment amount can still change while your checkout page is already rendered.
The customer can still adjust the basket, add additional items or apply discounts, while selecting their payment method. Right after they hit the submit button, your frontend should ask your backend for a payment_session_secret for the current checkout amount. Your backend should reuse an existing open payment for the same checkout snapshot, and only create a new Rootline payment when the amount, currency, account, or another payment-defining field changes. After you apply the session secret to the SDK, Rootline will complete the payment and if necessary redirect the customer to 3DS.
These are the steps you need to follow:
Your frontend initializes Rootline.js with accountPublishableKey, amount, and currency;
Using the SDK, you show the relevant components (i.e., Card Form, Wallet Buttons, or Payment Method List) to the customer. Depending on the payment methods, a Card Form and/or Wallet Buttons may be shown in your checkout;
Your customer chooses a payment method, provides payment details, and upon submission, your frontend calls the Rootline.js submit function;
This returns a selectedPaymentMethod with which you can instruct your backend to create a payment through Rootline's Payments API;
Your backend gets or creates a payment for the current checkout snapshot and returns a payment_session_secret;
Your frontend calls confirmPayment with the payment_session_secret to complete the payment;
The SDK handles 3DS authentication if required and redirects the customer to your return_url.
And here is a high-level overview of those steps:
Implementation
The following sections explain each step in detail. For complete, working code you can copy into your project, see Sample implementation.
Prepare the frontend
To prepare the frontend, add rootline.js to your website. You can use the latest version from our CDN.
Make sure to always load the SDK from our servers. Do not copy the SDK to your code base.
Create the form
Now create a form and DOM container on your checkout page where you want the component, such as a Card Form, to be rendered and give it a descriptive ID.
Initialize the Rootline SDK and the components for the payment methods you want to show in your checkout using the accountPublishableKey, amount and currency. We recommend to do this after the DOM loaded.
If the amount changes while the customer is on your checkout page (e.g., they add items to their basket), call components.update() with the new amount:
components.update({amount:"15.00"});
info
The final amount charged is determined by what you sent to the Payments API when creating the payment, not the amount shown in the SDK. However, keeping the SDK amount in sync ensures the customer sees accurate information.
warning
If the amount changes, do not reuse a payment_session_secret that belongs to the previous amount. A payment session is tied to the payment created by your backend. Reuse it only while the server-side checkout snapshot is still the same.
Mount the component
The SDK provides several components you can mount depending on your needs:
Card Component — Secure input fields for card payments
You can mount multiple components on the same page. For example, you might want to show the Wallet Buttons prominently at the top and the Card Form below:
Configure payment methods (Payment Methods List only)
When using the payment-methods-list, you can control which payment methods are displayed and in what order. See Customize for details on ordering, excluding, and filtering payment methods.
Complete the payment
In the deferred flow, completing a payment requires three steps:
Call components.submit() to validate the payment details and get the selectedPaymentMethod. The selectedPaymentMethod is a string containing the payment method ID (e.g., "card", "ideal", "applepay").
Send the payment method to your backend to get or create a payment via the Rootline API
Call rootline.confirmPayment() with the paymentSessionSecret returned from your backend
How you trigger this flow depends on which payment method the customer uses.
info
You can use the selectedPaymentMethod in the call to your backend to calculate the fee for the payment.
warning
Do not create a new payment on every click. Repeated clicks, retries, or wallet sheet cancellations for the same basket should reuse the same payment and payment_session_secret. When the basket amount changes, discard the old session secret and create a new payment for the new amount.
Your backend should make the reuse decision from server-side checkout state, not from the amount sent by the browser. Store one active Rootline payment ID on your order, and lock that order while deciding whether to reuse, cancel, or replace the payment:
defget_or_create_payment_session(order_id,selected_payment_method):withdb.lock(f"checkout-payment:{order_id}",timeout=10):order=db.load_order(order_id)amount=f"{order['total']:.2f}"currency=order["currency"]iforder["payment_id"]:payment=rootline_get_payment(order["payment_id"])ifpayment["checkout_status"]=="succeeded":raiseException("Order is already paid")same_amount=(payment["amount"]["quantity"]==amountandpayment["amount"]["currency"]==currency)ifpayment["checkout_status"]=="open"andsame_amount:returnorder["payment_session_secret"]ifpayment["checkout_status"]=="open":rootline_cancel_payment(order["payment_id"])payment=rootline_create_payment(order,selected_payment_method)db.update_order_payment(order_id,payment,amount,currency)returnpayment["payment_session_secret"]
db.lock(...) prevents two submit requests for the same order from running this decision at the same time. Without it, both requests can read the same old order state, both decide that no reusable payment exists, and both create a new payment. Re-read the order inside the lock, then reuse, cancel, create, and update while still holding that lock. Implement it with your database's per-order locking primitive, such as PostgreSQL advisory locks, MySQL GET_LOCK, SQL Server sp_getapplock, or an atomic conditional update in NoSQL stores.
Card payments
For card payments, you control the flow via your own pay button:
constpayButton=document.getElementById("pay-button");payButton.addEventListener("click",asyncfunction(e){e.preventDefault();// 1. Validate and get selected payment methodconst{error,selectedPaymentMethod}=awaitcomponents.submit();if(error){console.error("Submit failed:",error.code,error.message);return;}payButton.disabled=true;// 2. Get or create a payment session on your backendconstres=awaitfetch("/payment-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({selected_payment_method:selectedPaymentMethod,order_id:"ORDER_ID_FROM_YOUR_SERVER"}),});if(!res.ok){console.error("Failed to get payment session");payButton.disabled=false;return;}// 3. Confirm with the session secretconstdata=awaitres.json();const{error:confirmError}=awaitrootline.confirmPayment({paymentSessionSecret:data.payment_session_secret,components});if(confirmError){console.error("Confirmation failed:",confirmError.code,confirmError.message);payButton.disabled=false;}});
Wallet payments (Apple Pay, Google Pay)
Wallet Buttons trigger their own click events. Listen for the click event using on(), then follow the same three-step flow:
walletButtons.on("click",async(event)=>{// event.paymentMethod is "applepay" or "googlepay"// 1. Validate and get selected payment methodconst{error,selectedPaymentMethod}=awaitcomponents.submit();if(error){console.error("Submit failed:",error.code,error.message);return;}// 2. Get or create a payment session on your backendconstres=awaitfetch("/payment-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({selected_payment_method:selectedPaymentMethod,order_id:"ORDER_ID_FROM_YOUR_SERVER"}),});if(!res.ok){console.error("Failed to get payment session");return;}// 3. Confirm with the session secretconstdata=awaitres.json();const{error:confirmError}=awaitrootline.confirmPayment({paymentSessionSecret:data.payment_session_secret,components});if(confirmError){console.error("Confirmation failed:",confirmError.code,confirmError.message);}});
The click event payload contains:
paymentMethod — The wallet clicked ("applepay" or "googlepay")
Your backend creates the payment with Rootline. The response to the POST /payments will return the payment_session_secret.
warning
Do not set payment_rails in the POST /payments request. The payment_rails field is intended for custom integrations where you build your own payment method selection without the SDK. When using Checkout Components, the SDK already knows which payment method the customer selected and will apply it during confirmPayment().
After a successful payment (or 3DS authentication), the SDK redirects the customer to your return_url. Rootline automatically replaces [PAYMENT_ID] in your return URL with the actual payment ID.
On your return page, verify the payment status by calling GET /payments/{payment_id} from your server. See Read payment statuses for details on interpreting the response.
Sample implementation
Complete, working code you can copy into your project. This example uses the Card Form to collect card details securely.
<!DOCTYPEhtml><html><head><title>Checkout</title><scriptsrc="https://js.rootline.com/js/rootline.js"></script></head><body><formid="payment-form"><divid="card-form"></div><buttonid="pay-button"type="button">Pay</button></form><script>document.addEventListener("DOMContentLoaded",()=>{// 1. Initialize the SDKconstrootline=newRootline();// 2. Create components with your publishable key and payment detailsconstcomponents=rootline.components("checkout",{accountPublishableKey:"YOUR_ACCOUNT_PUBLISHABLE_KEY",amount:"10.00",currency:"EUR"});// 3. Create and mount the card componentconstcardForm=components.create("card-form");cardForm.mount("#card-form");// 4. Handle button clickconstpayButton=document.getElementById("pay-button");payButton.addEventListener("click",asyncfunction(e){e.preventDefault();// 5. Validate and submit — returns error or selectedPaymentMethodconst{error,selectedPaymentMethod}=awaitcomponents.submit();if(error){console.error("Submit failed:",error.code,error.message);return;}// 6. Disable button to prevent duplicate submissionspayButton.disabled=true;// 7. Get or create a payment session on your backendconstres=awaitfetch("/payment-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({selected_payment_method:selectedPaymentMethod,order_id:"ORDER_ID_FROM_YOUR_SERVER"}),});if(!res.ok){console.error("Failed to get payment session");payButton.disabled=false;return;}// 8. Get the payment_session_secret from your backendconstdata=awaitres.json();// 9. Confirm the payment — the SDK handles 3DS if requiredconst{error:confirmError}=awaitrootline.confirmPayment({paymentSessionSecret:data.payment_session_secret,components});if(confirmError){console.error("Confirmation failed:",confirmError.code,confirmError.message);payButton.disabled=false;}// On success, the SDK redirects to your return_url});});</script></body></html>
Sample implementation with Wallet Buttons
This example shows how to handle both wallet payments (Apple Pay, Google Pay) and card payments on the same checkout page using the deferred flow. Wallet Buttons require event handling since they trigger their own click events.
<!DOCTYPEhtml><html><head><title>Checkout</title><scriptsrc="https://js.rootline.com/js/rootline.js"></script></head><body><formid="payment-form"><divid="wallet-buttons"></div><hr/><divid="card-form"></div><buttonid="pay-button"type="button">PaywithCard</button></form><script>document.addEventListener("DOMContentLoaded",()=>{// 1. Initialize the SDKconstrootline=newRootline();// 2. Create components with your publishable key and payment detailsconstcomponents=rootline.components("checkout",{accountPublishableKey:"YOUR_ACCOUNT_PUBLISHABLE_KEY",amount:"10.00",currency:"EUR"});// Helper function to get or create a payment session and confirmasyncfunctiongetPaymentSessionAndConfirm(selectedPaymentMethod){constres=awaitfetch("/payment-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({selected_payment_method:selectedPaymentMethod,order_id:"ORDER_ID_FROM_YOUR_SERVER"}),});if(!res.ok){thrownewError("Failed to get payment session");}constdata=awaitres.json();returnrootline.confirmPayment({paymentSessionSecret:data.payment_session_secret,components});}// 3. Create and mount the wallet buttonconstwalletButtons=components.create("wallet-buttons");walletButtons.mount("#wallet-buttons");// 4. Handle wallet button clicks (Apple Pay, Google Pay)walletButtons.on("click",async(event)=>{console.log("Wallet clicked:",event.paymentMethod);const{error,selectedPaymentMethod}=awaitcomponents.submit();if(error){console.error("Submit failed:",error.code,error.message);return;}try{const{error:confirmError}=awaitgetPaymentSessionAndConfirm(selectedPaymentMethod);if(confirmError){console.error("Confirmation failed:",confirmError.code,confirmError.message);}}catch(e){console.error(e.message);}// On success, the SDK redirects to your return_url});// 5. Create and mount the card componentconstcardForm=components.create("card-form");cardForm.mount("#card-form");// 6. Handle card payment via pay buttonconstpayButton=document.getElementById("pay-button");payButton.addEventListener("click",asyncfunction(e){e.preventDefault();const{error,selectedPaymentMethod}=awaitcomponents.submit();if(error){console.error("Submit failed:",error.code,error.message);return;}payButton.disabled=true;try{const{error:confirmError}=awaitgetPaymentSessionAndConfirm(selectedPaymentMethod);if(confirmError){console.error("Confirmation failed:",confirmError.code,confirmError.message);payButton.disabled=false;}}catch(e){console.error(e.message);payButton.disabled=false;}// On success, the SDK redirects to your return_url});});</script></body></html>
Sample implementation with Payment Methods List
This example shows how you can add the Payment Methods List to your checkout page using the deferred flow.
<!DOCTYPEhtml><html><head><title>Checkout</title><scriptsrc="https://js.rootline.com/js/rootline.js"></script></head><body><formid="payment-form"><divid="payment-methods-list"></div><buttonid="pay-button"type="button">Pay</button></form><script>document.addEventListener("DOMContentLoaded",()=>{// 1. Initialize the SDKconstrootline=newRootline();// 2. Create components with your publishable key and payment detailsconstcomponents=rootline.components("checkout",{accountPublishableKey:"YOUR_ACCOUNT_PUBLISHABLE_KEY",amount:"10.00",currency:"EUR"});// Helper function to get or create a payment session and confirmasyncfunctiongetPaymentSessionAndConfirm(selectedPaymentMethod){constres=awaitfetch("/payment-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({selected_payment_method:selectedPaymentMethod,order_id:"ORDER_ID_FROM_YOUR_SERVER"}),});if(!res.ok){thrownewError("Failed to get payment session");}constdata=awaitres.json();returnrootline.confirmPayment({paymentSessionSecret:data.payment_session_secret,components});}// 3. Create and mount the Payment Methods ListconstpaymentMethodList=components.create("payment-methods-list");paymentMethodList.mount("#payment-methods-list");// 4. Handle payment via pay buttonconstpayButton=document.getElementById("pay-button");payButton.addEventListener("click",asyncfunction(e){e.preventDefault();const{error,selectedPaymentMethod}=awaitcomponents.submit();if(error){console.error("Submit failed:",error.code,error.message);return;}payButton.disabled=true;try{const{error:confirmError}=awaitgetPaymentSessionAndConfirm(selectedPaymentMethod);if(confirmError){console.error("Confirmation failed:",confirmError.code,confirmError.message);payButton.disabled=false;}}catch(e){console.error(e.message);payButton.disabled=false;}// On success, the SDK redirects to your return_url});});</script></body></html>