ZohoBooks: Broken UK Banking Feed
- Category: Zoho Books
- Hits: 16489
This is an article on restoring an automatic banking feed within Zoho Books, specifically within the United Kingdom. The article serves as a store for both a possible resolution as well as findings from the research/investigation into the issue.
Why?
The workaround for a lot of our clients is to do a manual import (CSV) of the transactions for a particular account and then to reconcile these. This particular case covers where the Cx had a working automatic bank feed for a while but at some point simply stopped working.
How?
First let's go through a resolution and get the official response on what impact this has on refreshing the token. Then I'll put all my notes I made along the way.
ZohoCRM to ZohoBooks API: Rounding the cents or pennies when creating an invoice
- Category: Zoho Books
- Hits: 39875
A frustrated article about an issue that took me a fair few hours to resolve. So I'm putting it here as I thought I covered this previously but couldn't find it on my website. This was previously titled something along the lines of truncating to 2 decimals but finance is so much more complicated.
Why?
Playing around with VAT / Tax, inclusive /exclusive, but in this case it is the rate that gets rounded. The numbers at the end still need to match what's in CRM and with ZohoBooks rounding differently can make cent/penny errors into 100s of dollar/pound errors.
Zoho CRM has a fun way of rounding which differs to Zoho Books so when pushing an invoice, there's a strong chance of being a cent/penny off. Let's take the following example of an item with a list price of 12345.545:
ZohoCRM Line Item

ZohoBooks Line Item

How?
Zoho Books: Estimates/Quotes: Accept & Decline Buttons on Template
- Category: Zoho Books
- Hits: 46053
An article on adding an accept and decline button on the estimate (aka Quote) notification template within ZohoBooks.
Why?
The use-case is simply that my client wants to make it easier for their customers to accept or decline a quote. Sure there's a portal and you can probably do it from there but this is a one-click accept or decline then done.
One of the biggest hurdles here, which may sound trivial, was the response when an end customer clicks on either the accept or decline button. Using ZohoFlow or other Zoho app for a webhook response, would result in the end customer suddenly downloading a JSON file. Looked a bit suspicious. The code below opens a new tab in their web-browser displaying a plain output message.
How?
So there are 2 caveats to this solution: 1 is that you will need ZohoCRM. I've tried using the "Incoming Webhooks" feature of ZohoBooks but realised the word "Incoming" is the operative word as the response cannot be configured in ZohoBooks like we need to in this solution; namely the webhook response needs to have a header and a body.
The 2nd caveat is a concern around security. It is difficult to guess a customer's Zoho ID; some might say almost impossible. To use other fields that could be sent via the URL as a verification to be checked at the webhook endpoint would be good as well; but I couldn't spend time finding fields that can be 'placeheld', other than what the interface offers, into the template.
ZohoAnalytics & ZohoBooks: Custom Related List from Analytics
- Category: Zoho Books
- Hits: 40155
A quick article to document 2 features in deluge code: a custom related list in ZohoBooks, and a reminder on how to read a table from ZohoAnalytics.
Why?
My use-case here is that we have a client who uses their purchase orders and sales orders as part of a logistics solution where items are purchased from a supplier, sent to another supplier for grading, and then sent on to the end user/customer.
A custom field against the item record has been added which is a lookup to the Sales Order module. This means that on a purchase order, and per line item, the staff can specify which Sales Order the item relates to.
How?
At time of print, adding the lookup to the line item will automatically display a related list on the Sales Order but does not associate any records... not sure why this is. So we have a workaround, by sending all the data including the custom fields per line items to ZohoAnalytics; and in ZohoAnalytics importing a table based on a query which joins the sales orders and purchase order items by the custom lookup field.
Zoho Books / Inventory: Get Item Rate from a Price Book/List
- Category: Zoho Books
- Hits: 34857
A quick article on how to get the pricebook entry using Zoho Deluge for a specific product in your ZohoBooks or ZohoInventory instance.
Why?
This took me the best part of an hour to determine by going through forum posts from 7 years to 2 years ago. The following will work in May 2024 following the API domain change.
The use-case is that the customer wants the item/product rate taken from the item record, if a pricebook is specified and the item exists within it, then it takes the rate from the pricelist. Note that when I refer to pricebook, this is also referred to as the pricelist... and vice-versa.
And in this use-case, my client has added an incredible number of price books/ price lists and then has in excess of 20k items. Previously, the function that would need to get the price book rate post saving a record in Zoho Deluge would fail because it was making too many function statements.
How?
I have this code triggered in a workflow when the sales order is created to do some further calculations based on a surcharge rate held in a custom module. The custom module could only be created in Zoho Books at time of print but Zoho Inventory is where our workflow and code is sitting right now.
ZohoBooks: Stripe Terminal Integration
- Category: Zoho Books
- Hits: 42874
A quick article on some code added to a button in ZohoBooks off the invoice module to initiate your Stripe terminal to take a payment.
Why?
Just to make it easy on the staff at a counter or on the phone. They bring up the invoie in ZohoBooks, click on the button, and the Stripe terminal will ask for the amount on the invoice.
Well almost. We've gone the extra step in that we added a custom field that can override the full balance, to allow partial payments such as a deposit or instalment.
How?
I won't go in to how to create a button in ZohoBooks but you simply add it to the invoice and then when it prompts for some code, you give it the snippet below.
The Magic
You would create a button for each terminal
/* *******************************************************************************
Function: Map Take_Payment( Map invoice, Map organization, Map user)
Label: Take Payment
Trigger: On button click
Purpose: Preps stripe terminal to take payment for balance of invoice.
Inputs: invoice
Outputs: -
Date Created: 2023-02-24
- Initial release
- Reads Books Invoice and sends the amount to the reader for a payment atempt
Date Modified: 2023-02-24 (Joel Lipman)
- If custom field "Amount To Be Taken" is not greater than zero, then defaults to balance due of invoice.
More Information:
TEST PAYMENT SCENARIOS WITH PHYSICAL TEST CARD
// Send in as payment endings: eg. $100.00 == payment approved
00 Payment Approved
01 Payment Declined // call issuer code
05 Payment Declined Generic
55 Payment Declined Incorrect Pin
65 Payment Declined withdrawal_count_limit_exceeded
75 Pin try exceeded
******************************************************************************* */
v_BooksOrgID = organization.get("organization_id");
//
// some Stripe variables (add your own here)
v_StripeTerminalID = "tmr_ABCDEFGabcdefg";
v_StripeCustomerKey = "sk_live_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ";
V_StripeLocationsEndpoint = " https://api.stripe.com/v1/terminal/locations";
//
// set Stripe header
m_Headers = Map();
m_Headers.put("Authorization","Bearer " + v_StripeCustomerKey);
//
// default to balance due on this Invoice
v_AmountToPay = ifnull(invoice.get("balance"),0);
//
// Get custom Amount to Pay from Invoice
l_CustomFields = invoice.get("custom_fields");
if(l_CustomFields.size() > 0)
{
for each m_CustomField in l_CustomFields
{
if(m_CustomField.get("label") == "Amount To Be Taken")
{
if(m_CustomField.get("value") > 0)
{
v_AmountToPay = m_CustomField.get("value");
}
}
}
}
//
// format to Stripe amount
v_AmountToPay = v_AmountToPay.truncate(2);
v_StripeAmount = v_AmountToPay * 100;
v_StripeAmount = v_StripeAmount.floor();
v_StripeAmount = v_StripeAmount.toNumber();
info v_StripeAmount;
//
// Create payment intent in Stripe
v_PaymentIntentEndpoint = "https://api.stripe.com/v1/payment_intents";
m_Params = Map();
m_Params.put("amount",v_StripeAmount);
m_Params.put("currency","gbp");
//m_Params.put("automatic_payment_methods[enabled]", false);
m_Params.put("payment_method_types[]","card_present");
m_Params.put("capture_method","manual");
v_DescriptionString = "IN: " + invoice.get("invoice_number") + " ID: " + invoice.get("invoice_id");
m_Params.put("description",v_DescriptionString);
// Later Add Code to "customer_id": "123456700000001234567", get customer id then Email
v_BooksCustomerID = invoice.get("customer_id");
r_CustomerDetails = zoho.books.getRecordsByID("contacts",v_BooksOrgID,v_BooksCustomerID,"zbooks");
v_CustomerCheckCode = r_CustomerDetails.get("code");
if(v_CustomerCheckCode == 0)
{
m_ContactDetails = r_CustomerDetails.get("contact");
if(m_ContactDetails != null)
{
m_Params.put("receipt_email",m_ContactDetails.get("email"));
}
}
r_CreatePaymentIntent = invokeurl
[
url :v_PaymentIntentEndpoint
type :POST
parameters:m_Params
headers:m_Headers
];
info "Payment Intent Create";
info r_CreatePaymentIntent;
//
v_CheckObject = ifnull(r_CreatePaymentIntent.get("object"),"-");
v_CheckAmount = ifnull(r_CreatePaymentIntent.get("amount"),"-");
//
// Process Payment Intent
if(v_CheckObject == "payment_intent")
{
v_PaymentIntentID = r_CreatePaymentIntent.get("id");
info "Payment Intent Created Successfuly!!! ID: ";
info v_PaymentIntentID;
//
// Hand Off Payment Intent to Reader
v_ReaderHandOffEndpoint = "https://api.stripe.com/v1/terminal/readers/" + v_StripeTerminalID + "/process_payment_intent";
m_PaymentHandOffParams = Map();
m_PaymentHandOffParams.put("payment_intent",v_PaymentIntentID);
r_ReaderPaymentHandOff = invokeurl
[
url :v_ReaderHandOffEndpoint
type :POST
parameters:m_PaymentHandOffParams
headers:m_Headers
];
info r_ReaderPaymentHandOff;
}
return r_ReaderPaymentHandOff;
The incoming webhook
You now need to receive the Stripe webhook when it comes back into ZohoBooks to record it against the invoice. Note how we added the invoice reference and Zoho ID in the description of the payment capture in our previous bit of code:
/* *******************************************************************************
Function: Map stripe_terminal_payment( Map invoice, Map organization, Map user)
Label: stripe_terminal_payment
Trigger: Incoming Webhook
Purpose: Listens for stripe terminal payments.
OAuth URL: https://www.zohoapis.com/books/v3/settings/incomingwebhooks/iw_stripe_terminal_payment/execute?auth_type=oauth
Inputs: invoice
Outputs: -
Date Created: 2023-03-23
- Initial release
- been successfully proccessed by the physical reader
Date Modified: 2024-05-21 (Joel Lipman)
- Revamp of code as per best practices
- Correct error: Check and update the code in line 74 as there is a Exception : Value is empty and 'get' function cannot be applied
More Information:
Navigate to payments, then find pending webhook response event, terminal.reader.action_succeeded, then view event details
******************************************************************************* */
//
// initialize
m_Blank = Map();
m_Response = Map();
v_BooksOrgID = organization.get("organization_id");
//
// Stripe API Key (add your own here)
v_StripeCustomerKey = "sk_live_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ";
//
// set Stripe header
m_Headers = Map();
m_Headers.put("Authorization","Bearer " + v_StripeCustomerKey);
//
// capture response webhook
m_Webhook = Map();
m_Webhook.put("data",body.get("data"));
m_Webhook.put("type",body.get("type"));
//
// Check Type
m_Data = ifnull(m_Webhook.get("data"),m_Blank);
v_Type = ifnull(m_Webhook.get("type"),"-");
if(v_Type == "terminal.reader.action_succeeded")
{
// Get Payment Intent that needs to be Captured
m_Object = m_Data.get("object");
m_Action = m_Object.get("action");
m_ProcessPaymentIntent = m_Action.get("process_payment_intent");
v_PaymentIntentID = m_ProcessPaymentIntent.get("payment_intent");
//
// Retrieve details on the Payment Intent
v_PaymentIntentEndpoint = "https://api.stripe.com/v1/payment_intents/" + v_PaymentIntentID;
r_PaymentIntentDetails = invokeurl
[
url :v_PaymentIntentEndpoint
type :GET
headers:m_Headers
];
//
// get amount (used to capture payment intent)
v_Amount = ifnull(r_PaymentIntentDetails.get("amount"),0);
v_AmountReceived = ifnull(r_PaymentIntentDetails.get("amount_received"),0);
v_AmountCapturable = ifnull(r_PaymentIntentDetails.get("amount_capturable"),0);
v_CaptureIntentEndpoint = "https://api.stripe.com/v1/payment_intents/" + v_PaymentIntentID + "/capture";
m_CaptureParams = Map();
m_CaptureParams.put("amount_to_capture",v_AmountCapturable);
r_CapturePayment = invokeurl
[
url :v_CaptureIntentEndpoint
type :POST
parameters:m_CaptureParams
headers:m_Headers
];
//
// get card details (we need to store last 4 digits)
v_StripeReference = "";
v_Last4Digits = "";
v_ZB_InvoiceID = 0;
m_Charges = ifnull(r_PaymentIntentDetails.get("charges"),m_Blank);
l_Data = ifnull(m_Charges.get("data"),{});
for each m_Data in l_Data
{
if(m_Data.get("id") != null)
{
v_StripeReference = m_Data.get("id");
}
if(m_Data.get("payment_method_details") != null)
{
m_CardPresent = ifnull(m_Data.get("payment_method_details").get("card_present"),m_Blank);
v_Last4Digits = ifnull(m_CardPresent.get("last4"),"");
}
if(m_Data.get("description") != null)
{
v_ChargeDescription = ifnull(m_Data.get("description"),"");
v_ZB_InvoiceID = v_ChargeDescription.getSuffix("ID: ");
v_ZB_InvoiceID = if(isNumber(v_ZB_InvoiceID),v_ZB_InvoiceID,0).toLong();
}
}
//
// create payment record
if(v_ZB_InvoiceID != 0)
{
//
// get ZohoBooks nominal account for Stripe
v_NominalAccountID = "";
r_ChartOfAccounts = invokeurl
[
url :"https://www.zohoapis.com/books/v3/chartofaccounts?organization_id=" + v_BooksOrgID
type :GET
connection:"zbooks"
];
if(r_ChartOfAccounts.get("chartofaccounts") != null)
{
for each m_NomAccount in r_ChartOfAccounts.get("chartofaccounts")
{
if(m_NomAccount.get("account_name").equalsIgnoreCase("Stripe Clearing"))
{
v_NominalAccountID = m_NomAccount.get("account_id");
}
}
}
//
// retrieve invoice details from ZohoBooks
r_InvoiceDetails = zoho.books.getRecordsByID("invoices",v_BooksOrgID,v_ZB_InvoiceID,"zbooks");
m_Invoice = ifnull(r_InvoiceDetails.get("invoice"),m_Blank);
if(m_Invoice.get("customer_id") != null)
{
m_CreatePayment = Map();
m_CreatePayment.put("customer_id",m_Invoice.get("customer_id"));
m_CreatePayment.put("payment_mode","In Person - Card");
m_CreatePayment.put("amount",v_AmountReceived / 100);
m_CreatePayment.put("date",zoho.currentdate.toString("yyyy-MM-dd"));
//
l_Invoices = List();
m_ThisInvoice = Map();
m_ThisInvoice.put("invoice_id",v_ZB_InvoiceID.toString());
m_ThisInvoice.put("amount_applied",v_AmountReceived / 100);
l_Invoices.add(m_ThisInvoice);
m_CreatePayment.put("invoices",l_Invoices);
m_CreatePayment.put("invoice_id",v_ZB_InvoiceID.toString());
m_CreatePayment.put("amount_applied",v_AmountReceived / 100);
//
v_PaymentRef = if(v_StripeReference=="", m_Invoice.get("invoice_number"), v_StripeReference);
m_CreatePayment.put("reference_number",v_PaymentRef);
m_CreatePayment.put("account_id",v_NominalAccountID);
//info m_CreatePayment;
//
r_CreatePayment = zoho.books.createRecord("customerpayments",v_BooksOrgID,m_CreatePayment,"zbooks");
//info r_CreatePayment;
if(r_CreatePayment.get("message") != null)
{
if(r_CreatePayment.get("message").contains("payment has been created"))
{
//
// update the invoice (request by client to store last 4 digits on invoice)
l_CustomFields = List();
m_CustomField = Map();
m_CustomField.put("api_name","cf_last_4_digits");
m_CustomField.put("value",v_Last4Digits);
l_CustomFields.add(m_CustomField);
m_UpdateInvoice = Map();
m_UpdateInvoice.put("custom_fields",l_CustomFields);
r_UpdateInvoice = zoho.books.updateRecord("Invoices",v_BooksOrgID,v_ZB_InvoiceID,m_UpdateInvoice,"zbooks");
}
}
}
}
}
m_Response.put("message",r_CreatePayment.get("message"));
m_Response.put("code",0);
return m_Response;
Source(s):
- Stripe Dashboard - Developers - API Keys
- Stripe Docs - Decline codes - Learn about decline codes and how to resolve them when a charge fails.
Zoho Books: Error 7008: There are no contact persons associated with this Invoice
- Category: Zoho Books
- Hits: 29203
A quick article on how to get around this error.
Why?
The use-case here is that I am using the send invoice API and I kept getting this error despite including the customer_id and there was a primary contact on the customer record.
This is from within ZohoCreator, requesting for an invoice in ZohoBooks to be sent.
How?
So the key here is that there were no contact persons on the customer record. Instead, when building the invoice, this had to be added (don't ever remember having to do this) as contact_ids (array/list). Instead the below script will show you how to use this API having a few email addresses as the recipients:
Zoho Books: System Values Maps
- Category: Zoho Books
- Hits: 26095
A collection of code snippets I seem to be regularly using to generate a dynamic map of system values held in a ZohoBooks instance.
Why?
Rather than hard-coding and having a ton of if..then statements, I can feed these maps a textual value and it returns the ID to use. Some of these can be found elsewhere in my site but I'm putting all of them here just for quick reference.
How?
Note that for the below, I recently updated this article (2024-05-21) due to the API domain name change from https://books.zoho.com to https://www.zohoapis.com/books and changed the Top Level Domain (or Zoho DataCenter) from EU to COM as I was using this for a client on the US datacenter.
Recent Posts
Joes Word Cloud
Accreditation
Donate & Support
If you like my content, and would like to support this sharing site, feel free to donate using a method below:

bc1qf6elrdxc968h0k673l2djc9wrpazhqtxw8qqp4
0xb038962F3809b425D661EF5D22294Cf45E02FebF
Paypal:

Bitcoin:
bc1qf6elrdxc968h0k673l2djc9wrpazhqtxw8qqp4
Ethereum:
0xb038962F3809b425D661EF5D22294Cf45E02FebF
Credit where Credit is Due:
Feel free to copy, redistribute and share this information. All that we ask is that you attribute credit and possibly even a link back to this website as it really helps in our search engine rankings.
Disclaimer: Please note that the information provided on this website is intended for informational purposes only and does not represent a warranty. The opinions expressed are those of the author only. We recommend testing any solutions in a development environment before implementing them in production. The articles are based on our good faith efforts and were current at the time of writing, reflecting our practical experience in a commercial setting.
Thank you for visiting and, as always, we hope this website was of some use to you!
Kind Regards,
Joel Lipman
www.joellipman.com

