I'm building a simple widget to export Contact data to CSV in Zoho CRM, triggered via a custom button. The widget uploads cleanly, appears in the widget list, and is successfully assigned to a Contact detail view via a custom button. But when clicked, it displays a “Page Not Found” error instead of loading the index.html.
ZIP File Structure (Flat ZIP):
files.zip
├── index.html
├── script.js
└── manifest.json
manifest.json:
{
"platform": "CRM",
"widgetType": "web",
"name": "Contact CSV Export",
"location": {
"Contacts": {
"button": {
"label": "Download CSV"
}
}
},
"url": "/index.html",
"version": "1.0"
}
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Download Contact CSV</title>
</head>
<body>
<button onclick="downloadCSV()">Download CSV</button>
<script src="script.js"></script>
</body>
</html>
script.js:
function downloadCSV() {
ZOHO.CRM.API.getRecord({ Entity: "Contacts", RecordID: recordId }).then(function(data) {
let contact = data.data[0];
let csvContent = `ID,First Name,Last Name,Email\n${contact.id},${contact.First_Name},${contact.Last_Name},${contact.Email}`;
let blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
let link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "contact.csv";
link.click();
});
}
let recordId = null;
ZOHO.embeddedApp.on("PageLoad", function(data) {
recordId = data.EntityId[0];
console.log("Contact ID loaded:", recordId);
});
ZOHO.embeddedApp.init();
What I've Tried:
Ensured ZIP includes only the three required files with no nested folders.
Manifest references /index.html (with leading slash, as required).
Deleted and recreated the widget and associated button.
Reuploaded ZIP multiple times via Developer Hub.
Cleared browser cache and logged out/in to reset possible caching issues.
Still getting “Page Not Found” when clicking the custom button from the Contact detail page.
Help Requested:
What else can I check to ensure Zoho loads index.html correctly? Is there a deployment or validation step I’m missing—even after successful upload?
Any guidance or proven examples would be hugely appreciated.