Free for Craft Commerce 5 · Pro $129
Loyalty points you can account for
WooCommerce shops have had Points and Rewards for a decade. Craft Commerce had nothing. Pointz adds earning rules a merchandiser can write, redemption at checkout that spends nothing until the order completes, and a ledger where every balance adds up to the lots behind it.
Features
Built on one rule: balances are derived, never typed in. Anything that couldn't survive that rule didn't ship.
Balances are derived
Every movement is a row in an append-only ledger, and every credit is a lot with its own remaining figure and its own expiry date. A balance is the sum of those lots, and one console command rebuilds every balance in the store from them.
- A correction is written as its opposite, never edited in place
- The rebuild is safe to run at any time
Soonest expiry first
Spending takes the lots that expire soonest, and a lot that never expires is spent last. Spend in any other order and dated points lapse while the customer still had points to spare.
- Every spend records which lots it drew on, and how much
Refunds that reverse exactly
Points an order spent go back into the lots they came from, with their original expiry. Points it earned are reversed pro rata and cumulatively, so two 20% refunds take 40%. This is in Lite: a free edition that over-awards is a bug, not an upsell.
- Points already spent are reported as a shortfall, never a negative balance
Redemption that can't compound
The customer's request is stored as intent and re-clamped on every recalculation by an adjuster that runs last, after shipping, discounts and tax. Nothing is spent until the order completes, and then exactly what the adjustment says.
- An abandoned cart costs the customer nothing
- A rate change between cart and checkout can't alter the spend
Rules a merchandiser can write
A rate per unit spent or a fixed award, plus a signup bonus. Pro adds stacked and exclusive rules, per-line-item rules, product, order and customer conditions, campaign windows with multipliers, and per-rule and per-customer caps.
- Installs with one rule per store, switched off
- Awarding is idempotent: an order can't earn twice
Store credit, in Pro
A second balance in the store's own currency, moving through the same ledger, so it expires, reverses and audits the same way. Award it from a rule, grant it by hand, or issue it as a refund from the order panel.
- Inactivity expiry, backfill with a dry run and an undo, CSV export
- A dashboard widget for the unredeemed liability
A balance you can take apart
A customer's page from the control panel, running the ledger's own rules. Spend at checkout and see which lots it takes. Refund the order and the points go back into those same lots, expiry dates intact. Move the clock past an expiry first, and the refunded share comes back as a fresh lot instead.
Where the balance sits
Spending takes from whatever expires soonest, so this is the order it will go in.
| Lot | From | Remaining | Of | Earned | Expires |
|---|
History
| When | What | Amount | Balance | Order | Note |
|---|
Complete the order, then refund #1042: the 800 points go back into the same three lots, with the same expiry dates. Spend again, sweep on 16 Oct 2026 and refund: the lot that expired can't be refilled, so its share comes back as a fresh lot. Refund 20% of #1019 twice and 360 points come back out in total, not 540.
Checkouts in this demo spend points but earn none, so every movement stays readable on one screen. Refunds follow the default settings: earned points are reversed in proportion to the refund, and points an order spent are handed back.
Nothing in Twig can move a balance
Every method on craft.pointz reads. A quote can't spend a point and a product-page preview can't award one, so the balance, the redemption form and the earn line can go anywhere. The preview runs the same rule engine as the real accrual, so the number on the product page is the number that lands in the ledger.
{# Product page: the same rule engine the checkout runs #}
{% set award = craft.pointz.earnFor(product.defaultVariant) %}
{% if award.points > 0 %}
<p>Earn {{ craft.pointz.formatted(award.points) }} with this order.</p>
{% endif %}
{# Cart: store the customer's intent. Nothing is spent until the order completes. #}
{% set quote = craft.pointz.quote() %}
{% if quote and quote.maxPoints > 0 %}
<form method="post">
{{ csrfInput() }}
{{ actionInput('pointz/cart/redeem') }}
<input type="number" name="points" max="{{ quote.maxPoints }}" value="{{ quote.points }}">
<button>Apply</button>
</form>
<p>You have {{ craft.pointz.formatted(quote.pointsBalance) }}.</p>
{% for notice in quote.notices %}
<p>{{ notice }}</p>
{% endfor %}
{% endif %}
Frequently Asked Questions
The questions worth answering before you install it.
Lite is, and it runs a complete points scheme rather than a trial: one earning rule per store, a signup bonus, redemption at checkout with its caps, the ledger, holds, refund reversal and a site-wide expiry policy. Pro is $129 with a $99/year renewal. It adds several rules per store, line-item rules, conditions, campaigns, caps, store credit, per-rule and inactivity expiry, backfill, CSV export and the liability widget.
No. Pointz installs one earning rule per store, switched off. Nothing is awarded until you open the rule, set a rate and turn it on.
Yes. The redemption is an ordinary order adjustment, and Pointz's adjuster runs last, so shipping, discounts and tax are already on the order before points are applied. The cap is measured against a real total rather than an optimistic one.
By default, the refunded share of them. Reversal is cumulative, so two 20% refunds take 40% in total. Points the customer has already spent can't come back, so Pointz reports the shortfall rather than pushing the balance negative. The points an order spent are handed back, because a customer who paid partly in points and got their money back is owed them.
Only if you switch it on, and only when the daily sweep runs. Nothing expires just because a date passed; it expires because the sweep noticed, and the sweep writes a ledger row saying so. A balance that evaporates without an audit trail is a support ticket waiting to happen.
The cart re-quotes on every recalculation, so this only matters when the balance moves in between: a second tab, a manual deduction, an expiry sweep. By default the customer keeps the discount they were quoted, the ledger records what was actually there, and a notice on the order shows the difference. You can choose to fail the completion instead.
Points are a loyalty currency with an exchange rate you set. Store credit is money, in the store's own currency, and it's a Pro feature. Both share the ledger, the expiry machinery and the refund behaviour. The percentage cap applies to points only, because credit is the customer's own money.
Open the customer's page and press Rebuild from the ledger. A balance is a cache of the lots, so if the cache is ever wrong, the lots are right and the rebuild fixes it. If it still looks wrong afterwards, the ledger on that page shows exactly which movement was unexpected.
No. Every movement is written in the request that caused it. The only scheduled work is the sweep, which is a cron job rather than a queue job so a stalled queue can't quietly stop expiry.
Craft CMS 5.3+, Craft Commerce 5.0+ and PHP 8.2+.
Start free, upgrade when the scheme grows
Lite is free and runs a complete points scheme, refund reversal and expiry included. Pro is $129 with a $99/year renewal, and adds rule conditions, campaigns, caps, store credit, backfill and the liability widget.