Skip to content

Quick start

Build a custom payment flow with Liquido Elements

Learn how to embed a custom payment form in your website. The client-side and server-side code builds a checkout form with Elements to complete a payment using various payment methods.

Payment Flow overview

Liquido uses a PaymentOrder object to represent your business order to collect payment from a customer, tracking charge attempts and payment state changes throughout the process.

The payment flow with PaymentOrder as below:

payment-flow

Sample code preview

package com.liquido.sample.controller.PaymentController;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;

import com.liquido.Liquido;
import com.liquido.model.PaymentOrder;
import com.liquido.param.PaymentOrderCreateParams;

@RestController
public class PaymentController {

    @PostMapping("/create-payment-order")
    public ResponseEntity<Object> createPaymentOrder(){
        // This is your client id, client secret and API key.
        Liquido.clientId = "6nvu4u2fnpc7ja0r8g69fjg72k";
        Liquido.clientSecret = "14ltoodv6cr40j0498t8b66d26sqqd5td245dj6isnla2fqa0vv9"
        Liquido.apiKey = "fztXT5QuK755svjly94H6anwAYD1Ap3249jH2djc"

        PaymentOrderCreateParams params = 
            PaymentOrderCreateParams.builder()
                .amount(1000)
                .currency("BRL")
                .country("BR")
                .build();
        PaymentOrder paymentOrder = PaymentOrder.create(params);

        Map<String, String> responseBody = new HashMap<>();
        responseBody.put("orderCredentials", paymentOrder.getOrderCredentials())
        retrun responseBody;
    }

}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Accept a payment</title>
    <meta name="description" content="A demo of a payment on Liquido" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="checkout.css" />
    <script src="https://js.stripe.com/v3/"></script>
    <script src="checkout.js" defer></script>
  </head>
  <body>
    <!-- Display a payment form -->
    <form id="payment-form">
      <div id="payment-element">
        <!--Liquido.js injects the Payment Element-->
      </div>
      <button id="submit">
        <div class="spinner hidden" id="spinner"></div>
        <span id="button-text">Pay now</span>
      </button>
      <div id="payment-message" class="hidden"></div>
    </form>
  </body>
</html>
let elements;

initialize();
checkStatus();

document
  .querySelector("#payment-form")
  .addEventListener("submit", handleSubmit);

// Fetches a payment order and captures the order_credentials
async function initialize() {
  const response = await fetch("/create-payment-order", {
    method: "POST"
  });
  const { orderCredentials } = await response.json();

  const appearance = {
    theme: 'liquido',
  };
  elements = liquido.elements({ appearance, orderCredentials });

  const paymentElement = elements.create("payment");
  paymentElement.mount("#payment-element");
}

async function handleSubmit(e) {
  e.preventDefault();
  setLoading(true);

  const { error } = await liquido.confirmPayment({
    elements,
    confirmParams: {
      // Make sure to change this to your payment completion page
      return_url: "http://localhost:4242/checkout.html",
    },
  });

  // This point will only be reached if there is an immediate error when
  // confirming the payment. Otherwise, your customer will be redirected to
  // your `return_url`. For some payment methods like iDEAL, your customer will
  // be redirected to an intermediate site first to authorize the payment, then
  // redirected to the `return_url`.
  if (error.type === "card_error" || error.type === "validation_error") {
    showMessage(error.message);
  } else {
    showMessage("An unexpected error occurred.");
  }

  setLoading(false);
}

// Fetches the payment order status after payment submission
async function checkStatus() {
  const orderCredentials = new URLSearchParams(window.location.search).get(
    "order_credentials"
  );

  if (!orderCredentials) {
    return;
  }

  const { paymentOrder } = await liquido.retrievePaymentOrder(orderCredentials);

  switch (paymentOrder.status) {
    case "SETTLED":
      showMessage("Payment succeeded!");
      break;
    case "IN_PROGRESS":
      showMessage("Your payment is processing.");
      break;
    case "REJECTED":
      showMessage("Your payment was rejected, please try again.");
      break;
    default:
      showMessage("Something went wrong.");
      break;
  }
}

// ------- UI helpers -------

function showMessage(messageText) {
  const messageContainer = document.querySelector("#payment-message");

  messageContainer.classList.remove("hidden");
  messageContainer.textContent = messageText;

  setTimeout(function () {
    messageContainer.classList.add("hidden");
    messageText.textContent = "";
  }, 4000);
}

// Show a spinner on payment submission
function setLoading(isLoading) {
  if (isLoading) {
    // Disable the button and show a spinner
    document.querySelector("#submit").disabled = true;
    document.querySelector("#spinner").classList.remove("hidden");
    document.querySelector("#button-text").classList.add("hidden");
  } else {
    document.querySelector("#submit").disabled = false;
    document.querySelector("#spinner").classList.add("hidden");
    document.querySelector("#button-text").classList.remove("hidden");
  }
}
/* Variables */
* {
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  display: flex;
  justify-content: center;
  align-content: center;
  height: 100vh;
  width: 100vw;
}

form {
  width: 30vw;
  min-width: 500px;
  align-self: center;
  box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
    0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
  border-radius: 7px;
  padding: 40px;
}

.hidden {
  display: none;
}

#payment-message {
  color: rgb(105, 115, 134);
  font-size: 16px;
  line-height: 20px;
  padding-top: 12px;
  text-align: center;
}

#payment-element {
  margin-bottom: 24px;
}

/* Buttons and links */
button {
  background: #5469d4;
  font-family: Arial, sans-serif;
  color: #ffffff;
  border-radius: 4px;
  border: 0;
  padding: 12px 16px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  display: block;
  transition: all 0.2s ease;
  box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
  width: 100%;
}
button:hover {
  filter: contrast(115%);
}
button:disabled {
  opacity: 0.5;
  cursor: default;
}

/* spinner/processing state, errors */
.spinner,
.spinner:before,
.spinner:after {
  border-radius: 50%;
}
.spinner {
  color: #ffffff;
  font-size: 22px;
  text-indent: -99999px;
  margin: 0px auto;
  position: relative;
  width: 20px;
  height: 20px;
  box-shadow: inset 0 0 0 2px;
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  transform: translateZ(0);
}
.spinner:before,
.spinner:after {
  position: absolute;
  content: "";
}
.spinner:before {
  width: 10.4px;
  height: 20.4px;
  background: #5469d4;
  border-radius: 20.4px 0 0 20.4px;
  top: -0.2px;
  left: -0.2px;
  -webkit-transform-origin: 10.4px 10.2px;
  transform-origin: 10.4px 10.2px;
  -webkit-animation: loading 2s infinite ease 1.5s;
  animation: loading 2s infinite ease 1.5s;
}
.spinner:after {
  width: 10.4px;
  height: 10.2px;
  background: #5469d4;
  border-radius: 0 10.2px 10.2px 0;
  top: -0.1px;
  left: 10.2px;
  -webkit-transform-origin: 0px 10.2px;
  transform-origin: 0px 10.2px;
  -webkit-animation: loading 2s infinite ease;
  animation: loading 2s infinite ease;
}

@-webkit-keyframes loading {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@keyframes loading {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@media only screen and (max-width: 600px) {
  form {
    width: 80vw;
    min-width: initial;
  }
}

Set up the server

create a payment order

Add an endpoint on your server that creates a PaymentOrder. A PaymentOrder tracks the customer’s payment lifecycle, keeping track of any failed payment attempts and ensuring the customer is only charged once. Return the PaymentOrder’s order_credentials in the response to finish the payment on the client.

Build a checkout page on the client

Load Liquido.js

Use Liquido.js to remain PCI compliant by ensuring that payment details are sent directly to Liquido without hitting your server. Always load Liquido.js from js.liquido.com to remain compliant. Don’t include the script in a bundle or host it yourself.

Define the payment form

Add an empty placeholder div to your checkout form. Liquido inserts an iframe into this div that securely collects payment information.

Fetch a PaymentOrder

Immediately make a request to the endpoint on your server to create a new PaymentOrder as soon as your checkout page loads. The orderCredentials returned by your endpoint is used to complete the payment.

Initialize Liquido Elements

Initialize the Liquido Elements UI library with the order credentials. Elements manages the UI components you need to collect payment details.

Create the PaymentElement

Create a PaymentElement and mount it to the placeholder <div> in your payment form. This embeds an iframe with a dynamic form that displays configured payment method types available from the PaymentOrder, allowing your customer to select a payment method. The form automatically collects the associated payments details for the selected payment method type.

Complete the payment on the client

Handle the submit event

Listen to the form’s submit event to know when to confirm the payment through the Liquido API.

Complete the payment

Call confirmPayment(), passing along the PaymentElement and a return_url to indicate where Liquido should redirect the user after they complete the payment.

Handle errors

If there are any immediate errors (for example, your customer’s card is declined), Liquido.js returns an error. Show that error message to your customer so they can try again.

Show a payment status message

When Liquido redirects the customer to the return_url, the order_credentials query parameter is appended by Liquido.js. Use this to retrieve the PaymentOrder to determine what to show to your customer.

Back to top