## The foundation every CRM widget shares

A widget is a **sandboxed HTML page that Zoho embeds in an iframe**. It is not a Zoho app, it has no server, and it never sees an OAuth token. It talks to CRM through the JS SDK that the parent frame exposes, and to the outside world through connections or your own hosted backend.

### Build, serve, package

```bash
zone zet init                 # scaffold — pick "Zoho CRM", give the project a name
cd <project>
zone zet run                  # serves app/widget.html on http://localhost:5000 (HTTPS cert included)
zone zet validate             # check against the packaging rules before uploading
zone zet pack                 # zip into dist/ for the developer console
```

The packaging command is **`pack`**, not `package`. `zet package` is an unknown command: it prints usage and exits 0, so a build script "succeeds" having produced nothing.

Project layout: everything the browser loads lives in `app/`, with `app/widget.html` as the default entry. `plugin-manifest.json` sits at the root and declares at minimum `{"service": "CRM"}`.

To test against a real org, register the widget with **External hosting** and give the URL from `zet run` as the Base URL, in a sandbox or a real org. A locally served widget only works on that machine, and CRM caches the page — reload the CRM tab to pick up changes.

### Initialise in the right order — this is the single most common bug

```html
<script src="https://live.zwidgets.com/js-sdk/1.2/ZohoEmbededAppSDK.min.js"></script>
<script>
  // 1. subscribe FIRST
  ZOHO.embeddedApp.on("PageLoad", function (data) {
    // `data` is your only context: which record, which module, which button
    console.log(data);
  });

  // 2. then initialise — this is what asks CRM to send the event
  ZOHO.embeddedApp.init();
</script>
```

Calling `init()` before `on("PageLoad", …)` means the event fires before your handler exists and you silently receive nothing. Subscribe, then init, always.

### Events CRM emits

| Event | Fires when |
|---|---|
| `PageLoad` | an entity (detail) page carrying the widget is loaded — the main source of context |
| `Dial` | the call icon in CRM is clicked |
| `DialerActive` | the softphone window is toggled |
| `Notify` | a Client Script flyout notification is raised |
| `NotifyAndWait` | the same, synchronously — reply with `ZDK.Client.sendResponse(data.id, {...})` |

### The JS SDK surface (v1.2 CDN, 60 methods)

| Namespace | Methods |
|---|---|
| `ZOHO.CRM.API` | `getRecord`, `getAllRecords`, `searchRecord`, `insertRecord`, `updateRecord`, `upsertRecord`, `deleteRecord`, `coql`, `addNotes`, `attachFile`, `uploadFile`, `getFile`, `getRelatedRecords`, `updateRelatedRecords`, `delinkRelatedRecord`, `getUser`, `getAllUsers`, `getProfile`, `getAllProfiles`, `updateProfile`, `getOrgVariable`, `getBluePrint`, `updateBluePrint`, `approveRecord`, `getApprovalById`, `getApprovalRecords`, `getApprovalsHistory`, `getAllActions` |
| `ZOHO.CRM.META` | `getModules`, `getFields`, `getLayouts`, `getRelatedList`, `getCustomViews`, `getAssignmentRules` |
| `ZOHO.CRM.CONFIG` | `getCurrentUser`, `getOrgInfo` |
| `ZOHO.CRM.UI` | `Resize` |
| `ZOHO.CRM.UI.Record` | `open`, `create`, `edit`, `populate` |
| `ZOHO.CRM.UI.Popup` | `close`, `closeReload` |
| `ZOHO.CRM.UI.Widget` | `open` |
| `ZOHO.CRM.UI.Dialer` | `maximize`, `minimize`, `notify` |
| `ZOHO.CRM.HTTP` | `get`, `post`, `put`, `patch`, `delete` |
| `ZOHO.CRM.CONNECTION` | `invoke` |
| `ZOHO.CRM.CONNECTOR` | `authorize`, `invokeAPI` |
| `ZOHO.CRM.FUNCTIONS` | `execute` |
| `ZOHO.CRM.BLUEPRINT` | `proceed` |
| `ZOHO.CRM.WIZARD` | `post` |
| `ZDK.Client` | `sendResponse` |
| `$Client` | `close` |

Every method returns a **Promise**:

```javascript
ZOHO.CRM.API.getRecord({ Entity: "Leads", RecordID: id })
  .then(function (r) { const rec = r.data[0]; })
  .catch(function (e) { /* always handle: a rejected promise is silent otherwise */ });
```

Responses mirror the REST API: `{ data: [ … ] }`, and a write returns per-record `code`/`details.id`. **A resolved promise is not a successful write** — check `data[0].code === "SUCCESS"`, exactly as with the REST API.

### Reaching the outside world

- `ZOHO.CRM.CONNECTION.invoke("<connection_name>", {...})` — a Connection configured under Setup. **This is the right way to call a third-party API.**
- `ZOHO.CRM.CONNECTOR.authorize` / `invokeAPI` — connector-based auth.
- `ZOHO.CRM.FUNCTIONS.execute("<function_name>", {...})` — run a Deluge standalone function and get its result. Good for anything that needs a server-side secret or logic you already wrote in Deluge.
- `ZOHO.CRM.HTTP.get/post/…` — proxied HTTP from CRM's servers, which avoids the browser's CORS.

**Never put an API key, client secret or token in widget code.** The page is downloadable by anyone who can open the widget, and the zip is inspectable. Use a Connection, an org variable (`getOrgVariable`), or a Deluge function.

### Sizing, and knowing you are inside an iframe

The widget cannot resize its own frame by changing CSS — ask the host:

```javascript
ZOHO.CRM.UI.Resize({ height: "600", width: "800" });
```

`window.parent` is CRM and is cross-origin: you cannot read or script it. Anything you need from the page comes through `PageLoad` data or an SDK call.

### Before you ship

1. Subscribed to `PageLoad` **before** `init()`.
2. Every SDK promise has a `.catch`, and every write checks `data[0].code`.
3. No secrets in the bundle; third-party calls go through a Connection or a Deluge function.
4. Field API names came from `ZOHO.CRM.META.getFields`, not from memory.
5. `zone zet validate` passes, then `zone zet pack`.
