For Craft Commerce 5
Your Commerce orders, already in FreshBooks
Every month the same job: export the orders, work out what the tax actually was, and retype it into FreshBooks. Freshh does it as the orders complete — the invoice, the client and the payment — and checks the arithmetic before it sends anything.
Rehearses before it sends
A dry run needs no FreshBooks connection at all. Point it at two years of orders and see exactly what would be created, and whether every invoice balances, before you connect anything.
$ php craft freshh/sync/push --dry-run --limit=3
1042 → invoice INV-1042, USD 124.33 (order total 124.33)
1043 → invoice INV-1043, USD 25.00 (order total 25.00)
1044 → invoice INV-1044, USD 60.00 (order total 60.00)
Dry run: nothing was sent.
# Then, once you are happy with the mapping:
$ php craft freshh/sync/push --limit=500
Features
An accounting bridge is only worth having if you can trust the numbers on the other side of it. Most of Freshh is about earning that.
Money that maps exactly
FreshBooks multiplies quantity by unit cost and adds percentages. Commerce stores totals and tax amounts. Freshh does the translation — including tax Commerce included in the price, which a FreshBooks line has no concept of.
- Tax percentages come from the Commerce rate itself, not derived and rounded
- Inclusive tax is split into net price plus rate — same total, tax still visible in FreshBooks' reports
Checks the total before it sends
Freshh computes the total FreshBooks will arrive at and compares it with what the customer actually paid. A cent of drift gets a rounding line. Anything larger is refused, with a notice naming the adjustment that could not be expressed.
- An invoice a pound out is worse than no invoice — nobody notices until a reconciliation fails months later
Never invoiced twice
FreshBooks accepts no idempotency key, and a queue worker can die between creating an invoice and recording that it did. Freshh claims the order first, then asks FreshBooks whether the invoice already exists, and adopts it if so.
- A unique index means two queue workers cannot both own one order
- A retry after a crash picks up the invoice it already made
Payments and refunds, both ways
Every successful transaction becomes a FreshBooks payment against the invoice, with the gateway reference kept so a chargeback stays traceable. Every refund becomes a credit note — the instrument FreshBooks' own documentation points at.
Nothing happens during checkout
Syncing is queued, always. A FreshBooks outage cannot fail an order, and no shopper ever waits on somebody else's API. Token refresh runs under a mutex, because FreshBooks refresh tokens are one-time-use and two workers refreshing at once would kill the connection.
You can see what it will do
Every order has a preview, and the settings screen previews your latest one. Both run the same code the queue job runs, so the payload you are shown is byte-for-byte what FreshBooks receives — with both totals side by side.
- Every call is logged with its payload, status and duration; tokens are redacted
Frequently Asked Questions
The questions worth answering before you install it.
No. Everything that talks to FreshBooks happens in a queue job. A FreshBooks outage cannot fail an order, and no shopper ever waits on someone else's API.
Not from a retry. FreshBooks accepts no idempotency key, so Freshh claims a row keyed on the order, and if a previous attempt created the invoice but died before recording it, the next attempt finds it by invoice number and adopts it.
Nothing, until you ask. freshh/sync/push --limit=500 backfills them, and --dry-run rehearses first — without needing a FreshBooks connection at all.
As a percentage on each line, taken from the Commerce tax rate's own snapshot where there is one and derived from the amount where there is not. FreshBooks allows two taxes per line; a third and fourth are summed into one, and the sync says so rather than dropping them.
A FreshBooks line has no inclusive-tax concept. By default Freshh sends the net unit price plus the rate, which comes to the same total and keeps the tax visible in FreshBooks' reports. You can also tell it to leave the tax in the price and show none.
Freshh works out the total FreshBooks will reach and compares it before sending. A gap of cents gets a rounding line; anything larger is refused, with a notice saying which adjustment could not be expressed. That refusal is deliberate.
Yes. Clients are matched on email rather than on a Craft user, so three guest orders from one address are one FreshBooks client rather than three.
Only to get invoice status back into Craft. Pushing orders works without them. If FreshBooks cannot reach your site — local development, staging behind basic auth — freshh/sync/pull does the same job on a schedule.
Yes. It verifies the HMAC signature, then throws the payload away and re-fetches the resource from the API. A forged callback costs one wasted API call and cannot change a number in your books.
Encrypted with Craft's security key, in the database — not in project config, which is committed to version control. They never appear in the log; every payload is redacted on the way in.
No. Freshh covers the Commerce side of FreshBooks — invoices, clients, payments, credit notes and items. Expenses, projects, time entries, estimates and bills are out of scope.
Craft CMS 5.3+, Craft Commerce 5.0+, PHP 8.2+.
Stop retyping your orders
Freshh is $79 for one Craft installation. Install it, run a dry run against your real orders, and see the mapping before you connect anything.