Skip to main content

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, you should send a request from your server to Rootline's Payments API to create a payment and retrieve the payment_session_secret. 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 sends a POST /payments to create the payment and receives 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 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.

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

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. Create payment on your backend
const res = await fetch("/create-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
selected_payment_method: selectedPaymentMethod,
amount: "10.00",
currency: "EUR"
}),
});

if (!res.ok) {
console.error("Failed to create 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. Create payment on your backend
const res = await fetch("/create-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
selected_payment_method: selectedPaymentMethod,
amount: "10.00",
currency: "EUR"
}),
});

if (!res.ok) {
console.error("Failed to create 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.

POST /payments
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. Send to your backend to create a payment
const res = await fetch("/create-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
selected_payment_method: selectedPaymentMethod,
amount: "10.00",
currency: "EUR"
}),
});

if (!res.ok) {
console.error("Failed to create 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 create payment and confirm
async function createPaymentAndConfirm(selectedPaymentMethod) {
const res = await fetch("/create-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
selected_payment_method: selectedPaymentMethod,
amount: "10.00",
currency: "EUR"
}),
});

if (!res.ok) {
throw new Error("Failed to create 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 createPaymentAndConfirm(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 createPaymentAndConfirm(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>