@getpopapi/n8n-nodes-pop-zoho
v0.1.0
Published
n8n custom node — maps POP API payload to Zoho Invoice / Zoho Books
Readme
n8n-nodes-pop-zoho
An n8n community node that receives a POP API payload and automatically creates the corresponding document in Zoho Invoice or Zoho Books.
Supported document types:
TD01→ Invoice (POST /invoices)TD04→ Credit Note (POST /creditnotes)

Table of Contents
- How It Works
- Prerequisites
- Installation
- Credentials Setup
- Workflow Setup
- Node Parameters
- Trigger Paths
- Consumer Guides
- Security Model
- Error Codes
- Sandbox / Dry Run Mode
- Contact Resolution
- POP Payload Field Mapping
- Known Limitations (v1)
- Development
- License
How It Works
The node sits inside a standard n8n webhook workflow. POP API calls the webhook with a signed payload; the node verifies the request, maps the POP fields to Zoho's data model, resolves or creates the customer contact, and creates the invoice or credit note via Zoho's REST API.
POP API ──► Webhook node ──► POP → Zoho ──► Respond to Webhook nodePOP API handles the entry point (license validation, sandbox/live mode, quota billing). This node owns: signature verification, payload mapping, contact resolution, tax lookup, and Zoho API calls.
The connector can be triggered in two ways (see Trigger Paths):
- Via webhook panel — used by WooCommerce, Shopify, and any platform that calls a document-generation route
- Via direct
/connector/zohoroute — used for Postman, direct API calls, and LLM/MCP agents
Prerequisites
POP API
- An active POP API account with a valid license key
- The n8n Connector enabled in your POP API account settings
- The n8n webhook URL registered in your POP API webhook panel
Zoho
- A Zoho Invoice or Zoho Books account
- An OAuth2 application created in the Zoho API Console (or the console for your region)
- Organization ID — found in Zoho → Settings → Organization Profile
- Tax rates configured in Zoho → Settings → Taxes, one entry per percentage used in your POP payloads (e.g. 22%, 10%, 4%, 0%)
n8n
- n8n self-hosted (v2.x or later) or n8n Cloud Enterprise (custom nodes required)
- Node.js v18+
Installation
From npm (recommended)
In your n8n instance go to Settings → Community Nodes → Install and enter:
n8n-nodes-pop-zohoManual / local development
# Clone the repository
git clone https://github.com/getpopapi/n8n-nodes-pop-zoho.git
cd n8n-nodes-pop-zoho
# Install dependencies and build
npm install
npm run build
# Symlink into n8n custom nodes directory
ln -s $(pwd)/dist/nodes/PopZohoInvoice/PopZohoInvoice.node.js ~/.n8n/custom/PopZohoInvoice.node.js
ln -s $(pwd)/dist/credentials/ZohoInvoiceOAuth2Api.credentials.js ~/.n8n/custom/ZohoInvoiceOAuth2Api.credentials.js
ln -s $(pwd)/dist/nodes/PopZohoInvoice/pop-zoho-invoice.svg ~/.n8n/custom/pop-zoho-invoice.svg
# Restart n8n
n8n startCredentials Setup
1. Create the Zoho OAuth2 Application
- Go to the Zoho API Console for your region
- Create a new Server-based Application
- Set the redirect URI to your n8n OAuth callback URL:
https://<your-n8n-instance>/rest/oauth2-credential/callback - Note the Client ID and Client Secret

2. Configure the Credential in n8n
In n8n go to Credentials → Add Credential → Zoho OAuth2 (Invoice / Books) and fill in:
| Field | Description |
|-------|-------------|
| Product | Zoho Invoice or Zoho Books — use Books for UAE e-invoicing |
| Region | Your Zoho data center (EU, US, IN, AU, JP, CA) |
| Organization ID | From Zoho → Settings → Organization Profile |
| Authorization URL | Auto-filled based on region — e.g. https://accounts.zoho.eu/oauth/v2/auth |
| Access Token URL | Same host as Authorization URL, path /oauth/v2/token |
| Client ID | From your Zoho API Console application |
| Client Secret | From your Zoho API Console application |
| Scope | Pre-filled with all required scopes — do not change unless you know what you are doing |
Click Connect to complete the OAuth2 authorization flow. Zoho will ask you to confirm the requested permissions.

Note on scopes: The pre-filled scope string includes all permissions required for full connector operation: invoices, credit notes, contacts, and settings. If you previously connected with a narrower scope, revoke the authorization from your Zoho account → Connected Apps, then reconnect to grant the updated permissions.
Workflow Setup
Recommended Workflow
[Webhook node]
↓
[POP → Zoho node]
↓
[Respond to Webhook node]- Webhook node — set Authentication to
None(security is handled by the POP → Zoho node via HMAC + RSA JWT). Set Respond toUsing 'Respond to Webhook' Node. Set the path towebhook/zoho. - POP → Zoho node — configure as described below.
- Respond to Webhook node — leave default settings; it will forward the node output as the HTTP response.

Register the Webhook URL in POP API
Copy the Production URL from the Webhook node (e.g. https://your-n8n.example.com/webhook/zoho) and paste it into your POP API account → Webhooks panel. Note the webhook ID (e.g. popWh_xxxxxxxx) — you will need it in every API call.
Webhook path naming: Use
webhook/zohoas the path in the n8n Webhook node. The node supports both Zoho Invoice and Zoho Books, so a product-neutral path avoids confusion and matches the connector slug used by POP API.

Node Parameters
Security
| Parameter | Description |
|-----------|-------------|
| POP API URL | Base URL of your POP API instance (default: https://popapi.io). Used to auto-fetch the RSA public key. |
| POP API License Key | Your default POP API license key. Used to verify the HMAC signature on every incoming request. |
| POP API RSA Public Key | Click the Refresh button to auto-fetch the public key from your POP API instance. Required for RSA JWT verification. |

Which license key to use: Always use the default API key — found in your POP API account at popapi.io → Account → API → API Key (default). Do not use platform-specific keys (e.g. the key generated for a Shopify or WooCommerce integration). POP API always signs connector requests with the default key regardless of which platform triggered the call, so the node must verify against that same key.
How to get the RSA Public Key: Set the POP API URL field, then click the refresh icon (🔄) next to POP API RSA Public Key. The key is fetched automatically from your POP API instance. You only need to do this once per credential setup (or after a key rotation on the POP API server).
Zoho Behavior
| Parameter | Default | Description |
|-----------|---------|-------------|
| Contact Match Strategy | VAT → Email → Name | How to find an existing Zoho contact for the invoice customer. |
| Create Contact If Missing | true | Automatically create the contact in Zoho when no match is found. |
| Invoice Status | Draft | Status to set on the created invoice in Zoho (Draft or Sent). |
| Send Email to Customer | false | Trigger Zoho's built-in invoice email to the customer after creation. |
| Place of Supply | Not specified | Required for UAE e-invoicing — select the emirate. Leave empty for standard invoices. |
| Deferred Payment Terms (days) | 30 | Days granted for payment when the payload uses deferred payment terms (TP02). |
Other
| Parameter | Default | Description |
|-----------|---------|-------------|
| Dry Run | false | Validate the payload and return the Zoho body that would be sent, without making any Zoho API call. Useful for testing field mapping. |
Trigger Paths
The connector can be triggered in two distinct ways depending on the client.
Path A — Via Webhook Panel (WooCommerce, Shopify, any platform using document-generation routes)
The client calls one of POP API's document-generation routes (create-xml, create-ubl, create-ksef-xml) with a webhook integration. POP API looks up the webhook entry, sees that connector_type: zoho is set, and automatically routes the call through the connector.
Setup steps:
- Register the n8n webhook URL in the POP API Webhooks panel
- Set the Connector field on that webhook entry to n8n Connector — Zoho
- In your API call, pass
integration.use: "webhook"andintegration.id: "<webhook-id>"
Request example:
{
"integration": {
"use": "webhook",
"id": "<webhook-id>"
},
"environment": "sandbox",
"data": { ... }
}This path is used automatically by the WooCommerce and Shopify integrations — no extra configuration is needed on the client side once the webhook panel is set up.
Path B — Direct /connector/zoho Route (Postman, API, LLM/MCP)
The client calls POP API's dedicated connector endpoint directly. The webhook entry still needs to exist (to store the n8n URL), but the routing is explicit.
Request:
POST https://popapi.io/wp-json/api/v2/connector/zoho
Headers:
Content-Type: application/json
X-Api-Key: <your-default-license-key>Body:
{
"integration": {
"use": "n8n-zoho",
"id": "<webhook-id>"
},
"environment": "sandbox",
"data": {
"invoice_body": {
"general_data": {
"doc_type": "TD01",
"date": "2026-03-04",
"invoice_number": "2026/0001",
"currency": "EUR"
}
},
"transferee_client": {
"personal_data": {
"company_name": "Cliente Test SRL",
"email": "[email protected]",
"tax_id_vat": {
"id_code": "12345678901",
"country_id": "IT"
},
"tax_id_code": "RSSMRA80A01H501U"
},
"place": {
"address": "Via Milano 10",
"city": "Milano",
"zip_code": "20100",
"province_id": "MI",
"country_id": "IT"
}
},
"order_items": [
{
"description": "Servizio consulenza",
"quantity": "1.00",
"unit_price": "100.00",
"rate": "22.00",
"unit": "N.",
"item_code": { "type": "SKU", "value": "PROD-001" },
"discount_percent": "",
"discount_amount": ""
}
],
"payment_data": {
"terms_payment": "TP02",
"payment_details": "MP05",
"payment_amount": "122.00"
},
"purchase_order_data": { "id": "#1001", "date": "2026-03-04" },
"connected_invoice_data": { "id": "", "date": "" }
}
}| Field | Values | Description |
|-------|--------|-------------|
| integration.id | your webhook ID | The webhook ID registered in your POP API account |
| environment | sandbox / live | sandbox runs a dry-run — no Zoho calls, no quota deducted |
| data.invoice_body.general_data.doc_type | TD01 / TD04 | Document type: TD01 = invoice, TD04 = credit note |
For TD04 credit notes, include connected_invoice_data.id with the Zoho invoice ID of the original document being reversed (returned as zoho_invoice_id in the invoice creation response).
Successful Response
{
"success": true,
"environment": "live",
"connector": "zoho",
"message": "Invoice created successfully.",
"status_code": 200,
"data": {
"success": true,
"zoho_product": "books",
"zoho_document_type": "invoice",
"zoho_invoice_id": "994269000000070002",
"zoho_invoice_number": "INV-000001",
"zoho_status": "draft",
"zoho_total": 1220.00,
"contact_id": "994269000000062001",
"contact_created": false
}
}Consumer Guides
WooCommerce
- In your POP API account → Webhooks panel, add the n8n webhook URL and set the Connector field to n8n Connector — Zoho.
- In your WooCommerce numeration settings, select the webhook entry you just created.
- When an order is processed, POP API automatically calls the connector — no changes needed in WooCommerce.
- The connector is triggered both during AJAX (immediate) and scheduled cron flows.
The license key configured in the WooCommerce plugin is the standard POP API key. POP API always signs with the default key, so the n8n credential must use that same key.
Shopify
- Register the n8n webhook URL in the POP API webhook panel and set Connector to n8n Connector — Zoho.
- In the Shopify app, select that webhook entry in the numeration settings for the relevant document type.
- When the Shopify app generates a document, POP API routes the call through the connector automatically.
The Shopify integration uses a platform-specific license key internally, but POP API always signs connector requests with the default key. Make sure the
POP API License Keycredential in n8n is the default API key from popapi.io → Account → API → API Key (default), not the Shopify-specific key — using the wrong key causes anauth_error(HMAC mismatch).
Direct API / Postman
Use Path B above. Send the request to /wp-json/api/v2/connector/zoho with the X-Api-Key header set to your default license key.
A Postman collection with ready-to-use examples (sandbox and live) is available in the examples/ directory of this repository.
LLM / MCP Agents
The connector is fully usable from LLM agents and MCP-based tools. Use Path B (direct /connector/zoho route).
Minimal call sequence for an agent:
- Retrieve the webhook ID from the user's POP API account (or have it pre-configured)
- Build the
dataobject from the order information available to the agent - POST to
/wp-json/api/v2/connector/zohowithenvironment: "sandbox"first to verify mapping - On success, repeat with
environment: "live"to create the real document in Zoho
Use
environment: "sandbox"for all test or uncertain calls — no quota is consumed and no document is created in Zoho.
Security Model
Every request from POP API to the n8n connector is protected by two independent verification layers:
1. HMAC-SHA256 (license key binding)
POP API computes HMAC-SHA256(body + timestamp, default_license_key) and sends it in the X-POP-Signature header. The node verifies the signature using the default license key configured in the node parameters. This proves the payload belongs to the customer who owns that account.
POP API always uses the default license key for signing, regardless of which platform (WooCommerce, Shopify, direct API, LLM) triggered the request. Configuring a platform-specific key in the node will cause all requests to fail with auth_error.
2. RSA JWT (POP API origin proof)
POP API signs a short-lived JWT (5 min TTL) with its RSA-2048 private key and injects it as _pop_jwt in the payload. The node verifies the JWT using the public key fetched from your POP API instance. This proves the request originated from POP API servers — a stolen license key alone is not enough to forge requests.
Both checks are always mandatory and cannot be disabled.
Replay Protection
Requests older than 5 minutes are automatically rejected based on the X-POP-Timestamp header.
Error Codes
| Code | Cause | Fix |
|------|-------|-----|
| auth_error | Invalid HMAC signature, expired JWT, or missing security headers | Make sure the POP API License Key in the node is the default key from popapi.io → Account → API → API Key (default). Platform-specific keys (Shopify, WooCommerce) will not match. Also ensure requests always go through POP API — never call n8n directly. |
| config_error | License Key or RSA Public Key not set in node parameters | Configure the node parameters and refresh the public key |
| validation_error | Required POP field missing in the payload | Check that data.invoice_body.general_data.*, data.transferee_client.*, and data.order_items are all present |
| unsupported_doc_type | doc_type is not TD01 or TD04 | Only TD01 (invoice) and TD04 (credit note) are supported in v1 |
| tax_not_found | A tax rate in order_items[].rate is not configured in Zoho | Go to Zoho → Settings → Taxes and add the missing rate |
| contact_not_found | Customer not found in Zoho and Create Contact If Missing is disabled | Enable the option or create the contact manually in Zoho |
| contact_ambiguous | Multiple Zoho contacts match the same VAT, email, or name | Resolve the duplicate contacts in Zoho before retrying |
| contact_creation_failed | Zoho refused the contact creation request | Check the Zoho error details in the n8n execution log |
| zoho_api_error | Generic Zoho API error | Check the Zoho error details in the n8n execution log |
Sandbox / Dry Run Mode
Two independent mechanisms to avoid creating real documents during testing:
environment: "sandbox"in the POP API request body — POP API injects_pop_dry_run: trueinto the payload (HMAC-signed, cannot be spoofed). No quota is deducted. Use this for end-to-end testing from any consumer (WooCommerce, Shopify, Postman, LLM).- Dry Run parameter on the node — skips Zoho API calls regardless of the
_pop_dry_runflag. Useful for local development without Zoho credentials.
When dry run is active, the response includes dry_run: true and the full zoho_body that would have been sent to Zoho, allowing you to verify the field mapping before going live.
Contact Resolution
The node searches for an existing Zoho contact before creating a new one, using the strategy configured in Contact Match Strategy:
- VAT → Email → Name (default): searches by VAT/TRN code first, then email address, then company/person name
- Email → Name: skips the VAT lookup (useful if VAT codes are not stored in Zoho)
If multiple contacts match, the node returns a contact_ambiguous error — resolve the duplicate in Zoho and retry.
If no contact is found and Create Contact If Missing is enabled, the node creates a new contact using the transferee_client data from the payload, including billing address and VAT treatment for UAE/GCC accounts.
POP Payload Field Mapping
| POP field | Zoho field |
|-----------|-----------|
| invoice_body.general_data.date | date |
| invoice_body.general_data.currency | currency_code |
| invoice_body.general_data.invoice_number | (informational — Zoho auto-assigns) |
| transferee_client.* | contact lookup / creation |
| order_items[].description | line_items[].name |
| order_items[].unit_price | line_items[].rate |
| order_items[].quantity | line_items[].quantity |
| order_items[].rate | tax lookup by percentage → line_items[].tax_id |
| order_items[].discount_percent | line_items[].discount (percentage only) |
| purchase_order_data.id | reference_number |
| payment_data.terms_payment | payment_terms (days) |
| connected_invoice_data.id | reference_invoice_id (TD04 only) |
Known Limitations (v1)
- Line discount precision — Zoho line items only accept percentage discounts. When the POP payload provides an absolute
discount_amount, the node converts it to a percentage usingdiscount_amount / unit_price × 100. Rounding may produce a small discrepancy (< 0.01%) on the Zoho total. - Contact updates — when an existing contact is found in Zoho, its fields are not updated even if they differ from the POP payload. Update the contact manually in Zoho if needed.
Development
npm install # install dependencies
npx tsc --noEmit # type check only
npm run build # compile to dist/
npm run dev # watch modeLicense
MIT — see LICENSE
Related
- n8n-nodes-pop — POP API node for n8n (FatturaPA, Peppol, VAT validation)
- POP API Documentation
