A few months ago a client came to us with what sounded like a five-minute job:
“When someone buys on our WooCommerce store, the order should show up as an invoice in Zoho Billing. That’s it.”
Simple on paper. And they were already doing it the obvious way — with Zoho Flow, Zoho’s own no-code automation tool. New order in WooCommerce, create an invoice in Zoho Billing, done. For a flat-rate product sold into a single tax jurisdiction, that setup is honestly fine, and we’d recommend it to anyone.
Their store wasn’t that store.
Where the cracks started
This was a Canadian subscription business: customers across multiple provinces, a “package” built as a WooCommerce bundle product, bank-transfer orders that sit in a different status before they’re paid — and an accountant who needed every invoice to be correct down to the cent, because tax authorities don’t grade on a curve.
Three things broke almost immediately.
Tax was wrong — in two different directions. Either Zoho applied its org default (GST 5%) to everyone, including Quebec customers who actually owe GST + QST (14.975%), or the flow passed WooCommerce’s own tax line into Zoho and then Zoho calculated tax on top of it. Double taxation. The invoice total no longer matched what the customer had paid at checkout. If you’ve ever searched the Zoho forums, you know “my integration is double-taxing invoices” is not a rare complaint.
Bundles fell apart. The package was a WooCommerce bundle, and its child items — including a $1 placeholder line — were being pushed into Zoho as separate invoice line items. Customers received invoices with mystery $1 rows and a total that didn’t add up to their order.
Duplicates were a real risk. A WooCommerce order can fire a status-change event more than once, and a no-code flow has no native “only ever do this once” guard. Meanwhile, bank-transfer orders land in on-hold first, so a flow listening only for the paid status either fired too early or never fired at all.
On top of all that, we needed surgical control over the exact JSON sent to Zoho’s invoice endpoint. A drag-and-drop builder hands you fields; it doesn’t hand you the payload.
The decision
We spent a while trying to bend the no-code flow — and a couple of Deluge scripts — into the right shape. At some point the honest answer was: this would be smaller, faster, and far more reliable as a focused WordPress plugin that talks straight to the Zoho Billing API.
So we built one. Internally we call it WF Zoho Sync. Here’s the approach, in case you want to build the same thing yourself.
How we built it
1. Hook the right event — and only the right event
We sync on woocommerce_order_status_changed, but only for the statuses that actually mean “money is in.” In production that’s processing; while testing we temporarily add on-hold so we can validate bank-transfer orders.
php
add_action( 'woocommerce_order_status_changed', 'wf_maybe_sync_to_zoho', 10, 4 );
function wf_maybe_sync_to_zoho( $order_id, $old_status, $new_status, $order ) {
// Production: paid orders only.
$sync_statuses = apply_filters( 'wf_zoho_sync_order_statuses', array( 'processing' ) );
if ( ! in_array( $new_status, $sync_statuses, true ) ) {
return;
}
// ...continue to the sync
}
2. Sync once, on purpose
The fix for duplicate invoices is boring and bulletproof: write the Zoho invoice ID back onto the order, and bail out early if it already exists.
php
// Never invoice the same order twice.
if ( $order->get_meta( '_wf_zoho_invoice_id' ) ) {
return; // already synced
}
3. Get the tax right (this is the whole ballgame)
This is the part the no-code flow couldn’t get right, so it’s worth being precise about it.
The rule we landed on: send tax-exclusive prices, never send WooCommerce’s tax lines, and tell Zoho which tax to apply by passing a tax_id on every line. WooCommerce’s own tax lines are exactly what cause the double-taxation problem, so they stay out of the payload entirely. Zoho calculates the tax from the tax_id you give it.
To do that, we map each billing province to its Zoho tax_id:
php
// Billing province → the Zoho tax Zoho should apply.
$tax_map = array(
'QC' => '47510750000015555xx', // GST + QST 14.975%
'ON' => '47510750000000841xx', // HST 13%
'BC' => '47510750000015555xx', // GST + PST 12%
'DEFAULT' => '47510750000000841xx', // GST 5%
);
$state = $order->get_billing_state();
$tax_id = $tax_map[ $state ] ?? $tax_map['DEFAULT'];
You can find each tax_id in Zoho Billing → Settings → Tax Rates (it’s the number in the browser URL when you open a tax), or by calling the “list taxes” API endpoint once.
4. Skip the bundle children
For bundle products, we sync the parent line and skip anything flagged as a bundled/composited child. No more $1 mystery rows.
5. Build a clean, explicit payload
Everything comes together as one predictable JSON object posted to Zoho’s invoice endpoint:
json
{
"customer_id": "ZOHO_CUSTOMER_ID",
"reference_number": "WC-1042",
"date": "2026-06-19",
"is_inclusive_tax": false,
"invoice_items": [
{ "name": "Pro Package", "price": 1599, "quantity": 1, "tax_id": "47510750000015555xx" }
],
"shipping_charge": 28,
"shipping_charge_tax_id": "47510750000015555xx"
}
Two details matter here: is_inclusive_tax is false (we’re sending net prices), and the same tax_id is attached to both the line items and the shipping charge, so shipping is taxed correctly too.
6. Draft first, then go live — and log everything
While testing, invoices are created as drafts so nothing accidental hits a customer. In production we flip that off and the invoice opens automatically. And every OAuth call, API request, and sync decision is written to WooCommerce → Status → Logs, so when something looks off we can read exactly what was sent instead of guessing.
The result
After the switch: correct provincial tax on every invoice, totals that match WooCommerce to the cent, zero duplicate invoices, no phantom $1 lines, and a log the team can actually read. The accountant stopped emailing us. That’s the real KPI.
So… should you ditch Zoho Flow?
Probably not, to be fair to it. If you sell a single product, in one tax jurisdiction, with no bundles and no subscriptions, Zoho Flow is genuinely the right tool — don’t build and maintain a plugin you don’t need.
But the moment real-world tax rules, bundle products, subscriptions, or a hard “this must reconcile perfectly” requirement enter the picture, a small, focused custom integration almost always pays for itself — in correct numbers, in time saved, and in not having to apologise to your customers.
Stuck on a sync that almost works?
Connecting WooCommerce to Zoho Billing, Zoho Books, or Zoho CRM — with correct taxes, clean invoices, and a setup you can actually maintain — is squarely our wheelhouse.
Tell us what’s breaking and we’ll build it for you: info@woofocus.com