@spektra-cloudevents/zoho-asap-widget

Framework-agnostic TypeScript library for Zoho Desk ASAP 2.0 with JWT authentication and session management. Works with Angular, React, Vue or plain JavaScript.
1,127
Install
npm install @spektra-cloudevents/zoho-asap-widget
Latest Version:3.1.2
License:MIT
Last Updated:Oct 6, 2026
Links: npm
Maintainer: s-spektra-25

@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, loggedOut and error, for example to show a loader.
  • Opens on the right page: open() always shows Home and openTicketForm() 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() and openTicketForm() 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:

  1. 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
  2. clears Zoho's storage, so the other org's session can't carry over
  3. 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:

  • localStorage keys containing asap, zoho, desk, clientinfo, currenttabid, nooftabs, onlyone, oauth or token
  • sessionStorage keys containing asap, zoho or desk
  • cookies containing zoho, asap, desk or oauth

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/orgId from the one loaded now removes the loaded Zoho app (with Zoho's UnInstall), 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() and openTicketForm() now set Zoho's ticket.form.defaultLayoutId to the configured departmentId&&&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 new homePage option sets the page, and null keeps 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, and openTicketForm() 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, loggedOut and error events.

2.2.0

  • Last Angular version. Added the widgetReady, widgetOpened and widgetError outputs.

Troubleshooting

  • Widget not loading: check appId and orgId, and look for an error event. The page's Content Security Policy must allow https://desk.zoho.com (pass nonce if you use one).
  • Prefill does nothing: set both departmentId and layoutId. The console warns when they're missing.
  • The widget opens on Home instead of the knowledge base (or the other way round): set homePage to 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, sub or email claim.
  • Two widgets fighting over the session: only create one ZohoAsapWidget per 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/orgId changes (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

sagegreytechnologies-nkiru-ai-chat-widget-webcomponent

Native Web Component chat widget with complete CSS isolation using Shadow DOM

1,532
@irisidea/kalrav-ai

Kalrav AI chatbot widget for React, Next.js, Vue, Angular, and Vanilla JavaScrip...

3,290
auth.js

A simple authentication and authorization module for building APIs when using fr...

2,037
@musikhood-dev/auth-client

Auth client for browser apps. Cookies + automatic refresh + React and Vue 3 adap...

3,326 0
ff-widget

FeedForward Widget - Universal Web Component for feedback collection. Works with...

2,421

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