Knox for Craft CMS

Usage

When an invoice is created

Three choices, under When to invoice:

  • When Commerce completes the order — the default.
  • When the order reaches a status (Pro) — for shops that pick, pack and then invoice.
  • Only when asked — nothing automatic; you press the button or run the command.

Invoicing always happens on the queue. The order-complete handler pushes a job inside a try/catch, so an accounting outage is never the customer's problem. Nothing Knox does can break a checkout.

You can also require that an order be fully paid before it is invoiced, and restrict Knox to particular stores.

The order screen

Every completed order gets a Fortnox panel inside Commerce's own order screen: the invoice number as a link into Fortnox, the total, the voucher once it is booked, the VAT type, and any reason it was blocked. An order with no invoice yet gets a Send to Fortnox button.

The invoice list

Knox → Invoices lists everything Knox has created or tried to, filterable by state:

StateMeans
In FortnoxCreated, and the totals agree.
Does not match the orderCreated, but Fortnox's total differs from what was charged. Both figures are on it.
BlockedThe build refused. Nothing was sent, and the reason is on the row.
UnknownFortnox never answered. It may or may not hold the invoice.
FailedFortnox rejected it. Safe to try again once the cause is fixed.
In flightAn attempt was in progress.

Open one for the full reconciliation — charged, invoiced, rounding, net, VAT, outstanding — plus the VAT reasoning and the exact payload that was sent.

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 read timeout leaves the invoice Unknown and stops. Nothing retries it, including you pressing the button again.

To resolve it, Knox looks:

php craft knox/sync/reconcile

Every invoice Knox creates carries the Craft order reference in ExternalInvoiceReference1, and Fortnox lets you filter on that field — 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. The same thing is a Check with Fortnox button on the invoice detail screen.

Bookkeeping

Booking an invoice puts it in the ledger and allocates a voucher. It is irreversible — a booked invoice can be credited but never edited or deleted — so it is off by default and Pro-only.

Before booking, Knox checks that a financial year covers the invoice date. This is the most common January failure in every Fortnox integration: nobody has created the new year yet, and Fortnox's own error never mentions financial years. Knox says:

Fortnox has no financial year covering 2027-01-04, so invoice 1042 cannot be booked. Create the year in Fortnox and try again.

Pressing Bookkeep in the control panel re-asks Fortnox rather than trusting a cached answer, which is what makes "I have just fixed it" work immediately.

Payments

Two directions, both Pro, and most shops want only one.

Register payments in Fortnox — your shop took the money at checkout, so the invoice is already paid before it exists. Without this, Fortnox fills up with unpaid invoices and the kundreskontra is meaningless. Needs booked invoices and the payment scope.

Poll Fortnox for payments — for shops that invoice on account. Run it on a schedule:

php craft knox/payments/poll

This is a poll rather than a webhook on purpose. Fortnox's webhook coverage is a small and shifting set of resources, and a webhook that never arrives is indistinguishable from a quiet week — silence is the one failure mode a bookkeeping integration must not have. A poll over lastmodified cannot miss anything and costs a handful of requests a run.

Optionally, a Fortnox payment can write a Commerce transaction and move the order to a status. Both are off by default: writing a transaction creates money in Commerce's ledger, and a shop that already took the payment at checkout does not want a second record of it.

Credits

With Credit invoices on refund on, a refund captured in Commerce raises a credit invoice.

A full refund uses Fortnox's own credit action, which copies the rows and links the two documents. That is both simpler and more correct than rebuilding them.

A partial refund is refused by default, and says so. Fortnox documents crediting a whole invoice and publishes no way to credit part of one, so Knox will not invent one behind your back. 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. Confirm it works on your account before relying on it.

Two refunds are two credit invoices; the same refund replayed is one. The Commerce transaction hash is what makes that true.

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 <orderId> --dry   # what would be sent, without sending it
php craft knox/sync/order <orderId>         # 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
php craft knox/log/prune
php craft knox/log/last-error               # exits non-zero if anything is wrong

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 that deadline is inside a fortnight.

Twig

{% set invoice = craft.knox.invoice(order) %}
{% if invoice and invoice.existsInFortnox() %}
    <p>Faktura {{ invoice.documentNumber }}</p>
    {% set url = craft.knox.downloadUrl(order) %}
    {% if url %}<a href="{{ url }}">Ladda ner PDF</a>{% endif %}
{% endif %}

{# Tell an EU business buyer what will happen, before they pay #}
{% set tax = craft.knox.taxTreatment(cart) %}
{% if tax.zeroRated %}
    <p>Omvänd betalningsskyldighet — no VAT will be charged.</p>
{% endif %}
MethodReturns
craft.knox.invoice(order)The invoice document, or null
craft.knox.documents(order)The invoice and any credit invoices
craft.knox.invoiceNumber(order)The Fortnox invoice number, or null
craft.knox.downloadUrl(order)A customer download URL, or null
craft.knox.taxTreatment(order)The VAT type, bucket, zero-rated flag and reasoning
craft.knox.isConnected()Whether Knox has a live Fortnox connection

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.

Customer downloads serve the PDF from Fortnox on demand rather than caching it, so an invoice that has been credited or corrected stops being downloadable in its old form.