PHPackages                             justinholtweb/craft-knox - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. justinholtweb/craft-knox

ActiveCraft-plugin

justinholtweb/craft-knox
========================

Fortnox invoicing for Craft Commerce — turn completed orders into real Swedish customer invoices, with VAT resolved per order and öresavrundning that never silently disagrees with what the customer paid.

5.0.0(today)11↑2900%proprietaryPHPPHP ^8.2

Since Aug 24Pushed today1 watchersCompare

[ Source](https://github.com/justinholtweb/craft-knox)[ Packagist](https://packagist.org/packages/justinholtweb/craft-knox)[ RSS](/packages/justinholtweb-craft-knox/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (2)Versions (2)Used By (0)

Knox
====

[](#knox)

Fortnox invoicing for Craft Commerce.

Completed orders become real Swedish customer invoices in Fortnox — with the VAT resolved per order, the right BAS accounts, and a total that matches what the customer actually paid. Payments flow both ways. Refunds raise credit invoices.

- **Craft CMS 5.3+**, **Craft Commerce 5.0+**, **PHP 8.2+**
- **$99** one-off, **$79/year** renewal — one edition, everything in it
- `composer require justinholtweb/craft-knox`

---

Why it exists
-------------

[](#why-it-exists)

The usual options are a Zapier hop or hand-keying orders. Both produce invoices that look right, and both are quietly wrong in the same way.

A Swedish Fortnox company almost always has **öresutjämning** switched on: Fortnox rounds every invoice total to the nearest whole krona and books the difference to account 3740. A webshop, at the same time, charges the customer an exact amount to the öre. Those two facts cannot both hold, so the invoice and the payment differ by a few öre on most orders — and neither system ever says so. It surfaces at the bank reconciliation, months later, as a pile of small unexplained differences.

Knox's edge is that it *refuses* to be quiet about it. It models Fortnox's own arithmetic before sending, tells you what the company's rounding setting is about to do in plain words, re-checks the total Fortnox returns, and flags the invoice if the two disagree. Every other feature here follows from the same principle: an invoice that does not match the order is not an invoice.

What it does
------------

[](#what-it-does)

Create invoices from completed orders●VAT resolved per order, with the reasoning recorded●BAS account mapping, validated against Fortnox's chart●Shipping and discounts mapped correctly●**Reconciliation guard** — never sends an invoice that disagrees with the order●**Öresavrundning** modelled, predicted and explained●Re-check the total Fortnox returns after every create●Never retries a create whose outcome is unknown●Fortnox **customers** created and reused per buyer●**Reverse-charge**, **OSS** and **export** invoices●**Bookkeeping** — book the invoice, with a financial-year pre-check●Register the checkout payment in Fortnox●Poll Fortnox for invoices paid there, and mark the order paid●**Credit invoices** when a refund is captured in Commerce●Match order lines to Fortnox articles by SKU●Customer-facing invoice download●Order panel, invoice list and PDF in the control panel●Full request/response connection log●Backfill, preview and reconcile from the console●Trigger on an order status rather than completion●Getting started
---------------

[](#getting-started)

1. Register an integration at [apps.fortnox.se/developer](https://apps.fortnox.se/developer) and note its **Client ID** and **Client Secret**.
2. Put them in your `.env`:

    ```
    FORTNOX_CLIENT_ID="your-client-id"
    FORTNOX_CLIENT_SECRET="your-client-secret"

    ```
3. In Craft, go to **Settings → Plugins → Knox** and set the two credentials to `$FORTNOX_CLIENT_ID` and `$FORTNOX_CLIENT_SECRET`. Save.
4. Copy the **Redirect URI** Knox shows you and register it, character for character, on your Fortnox integration. Fortnox compares it as a string.
5. Pick your **scopes** before connecting. Fortnox freezes the scope set at the moment the merchant approves the integration — adding one later does nothing at all for an already-connected account until they reconnect.
6. Press **Connect to Fortnox** and approve.
7. Press **Detect from Fortnox** in the Öresavrundning section. Knox reads the company's recent invoices and works out whether öresutjämning is on. Read what it says.
8. Press **Check these exist in Fortnox** under Accounts. An invoice row posted to an account that is not in the chart is rejected, and finding that out on order 1 beats finding it out on order 200.
9. Set a **collective customer number**, or turn on **Sync customers** to give each buyer their own record. Fortnox requires an existing customer on every invoice — there is no such thing as an invoice without one.
10. Try a dry run before you let it loose:

    ```
    php craft knox/sync/backfill --dry --limit=10
    ```

    That prints what each order would be invoiced at, what Knox thinks the VAT treatment is, and which orders it would refuse and why. Nothing is sent.

The öresavrundning problem, in detail
-------------------------------------

[](#the-öresavrundning-problem-in-detail)

Say a customer pays **499,50 kr**.

- With öresutjämning **off**, Knox sends rows totalling 499,50, Fortnox invoices 499,50, and the invoice matches the payment. Done.
- With öresutjämning set to **nearest krona**, Fortnox will invoice **500,00** and book 0,50 to 3740 — whatever Knox sends. No payload can produce a 499,50 invoice in that company.

Knox handles this in three places:

1. **Before sending**, it models the rounding and says so: *"Fortnox will round this invoice to 500,00 because öresutjämning is set to krona on the company — 0,50 away from the 499,50 the customer paid. No invoice can match an öre amount while that setting is on; it lives under Inställningar → Fakturering → Registreringsvy."*
2. **When the rows themselves** come to a few öre off — because Fortnox groups VAT by rate and Commerce rounds per line — Knox adds an explicit **öresavrundning row** at 0% VAT against 3740, which closes the gap exactly and is what a bookkeeper does by hand. You can make it refuse instead.
3. **After sending**, it compares the total Fortnox actually returned against what was charged. A disagreement makes the invoice **Does not match the order** in the control panel, with both figures and the cause on it — which is also how you find out that someone changed the setting three months after go-live.

If you want invoices that match payments, turn öresutjämning off in Fortnox. Knox will keep working either way; it just will not pretend.

VAT
---

[](#vat)

`auto` resolves the treatment per order from three facts and nothing else: where the goods go, whether the buyer gave a VAT number, and whether you are registered for the One Stop Shop.

DestinationBuyerFortnox VAT typeDefault accountSwedenanyone`SEVAT`3001 / 3002 / 3003 / 3004 by rateEUgave a VAT number`EUREVERSEDVAT`3108 goods, 3308 servicesEUconsumer, OSS on`EUVAT`3106EUconsumer, OSS off`SEVAT`as domesticOutside the EUanyone`EXPORT`3105 goods, 3305 servicesThe reasoning behind each decision is stored on the invoice and shown in the control panel. `EUREVERSEDVAT` is a claim to Skatteverket that lands in your periodisk sammanställning with the buyer's VAT number attached; when it turns out to be wrong, you need to see why it was chosen.

Two things Knox deliberately does **not** do:

- **It does not validate VAT numbers.** It checks the shape and says so. Confirming registration is a VIES lookup and your responsibility — a plugin that asserted otherwise would be asserting something it cannot know.
- **It does not turn on OSS for you.** Below the SEK 99,680 distance-sales threshold, charging Swedish VAT on an EU consumer sale is correct. Above it, it is not. Knox says which one it is doing and why.

Everything except a plain Swedish sale needs a real Fortnox **customer record**, because Fortnox keeps the VAT type on the customer rather than the invoice. So with customer syncing off, Knox *refuses* a reverse-charged or export order rather than invoicing it as a domestic Swedish sale, which would charge VAT on a sale that does not owe it.

Never twice
-----------

[](#never-twice)

A Swedish customer invoice is a numbered document in a consecutive series. A duplicate is not an inconvenience; it is a credit invoice, an explanation, and a question from your accountant.

So Knox never repeats a create it cannot prove failed. A timeout leaves the invoice in an **Unknown** state and nothing more happens. To resolve it, Knox *looks*:

```
php craft knox/sync/reconcile
```

Every invoice Knox creates carries the Craft order reference in `ExternalInvoiceReference1`, which is a field Fortnox lets you filter on — so this is a lookup, not a guess. It either claims the invoice Fortnox already holds, or reports that nothing is there and it is safe to send again.

Console
-------

[](#console)

```
php craft knox/connect/status               # what Knox knows about the connection
php craft knox/connect/refresh              # rotate the tokens now
php craft knox/connect/rounding             # detect the company's öresutjämning setting

php craft knox/sync/order  --dry   # what would be sent, without sending it
php craft knox/sync/order          # invoice one order
php craft knox/sync/backfill --dry          # what every uninvoiced order would do
php craft knox/sync/backfill --since=2026-01-01 --queue
php craft knox/sync/reconcile               # resolve every unknown outcome

php craft knox/payments/poll                # ask Fortnox which invoices are paid
php craft knox/log/prune
php craft knox/log/last-error               # exits non-zero if anything is wrong
```

Two of those are worth a cron entry:

```
*/15 * * * * php craft knox/payments/poll
0 3 * * *    php craft knox/sync/reconcile

```

And on a low-traffic shop, one more. **Fortnox invalidates a refresh token 45 days after it is issued**, and the clock only restarts when it is used — so a shop that takes no orders over a quiet season comes back to a dead connection:

```
0 4 * * 0    php craft knox/connect/refresh

```

Knox also warns in the control panel once the deadline is inside a fortnight.

Templates
---------

[](#templates)

```
{% set invoice = craft.knox.invoice(order) %}
{% if invoice and invoice.existsInFortnox() %}
    Faktura {{ invoice.documentNumber }}
    {% set url = craft.knox.downloadUrl(order) %}
    {% if url %}Ladda ner PDF{% endif %}
{% endif %}

{# Tell an EU business buyer what will happen, before they pay #}
{% set tax = craft.knox.taxTreatment(cart) %}
{% if tax.zeroRated %}
    Omvänd betalningsskyldighet — no VAT will be charged.
{% endif %}
```

`craft.knox` is read-only. Nothing in it creates, credits or books anything: a template render is not a place to write to someone's ledger, and a page that got refreshed twice must not produce two invoices.

Where the buyer's VAT number comes from
---------------------------------------

[](#where-the-buyers-vat-number-comes-from)

Commerce has no VAT-number field of its own. Knox reads, in order:

1. the address's `organizationTaxId` (where Commerce's own EU VAT integrations put it),
2. a custom field on the address called `vatNumber`, `vatId` or `momsnummer`.

If your shop keeps it somewhere else, copy it onto `organizationTaxId` when the order completes.

Requirements and limits
-----------------------

[](#requirements-and-limits)

- The Fortnox account needs an **API licence**. Knox reports error 2001103 in plain words when it does not.
- Fortnox allows **25 requests per rolling five seconds** per client ID and company. Knox paces itself across every queue worker on the site so they share one budget rather than each politely staying under the limit and collectively blowing it.
- A Fortnox invoice row carries VAT as a whole percentage: **25, 12, 6 or 0**. An order taxed at a rate Fortnox cannot represent is refused rather than rounded into a different tax.
- A row carries **one** VAT rate. An order line with two is refused.
- An order with VAT both included in its prices and added on top is refused — a Fortnox invoice is one or the other, and guessing produces something 25% wrong that still looks plausible.
- **Partial refunds**: Fortnox's credit action credits a whole invoice, and they publish no documented way to credit part of one. Knox refuses by default and says so. There is an opt-in mode that raises a separate linked credit invoice — the shape Fortnox's own data model describes, through a path they do not document, so confirm it on your account before relying on it.
- Knox does not use Fortnox **webhooks**. Their coverage is a small and shifting set of resources, and a webhook that never arrives is indistinguishable from a quiet week. A poll over `lastmodified` cannot miss anything and costs a handful of requests a run.

Nothing Knox does can break a checkout
--------------------------------------

[](#nothing-knox-does-can-break-a-checkout)

Invoicing happens on the queue. The order-complete handler pushes a job inside a try/catch. An accounting outage is never the customer's problem.

Licence
-------

[](#licence)

The Craft License. See `LICENSE.md`. Knox is commercial software with a single edition at $99 and a $79/year renewal; the price lives on the Craft Console listing.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance100

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/035cb655c55af0e9e5b96754b80fd9703e195c32dbdfc49ae9a43ab9cf8db560?d=identicon)[justinholtweb](/maintainers/justinholtweb)

---

Top Contributors

[![justinholtweb](https://avatars.githubusercontent.com/u/295903?v=4)](https://github.com/justinholtweb "justinholtweb (3 commits)")

---

Tags

craftcmscraft-pluginswedeninvoicingvatAccountingcommercemomscraft-commerceFortnoxbokforing

### Embed Badge

![Health badge](/badges/justinholtweb-craft-knox/health.svg)

```
[![Health](https://phpackages.com/badges/justinholtweb-craft-knox/health.svg)](https://phpackages.com/packages/justinholtweb-craft-knox)
```

###  Alternatives

[verbb/events

A full-featured plugin for event management and ticketing.

2312.1k](/packages/verbb-events)[verbb/formie

The most user-friendly forms plugin for Craft.

101400.6k79](/packages/verbb-formie)[verbb/hyper

A user-friendly links field for Craft.

24153.5k15](/packages/verbb-hyper)[verbb/vizy

A flexible visual editor field for Craft.

4251.5k1](/packages/verbb-vizy)[verbb/gift-voucher

Sell and redeem digital gift vouchers for Craft Commerce.

1323.1k1](/packages/verbb-gift-voucher)[verbb/postie

Get shipping rates, tracking status and print labels with Australia Post, UPS, USPS, FedEx, DHL Express, and more.

1219.4k](/packages/verbb-postie)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
