@spektra-cloudevents/zoho-asap-widget
| Install | |
|---|---|
npm install @spektra-cloudevents/zoho-asap-widget |
|
| Latest Version: | 3.1.2 |
| License: | MIT |
| Last Updated: | Oct 6, 2026 |
| Links: | npm |
@spektra-cloudevents/zoho-asap-widget
A framework-agnostic TypeScript wrapper for the Zoho Desk ASAP 2.0 widget. It has no dependencies and works with Angular, React, Vue or plain JavaScript.
The library takes care of loading the Zoho script, JWT login, token refresh, detecting a user switch, clearing Zoho's storage and prefilling the ticket form. Your app gives it a Zoho JWT and listens for events.
- Handles user switches safely: a new token for the same user logs in again silently. A token for a different user does a full logout and wipes Zoho's storage before logging in.
- Events for your UI:
ready,opened,closed,loggedIn,loggedOutanderror, for example to show a loader. - Opens on the right page:
open()always shows Home andopenTicketForm()always shows the ticket form, without briefly showing the previous page first. - Prefill only where you asked for it:
open(prefill)shows Home with the ticket form ready to be filled in.open()gives a blank form. Values never carry over to the next ticket. - Early calls are queued:
open()andopenTicketForm()called before Zoho is ready run once it is. - Token changes run in order: if a new token arrives while a login is still running, it runs next instead of being dropped.
- Ships ESM and CommonJS with TypeScript types.
Installation
npm install @spektra-cloudevents/zoho-asap-widget
Quick start
import { ZohoAsapWidget } from '@spektra-cloudevents/zoho-asap-widget';
const widget = new ZohoAsapWidget({
appId: 'YOUR_APP_ID',
orgId: 'YOUR_ORG_ID',
departmentId: 'YOUR_DEPARTMENT_ID', // needed for prefill / ticket form
layoutId: 'YOUR_LAYOUT_ID', // needed for prefill / ticket form
hideLauncher: true
});
widget.on('opened', () => hideLoader());
widget.on('closed', () => goToHomePage());
widget.on('error', err => showError(err.message));
// Loads the widget (first call) and logs the user in
widget.setToken(zohoJwt);
// Button handlers
helpButton.onclick = () => { showLoader(); widget.open(); }; // Home, blank form
ticketButton.onclick = () => {
showLoader();
// Home; the form is prefilled when the user clicks Submit
widget.open({ cf_event_request_id: '12345', email: 'user@example.com' });
};
// Sign-out: end the Zoho session before leaving
await widget.setToken(null);
Use one instance per page. Zoho's SDK is a single global object (
window.ZohoDeskAsap) shared by the whole page. Create the widget once, for example in a singleton service or module, and share it.
Several Zoho apps or orgs on one page
If different ticket types use different Zoho ASAP apps (a different appId/orgId, for example a separate Zoho Desk org for each product), destroy the current widget and create a new one with the other app's settings. When the new widget starts, the library:
- removes the loaded app from the page, using Zoho's built-in (undocumented)
ZohoDeskAsap.UnInstall('COMPLETELY'), plus the loader script, SDK script and settings it leaves behind - clears Zoho's storage, so the other org's session can't carry over
- loads the new app's script, and logs in when you call
setToken()with that app's JWT
let widget: ZohoAsapWidget | null = null;
let currentApp = '';
function widgetFor(config: { appId: string; orgId: string; departmentId: string; layoutId: string }) {
const key = `${config.appId}|${config.orgId}`;
if (!widget || key !== currentApp) {
widget?.destroy();
widget = new ZohoAsapWidget({ ...config, hideLauncher: true });
currentApp = key;
}
return widget;
}
Switching orgs takes a few seconds, because the other org's widget has to load and log in. It also ends the previous org's Zoho session. Opening the widget again for the app that's already loaded doesn't reload anything.
API
new ZohoAsapWidget(options)
| Option | Type | Default | Description |
|---|---|---|---|
appId |
string |
— | Required. Zoho ASAP application ID |
orgId |
string |
— | Required. Zoho organization ID |
departmentId |
string |
— | Needed, together with layoutId, for prefill and for routing to the ticket form |
layoutId |
string |
— | Needed, together with departmentId, for prefill and for routing to the ticket form |
homePage |
string | null |
'home' |
Page that open() goes to, so the widget doesn't reopen on the last page shown (for example a ticket form). 'home' is the widget's Home view; it isn't in Zoho's public docs but is part of the widget's own route table. Any other routeTo page works too, for example 'kb.category.list'. null keeps Zoho's own behaviour. |
hideLauncher |
boolean |
false |
Hide Zoho's floating launcher button |
locale |
string |
'en' |
Widget language |
nonce |
string |
— | CSP nonce for the Zoho script tag |
clearStorageOnInit |
boolean |
true |
Wipe Zoho's saved session data when the widget first loads |
loginTimeoutMs |
number |
8000 |
How long to wait for Zoho to confirm a login or logout |
readyTimeoutMs |
number |
15000 |
How long to wait for Zoho's SDK after the script loads |
The constructor has no side effects. Nothing touches window until init(), setToken(), open() or openTicketForm() is called, so the widget is safe to create in server-side rendering (SSR) code.
Methods
| Method | Description |
|---|---|
setToken(jwt | null): Promise<void> |
Log in with a Zoho JWT, or log out with null. The first call also loads the widget. Resolves when the login or logout is done. |
open(prefill?): void |
Open the widget on the homePage view (Home by default). With prefill, the ticket form the user opens from there is filled with exactly these values. Without it, any earlier prefill is cleared and that form is blank. |
openTicketForm(prefill?): void |
Open the widget on the ticket form, prefilled with exactly these values. Fields prefilled earlier that aren't in prefill are cleared. With no values, the form is blank. |
close(): void |
Close the widget. If the SDK isn't ready yet, this cancels a queued open. |
init(): Promise<void> |
Load the Zoho script and wait for the SDK. Calling it again does nothing, and the other methods call it for you. It rejects if the script fails to load, and calling it again retries. |
whenReady(): Promise<void> |
Resolves once the widget is ready, whether or not ready has already fired. |
on(event, handler): () => void |
Subscribe to an event. Returns a function that unsubscribes. |
off(event, handler): void |
Unsubscribe from an event. |
destroy(): void |
Stop emitting events and remove all handlers. It doesn't log out, so call await setToken(null) first if the session should end. |
isReady / isOpen |
Read-only state. |
Events
| Event | Payload | When |
|---|---|---|
ready |
— | Once. Fires when the SDK is ready. If a token was set before then, it waits for that first login to finish (or time out). |
opened |
— | Each time the widget window opens |
closed |
— | Each time the widget window closes |
loggedIn |
— | Zoho confirmed a login |
loggedOut |
— | Zoho confirmed a logout |
error |
Error |
The Zoho script failed to load, or the SDK never became ready |
Loader pattern: show the loader when the user clicks, and hide it on opened or error. Zoho doesn't fire opened again for a widget that is already open, so check widget.isOpen before showing the loader.
Opening the widget and prefill
widget.open(); // Home; Submit opens a blank form
widget.open({ cf_event_request_id: 'E1' }); // Home; Submit opens the form prefilled
widget.openTicketForm({ cf_event_request_id: 'E1' }); // straight to the ticket form, prefilled
widget.openTicketForm(); // straight to the ticket form, blank
Navigation. Zoho's widget reopens on whatever page it showed last. The library routes to the page you asked for every time: Home for open(), the ticket form for openTicketForm(). After the first open, it switches page while the widget is still hidden and then shows it, so the previous page never flashes. On the very first open, Zoho has to build its app before it can change page. So if that first open goes straight to the ticket form, Home can show for a moment.
Prefill. Zoho merges new prefill values into the ones it already holds, so leaving a field out doesn't clear it. The library keeps track of every field it has prefilled and sends the ones you leave out as empty values:
| Call | Ticket form shows |
|---|---|
open({ cf_event_request_id: 'E1', email: 'a@x.com' }) → user clicks Submit on Home |
E1, a@x.com |
open() → user clicks Submit on Home |
blank |
openTicketForm({ cf_event_request_id: 'E2' }) |
E2, empty email |
undefined and null values are sent as empty values too.
Prefill and routing to the ticket form need both departmentId and layoutId. Zoho's prefill key is ${departmentId}&&&${layoutId}. The library also makes that department and layout the form Zoho opens from Home's Submit button, so the prefill shows up there too.
How authentication works
setToken(jwt)
├─ same token as current → nothing to do
├─ same user (oid/sub/email) → silent re-login with the new token
└─ different user → logout → wipe storage → wait for Zoho to finish → login
setToken(null) → logout → wipe storage
The user is identified from the token's oid, sub, email or preferred_username claim. If either token can't be decoded, the change is treated as a user switch, so one user's session is never carried over to another.
Clearing Zoho's storage
When the widget first loads (unless clearStorageOnInit: false) and on every logout, the library removes Zoho session data:
localStoragekeys containingasap,zoho,desk,clientinfo,currenttabid,nooftabs,onlyone,oauthortokensessionStoragekeys containingasap,zohoordesk- cookies containing
zoho,asap,deskoroauth
Your app's own storage keys shouldn't contain these words, or they'll be removed too. clearZohoStorage() is also exported if you want to call it yourself.
Logging
The library logs nothing during normal use. It writes to the console only when something is wrong, using console.error or console.warn with a [ZohoAsapWidget] prefix. That happens when the Zoho script fails to load, login fails, prefill is skipped because departmentId/layoutId is missing, or a second widget instance is created.
Framework examples
Angular (a service shared by the whole app)
import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { PrefillValues, ZohoAsapWidget } from '@spektra-cloudevents/zoho-asap-widget';
@Injectable({ providedIn: 'root' })
export class SupportWidgetService implements OnDestroy {
private readonly widget = new ZohoAsapWidget({ appId: '…', orgId: '…', departmentId: '…', layoutId: '…', hideLauncher: true });
readonly loading$ = new BehaviorSubject(false);
constructor(zone: NgZone) {
// Zoho callbacks can run outside Angular's zone, so re-enter it for change detection
this.widget.on('opened', () => zone.run(() => this.loading$.next(false)));
this.widget.on('error', () => zone.run(() => this.loading$.next(false)));
}
setToken(jwt: string | null) { return this.widget.setToken(jwt); }
openTicket(prefill: PrefillValues) {
if (!this.widget.isOpen) this.loading$.next(true);
this.widget.openTicketForm(prefill);
}
ngOnDestroy() { this.widget.destroy(); }
}
React
import { useEffect, useState } from 'react';
import { ZohoAsapWidget } from '@spektra-cloudevents/zoho-asap-widget';
// Create it once, outside the component tree
export const supportWidget = new ZohoAsapWidget({ appId: '…', orgId: '…', hideLauncher: true });
export function useSupportWidget(jwt: string | null) {
const [loading, setLoading] = useState(false);
useEffect(() => { supportWidget.setToken(jwt); }, [jwt]);
useEffect(() => {
const offs = [
supportWidget.on('opened', () => setLoading(false)),
supportWidget.on('error', () => setLoading(false))
];
return () => offs.forEach(off => off());
}, []);
const open = () => { if (!supportWidget.isOpen) setLoading(true); supportWidget.open(); };
return { loading, open };
}
Plain HTML page (script tag)
<script type="module">
import { ZohoAsapWidget } from 'https://cdn.jsdelivr.net/npm/@spektra-cloudevents/zoho-asap-widget@3/dist/esm/index.js';
const widget = new ZohoAsapWidget({ appId: '…', orgId: '…' });
widget.setToken(window.ZOHO_JWT);
document.getElementById('help').onclick = () => widget.open();
</script>
Migrating from 2.x (Angular component)
2.x (<zoho-asap-widget>) |
3.x |
|---|---|
ZohoAsapWidgetModule import |
Removed. Create a new ZohoAsapWidget(...) in a service instead |
[appId], [orgId], [departmentId], [layoutId], [hideLauncher], [nonce] |
Constructor options |
[jwtToken]="token" |
widget.setToken(token) |
[enabled]="false" / removing the component with *ngIf |
await widget.setToken(null) (logs out), then destroy() if needed |
[prefillFields] + _timestamp / _fromNavbar workarounds |
widget.openTicketForm(prefill) / widget.open() |
openWidget() / closeWidget() via @ViewChild |
widget.open() / widget.close() |
(widgetReady), (widgetOpened), (widgetClosed), (widgetError) |
widget.on('ready' | 'opened' | 'closed' | 'error', …) |
Changelog
3.1.2
- Several Zoho apps or orgs on one page. Creating a widget for a different
appId/orgIdfrom the one loaded now removes the loaded Zoho app (with Zoho'sUnInstall), clears its storage and loads the new one. Before, the first app's widget kept running, so the second app's prefill and login went to the wrong org. - Fixed: prefill didn't appear when the user clicked Submit on Home for a form other than Zoho's default.
open()andopenTicketForm()now set Zoho'sticket.form.defaultLayoutIdto the configureddepartmentId&&&layoutId, so Home → Submit opens the form the prefill was stored for.
3.1.1
open(prefill): opens Home with the ticket form ready to be filled in, so the form the user opens from Home is prefilled.open()with no values still clears the prefill.
3.1.0
open()now always shows Home. Before, it reopened on the last page shown, such as the ticket form. The newhomePageoption sets the page, andnullkeeps the old behaviour.- Changing page no longer shows the previous page for a moment. After the first open, the widget switches page while hidden and then shows it.
- Prefill no longer carries over.
open()clears it, andopenTicketForm()clears fields that aren't in the new values. Before, Zoho merged them in and old values came back. - Removed the debug logging. Only errors and warnings are logged.
3.0.0
- Rewritten as a plain TypeScript class with no dependencies. The Angular component and module are gone (see the migration table above).
- Added the
ready,opened,closed,loggedIn,loggedOutanderrorevents.
2.2.0
- Last Angular version. Added the
widgetReady,widgetOpenedandwidgetErroroutputs.
Troubleshooting
- Widget not loading: check
appIdandorgId, and look for anerrorevent. The page's Content Security Policy must allowhttps://desk.zoho.com(passnonceif you use one). - Prefill does nothing: set both
departmentIdandlayoutId. The console warns when they're missing. - The widget opens on Home instead of the knowledge base (or the other way round): set
homePageto the page you want, for example'kb.category.list'. - A token refresh logs the user out: the old and new JWTs must carry the same
oid,suboremailclaim. - Two widgets fighting over the session: only create one
ZohoAsapWidgetper page. The console warns if a second one loads. - Prefill or login goes to the wrong Zoho org: make sure you destroy the old widget and create a new one when the
appId/orgIdchanges (see Several Zoho apps or orgs on one page). Versions before 3.1.2 kept the first app loaded.
Building & publishing
cd projects/zoho-asap-widget
npm run build # dist/esm + dist/cjs, with .d.ts
npm version patch # or minor / major
npm publish --access public # prepublishOnly rebuilds automatically
License
MIT
Related Packages
Native Web Component chat widget with complete CSS isolation using Shadow DOM
Kalrav AI chatbot widget for React, Next.js, Vue, Angular, and Vanilla JavaScrip...
Auth client for browser apps. Cookies + automatic refresh + React and Vue 3 adap...
Version History
| Version | Released | License |
|---|---|---|
| 3.1.2 | MIT | |
| 3.1.1 | MIT | |
| 3.1.0 | MIT | |
| 3.0.0 | MIT | |
| 2.2.0 | MIT | |
| 2.1.0 | MIT | |
| 2.0.0 | MIT |