Deferred flow: Collect payment details first

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:

  1. Your frontend initializes Rootline.js with accountPublishableKey, amount, and currency;
  2. 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;
  3. Your customer chooses a payment method, provides payment details, and upon submission, your frontend calls the Rootline.js submit function;
  4. This returns a selectedPaymentMethod with which you can instruct your backend to create a payment through Rootline's Payments API;
  5. Your backend gets or creates a payment for the current checkout snapshot and returns a payment_session_secret;
  6. Your frontend calls confirmPayment with the payment_session_secret to complete the payment;
  7. 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.

<script src="https://js.rootline.com/js/rootline.js"></script>
warning

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.

<form id="payment-form"> <div id="card-form"></div> <button id="pay-button" type="button">Pay</button> </form>

Initialize the Rootline SDK and components

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.

const rootline = new Rootline(); const components = rootline.components("checkout", { accountPublishableKey: "YOUR_ACCOUNT_PUBLISHABLE_KEY", amount: "10.00", currency: "EUR" });

Updating the amount

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

const cardForm = components.create("card-form"); cardForm.mount("#card-form");

Wallet Button — Apple Pay and Google Pay buttons

const walletButtons = components.create("wallet-buttons"); walletButtons.mount("#wallet-buttons");

Payment Component — A list of all available payment methods (cards, wallets, and alternative payment methods)

const paymentMethodsList = components.create("payment-methods-list"); paymentMethodsList.mount("#payment-methods-list");

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:

const walletButtons = components.create("wallet-buttons"); walletButtons.mount("#wallet-buttons"); const cardForm = components.create("card-form"); cardForm.mount("#card-form");

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:

  1. 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").
  2. Send the payment method to your backend to get or create a payment via the Rootline API
  3. 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:

def get_or_create_payment_session(order_id, selected_payment_method): with db.lock(f"checkout-payment:{order_id}", timeout=10): order = db.load_order(order_id) amount = f"{order['total']:.2f}" currency = order["currency"] if order["payment_id"]: payment = rootline_get_payment(order["payment_id"]) if payment["checkout_status"] == "succeeded": raise Exception("Order is already paid") same_amount = ( payment["amount"]["quantity"] == amount and payment["amount"]["currency"] == currency ) if payment["checkout_status"] == "open" and same_amount: return order["payment_session_secret"] if payment["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) return payment["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:

const payButton = document.getElementById("pay-button"); payButton.addEventListener("click", async function (e) { e.preventDefault(); // 1. Validate and get selected payment method const { error, selectedPaymentMethod } = await components.submit(); if (error) { console.error("Submit failed:", error.code, error.message); return; } payButton.disabled = true; // 2. Get or create a payment session on your backend const res = await fetch("/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 secret const data = await res.json(); const { error: confirmError } = await rootline.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 method const { error, selectedPaymentMethod } = await components.submit(); if (error) { console.error("Submit failed:", error.code, error.message); return; } // 2. Get or create a payment session on your backend const res = await fetch("/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 secret const data = await res.json(); const { error: confirmError } = await rootline.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")
tip

When using both Wallet Buttons and card components, implement both handlers. See Sample implementation with Wallet Buttons for a complete example.

Create a payment with Rootline (backend)

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().

curl 'https://payment-api.staging.rootline.com/v1/payments' \ --request POST \ --header 'content-type: application/json' \ --header 'x-api-key: [paste-your-api-key]' \ --header 'rootline-version: 2024-04-23' \ --data '{ "account_id": "[platform-account-id]", "reference": "your-reference", "amount": { "currency": "EUR", "quantity": "10.00" }, "create_payment_session_secret": true, "return_url": "https://your-website.com/[PAYMENT_ID]" }'

Handle the return URL

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.

<!DOCTYPE html> <html> <head> <title>Checkout</title> <script src="https://js.rootline.com/js/rootline.js"></script> </head> <body> <form id="payment-form"> <div id="card-form"></div> <button id="pay-button" type="button">Pay</button> </form> <script> document.addEventListener("DOMContentLoaded", () => { // 1. Initialize the SDK const rootline = new Rootline(); // 2. Create components with your publishable key and payment details const components = rootline.components("checkout", { accountPublishableKey: "YOUR_ACCOUNT_PUBLISHABLE_KEY", amount: "10.00", currency: "EUR" }); // 3. Create and mount the card component const cardForm = components.create("card-form"); cardForm.mount("#card-form"); // 4. Handle button click const payButton = document.getElementById("pay-button"); payButton.addEventListener("click", async function (e) { e.preventDefault(); // 5. Validate and submit — returns error or selectedPaymentMethod const { error, selectedPaymentMethod } = await components.submit(); if (error) { console.error("Submit failed:", error.code, error.message); return; } // 6. Disable button to prevent duplicate submissions payButton.disabled = true; // 7. Get or create a payment session on your backend const res = await fetch("/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 backend const data = await res.json(); // 9. Confirm the payment — the SDK handles 3DS if required const { error: confirmError } = await rootline.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.

<!DOCTYPE html> <html> <head> <title>Checkout</title> <script src="https://js.rootline.com/js/rootline.js"></script> </head> <body> <form id="payment-form"> <div id="wallet-buttons"></div> <hr /> <div id="card-form"></div> <button id="pay-button" type="button">Pay with Card</button> </form> <script> document.addEventListener("DOMContentLoaded", () => { // 1. Initialize the SDK const rootline = new Rootline(); // 2. Create components with your publishable key and payment details const components = rootline.components("checkout", { accountPublishableKey: "YOUR_ACCOUNT_PUBLISHABLE_KEY", amount: "10.00", currency: "EUR" }); // Helper function to get or create a payment session and confirm async function getPaymentSessionAndConfirm(selectedPaymentMethod) { const res = await fetch("/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) { throw new Error("Failed to get payment session"); } const data = await res.json(); return rootline.confirmPayment({ paymentSessionSecret: data.payment_session_secret, components }); } // 3. Create and mount the wallet button const walletButtons = 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 } = await components.submit(); if (error) { console.error("Submit failed:", error.code, error.message); return; } try { const { error: confirmError } = await getPaymentSessionAndConfirm(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 component const cardForm = components.create("card-form"); cardForm.mount("#card-form"); // 6. Handle card payment via pay button const payButton = document.getElementById("pay-button"); payButton.addEventListener("click", async function (e) { e.preventDefault(); const { error, selectedPaymentMethod } = await components.submit(); if (error) { console.error("Submit failed:", error.code, error.message); return; } payButton.disabled = true; try { const { error: confirmError } = await getPaymentSessionAndConfirm(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.

<!DOCTYPE html> <html> <head> <title>Checkout</title> <script src="https://js.rootline.com/js/rootline.js"></script> </head> <body> <form id="payment-form"> <div id="payment-methods-list"></div> <button id="pay-button" type="button">Pay</button> </form> <script> document.addEventListener("DOMContentLoaded", () => { // 1. Initialize the SDK const rootline = new Rootline(); // 2. Create components with your publishable key and payment details const components = rootline.components("checkout", { accountPublishableKey: "YOUR_ACCOUNT_PUBLISHABLE_KEY", amount: "10.00", currency: "EUR" }); // Helper function to get or create a payment session and confirm async function getPaymentSessionAndConfirm(selectedPaymentMethod) { const res = await fetch("/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) { throw new Error("Failed to get payment session"); } const data = await res.json(); return rootline.confirmPayment({ paymentSessionSecret: data.payment_session_secret, components }); } // 3. Create and mount the Payment Methods List const paymentMethodList = components.create("payment-methods-list"); paymentMethodList.mount("#payment-methods-list"); // 4. Handle payment via pay button const payButton = document.getElementById("pay-button"); payButton.addEventListener("click", async function (e) { e.preventDefault(); const { error, selectedPaymentMethod } = await components.submit(); if (error) { console.error("Submit failed:", error.code, error.message); return; } payButton.disabled = true; try { const { error: confirmError } = await getPaymentSessionAndConfirm(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>