For Zoho services only


I'm currently part of a wider delivery team at Ascent Business Solutions, recognised as a leading Zoho Premium Solutions Partner in the United Kingdom.

Ascent Business Solutions support organisations with everything from targeted technical fixes through to full Zoho CRM implementations and long-term platform adoption. Working as a team rather than a one-person consultancy allows projects to move forward consistently, with access to the right skills at each stage.

The team I manage specialises in API integrations between Zoho and third-party finance and commerce platforms such as Xero, Shopify, WooCommerce, and eBay. Much of our work involves solving integration challenges that fall outside standard documentation, supporting new ideas, new sectors, and evolving business models.

Success is measured through practical outcomes and return on investment, ranging from scaling small operations into high-turnover businesses to delivering rapid gains through online payments, automation, and streamlined digital workflows.

If you are looking for structured Zoho expertise backed by an established consultancy, you can contact Ascent Business Solutions on 0121 392 8140 (UK), email info@ascentbusiness.co.uk, or visit https://www.ascentbusiness.co.uk.

Zoho Books

ZohoBooks: Broken UK Banking Feed

What?
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: Truncate number to 2 decimal points without Rounding

What?
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
ZohoCRM to ZohoBooks API: Truncate number to 2 decimal points without Rounding - Zoho CRM Line Item

ZohoBooks Line Item
ZohoCRM to ZohoBooks API: Truncate number to 2 decimal points without Rounding - Zoho Books Line Item

How?
Zoho Books: Estimates/Quotes: Accept & Decline Buttons on Template

What?
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

What?
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.

Humanoid robot looking up some details from a book

What?
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

What?
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):

ZohoBooks: Error 7008: There are no contact persons associated with this Invoice

What?
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

What?
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.

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