Zoho
Zoho Books and Zoho Invoice (cloud, API v3)
The merchant's 2 to 3 hours are: gathering the data from step 1, creating the Self Client and generating the code (30 minutes if all goes well), deciding with the accountant on the "Banking" account and the payment_mode (30 to 60 minutes, and it is usually what takes longest because you have to wait for the accountant), configuring the outgoing workflow rule (30 minutes) and accompanying the tests (1 hour). The 14 to 22 development hours are: middleware with encrypted storage of the refresh token and automatic renewal (3 to 4 h), looking up the invoice by number and building the customerpayment (3 to 4 h), public endpoint with HMAC validation, timestamp control and fast response (3 to 4 h), polling fallback process with idempotency (2 to 3 h), handling data centers, 401 and 429 errors with retries (2 h), and end-to-end testing plus deployment (2 to 3 h). Add 4 to 6 hours if the merchant handles multiple currencies, because exchange_rate and the exchange-difference accounts come into play. Add another 6 to 10 hours if instead of a Self Client you have to do the "Server-based Applications" flow with a consent screen to connect several merchants. If someone promises you this in 4 hours, either they already have it built from another client, or they are not counting the polling fallback or the signature validation, which is exactly where money gets lost later.
Who does it
Shared work, and there is no 100% programmer-free path. The owner or the organization's administrator does everything that happens inside Zoho: creating the Self Client in the Zoho API Console, generating the code with the scopes, copying the Client ID, the Client Secret and the organization_id. That is clicks, not code, and it takes them a couple of hours. The accountant defines two business matters: which "Banking" account the payment gets deposited into and which payment_mode is used (usually "banktransfer" or "others"), because whether the reconciliation balances depends on that. But the integration itself needs an implementation partner or a programmer, no exceptions: Zoho Books and Zoho Invoice cannot receive ROKI's signed webhook or validate the HMAC signature on their own, so someone has to build and maintain a middleware. If someone sells you the idea that this can be solved with Zoho Flow alone and zero code, be suspicious: you can receive the notification, but you cannot verify the signature with any guarantees.
What has to exist before you start
Where: "Settings" > "Organization Profile"
The OAuth scopes and the base URL change: ZohoBooks.* against https://www.zohoapis.com/books/v3, or ZohoInvoice.* against https://www.zohoapis.com/invoice/v3. If the organization has Books contracted, use the Books scopes even if the invoicing screens look like Invoice. Copying a Books scope into an Invoice organization returns an authorization error and hours get lost hunting for where the failure is.
Where: Dropdown menu with the organization's name, top right > "Manage Organizations"
It goes as a query parameter in ALL API calls, without exception. It can also be obtained with GET /organizations.
Where: The browser's address bar while inside Zoho: books.zoho.com, books.zoho.eu, books.zoho.in, books.zoho.com.au, books.zoho.jp, books.zoho.sa, books.zohocloud.ca
It defines which accounts server the tokens are generated against and which domain the API is called on. It is the number one cause of broken Zoho integrations.
Where: "Settings" > "Users & Roles" (I did not find this label confirmed in the official documentation I read; verify how it appears on your screen)
The refresh token inherits the permissions of the user who generated it. If you generate the token with an employee's personal account and that account gets deactivated when they quit, the integration dies that very day and nobody knows why.
Where: Left side menu, "Banking" module
It is the payment's account_id field. It is optional in the API, but if you do not send it Zoho assigns a default account and the accountant finds out two months later, when the reconciliation does not balance. Define it before the first test.
Where: Left side menu, "Sales" > "Invoices" (the automatic numbering setting exists, but I did not verify its exact label in the official documentation)
ROKI returns the external_reference in the webhook and the middleware uses it to find the invoice. Zoho's documentation clarifies that invoice_number has a maximum of 100 characters and must be unique within the organization. If the merchant reuses numbering across series, the payment gets recorded against the wrong invoice.
Where: This is not a Zoho screen: it is the firewall of the middleware's server
All communication with Zoho is outbound over HTTPS. No inbound port has to be opened at the merchant's office, because Zoho is cloud and nothing is installed there. The only thing that needs to be reachable from the internet is the middleware, so that ROKI can hit it with the webhook.
The steps
- 1. Confirm product, data center and organization_id before touching anything
Go into the organization and write down three pieces of data on paper: (a) whether it is Books or Invoice, looking at "Settings" > "Organization Profile"; (b) the domain that appears in the browser (books.zoho.com, books.zoho.eu, books.zoho.in, etc.); (c) the organization_id, from the dropdown of the organization's name > "Manage Organizations". With those three pieces of data you build the pair of URLs you will use for the rest of the guide. Example for the United States (.com): accounts server https://accounts.zoho.com and API https://www.zohoapis.com/books/v3. For Europe: https://accounts.zoho.eu and https://www.zohoapis.eu/books/v3. For India: https://accounts.zoho.in and https://www.zohoapis.in/books/v3. Australia: accounts.zoho.com.au and www.zohoapis.com.au. Japan: accounts.zoho.jp and www.zohoapis.jp. Saudi Arabia: accounts.zoho.sa and www.zohoapis.sa. Canada is the odd exception: the accounts server is https://accounts.zohocloud.ca (not accounts.zoho.ca) while the API is https://www.zohoapis.ca/books/v3.
- 2. Create the client in the Zoho API Console (and understand what the Self Client is)
With the administrator user open in the browser, go to https://api-console.zoho.com/ and click "GET STARTED". You will see the list of client types: "Client-based Applications", "Server-based Applications", "Mobile-based Applications", "Non-browser Applications" and "Self Client".
What the Self Client is: it is an OAuth client with no redirect URL and no consent screen. It is designed for a background program that runs on a server and always works against the SAME Zoho account. You authorize yourself from the console and take the code with you.
When it makes sense: in this case, almost always. One merchant, one Zoho Books organization, one ROKI Connect middleware. It is the shortest path and the one that breaks the least.
When it does NOT make sense: if ROKI Connect is going to connect many different merchants with a single piece of software, or if the merchant does not want its token to depend on one particular user. In that case you have to register "Server-based Applications", fill in "Homepage URL" and "Authorized Redirect URLs", and have each merchant go through the consent screen. It is more work but it scales.
For the Self Client: hover the mouse over the "Self Client" option and click "CREATE NOW", then "CREATE" and then "OK". The Client ID and the Client Secret are shown in the "Client Secret" tab. Copy them to a safe place; the secret is a password, do not send it over WhatsApp.
- 3. Enable the data centers you are going to use
If the merchant is in a data center different from yours, or if you are going to serve merchants in several, it has to be enabled. In the API Console, select the application from the list of applications and go to the "Settings" tab. There you enable the switch for each data center you need. The documentation clarifies that the Client ID is common to all data centers, but the Client Secret can be common or different per data center depending on what you choose. The official documentation I read describes the switch but does not give the exact label of each control, so I will not invent it for you: it looks like a list of data centers with a switch next to each one.
- 4. Generate the code with the minimum scopes
Inside the Self Client, go to the "Generate Code" tab. Enter the scopes separated by commas, a description in "Scope Description", choose the "Time Duration" value (it is the code's validity, in minutes) and click "CREATE".
Minimum scopes for Zoho Books, which is exactly what ROKI Connect needs:
ZohoBooks.invoices.READ,ZohoBooks.customerpayments.CREATEThe first one is for finding the invoice by its number and getting its invoice_id and customer_id. The second one is for recording the payment. Nothing more. Do not request ZohoBooks.fullaccess.all or the .ALL of each module: if that token leaks, with fullaccess they empty out your accounting.
Optional, only if you truly need them: ZohoBooks.customerpayments.READ (to check before creating and avoid duplicating payments, recommended), ZohoBooks.settings.READ (to list organizations with GET /organizations), ZohoBooks.banking.READ (to read the deposit account's id a single time; afterwards you leave it fixed in the configuration and can remove the scope).
For Zoho Invoice the names change prefix:
ZohoInvoice.invoices.READ,ZohoInvoice.customerpayments.CREATE
(the Invoice documentation lists them written as ZohoInvoice.customerpayments.Create, ZohoInvoice.customerpayments.READ, etc.)The code appears on screen only once. Copy it NOW and move on to the next step without going for coffee: it expires in the minutes you chose and can be used only once.
- 5. Exchange the code for an access token and refresh token
This is done by whoever is programming, from a terminal, against the accounts server of the correct data center. It is a POST to {Accounts_URL}/oauth/v2/token with these parameters: grant_type=authorization_code, client_id, client_secret and code (the code from the previous step). In the web flow redirect_uri goes along too; in Self Client there is no registered redirect URL, so try first without that parameter.
Example with the .com data center:
curl -X POST https://accounts.zoho.com/oauth/v2/token -d "grant_type=authorization_code" -d "client_id=1000.XXXX" -d "client_secret=YYYY" -d "code=1000.ZZZZ"The response brings access_token, refresh_token, expires_in (3600 seconds, that is, one hour) and api_domain. Save the api_domain Zoho returns and use it as the base for the calls instead of writing the domain by hand: it is the simplest way not to get the data center wrong.
If the exchange fails with an invalid code error, 90% of the time it is one of two things: you hit the wrong accounts server (you generated the code in .eu and exchanged it in .com), or the code already expired.
- 6. Store the refresh token and renew the access token every hour
The access token lasts one hour. The refresh token does not expire as long as it is not revoked. Renewing is a POST to {Accounts_URL}/oauth/v2/token with grant_type=refresh_token, client_id, client_secret and refresh_token. It returns a new access_token.
The refresh token is stored encrypted on the middleware's server, never in the source code, in a repository, or in a shared spreadsheet. The correct way to program it is: request a new access token when it is close to expiring, or when the API responds 401, retry a single time and move on. Do not renew on every call.
An important and silent trap: Zoho allows a maximum of 20 refresh tokens per user, and when that number is exceeded it automatically deletes the oldest one. If the implementer keeps generating test tokens with the same account, one day they accidentally delete the token that was in production and the integration goes down without any code change. Use a separate service account for testing.
- 7. Test reading: find the invoice by its number
Before touching money, test reading. Zoho's authorization header is NOT Bearer: it is Authorization: Zoho-oauthtoken <access_token>. The official examples in the customerpayments documentation show it that way.
curl --request GET --url 'https://www.zohoapis.com/books/v3/invoices?organization_id=10234695&invoice_number=FAC-001' --header 'Authorization: Zoho-oauthtoken 1000.41d9xxxx.8fccxxxx'
The invoices API accepts filters such as invoice_number (and its variants invoice_number_startswith and invoice_number_contains), customer_id, status and search_text. From the response you care about invoice_id, customer_id, total, balance and status. Save the invoice_id: it is what you need to record the payment. The external_reference you send to ROKI has to be the invoice_number ("FAC-001"), which is what a human recognizes, not the internal invoice_id.
- 8. Create the payment in ROKI Connect
When the invoice is issued or sent, the middleware calls ROKI:
POST https://aura.roki.systems/api/connect/v1/payments
Authorization: Bearer sk_test_... (sk_live_... only once you are done testing)
{ "amount": 1500.00, "external_reference": "FAC-001", "name": "Factura 001" }The amount goes in lempiras with decimals, NEVER in cents. This is a classic mistake when the programmer comes from other gateways: if you send 150000 thinking in cents, you are charging the customer one hundred and fifty thousand lempiras. Zoho also handles amounts as decimals (the amount field is a double), so the value passes through as-is, without multiplying or dividing.
The external_reference has to be the invoice_number, and ROKI responds 201 with { id, checkout_url, transaction_id: null }. Save ROKI's id next to Zoho's invoice_id in a middleware table: you will need it for the fallback in step 11.
What triggers this call: the clean option is an outgoing workflow rule in Zoho. Go to "Settings" > "Automation" > "Workflow Actions" > "Webhooks" > "+ New Webhook", enter the middleware's URL and the method (POST is the default; it also accepts PUT and DELETE), and then associate that webhook with a rule in "Settings" > "Automation" > "Workflow Rules" > "+ New Workflow Rule" on the invoices module. Watch out for the documented limits: only 1 webhook per workflow rule, and a maximum of 500 webhooks fired per day. If the merchant invoices more than that, use periodic polling instead of the outgoing webhook.
- 9. Get the checkout_url to the customer
Zoho does not have a native field for an external payment link, so there are two honest paths. The easy one: the middleware sends the checkout_url by WhatsApp or email from the ROKI side or from the middleware itself, and Zoho never knows. The tidy one: create a custom text field on the invoice and write the checkout_url there, which requires the ZohoBooks.invoices.UPDATE scope, which was not in the minimum list. Decide this before generating the code in step 4, because adding a scope afterwards forces you to regenerate the code and the tokens from scratch.
- 10. Record the payment: the customerpayment object
When the payment.approved event arrives, the middleware looks up the invoice by invoice_number using the external_reference, and creates the payment:
POST https://www.zohoapis.com/books/v3/customerpayments?organization_id=10234695
Authorization: Zoho-oauthtoken <access_token>
Content-Type: application/json{
"customer_id": "460000000012345",
"payment_mode": "banktransfer",
"amount": 1500.00,
"date": "2026-08-16",
"reference_number": "ROKI-<roki_payment_id>",
"description": "ROKI Connect payment, transaction <transaction_id>",
"account_id": "460000000067890",
"invoices": [ { "invoice_id": "460000000054321", "amount_applied": 1500.00 } ]
}Required scope: ZohoBooks.customerpayments.CREATE. It responds 201 with the complete payment object.
Mandatory fields according to the documentation: customer_id, payment_mode, amount, date (yyyy-mm-dd format) and the invoices array with at least one object carrying invoice_id and amount_applied. Allowed values of payment_mode: check, cash, creditcard, banktransfer, bankremittance, autotransaction, others. For a payment through a ROKI link the reasonable choice is banktransfer or others; let the accountant decide that, not the programmer.
Useful optionals: reference_number (maximum 100 characters, put ROKI's id in it so you can trace it later), description, account_id (the "Banking" account where the money lands), bank_charges if ROKI deducts a fee, and exchange_rate (default 1) if the invoice is in a currency other than the organization's base currency.
Golden rule of amount_applied: the sum of the amount_applied values cannot exceed the amount. If the customer paid too much, Zoho treats it as an advance and the accountant is going to call you. If they paid too little, the invoice remains partially paid, which is the correct behavior.
For Zoho Invoice it is the same, changing the base to https://www.zohoapis.com/invoice/v3/customerpayments and the scope to ZohoInvoice.customerpayments.CREATE.
- 11. Build the polling fallback, which is not optional
Webhooks get lost: the middleware goes down, a certificate expires, the hosting provider reboots. The middleware has to have a process that every 10 or 15 minutes goes through the ROKI payments created in the last 48 hours that still show as unrecorded and queries GET /payments/{id} against ROKI. If the status is approved and there is no payment in Zoho, it creates it.
For that process not to duplicate payments you need idempotency. The simplest way: before creating, search Zoho for a customerpayment whose reference_number is ROKI-<id>, and if it exists, do nothing. That requires the ZohoBooks.customerpayments.READ scope. The alternative without that scope is to store in the middleware's database the payment_id Zoho returned and trust that table, which works until someone restores an old backup.
Watch out for Zoho's limits when programming the frequency: 100 requests per minute per organization, and per day 1,000 on the free plan, 2,000 on Standard, 5,000 on Professional and 10,000 on Premium, Elite and Ultimate. Exceeding them returns HTTP 429. A badly written process that polls every minute eats up the daily quota of a free plan before noon.
- 12. Test end to end with sk_test_ and only then move to production
Order of the test: issue a small real invoice in Zoho, let the workflow trigger the middleware, verify that ROKI returned 201 with a checkout_url, pay from a phone, and confirm that the payment appears in Zoho against the correct invoice and in the correct "Banking" account. Have the accountant look at that first payment before a hundred come in.
Also test the ugly path: turn off the middleware, make a payment, turn it back on and verify that the polling fallback recovers it. If that does not work, you are not ready for production.
For reversals: if ROKI voids the payment before settlement, the payment in Zoho has to be deleted or voided by hand or through the API; if the refund comes later, the correct accounting treatment is normally a credit note, not deleting the payment. Define this with the accountant before starting, because deleting payments from already-closed periods is an audit problem, not a software problem.
Only then do you change sk_test_ to sk_live_.
How the webhook arrives
Short answer: neither Zoho Books nor Zoho Invoice can receive ROKI's webhook. You have to put a middleware in between, and that is non-negotiable.
The details. Zoho Books and Zoho Invoice do have webhooks, but they go in the opposite direction from the one you need. In "Settings" > "Automation" > "Workflow Actions" > "Webhooks" > "+ New Webhook" you configure an external URL and Zoho hits it when something happens inside (an invoice is created, a status changes). That is OUTGOING: Zoho calls your server. It works perfectly for step 8, to find out there is a new invoice and create the payment in ROKI. It does not work for step 10, because there is no URL in Zoho that accepts an arbitrary POST from you and turns it into a payment.
And even if one existed, you would still have the signature problem. ROKI sends the ROKI-Signature header with t=
So, the real architecture that works: ROKI calls a public HTTPS URL on the middleware (for example https://connect.elcomercio.hn/roki/webhook), the middleware validates the HMAC signature, checks that the timestamp is no more than a few minutes old to cut off replays, responds 200 quickly, and only then calls the Zoho API with the OAuth token to create the customerpayment. That middleware is the only thing that has to be reachable from the internet. It can live on a 5-dollar-a-month VPS, in a serverless function, or on the same server where something else of the merchant's already runs. What matters: HTTPS with a valid certificate, not self-signed.
About the merchant's office: the classic problem of the ERP installed on an office PC without a public IP does not apply here, because Zoho is pure cloud, there is nothing installed. The merchant does not have to open a single inbound port in their firewall or ask their internet provider for a fixed IP. The entire conversation with Zoho is outbound over HTTPS on port 443. The only component exposed to the internet is the middleware, and that one is hosted by the implementer, not the merchant.
The temptation of Zoho Flow, and why I look at it with distrust. Zoho Flow has a Webhook-type trigger that generates a unique public URL for you, receives JSON, and can then execute a Zoho Books action such as creating a payment. On paper, an integration with no server of your own. In practice there are two holes. First, to validate the signature you need the ROKI-Signature header and the raw, unmodified body; Flow hands you the payload already interpreted as fields, and I have not confirmed in the official documentation that it exposes the header and the original body intact. If it does not expose them, signature validation is impossible and you would be accepting any call to that URL as valid, which is an open invitation for someone to mark your invoices as paid. Second, even if it did expose them, the calculation has to be done in a Deluge custom function with zoho.encryption.hmacsha256(key, data, "hex"), and that is already programming, just in a strange language and with worse debugging than any server of your own.
If you go with Flow anyway, the only defensible way is: treat the webhook as a mere notification ("something happened with payment X"), ignore the entire content of the body, and have the flow query GET /payments/{id} against ROKI with the secret key to confirm the amount and the status from the source. At that point the signature stops being critical because you do not believe anything the caller told you. It is slower and spends an extra call, but it is honest.
What breaks
The data center. It is the number one reason half of Zoho integrations never get off the ground. A token generated on accounts.zoho.eu does not work against www.zohoapis.com, and the error it returns does not say 'wrong data center', it says you are not authorized, so the programmer spends half a day reviewing scopes that were fine. Practical rule: never write the domain by hand; use the api_domain that Zoho returns in the token response. And watch out for Canada, which is the exception: the accounts server is accounts.zohocloud.ca but the API is www.zohoapis.ca.
The authorization header is NOT Bearer. Zoho uses Authorization: Zoho-oauthtoken <access_token>. Anyone coming from Stripe or another gateway writes Bearer by reflex and takes a 401 with no explanation.
The Self Client code lasts minutes and can be used only once. If you generate it, go out for lunch and come back, you have to generate it again. The web flow's authorization code lasts 2 minutes.
The limit of 20 refresh tokens per user. When it is exceeded, Zoho deletes the oldest one automatically and without warning. The oldest one is usually exactly the one in production. Never use the same Zoho account to generate test tokens and the production token.
The token dies with the user. The refresh token inherits the permissions of the user who generated it. If you generated the token with the accountant's account and the accountant leaves and their user gets deactivated, the integration goes down that day. Use a dedicated service account.
organization_id goes in ALL calls, as a query parameter. Forgetting it is the second most frequent mistake after the data center.
Books and Invoice do not share scopes or base URL. ZohoBooks.customerpayments.CREATE against /books/v3, ZohoInvoice.customerpayments.CREATE against /invoice/v3. If the organization has Books contracted, the Books scopes apply even if the screens look like Invoice to you.
ROKI's amount goes in lempiras with decimals, NEVER in cents. Zoho also uses decimals. Do not multiply or divide by 100 at any point along the way, and write an automated test that verifies it, because this mistake is only discovered when you charge a real customer a hundred times too much.
The sum of the amount_applied values cannot exceed the payment's amount. If it does, Zoho rejects it or generates an advance, and money shows up hanging that the accountant does not know where it came from.
Not sending account_id. The payment lands in a default account and the bank reconciliation does not balance. Define the "Banking" account with the accountant BEFORE the first test, not after.
The scopes are defined when the code is generated, not afterwards. If in week 3 you realize you needed ZohoBooks.invoices.UPDATE to write the checkout_url into a custom field, code and tokens have to be redone from scratch. Think through the complete list in step 4.
Requesting ZohoBooks.fullaccess.all because it is faster. That token stored on a badly configured server is total access to the merchant's accounting. Two scopes are enough: invoices.READ and customerpayments.CREATE.
The API limits. 100 requests per minute per organization, and per day 1,000 on the free plan, 2,000 on Standard, 5,000 on Professional and 10,000 on Premium, Elite and Ultimate. A fallback process that polls every minute eats up the daily quota of a free plan before noon and returns HTTP 429.
The limits of Zoho's outgoing webhooks: only 1 webhook per workflow rule, and a maximum of 500 webhooks fired per day. If the merchant invoices more than that, the workflow trigger will not work for you and you have to poll periodically for new invoices.
Believing that Zoho can receive ROKI's webhook. It cannot. Without a middleware there is no integration, and whoever tells you otherwise did not read the part about the HMAC signature.
The external_reference has to be the invoice_number, not the invoice_id. The invoice_id is an internal number that no human recognizes; if something fails, the merchant needs to be able to search for 'FAC-001' and find the payment.
Invoice numbering repeated across series or branches. Zoho requires invoice_number to be unique within the organization, but if the merchant migrated data or uses odd prefixes, verify beforehand that the search by number returns exactly one invoice and not two.
Multi-currency. If the invoice is not in the organization's base currency, you have to send exchange_rate (the default is 1) and agree with the accountant on which exchange rate is used. Ignoring it generates cent-level differences that nobody can explain later.
Voids and refunds. ROKI voids before settlement and refunds afterwards. In Zoho that means deleting the payment or issuing a credit note, and those are different things accounting-wise. Define the criterion with the accountant before starting; deleting payments from a closed period is an audit problem.
What we could not confirm
Every menu name and port on this page was checked against the platform's official documentation. These 11 points could not be confirmed, and are flagged rather than presented as facts:
- Step 3: "In the API Console, select the application from the list of applications and go to the 'Settings' tab. There you enable the switch for each data center - does-not-exist. MOST SERIOUS FINDING. The Settings tab with the multi-DC switches does NOT exist for the Self Client. Zoho's official doc says literally: "This is available for all client types, except the Self Clien
- Prerequisite 1 and step 1: the path is "Settings" > "Organization Profile". - different-name. The menu item is called "Profile", inside the "Organization" section. The official doc (zoho.com/us/books/help/settings/organization-profile.html) says: "Navigate to Settings in the top-right corner a
- Prerequisite 2 and step 1: "Dropdown menu with the organization's name, top right > 'Manage Organizations'". - different-name. The dropdown option is called "Manage", not "Manage Organizations". The official doc (zoho.com/us/books/kb/general/manage-multiple-organizations.html) says: click the organization's name at the top ri
- Step 2: for the scalable application registration you have to fill in "Homepage URL" and "Authorized Redirect URLs". - different-name. "Homepage URL" is confirmed. The other field is URI, not URL: zoho.com/accounts/protocol/oauth-setup.html labels it "Authorized Redirect URI" (singular) and the CRM v8 doc labels it "Redirect URIs". T
- Step 4: "Enter the scopes separated by commas, a description in 'Scope Description'". - different-name. The label "Scope Description" does not appear in any official doc I was able to read. The Zoho Books OAuth doc labels the Generate Code tab fields as "Scope", "Time Duration" and "Description"; zoho.c
- Step 4: "Do not request ZohoBooks.fullaccess.all". - not-verifiable. The ZohoBooks.fullaccess.all scope does NOT appear in the official scopes table at zoho.com/books/api/v3/oauth/#scopes. That table only lists scopes per module with CREATE/READ/UPDATE/DELETE/ALL opera
- Prerequisite 3 and step 1: the list of data center domains includes "books.zohocloud.ca" for Canada. - not-verifiable. There is a conflict between Zoho sources. The domains table in the Zoho Books doc (v4, introduction) lists the Canadian app domain as books.zoho.ca; the v3 introduction only lists the ".ca" suffix wit
- Prerequisite 7: "Outbound HTTPS on port 443 ... to *.zohoapis.* and to accounts.zoho.*". - different-name. The accounts.zoho.* pattern does NOT cover accounts.zohocloud.ca, which is Canada's accounts server and which the document itself names correctly in step 1. A firewall rule written that way breaks the
- Prerequisite 3 and step 1: the data center list is books.zoho.com, .eu, .in, .com.au, .jp, .sa, zohocloud.ca. - different-name. China is missing. The official Zoho Books doc lists the China DC with the .com.cn domain and endpoint https://www.zohoapis.com.cn/books/v3. The Zoho Invoice doc also lists it (https://www.zohoapis.com
- Step 4: for Zoho Invoice the scopes are ZohoInvoice.invoices.READ, ZohoInvoice.customerpayments.CREATE (with a note that the doc writes them .Create). - different-name. The document's warning is correct and it is worth strengthening it. The official doc (zoho.com/invoice/api/v3/oauth/) literally writes: "ZohoInvoice.customerpayments.Create, ZohoInvoice.customerpaymen
- Prerequisite 6: the automatic invoice numbering setting (the document already flags it as unverified). - not-verifiable. Correct to leave it flagged as doubtful: I did not find the exact label of the automatic numbering screen in the official Zoho Books doc. Keep the hedge.
Sources
- https://www.zoho.com/books/api/v3/oauth/
- https://www.zoho.com/books/api/v3/introduction/
- https://www.zoho.com/books/api/v3/customer-payments/
- https://www.zoho.com/books/api/v3/invoices/
- https://www.zoho.com/invoice/api/v3/oauth/
- https://www.zoho.com/invoice/api/v3/introduction/
- https://www.zoho.com/accounts/protocol/oauth/multi-dc.html
- https://www.zoho.com/accounts/protocol/oauth/self-client/overview.html
- https://www.zoho.com/us/books/help/settings/automation/workflow-actions/webhooks.html
- https://www.zoho.com/us/books/help/settings/automation.html
- https://www.zoho.com/us/books/help/settings/organization-profile.html
- https://www.zoho.com/deluge/help/encryption/hmac-sha256.html
