Integrating an external payment gateway with Liferay Commerce involves more than simply calling a payment API. A production-ready implementation must also handle payment configuration, secure communication, callback processing, status synchronization, and multi-channel requirements.
In this implementation, we integrated Square Payments with Liferay Commerce using a Spring Boot Client Extension.
The objective was to keep the Square-specific integration outside the Liferay runtime while allowing Liferay Commerce to continue managing the checkout and order lifecycle.
This article explains the architecture, payment flow, webhook handling, and a few of the important design decisions involved in the implementation.
Liferay 7.4 introduced a massive paradigm shift in how we extend the platform: Client Extensions. By moving custom logic out of Liferay’s core OSGi container and into decoupled, cloud-native microservices, developers gain unprecedented flexibility.
In this comprehensive guide, we will walk through exactly how we built and deployed a production-ready Square Payment Integration for Liferay Commerce using a Spring Boot 3 Microservice.
Whether you are deploying to Liferay Cloud (LXC), a custom Kubernetes cluster, or an on-premise standalone server, this guide covers every configuration step, environment variable, and troubleshooting trick you need to know.
Prerequisites
Before you begin, ensure you have the following:Â
- Liferay Environment: Liferay DXP 7.4+ with Liferay Commerce enabled.Â
- Java: Java 21+ (Required for the Spring Boot 3 microservice).Â
- Square Account: A Square Developer Account (with Sandbox credentials for QA, and Production credentials for live deployments).Â
Key Features
- Native Commerce Integration: Seamlessly appears as a standard Payment Method within Liferay Commerce Channels.Â
- Webhook Failsafe: Ensures zero dropped orders. If a user completes a payment but their browser crashes or fails to redirect back to Liferay, Square’s asynchronous webhooks are securely captured, verified via HMAC-SHA256 signatures, and processed by the connector to finalize the Liferay order.Â
- Order ERC Synchronization: Automatically binds the Liferay Order External Reference Code (ERC) to the exact Square Order ID for permanent, reliable cross-referencing.Â
- OAuth2 Secured: All internal HTTP communication from Liferay to the microservice is fortified using OAuth2 Headless Server authentication.Â
- Cloud-Native Architecture: Designed for Liferay Cloud. Connects natively to LXC Config Trees without requiring hardcoded Docker environments.Â
- Sandbox Support: Easily toggle between Square Sandbox (QA) and Square Production.Â
Why We Used a Spring Boot Client Extension
Liferay Client Extensions provide a clean way to connect Liferay with external services without deploying custom business logic directly inside the Liferay JVM.
For the Square payment integration, the Spring Boot Client Extension acts as an integration layer between Liferay Commerce and Square.
At a high level, the flow is:
The responsibilities are separated clearly.Â
Liferay Commerce manages:Â
- Commerce ordersÂ
- CheckoutÂ
- Payment method configurationÂ
- Commerce channelsÂ
- Payment lifecycleÂ
Â
The Spring Boot Client Extension manages:Â
- Receiving payment requests from LiferayÂ
- Reading Square configurationÂ
- Calling Square APIsÂ
- Returning the checkout URLÂ
- Receiving Square webhooksÂ
- Validating webhook requestsÂ
- Updating the corresponding payment stateÂ
Â
Square remains responsible for the actual payment processing.Â
Configuring Square as a Commerce Payment Method
The Square payment method is configured in Liferay Commerce and can be enabled for the required Commerce channel.
One of the important requirements was to avoid hardcoding Square credentials inside the Spring Boot application.
Instead, payment configuration can be maintained as part of the payment method settings.
Depending on the implementation, the configuration can contain values such as:Â
- Square Application IDÂ
- Square Access TokenÂ
- Square Location IDÂ
- EnvironmentÂ
- Webhook-related configurationÂ
Â
This is especially useful when the same Liferay installation contains multiple Commerce channels.Â
For example:Â
- Channel A → Square Account AÂ
- Channel B → Square Account BÂ
- Channel C → Square Account CÂ
Â
The same Spring Boot Client Extension can therefore be reused while each Commerce channel maintains its own Square configuration.Â
Payment Initiation Flow
When the customer selects Square during checkout, Liferay invokes the payment Client Extension. The Spring Boot application receives the payment request along with the information needed to initiate the transaction.
The general flow is:
The Spring Boot service prepares the Square request using information such as the order amount, currency, order reference, and channel-specific Square configuration.
Square then returns the hosted payment URL.
The Client Extension sends the required response back to Liferay, allowing the customer to continue the payment process on Square's hosted checkout page.
Why Hosted Checkout Is Useful
Using a payment page hosted by Square keeps sensitive payment details outside the Liferay application. Instead of collecting card information directly inside Liferay, the customer enters the payment information on Square's infrastructure.
This simplifies the integration and reduces the amount of sensitive card-related data handled by the Liferay application and the Client Extension.
The flow becomes:
Once the customer completes or cancels the transaction, the user can be redirected back to the appropriate Liferay page. However, that browser redirect should not be treated as the authoritative payment confirmation.
Handling Square Webhooks
The asynchronous webhook flow is one of the most important parts of the integration.
After a payment event occurs, Square sends a request to the webhook endpoint exposed by the Spring Boot Client Extension.
A webhook event can contain information such as:Â
- Square payment IDÂ
- Square order IDÂ
- Payment statusÂ
- Event typeÂ
- Merchant-related informationÂ
Â
The Client Extension uses this information to identify the corresponding Liferay Commerce transaction and update the payment status.Â
Validating That the Webhook Really Came from Square
A public webhook endpoint can technically be called by anyone.
For example, someone could send a request from Postman and manually add a header named:
x-square-hmacsha256-signature
Therefore, checking whether the header exists is not sufficient.
The signature itself must be validated.
The general validation process is:
Correlating Square Payments with Liferay Orders
Another important requirement is maintaining a reliable relationship between the Square transaction and the Liferay Commerce order.
During payment creation, the integration should maintain identifiers that make later reconciliation possible.
Typical values include:Â
- Liferay Commerce Order IDÂ
- Liferay Payment Transaction IDÂ
- Commerce Channel IDÂ
- Square Order IDÂ
- Square Payment IDÂ
Â
This correlation is particularly important when the webhook arrives because the webhook originates from Square, not from Liferay.Â
Â
The integration needs to determine which Liferay order and Commerce channel belong to the incoming Square event.Â
Â
A stable correlation mechanism also helps during troubleshooting and payment reconciliation.Â
Multi-Channel Payment Configuration
Multi-channel support adds another architectural consideration.
During the initial Liferay-to-Client-Extension request, channel-specific configuration can be available to the payment integration.
However, when Square later calls the webhook, the request originates directly from Square.
Therefore, the webhook does not automatically include the Liferay payment method configuration.
The application must use available identifiers, such as the Square transaction or Liferay order reference, to resolve the correct configuration.
This is an important consideration when different Commerce channels are connected to different Square accounts.
A conceptual mapping can look like this:
This makes webhook processing deterministic and prevents the application from using the wrong Square account configuration.Â
Payment Status Should Not Depend on Browser Redirects
After completing a payment, Square can redirect the customer back to Liferay.
That redirect is useful for the customer experience, but it should not be considered final proof that payment succeeded.
The browser is client-controlled.
Therefore:Â
- Browser Redirect = Navigation / User ExperienceÂ
- Webhook or Square API = Payment ConfirmationÂ
Â
The authoritative payment state should come from a validated server-to-server source.Â
Â
This is particularly important when updating a Liferay Commerce payment from pending to completed.Â
Duplicate Webhook Handling
Payment providers can retry webhook delivery.
As a result, the same event may reach the Client Extension more than once.
The integration should therefore be idempotent.
For example:
This prevents the same Square event from triggering duplicate payment updates.Â
 Â
Idempotency is especially important for payment integrations because network retries and temporary service failures are normal operational scenarios.Â
Error Handling and Observability
Payment integrations should also be designed for situations where external systems are temporarily unavailable.
Some scenarios that should be considered include:
- Square API timeoutÂ
- Invalid Square configurationÂ
- Customer abandons checkoutÂ
- Payment is declinedÂ
- Duplicate webhook deliveryÂ
- Invalid webhook signatureÂ
- Liferay is unavailable when a webhook arrivesÂ
- Payment succeeds but Liferay update temporarily failsÂ
Â
Logging should make it possible to trace a transaction across systems.Â
Useful identifiers include:Â
- Liferay Order IDÂ
- Channel IDÂ
- Square Order IDÂ
- Square Payment IDÂ
- Webhook Event IDÂ
- Correlation IDÂ
Â
At the same time, secrets such as Square access tokens, webhook signature keys, or authentication headers should never be written to application logs.Â
Final Architecture
Unlike legacy OSGi plugins that ran inside Liferay, this integration consists of two decoupled pieces:
- Liferay Client Extension YAML: A simple configuration file deployed to Liferay that registers the Square Payment method and sets up the secure OAuth2 routing rules.
- Spring Boot Microservice: A standalone Java application that receives the checkout payload from Liferay, communicates with the Square API, and securely pushes order updates back to Liferay Headless APIs.Â
Â
Data flows in both directions:
- Liferay -> Spring Boot: When a user clicks “Checkout”, Liferay uses an OAuth2 User Agent application to securely route the payload to the microservice.Â
- Spring Boot -> Liferay: The microservice uses an OAuth2 Headless Server application (Client ID/Secret) to authenticate back to Liferay to update the Order Status.Â
Â
The overall implementation can be summarized as follows:Â
The asynchronous payment update works in the opposite direction:
This architecture keeps Liferay focused on the commerce lifecycle while the Client Extension handles Square-specific integration logic.
Step-by-Step Deployment Guide
Step 1: Configure the Routing Dynamically
Unlike legacy configurations where URLs were hardcoded in source files, you do not need to modify the client-extension.yaml file for your specific environment (QA vs Prod).
Simply deploy the client-extension.yaml as-is. Once it deploys, Liferay generates the OAuth2 applications. You can then dynamically configure the routing directly from the Liferay Admin UI:
- Go to Control Panel → Security → OAuth2 Administration.Â
- Click Edit on the Square Payment Backend OAuth Application User Agent.Â
- Update the Website URL field to point to your microservice:Â
- For On-Premise Kubernetes: Use your internal K8s Service Name (e.g., http://nl-liferay-dxp-square-payment-api-svc:8080). This completely bypasses external Load Balancers and avoids frustrating Hairpin NAT (Loopback) connection timeouts!Â
- For External Standalone/VMs: Use your public microservice URL (e.g., https://payment-api.mycompany.com).Â
Pro-Tip: Updating the Website URL directly from the Liferay UI allows you to instantly change your routing on the fly without ever needing to redeploy the Client Extension YAML!Â
Step 2: Deploy to Liferay and Gather Credentials
- Zip up your client extension workspace (or just the YAML) and deploy it to Liferay’s deploy/ folder.
- Go to Control Panel → Security → OAuth2 Administration.
- You will see two new applications generated by Liferay: a Headless Server and a User Agent.
- Click Edit on the Headless Server application and copy the Client ID and Client Secret.
Step 3: Run the Spring Boot Microservice
Deploy your Spring Boot .jar via Docker, Kubernetes, or standalone Java.
To allow your Spring Boot code to securely call Liferay's Headless APIs, you must inject the Headless Server credentials into the container environment. The User Agent credentials are NOT required for the Spring Boot side. You must also inject your Square Webhook Signature Key (more on this in Step 4).
   square-payment-backend-oauth-application-headless-server.oauth2.headless.server.client.id=YOUR_HEADLESS_CLIENT_ID   Â
   square-payment-backend-oauth-application-headless-server.oauth2.headless.server.client.secret=YOUR_HEADLESS_CLIENT_SECRET   Â
        Â
   square.webhook.signature.key=YOUR_SQUARE_WEBHOOK_SIGNATURE_KEY   Â
        Â
   com.liferay.lxc.dxp.mainDomain=apps.yourdomain.com   Â
   com.liferay.lxc.dxp.server.protocol=https Â
Configuring Square & Webhooks
Step 4: Webhook Configuration (The Failsafe)
Webhooks guarantee your Liferay orders are finalized even if the buyer closes their browser immediately after paying.
- Log into the Square Developer Dashboard.Â
- Navigate to Webhooks → Subscriptions → Add Subscription.Â
- Set the webhook URL to your Spring Boot microservice (e.g., https://your-domain.com/square/webhook).
- Select the payment.updated event, save, and copy the Webhook Signature Key.
- Add this key to your Spring Boot environment variables (as shown in Step 3).Â
Step 5: Liferay Commerce Channel Settings
Finally, link your Square credentials to your Liferay storefront.
- In Liferay, navigate to Commerce → Channels → Payment Methods.
- Locate Square Payment and toggle the status to Active.
- Click the Configuration tab and paste your settings into the Type Settings box:
square.access.token=YOUR_SQUARE_ACCESS_TOKENÂ Â
square.location.id=YOUR_SQUARE_LOCATION_IDÂ Â Â Â
square.api.version=2026-05-20Â Â Â Â
square.currency=USDÂ Â Â Â
square.payment.link.api.url=https://connect.squareup.com/v2/online-checkout/payment-links Â
If you are testing this integration using a Square Sandbox Access Token (which usually starts with EAAAl), you must change the api.url in the settings above to https://connect.squareupsandbox.com/v2/online-checkout/payment-links.
If you accidentally pass a Sandbox token to Square's production URL, Square will reject it with a 401 UNAUTHORIZED error, and Liferay will display a generic "An error occurred" message on the checkout page!
Frequently Asked Questions
Architecture & Design
Why use a Client Extension instead of implementing Square directly inside Liferay?
A Spring Boot Client Extension keeps external payment logic outside the Liferay JVM. It can be deployed, maintained, scaled, and upgraded independently while still integrating with the Liferay Commerce payment lifecycle.Â
Can different Liferay Commerce channels use different Square accounts?
Yes. Channel-specific payment configuration can be maintained so that different Commerce channels can connect to different Square accounts while using the same Client Extension.
Is checking x-square-hmacsha256-signature enough?
No. The presence of the header alone does not prove that the request originated from Square. The received signature must be cryptographically validated using Square’s webhook signature verification mechanism.
Can someone call the webhook using Postman?
Yes, anyone who knows the public webhook URL can technically send an HTTP request to it. However, without a valid Square-generated signature, the request should fail signature validation and must not be processed.
Should we call the Square API again after receiving a webhook?
Not necessarily for every event. A correctly validated webhook can be used to process the payment event. However, in some scenarios, retrieving the payment from Square again can provide an additional verification layer for status, amount, or transaction reconciliation.
Should the success redirect from Square mark the order as paid?
No. The browser redirect should mainly control the customer experience. Payment completion should be based on trusted server-to-server confirmation, such as a validated Square webhook or Square API response.
How do we identify which Liferay order belongs to a Square webhook?
The integration should maintain a correlation between Liferay identifiers and Square transaction identifiers when the payment is created. This allows the Client Extension to resolve the correct Commerce order when a webhook arrives.
What happens if Square sends the same webhook more than once?
The webhook handler should be idempotent. Already processed events or payment states should be detected so that the same payment update is not executed multiple times.
Deployment & Troubleshooting
Why was the Webhook Signature Key moved to Environment Variables?
Webhooks are triggered directly by Square, bypassing Liferay completely. Because Square doesn’t know about Liferay’s “Type Settings” payload, the Spring Boot application must read the Webhook Signature Key directly from its own environment variables to cryptographically verify the incoming Square request.
Do I need to inject the User Agent Client ID/Secret into Spring Boot?
No. Spring Boot only uses the Headless Server credentials to authenticate its outbound calls back to Liferay (to update the order status). The User Agent credentials are used internally by Liferay to route the payload to Spring Boot.
I see "Client ID null" in my Spring Boot startup logs. Is this an error?
No! This is a harmless warning from the com.liferay.client.extension.spring.boot starter library. It complains if it doesn’t find a global default client ID. As long as you provided the headless-server environment variables correctly and your application accepts requests, this warning can be safely ignored.
I get "An error occurred while processing your payment" on the checkout page. How do I debug this?
For security reasons, Liferay intentionally hides raw payment gateway errors (like “Invalid API Key”) from the frontend customer. To debug, check the logs of your Spring Boot microservice. You will see the exact JSON error returned by Square (e.g., UNAUTHORIZED if your token is invalid or belongs to the wrong Sandbox/Production environment).
Why do I get a 401 Unauthorized when hitting public endpoints like /square-payment/ready?
This happens if your Ingress/Load Balancer is not stripping the URL prefix. The Spring Boot application strictly listens on the root path / (e.g., /ready). If your reverse proxy forwards the request as /square-payment/ready, Spring Security will reject the unknown path with a 401 Unauthorized. To fix this, configure your Ingress to strip the prefix (Rewrite Target) before forwarding to the container.
What happens if the old OSGi version of the Square integration is still installed?
It will cause routing conflicts. You must uninstall or deactivate any old OSGi modules (com.nirvanalab.commerce.payment.method.square) from the Liferay App Manager to ensure Liferay strictly invokes the new Client Extension.