How to merge subform fields and image field via Zoho Writer's Merge APIs?

How to merge subform fields and image field via Zoho Writer's Merge APIs

Overview

Zoho Writer's Merge API enables the dynamic population of documents using data from Zoho CRM. This includes merging not only standard fields but also related list (subform) fields and image fields. This guide outlines the steps required to merge subform and image fields into Writer templates using both Deluge scripts and API calls.

Steps

  1. Open Zoho Writer and create a new merge template.
  2. Insert merge fields for both main module and related list (subform) fields using the field insert option.
Example Template Content:

Hello <<Contacts.First Name>>
Send a mail to <<Contacts.Email>>

Invoice Owner
Invoice Number
Created By
Due Date
<<Invoices.Invoice Owner>>
<<Invoices.Invoice Number>>
<< Invoices. Created By>>
<<Invoices.Due Date>>

Product_Code

Product_Description

Item_Quantity

Item_Price

 

«Products.Product_Code»

 

«Products.Product_Description»

 

«Products.Item_Quantity»

 

«Products.Item_Price»

Step 2: Retrieve RL Field Information via API  

Use the following Deluge code to retrieve all merge fields from your template:
fields = invokeurl
[
type: GET
connection:"<CONNECTION_NAME>"
];
info fields;
Notes
  • Replace <DOCUMENT_ID> with your actual Writer document ID.

  • Reference Zoho Connections for setting up the connection.

Step 3: Map RL Fields via Deluge Script  

invoicesRecords = zoho.crm.getRecordById("Invoices", invoiceId);

 

CustomerInvoice = Map();

CustomerInvoice.put("First_Name", invoicesRecords.get("Account_Name").get("name"));

CustomerInvoice.put("Email", invoicesRecords.get("Account_Name").get("id"));

 

invoicesList = list();

invoiceDetails = Map();

 

invoiceDetails.put("Invoices.Owner", invoicesRecords.get("Owner").get("name"));

invoiceDetails.put("Invoices.Invoice_Number", invoicesRecords.get("Invoice_Number"));

invoiceDetails.put("Invoices.Created_By", invoicesRecords.get("Created_By").get("name"));

invoiceDetails.put("Invoices.Due_Date", invoicesRecords.get("Due_Date"));

invoicesList.add(invoiceDetails);

 

productList = list();

productDetails = Map();

productDetails.put("Products.Product_Code", invoicesRecords.get("Product_Code"));

productDetails.put("Products.Product_Description", invoicesRecords.get("Product_Description"));

productDetails.put("Products.Item_Quantity", invoicesRecords.get("Item_Quantity"));

productDetails.put("Products.Item_Price", invoicesRecords.get("Item_Price"));

productList.add(productDetails);

 

 

CustomerInvoice.put("Invoices", invoicesList);

CustomerInvoice.put("Products", productList)

 

 

mergedata = Map();

mergedata.put("merge_data", {"data": CustomerInvoice});

mergedata.put("subject", "Invoice Details");

 

//optional

mergedata.put("message", "Please find your invoice attached.");

info mergedata;

 

zoho.writer.mergeAndSend("eb4kob4cf65bb6d074af7a7de21e561119eb9", "pdf", invoicesRecords.get("email"), mergedata, "<CONNECTION_NAME>");

Step 4: Sample JSON data with RL fields (via APIs)

{

    "data": [

        {

            "First_Name": "Amelia",

            "Email": "amelia@zylker.com",

            "Invoices": [

                {

                    "Invoices.Owner": "John",

                    "Invoices.Invoice_Number": 1279,

                    "Invoices.Created_By": "Amelia",

                    "Invoices.Due_Date": "07/10/2020"

                }

            ]

            "Products": [

              {

                "Products.Product_Code": "A123",

                "Products.Product_Description": "Mobile Cases",

                "Products.Item_Quantity": 2,

                "Products.Item_Price": 1000

              }

            ]

        }

    ]


Step 5: Pass Image Field Values in Merge API  

To include image fields in the merge:
  • Invoke Get Record API (version 5 and above) and use the Preview Image ID [Preview_Id__s] from the CRM response.
  • Construct the image URL manually using the below pattern:
Sample values to replace the variables in the snippet shared below:
Info
Module = "Leads";   //Provide your respective module name
RecordId = "456789";   //CRM record id
RLModuleName = "Products";   //Related List Name
zohoapi_domain (for US DC) = "https://www.zohoapis.com";   //For other DCs, refer the API domains here.

For Standard Image Fields  

// get Records
recordInfo = invokeurl
[
url :zohoapi_domain+"/crm/v5/"+Module+"/"+RecordId;
type :GET
connection:"<CONNECTION_NAME>"
];
recordInfoData = recordInfo.get("data").get(0);
dataMap = Map();
dataMap.put("Email",recordInfoData.get("Email"));
dataMap.put("Last_Name",recordInfoData.get("Last_Name"));
// Image field
if(recordInfoData.get("Image_Upload") != null)
{
imageId = recordInfoData.get("Image_Upload").get(0).get("Preview_Id__s");
imageUrl = zohoapi_domain+"/crm/v2.1/__attachment_preview/" + imageId;    //constructed image url 
dataMap.put("Image_Upload",imageUrl);
}
info dataMap;

For Image Fields in Related Lists  

If image field is a RL record, then invoke this Related Records Data API (version 5 and above) to get the RL Record ID. Then, invoke the Get Records API with this RL Record ID to get the Preview Image ID.
dataMap = Map();
 //Get Related List IDs
relatedListsIds = invokeurl
[
url:zohoapi_domain+"/crm/v5/"+Module+"/"+RecordId+"/"+RLModuleName+"?fields=Parent_Id"
type :GET
connection:"<CONNECTION_NAME>"
];
rlListData = relatedListsIds.get("data");
rlProductsList = List();
//Get Related List Data
for each  record in rlListData
{
rlId = record.getJSON("id");
rlInfo = invokeurl
[
url :zohoapi_domain+"/crm/v5/+"RLModuleName+"/" + rlId
type :GET
connection:"crm"
];
rlInfoData = rlInfo.get("data").get(0);
rlMap = Map();
rlMap.put("Product_Name",rlInfoData.getJSON("Product_Name"));
rlMap.put("Product_Code",rlInfoData.getJSON("Product_Code"));
//RL Image Field
if(rlInfoData.get("Image_Upload") != null)
{
rlImageId = rlInfoData.get("Image_Upload").get(0).get("Preview_Id__s");
imageUrl = zohoapi_domain+"/crm/v2.1/__attachment_preview/" + rlImageId;  //constructed image url
rlMap.put("Image_Upload",imageUrl);
}
rlProductsList.add(rlMap);
}
//Add Related List to dataMap
dataMap.put("Products",rlProductsList);
info dataMap;

Additional Notes

  • Always use the /fields API to dynamically retrieve field names for accurate mapping.
  • Ensure that merge field names in the Writer template exactly match the JSON keys used in your API or Deluge script.
  • For all image fields, construct the image URL using the __attachment_preview endpoint:
    https://{zohoapi_domain}/crm/v2.1/__attachment_preview/{Preview_Id__s}
  • The CRM API only provides image Preview IDs, not full URLs — construction is mandatory.
  • Use the correct API versions (v5 for CRM, v2.1 for attachment preview).
  • If using Deluge:
    • Ensure connection is set up.
    • JSON mapping must match the Writer template structure.
  • The Writer Document ID can be found in the URL in this format:
    /writer/open/<document_id>

Troubleshooting

1. Error/Symptom: Images from subforms do not appear, or stop appearing after adding more subform records during mail merge.

Possible cause:
The image field may be empty, incorrectly mapped in the template, or not included correctly in the merge data.
Note: This is not caused by the number of subforms itself.

Recommended solution:
  1. Ensure the merge field name matches the field in your data source exactly.
  2. If you are using the API, retrieve the list of available merge fields using the Get All Fields API to confirm the correct field IDs and subform structure.
  3. Reinsert the merge field if needed.