More Tutorials

Advanced Routing with Payment Components

Chargebee.js
Checkout
Payments

Payment Components support the Advanced Routing feature in Chargebee Billing. If you have multiple payment gateways configured, Advanced Routing allows you to direct customer payments through specific gateways based on rules defined in Billing.

Sample application

This tutorial walks you through the sample application that demonstrates the use of Advanced Routing with Chargebee.js Payment Components.

Routing variables

The rules in Advanced Routing help select the payment gateway based on payment amount, currency code, billing country, and other variables. The following table lists the routing variables and their corresponding values in Payment Components.

Routing variableHow Chargebee determines the value
Amountpayment_intent.amount set while creating or updating the payment_intent.
Currency codepayment_intent.currency_code set while creating or updating the payment_intent.
Payment methodpayment_intent.payment_method_type set automatically by Chargebee when a customer selects a payment method on the Payment Component.
Plan item price*A plan item price is an item_price with item_type set to plan. You can pass the id of the plan item price in options.context.cart.lineItems[].id when creating or updating the Payment Component.
Billing country*The value passed for options.context.billingAddress.countryCode when creating or updating the Payment Component.
Shipping country*The value passed for options.context.shippingAddress.countryCode when creating or updating the Payment Component.

Prerequisites

Before you begin, ensure you have the following:

Running the sample application locally

You can download and run the sample application locally by following the instructions in its Readme on GitHub.

Code walkthrough

This section explains how the code in the sample app works.

Load Chargebee.js (client)

Load Chargebee.js on the checkout page by adding the following script to the <head> element of the page.

<script src="https://js.chargebee.com/v2/chargebee.js"></script>

Initialize Chargebee.js (client)

Once the page loads, initialize Chargebee.js with a publishable key. This creates a Chargebee object used to create components.

const chargebee = window.Chargebee.init({
   site: env.site,
   publishableKey: env.publishableKey,
})

Request a payment intent (client)

As soon as the customer provides the necessary details, send the information (checkoutData) to your server to create a new payment_intent. This data includes everything that is required to determine the amount and currency of payment. Depending on your implementation, this data typically includes the list of item prices, quantities, and shipping and billing addresses.

const url = "https://YOUR_DOMAIN/payment-intent";
const response = await fetch(url,{
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify(checkoutData[index])
});
if (!response.ok)
    throw new Error(`Error: ${response.status}`);
paymentIntent = await response.json();

Create a payment intent (server)

At your server, use the information received from the frontend to calculate the amount to be charged, after coupons, discounts, credits, taxes, or shipping charges. Then, create a payment_intent in Billing and send it to the frontend.

app.post('/payment-intent', async (req, res) => {
    const url = `https://${env.site}.chargebee.com/api/v2/payment_intents`;
    const checkoutData = req.body;
    console.log(`POST /payment-intent/ Request:\n${JSON.stringify(checkoutData)}`);
    try {
        const result = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': 'Basic ' + btoa(`${env.apiKey}:`),
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams({
                amount: calculateAmount(checkoutData), //Implement this function.
                currency_code: getCurrencyCode(checkoutData) //Implement this function.
            })
        })
        const response = await result.json();
        console.log(`POST /payment-intent/ Response:\n${response}`);
        res.status(200);
        res.send(response.payment_intent);
    } catch (error) {
        console.log(`POST /payment-intent/ Error:\n${error}`);
        res.status(500);
        res.send(error);
    }
});

Add a div for the Payment Component (client)

Add an empty <div> element to your page as a placeholder for the Payment Component.

<div id="payment-component"></div>

Create the Payment Component (client)

First, create a Components object. Use it to create a PaymentComponent object by passing the following parameters:

  • payment_intent ID
  • options.context object, configuring the following Advanced Routing variables as needed:
    • Plan item price
    • Billing country
    • Shipping country
components = chargebee.components({});
setPaymentComponentOptions(index);
paymentComponentOptions = {
  paymentIntent: paymentIntent,
  form: {
      customer: {
        firstName: {
          required: true
        },
        lastName: "default"
      }
  },
  layout: {
      type: 'accordion',
      showRadioButtons: true,
  },
  paymentMethods: {
      sortOrder: ["card"]
  },
  context: {
      cart: {
          lineItems: [{ id: "plan-a", type: "plan" }] // Advanced Routing variable.
      },
      customer: {
          firstName: "Jane",
          lastName: "Doe",
          billingAddress: {
            firstName: "Jane",
            lastName: "Doe",
            phone: "555-123-4567",
            addressLine1: "123 Main St",
            addressLine2: "Apt 4B",
            addressLine3: "",
            city: "Springfield",
            state: "Illinois",
            stateCode: "IL",
            countryCode: "US", // Advanced Routing variable.
            zip: "62701"
          },
          shippingAddress: {
              firstName: "Jane",
              lastName: "Doe",
              phone: "555-123-4567",
              addressLine1: "123 Main St",
              addressLine2: "Apt 4B",
              addressLine3: "",
              city: "Springfield",
              state: "Illinois",
              stateCode: "IL",
              countryCode: "US", // Advanced Routing variable.
              zip: "62701"
          }
      }
  }
}

Then create the Payment Component with these options and the callbacks that handle the payment.

paymentComponent = components.create(
    'payment',
    paymentComponentOptions,
    {
        onError,
        onSuccess,
        onPaymentMethodChange,
        onButtonClick,
        onClose
    },
);

Mount the Payment Component (client)

Mount the Payment Component on the placeholder <div>.

paymentComponent.mount("#payment-component");

Chargebee then:

  • Uses the Advanced Routing variables set earlier to evaluate the Advanced Routing rules and determine the appropriate payment gateway.
  • Renders an iframe with the Payment Component inside the placeholder <div>.

(Optional) Add a Payment Button Component (client)

The Payment Button Component is a dynamic, prebuilt UI button that submits the payment form when clicked.

Add a div for the Payment Button Component (client)

Add an empty <div> element to your page as a placeholder for the Payment Button Component.

<div id="payment-button-component"></div>

Create and mount the Payment Button Component (client)

Use the Components object to create a PaymentButton object and mount it to the placeholder <div>.

const paymentButtonComponent = components.create(
    "payment-button",
    {},
    {
        onError,
        onClose,
    },
);
paymentButtonComponent.mount("#payment-button-component");

The customer can now interact with the Payment Component and use the submission button to complete the payment.

Update Advanced Routing variables

Customers may want to modify their order after the Payment Component is rendered. Before allowing changes, check how far the payment process has progressed by retrieving the payment_intent from Chargebee and checking its status.

Request payment intent retrieval (client)

const url = `https://YOUR_DOMAIN/payment-intent/${paymentIntentId}`;
const response = await fetch(url, {
    method: "GET"
});

if(!response.ok) {
    throw new Error(`Response status: ${response.status}`);
}

paymentIntent = await response.json();

Retrieve the payment intent from Chargebee (server)

app.get('/payment-intent/:paymentIntentId', async (req, res) => {
    const paymentIntentId = req.params.paymentIntentId;
    const url = `https://${env.site}.chargebee.com/api/v2/payment_intents/${paymentIntentId}`;
    try {
        const result = await fetch(url, {
            method: 'GET',
            headers: {
                'Authorization': 'Basic ' + btoa(`${env.apiKey}:`),
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        })
        const response = await result.json();
        console.log("GET /payment-intent/ \n",response);
        res.status(200);
        res.send(response.payment_intent);
    } catch (error) {
        res.status(500);
        console.log("GET /payment-intent/ error:\n ",error);
        res.send(error);
    }
});

Check the payment intent status (client)

switch(paymentIntent.status){
    case 'inited':
    case 'in_progress':
        await updatePaymentIntent(paymentIntent.id,index);
        updatePaymentComponent(index);
        break;
    case 'expired':
        //It has been 30 minutes since the `payment_intent` was created.
        //Start over with a new `payment_intent`.
        await createPaymentIntent(index);
        updatePaymentComponent(index);
        break;
    case 'authorized':
        //Caution! `payment_intent` is authorized.
        break;
    case 'consumed':
        //Caution! `payment_intent` has been consumed and the payment has been collected.
}

The rest of this section covers what to do for each status.

If payment_intent.status is inited or in_progress

If payment_intent.status is inited or in_progress, the payment has not been collected. Allow the customer to modify their order and prepare updated checkoutData to send to your server.

Send new details to your server (client)

Send the updated data to your server to update the payment_intent.

const url = `https://YOUR_DOMAIN/payment-intent/${paymentIntent.id}`;
const response = await fetch(url, {
    method: "PUT",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify(checkoutData[index])
});

if(!response.ok) {
    throw new Error(`Error: ${response.status}`);
}
paymentIntent = await response.json();

Update the payment intent with new amount and currency code (server)

On your server, use the updated frontend data to recalculate the amount and currency, then update the payment_intent in Billing. Use the Estimate APIs to calculate the amount in calculateAmount().

app.put('/payment-intent/:paymentIntentId', async (req, res) => {
    const paymentIntentId = req.params.paymentIntentId;
    const checkoutData = req.body;
    console.log(`PUT /payment-intent/ Request:\n payment_intent.id: ${paymentIntentId}Data: \n${JSON.stringify(checkoutData)}`);
    const url = `https://${env.site}.chargebee.com/api/v2/payment_intents/${paymentIntentId}`;
    try {
        const result = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': 'Basic ' + btoa(`${env.apiKey}:`),
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams({
                amount: calculateAmount(checkoutData), //Implement this function.
                currency_code: getCurrencyCode(checkoutData) //Implement this function.
            })
        })
        const response = await result.json();
        console.log(`PUT /payment-intent/ Response:\n${response}`);
        res.status(200);
        res.send(response.payment_intent);
    } catch (error) {
        res.status(500);
        console.log(`PUT /payment-intent/ Error:\n${error}`);
        res.send(error);
    }
});

Update the Payment Component (client)

Update the Payment Component, updating any Advanced Routing variables as required.

paymentComponentOptions = {
    paymentIntent: paymentIntent,
    layout: {
        type: 'tab',
        showRadioButtons: false,
    },
    paymentMethods: {
        sortOrder: ["card"]
    },
    context: {
        cart: {
            lineItems: [{ id: "plan-b", type: "plan" }], // Advanced Routing variable.
        },
        customer: {
            firstName: "Erika",
            lastName: "Mustermann",
            billingAddress: {
                "firstName": "Erika",
                "lastName": "Mustermann",
                "phone": "634-067-4573",
                "addressLine1": "Arster Hemm 59",
                "city": "Bremen",
                "stateCode": "HB",
                "countryCode": "DE", // Advanced Routing variable.
                "zip": "28279"
            },
            shippingAddress: {
                "firstName": "Erika",
                "lastName": "Mustermann",
                "phone": "634-067-4573",
                "addressLine1": "Arster Hemm 59",
                "city": "Bremen",
                "stateCode": "HB",
                "countryCode": "DE", // Advanced Routing variable.
                "zip": "28279"
              }
        }
    }
}
paymentComponent.update(paymentComponentOptions);
If payment_intent.status is expired

If payment_intent.status is expired, then it has been 30 minutes since the payment_intent was created and it cannot be updated any further. Start again with a new payment_intent.

Send new details to your server (client)

Send the updated data to your server to create a new payment_intent.

const url = "https://YOUR_DOMAIN/payment-intent";
const response = await fetch(url,{
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify(checkoutData[index])
});
if (!response.ok)
    throw new Error(`Error: ${response.status}`);
paymentIntent = await response.json();

Create a new payment intent (server)

On your server, use the new frontend data to recalculate the amount and currency, then create the payment_intent in Billing. Use the Estimate APIs to calculate the amount in calculateAmount().

app.post('/payment-intent', async (req, res) => {
    const url = `https://${env.site}.chargebee.com/api/v2/payment_intents`;
    const checkoutData = req.body;
    console.log(`POST /payment-intent/ Request:\n${JSON.stringify(checkoutData)}`);
    try {
        const result = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': 'Basic ' + btoa(`${env.apiKey}:`),
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams({
                amount: calculateAmount(checkoutData), //Implement this function.
                currency_code: getCurrencyCode(checkoutData) //Implement this function.
            })
        })
        const response = await result.json();
        console.log(`POST /payment-intent/ Response:\n${response}`);
        res.status(200);
        res.send(response.payment_intent);
    } catch (error) {
        console.log(`POST /payment-intent/ Error:\n${error}`);
        res.status(500);
        res.send(error);
    }
});

Update the Payment Component (client)

Update the Payment Component, updating any Advanced Routing variables as required.

paymentComponentOptions = {
    paymentIntent: paymentIntent,
    layout: {
        type: 'tab',
        showRadioButtons: false,
    },
    paymentMethods: {
        sortOrder: ["card"]
    },
    context: {
        cart: {
            lineItems: [{ id: "plan-b", type: "plan" }], // Advanced Routing variable.
        },
        customer: {
            firstName: "Erika",
            lastName: "Mustermann",
            billingAddress: {
                "firstName": "Erika",
                "lastName": "Mustermann",
                "phone": "634-067-4573",
                "addressLine1": "Arster Hemm 59",
                "city": "Bremen",
                "stateCode": "HB",
                "countryCode": "DE", // Advanced Routing variable.
                "zip": "28279"
            },
            shippingAddress: {
                "firstName": "Erika",
                "lastName": "Mustermann",
                "phone": "634-067-4573",
                "addressLine1": "Arster Hemm 59",
                "city": "Bremen",
                "stateCode": "HB",
                "countryCode": "DE", // Advanced Routing variable.
                "zip": "28279"
              }
        }
    }
}
paymentComponent.update(paymentComponentOptions);
If payment_intent.status is authorized

If payment_intent.status is authorized, you can no longer update it. The meaning of an authorized payment_intent depends on the payment method and gateway. It can indicate one of the following:

  • The payment amount is blocked on the customer's payment method. The customer can't use those funds unless Chargebee releases the block. The funds are collected only when you consume the payment_intent in Billing, for example, to create or update a subscription or charge. If you don't consume the payment_intent within 30 minutes of its creation, the blocked amount is automatically released.
  • The payment has been collected from the customer. However, if you don't consume the payment_intent in Billing within 30 minutes of its creation, the amount is automatically refunded to the customer.

Once you confirm that the payment_intent won't be consumed, follow the steps below to start over with a new one.

Send new details to your server (client)

Send the updated data to your server to create a new payment_intent.

const url = "https://YOUR_DOMAIN/payment-intent";
const response = await fetch(url,{
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify(checkoutData[index])
});
if (!response.ok)
    throw new Error(`Error: ${response.status}`);
paymentIntent = await response.json();

Create a new payment intent (server)

On your server, use the new frontend data to recalculate the amount and currency, then create the payment_intent in Billing. Use the Estimate APIs to calculate the amount in calculateAmount().

app.post('/payment-intent', async (req, res) => {
    const url = `https://${env.site}.chargebee.com/api/v2/payment_intents`;
    const checkoutData = req.body;
    console.log(`POST /payment-intent/ Request:\n${JSON.stringify(checkoutData)}`);
    try {
        const result = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': 'Basic ' + btoa(`${env.apiKey}:`),
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams({
                amount: calculateAmount(checkoutData), //Implement this function.
                currency_code: getCurrencyCode(checkoutData) //Implement this function.
            })
        })
        const response = await result.json();
        console.log(`POST /payment-intent/ Response:\n${response}`);
        res.status(200);
        res.send(response.payment_intent);
    } catch (error) {
        console.log(`POST /payment-intent/ Error:\n${error}`);
        res.status(500);
        res.send(error);
    }
});

Update the Payment Component (client)

Update the Payment Component, updating any Advanced Routing variables as required.

paymentComponentOptions = {
    paymentIntent: paymentIntent,
    layout: {
        type: 'tab',
        showRadioButtons: false,
    },
    paymentMethods: {
        sortOrder: ["card"]
    },
    context: {
        cart: {
            lineItems: [{ id: "plan-b", type: "plan" }], // Advanced Routing variable.
        },
        customer: {
            firstName: "Erika",
            lastName: "Mustermann",
            billingAddress: {
                "firstName": "Erika",
                "lastName": "Mustermann",
                "phone": "634-067-4573",
                "addressLine1": "Arster Hemm 59",
                "city": "Bremen",
                "stateCode": "HB",
                "countryCode": "DE", // Advanced Routing variable.
                "zip": "28279"
            },
            shippingAddress: {
                "firstName": "Erika",
                "lastName": "Mustermann",
                "phone": "634-067-4573",
                "addressLine1": "Arster Hemm 59",
                "city": "Bremen",
                "stateCode": "HB",
                "countryCode": "DE", // Advanced Routing variable.
                "zip": "28279"
              }
        }
    }
}
paymentComponent.update(paymentComponentOptions);
If payment_intent.status is consumed

When payment_intent.status is consumed, the payment has been collected from the customer and used in Billing, for example, to create or update a subscription or charge.

Inform the customer and provision the product or service. Do not start another payment session at this stage.

Was this tutorial helpful ?
Need more help?

We're always happy to help you with any questions you might have! Click here to reach out to us.