Search by

qliro / module-qliroone

tdauria-qliroJenniferGus

QliroOne checkout integration for Magento 2

Package info

github.com/Qliro/Magento-2

Type:magento2-module

pkg:composer/qliro/module-qliroone

Statistics

Installs: 1 325

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 7

1.7.48 2026-09-22 10:45 UTC

This package is auto-updated.

Last update: 2026-09-27 21:58:15 UTC


README

Qliro One for Magento 2 is an extension that integrates the Qliro One payment and checkout service into the Magento 2 e-commerce platform. Qliro One is a Nordic payment solution offering invoice, part payment, card payment, and direct bank payment options. The Magento 2 module enables seamless embedding of Qliro’s hosted checkout within the store, supporting features such as dynamic shipping options, order management synchronization, and compliance with local payment regulations. The module is a fully functional implementation of a custom checkout that uses Qliro One functionality through its API.

All documentation, setup guides, and troubleshooting instructions are maintained in the Wiki.

👉 Go to the Wiki

Quick links

The Wiki is organized into the following main sections to help you quickly find what you need:

Analytics and purchase tracking

The checkout has its own success page, checkout/qliro/success, so its layout handle is checkout_qliro_success and not checkout_onepage_success. A tracking extension that declares its block in checkout_onepage_success.xml renders nothing here until that block is mapped onto this handle, in your own module or theme:

<!-- view/frontend/layout/checkout_qliro_success.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="Vendor\Tracking\Block\Purchase"
                   name="vendor.tracking.purchase"
                   template="Vendor_Tracking::purchase.phtml"
                   cacheable="false"/>
        </referenceContainer>
    </body>
</page>

<update handle="checkout_onepage_success"/> brings every block of the core success page over in one line, tracking blocks included, but it also brings checkout.success and checkout.registration, which duplicate what this module's own success block already shows.

On the success page the module provides what core provides:

  • the checkout session carries last_order_id, last_real_order_id, last_quote_id, last_success_quote_id and last_order_status for the placed order, so an extension that identifies the order through getLastRealOrderId() works unchanged
  • the checkout_onepage_controller_success_action event is dispatched with order_ids and order, once per order: reloading the success page does not fire it a second time

Magento's own GA4 block needs nothing, it is declared in Magento_GoogleGtag's default.xml and therefore renders on every page, this one included.

Client side tracking undercounts on this checkout. An order can be placed by Qliro's checkoutStatus callback while the buyer is still in the Qliro iframe, so a buyer who closes the tab or never returns from a bank app produces a paid order and no browser event at all. If the numbers have to be right, send the purchase server side, GA4 Measurement Protocol from an observer on sales_order_place_after, and offline conversion import or server side GTM for Google Ads.

What the module logs

Every API call and callback is logged to the qliroone_log database table and to var/log/qliroone.log, request and response bodies included. This happens at debug level on every request whatever Debug Mode is set to: that setting gates other behaviour, not the logging.

Before anything is written, Model/Logger/Redactor.php masks it as [redacted]:

  • Credentials, always, by name, by value and inside a url. Any key named like one, MerchantApiKey, MerchantApiSecret, Authorization, a token, a password, or anything containing secret or apikey, whatever its spelling and however deep in the payload it sits. The API key and secret the store is configured with are masked wherever they appear, whatever the key they were logged under is called. In a url, the user and password before the host, a token in the query and a JSON web token anywhere are masked too: the callback token carries the merchant API key in its payload, and with Callback HTTP Auth the url carries the username and password.
  • Customer data, always. Email, mobile number, personal identity number, VAT and organization number, date of birth, first and last name, care of, company name, street, postal code and city, under Qliro's spellings and Magento's own, taxvat, dob, vat_id and company included, and whether the field holds one value or a list. An email address and a Nordic identity number are masked wherever they appear in free text too.
  • An identity number, written with its separator or as twelve digits, always. A ten digit one written without a separator is not masked by pattern: it cannot be told apart from a Qliro order id, and it arrives under a key of its own in every payload the module sends or receives.
  • An international phone number, on the lines marked with the sensitive tag. That is every exchange with Qliro's APIs, every callback body Qliro posts back and every refusal Qliro explains, which is where a customer record travels.

Card data is not masked, and that is deliberate: Qliro sends only the first six and the last four digits of a saved card, CardBin and CardLast4Digits, which is what PCI DSS permits a merchant to retain and what support needs to identify a card. The card token itself is masked.

What stays readable is what a merchant needs in order to investigate: the merchant reference, the endpoint and the uri with their order ids, the request method, the status code, the order items, the amounts, the country and Qliro's own error codes. Those keep their digits even on a masked line, so an order id is never mistaken for an identity number. An exception keeps its class, its file, its code and its message and trace, with both masked. A merchant reference written as a date and a counter, 20260909-0001, has the shape of an identity number, so the reference of the line being written is held out of the patterns by value: it stays readable wherever it appears in that line, while any other value of that shape is still masked.

The masking runs on the log channel, so it applies to the table and to the files alike, to a payload that arrives as an object or as text, and to anything logged from a plugin of your own that uses the module's log manager.

How long rows are kept is a separate matter from what they hold, and it is the next section.

Log retention

The module logs every API call and callback, payloads included, to the qliroone_log table, whatever Debug Mode says. The nightly cron job qliroone_prune_log deletes the rows older than Stores > Configuration > Sales > Payment Methods > QliroOne Checkout > Debugging > Log Retention (days), 30 days on a fresh install. Set it to 0 to keep every row.

Upgrading from a version before 1.7.30: an installation whose log table already holds rows keeps every row, so nothing is deleted until you choose a window. Set the retention to 30, or to whatever the store needs, to start pruning.

The setting lives on the default scope only. A log row carries no store id, so one window covers the whole table, and a window saved on a website or a store view is refused rather than silently ignored.

The same pruning runs on demand:

bin/magento qliroone:log:prune            # the configured retention
bin/magento qliroone:log:prune --days=7   # this run only, the setting is untouched

Rows are deleted in batches of 5000, and a single run stops after 200 of them, so the job is safe to run while the store is serving traffic. A backlog of tens of millions of rows is worked off over several runs, and a run that stopped at that cap says so rather than looking like a finished one.

API timeouts

Every call to Qliro carries a connect timeout and a request timeout, in seconds, set under Stores > Configuration > Sales > Payment Methods > QliroOne Checkout > API Timeouts. Before 1.7.45 the HTTP client ran with Guzzle's defaults, which are neither, so a connection Qliro never answered held a PHP worker until the web server killed it and the customer watched a spinner for as long as that took.

Two pairs, and the call says which one it wants, because the class it goes through cannot: the same client fetches the order for the checkout page and for the status push Qliro sends afterwards, and the same admin client serves the order screen an admin is looking at and the capture behind a shipment.

Setting Default Applies to
Connect Timeout, Somebody Waiting 5 Checkout render, quote update, shipping change, the admin order screen
Request Timeout, Somebody Waiting 15 The same calls, whole call
Connect Timeout, Nobody Waiting 5 Capture, refund, cancel, status push, the pending page poll
Request Timeout, Nobody Waiting 60 The same calls, whole call

The first pair is short because somebody is watching the page it renders: a store that would rather show an error than a spinner can cut it further. The second is longer because nobody is, and abandoning a capture Qliro has already accepted is worse than waiting for the answer.

The request timeout covers the whole call, connecting included, so a request timeout shorter than the connect timeout is the one that decides: the connect never gets the window it was given. Setting the request timeout to the longest a call may take and the connect timeout to a few seconds is the useful shape.

Both are per store view, and both accept 1 to 300 seconds. A field left empty, or holding anything that is not a whole number of seconds, falls back to the default rather than to no timeout: 0 means "wait forever" to Guzzle, which is the state these settings exist to end.

A call that runs out of time fails the way a refused call already does, as a TerminalException, so the checkout answers the customer with its own message and the order management screens report the failure. It is logged with the same >>> and <<< lines as any other call, so a store that times out often is visible in qliroone_log rather than only in the web server's error log.

A capture or a refund that runs out of time may have been booked by Qliro before the answer was lost. Magento rolls its own document back, so the merchant invoices or refunds again, and that second attempt carries the same RequestId as the first: Qliro books a repeated id once. The id is built from what is being settled, the order, what it had already settled, the transactions and the amounts, because the invoice and the credit memo have no id of their own until Magento saves them, which happens after the call. A settlement the merchant really means a second time differs in what the order had already settled by then, so it is a request of its own and Qliro books it.

Callback security

Qliro pushes order and transaction updates to callback urls this module registers on the order when it is created. Each url carries a token this module signed with the store's API secret, and every callback controller refuses a request whose token does not verify.

The token expires. Stores > Configuration > Sales > Payment Methods > QliroOne Checkout > Notification Callbacks > Callback Token Lifetime (days) decides how long a newly minted one lasts, 1 to 1095 days, and 1095 by default.

Shorten it to match your order lifecycle. The url Qliro pushes to is the one registered when the order was created, so it has to still be valid when the last capture or refund of that order settles. The default is three years, the length of the Swedish right of complaint, so that no store is caught out by it. If your orders are done sooner, set it shorter: that is the whole point of the setting.

What an expired token costs, so the choice is an informed one: the callback is refused, the capture or the refund is never confirmed on the Magento order, an order that was held awaiting capture confirmation stays held, a queued sequential refund stops advancing, and nothing reconciles any of it afterwards, because the module does not poll Qliro for status. A refusal for this reason is logged as a warning naming the setting, so the symptom points at the cause.

Changing the setting is safe at any time: the expiry is written into each token, so a callback url already registered with Qliro keeps the lifetime it was given, and a shorter window applies only to orders created after the change. Tokens issued before this feature existed carry the old three year expiry and keep working until it passes.

On upgrade nothing changes for the orders you have already placed: their callback urls carry the expiry they were minted with, three years for anything from before this release, and the check reads it from the token rather than from the setting. Orders placed after the upgrade get the new default, so this is the moment to decide whether a year covers your returns.

The token the checkout page uses for its own ajax calls is a different one: it lasts two hours, is bound to the quote, and is not affected by this setting.

Stock

The module asks the same inventory Magento asks. On a store with the Multi Source Inventory modules installed, a line is judged by the salability of its sku in the stock of the website's sales channel, for the quantity in the cart, the way MSI judges it when it takes the stock for an order. On a store without them, the answer comes from cataloginventory_stock_item as before, which keeps a single scope for the whole installation whatever the website. Nothing needs configuring, and the inventory modules are not required: the module ships no dependency on them and reads the store.

Two things are decided this way: whether the validate callback declines an order for stock, and the OutOfStock flag the module sends Ingrid on each order line. Both read the cart the same way, so they cannot disagree: a line with children is answered by its children, a child counts for its own quantity times its parent's, and the same sku on two lines is one quantity.

A bundle, a configurable or a grouped product keeps no quantity of its own. Its stock row reads as a quantity of nothing and Magento never asks it, so neither does the module: the children carry the stock and are checked in its place. If the inventory cannot be read at all, the line is let through and the reason is logged, however often it happens: Magento checks the stock again for real when it places the order, so an unreadable inventory costs nothing here, while refusing on it would decline every order the store has. Watch the log for Could not read stock if a store's inventory needs looking at.

Where Qliro appears in the checkout

Payment Methods > QliroOne Checkout > General > Show as payment method decides whether the store keeps the native Magento checkout and offers Qliro as one payment method in it, or replaces the checkout with Qliro's own page. With it on, Payment method display decides what happens once the buyer picks Qliro:

  • Redirect to Qliro checkout page sends the buyer to the standalone Qliro page. This is what the setting did before it existed, and it stays the default.
  • Embedded iframe in checkout opens Qliro in the payment panel, and the buyer never leaves the checkout.

The iframe is fetched when the buyer picks Qliro, not when the payment step loads, so a buyer who pays with something else never creates a Qliro order.

In the iframe the native checkout owns identity, address and delivery, because it collected all three before Qliro was shown. The customer block reaches Qliro locked, so the widget states it and offers neither its change button nor the personal number lookup, and Qliro is sent only the delivery method the buyer already chose, so its own delivery picker has nothing to offer and cannot move the order off the method Magento rated. The order is created for the country on the quote, and that country stays on the quote: everywhere else it comes from the country selector, GeoIP and the store default, and is written back, which here would replace a country the buyer chose with one they did not.

The lock holds the whole block, the phone number with it: Qliro offers no way to keep one field of a locked block open. The buyer changes the phone where they entered it, in the checkout step above the widget, and a store whose buyers need to correct it inside Qliro should stay on the redirect mode, where Qliro owns the form.

Three things fall back rather than trap the buyer. An address that is still empty is not locked, and neither is the block around it, which matters for a virtual cart, where the native checkout collects the billing address inside the payment step and it can still be blank when Qliro is picked. A chosen delivery method that is not among the rated ones sends the whole list and logs why, because the cost of delivery travels on that list and has no line of its own. A Qliro order that cannot be built at all leaves a message in the panel and the buyer can try again.

A buyer who has already paid and comes back to the checkout, with the Back button or a reopened tab, is sent to the pending page that waits for their Magento order, which is what the standalone checkout does too.

Quantity

Qliro carries the quantity of an order line as a whole number, so a store selling by weight or length cannot take half a metre of cable through this payment method. A cart holding a part of an item is refused with a message naming the line, at the point a Qliro order would be created for it, and the buyer can change the quantity or pay another way. That point is the same in every mode, the Qliro checkout page, the payment method in Magento's own checkout and the merchant payment, and on the checkout page the message is shown in place of the widget. It is not truncated: half a metre sent as none would be a line Qliro never charges for, and two and a half sent as two would charge for less than the cart holds, with Magento recording the whole of it either way.

A product configured with is_qty_decimal, or with a qty_increments that is not a whole number, is what produces such a cart. An order that already holds one, placed before this release or through the admin or the API, is refused at the capture and at the shipment for the same reason, naming the line, and has to be settled outside Magento. The refund the module sends is a single line for the amount of the credit memo and carries no quantity, so it is unaffected.

📘 Documentation: For complete guides, detailed instructions, and technical references, please refer to the Wiki.