@ecodrix/erix-api 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1006 +1,659 @@
1
1
  # @ecodrix/erix-api
2
2
 
3
- <div align="center">
4
-
5
- [![NPM Version](https://img.shields.io/npm/v/@ecodrix/erix-api.svg?style=for-the-badge)](https://www.npmjs.com/package/@ecodrix/erix-api)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=for-the-badge&logo=typescript)](https://www.typescriptlang.org/)
8
- [![OpenAPI](https://img.shields.io/badge/OpenAPI-3.0-green?style=for-the-badge&logo=openapiinitiative)](./schema/openapi.yaml)
9
- [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen?style=for-the-badge&logo=nodedotjs)](https://nodejs.org/)
10
-
11
- **The official, isomorphic SDK for the [ECODrIx](https://ecodrix.com) platform.**
12
-
13
- Manage WhatsApp conversations, CRM leads, pipelines, automations, marketing campaigns, file storage, and Google Meet — all from a single, type-safe library.
14
-
15
- </div>
16
-
17
- ---
18
-
19
- ## Table of Contents
20
-
21
- - [Installation](#installation)
22
- - [Quick Start](#quick-start)
23
- - [Configuration](#configuration)
24
- - [Resources](#resources)
25
- - [WhatsApp](#whatsapp)
26
- - [Messages](#ecod-whatsapp-messages)
27
- - [Conversations](#ecod-whatsapp-conversations)
28
- - [Templates](#ecod-whatsapp-templates)
29
- - [Broadcasts](#ecod-whatsapp-broadcasts)
30
- - [CRM](#crm)
31
- - [Leads](#ecod-crm-leads)
32
- - [Pipelines & Stages](#ecod-crm-pipelines)
33
- - [Automations](#ecod-crm-automations)
34
- - [Sequences](#ecod-crm-sequences)
35
- - [Activities & Timeline](#ecod-crm-activities)
36
- - [Analytics](#ecod-crm-analytics)
37
- - [Scoring](#ecod-crm-scoring)
38
- - [Payments](#ecod-crm-payments)
39
- - [Automation Dashboard](#ecod-crm-automationdashboard)
40
- - [Events & Workflows](#events--workflows)
41
- - [Marketing](#marketing)
42
- - [Meetings](#meetings)
43
- - [Storage](#storage)
44
- - [Media](#media)
45
- - [Email](#email)
46
- - [Notifications & Logs](#notifications--logs)
47
- - [Queue Management](#queue-management)
48
- - [Health & Diagnostics](#health--diagnostics)
49
- - [Webhooks](#webhooks)
50
- - [Enterprise Capabilities](#enterprise-capabilities)
51
- - [Auto-Paginating Iterators](#auto-paginating-iterators)
52
- - [Bulk Data Chunking](#bulk-data-chunking)
53
- - [Idempotency Keys](#idempotency-keys)
54
- - [Webhook Signature Verification](#webhook-signature-verification)
55
- - [Raw Execution Engine](#raw-execution-engine)
56
- - [Real-time Events](#real-time-events)
57
- - [Error Handling](#error-handling)
58
- - [Browser / CDN Usage](#browser--cdn-usage)
59
-
60
- ---
61
-
62
- ## Installation
3
+ [![npm](https://img.shields.io/npm/v/@ecodrix/erix-api.svg)](https://www.npmjs.com/package/@ecodrix/erix-api)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+
6
+ The official TypeScript SDK for the [ECODrIx](https://ecodrix.com) platform.
7
+ Use it from your own backend to work with your workspace: CRM leads and
8
+ pipelines, Flow automations, the unified inbox (WhatsApp, Instagram, Facebook,
9
+ Telegram, email, webchat), invoices and payment links, file storage, and live
10
+ events. Every call is scoped to the workspace your key belongs to.
11
+
12
+ - Developer guide: https://api.ecodrix.com/docs
13
+ - API reference: https://api.ecodrix.com/reference
14
+
15
+ ## Install
63
16
 
64
17
  ```bash
65
- # pnpm (recommended)
66
18
  pnpm add @ecodrix/erix-api
67
-
68
- # npm
69
- npm install @ecodrix/erix-api
19
+ # or: npm install @ecodrix/erix-api
70
20
  ```
71
21
 
72
- > **Requires**: Node.js >= 18
22
+ - **Node.js 18+** (uses the global `fetch` and `Blob`). Ships ESM and CommonJS
23
+ builds with bundled type definitions.
24
+ - **Browsers**: supported through a bundler (Vite, Next.js, webpack) in
25
+ [proxy mode](#browser-use-proxy-mode) only, plus the keyless
26
+ [website chat visitor client](#website-chat-visitor-client). There is no
27
+ CDN build.
73
28
 
74
- ---
29
+ ## Quick start
75
30
 
76
- ## Quick Start
31
+ Run this on your server. Keep the key in a server-only environment variable.
77
32
 
78
- ```typescript
33
+ ```ts
79
34
  import { ECODrIxAPI } from "@ecodrix/erix-api";
80
35
 
81
36
  const ecod = new ECODrIxAPI({
82
37
  apiKey: process.env.ECOD_API_KEY!,
83
- clientCode: "ERIX_CLNT_JHBJHF",
84
- // Optional — pick the API version. Defaults to "v1".
85
- version: "v1",
86
- });
87
-
88
- // The SDK surface mirrors the server's domain ownership 1:1:
89
- // ecod.platform.* control plane (auth-scoped tenant/admin)
90
- // ecod.product.erix.* ERIX product (CRM, chat, commerce, events)
91
- // ecod.infra.connect.* channel transport (email, whatsapp, meet)
92
- // ecod.infra.storage.* file storage
93
-
94
- // Send a WhatsApp/chat message
95
- await ecod.product.erix.chat.messages.send({
96
- to: "+919876543210",
97
- text: "Hello from ECODrIx!",
38
+ clientCode: "ERIX_CLNT_XXXXXX", // Settings → Developer in the dashboard
98
39
  });
99
40
 
100
- // Create a CRM lead
101
- const lead = await ecod.product.erix.leads.create({
102
- firstName: "Priya",
103
- phone: "+919876543210",
41
+ const { data: lead } = await ecod.product.erix.leads.create({
42
+ firstName: "Test",
43
+ lastName: "Customer",
44
+ phone: "+91XXXXXXXXXX",
104
45
  source: "website",
105
46
  });
106
47
 
107
- // Fire an automation trigger
108
48
  await ecod.product.erix.events.trigger({
109
- trigger: "trial_started",
110
- phone: "+919876543210",
111
- createLeadIfMissing: true,
112
- });
113
- ```
114
-
115
- > **Note:** host projects never pass a URL. The domain is managed by the SDK;
116
- > you only choose an API `version`. Moving the whole SDK to `v2` later is a
117
- > single flag — `new ECODrIxAPI({ ..., version: "v2" })` — because every
118
- > endpoint is issued relative to `<host>/<version>/api`.
119
-
120
- ---
121
-
122
- ## Configuration
123
-
124
- Pass an options object to `new Ecodrix(options)`:
125
-
126
- | Option | Type | Required | Default | Description |
127
- | ------------ | ---------------------- | ----------- | ------- | ------------------------------------------------------------------ |
128
- | `apiKey` | `string` | ✅ Yes | — | Your ECOD Platform API key |
129
- | `clientCode` | `string` | Recommended | — | Your tenant ID — scopes all requests |
130
- | `version` | `"v1" \| "v2" \| "v3"` | No | `"v1"` | API route version. The single knob to target a new server version. |
131
- | `coreApiKey` | `string` | No | — | Elevated key for platform-admin surfaces (`ecod.platform.admin.*`) |
132
-
133
- ```typescript
134
- const ecod = new ECODrIxAPI({
135
- apiKey: process.env.ECOD_API_KEY!,
136
- clientCode: "ERIX_CLNT_JHBJHF",
137
- version: "v1",
138
- });
139
- ```
140
-
141
- ---
142
-
143
- ## Resources
144
-
145
- ### WhatsApp
146
-
147
- Access via `ecod.whatsapp`.
148
-
149
- ---
150
-
151
- #### `ecod.whatsapp.messages`
152
-
153
- **Send a text message**
154
-
155
- ```typescript
156
- await ecod.whatsapp.messages.send({
157
- to: "+919876543210",
158
- text: "Your appointment is confirmed!",
159
- });
160
- ```
161
-
162
- **Send a media message (image, video, document, audio)**
163
-
164
- ```typescript
165
- await ecod.whatsapp.messages.send({
166
- to: "+919876543210",
167
- mediaUrl: "https://cdn.ecodrix.com/invoice.pdf",
168
- mediaType: "document",
169
- filename: "invoice.pdf",
49
+ trigger: "enquiry_received",
50
+ phone: "+91XXXXXXXXXX",
170
51
  });
171
- ```
172
-
173
- **Send a template message via queue**
174
52
 
175
- ```typescript
176
- await ecod.whatsapp.messages.sendTemplate({
177
- to: "+919876543210",
178
- templateName: "appointment_reminder",
179
- language: "en_US",
180
- variables: ["Priya", "Tomorrow 10AM"],
181
- });
53
+ // The client keeps a realtime socket open. Close it when your process stops.
54
+ process.on("SIGTERM", () => ecod.disconnect());
182
55
  ```
183
56
 
184
- **Mark messages as read**
57
+ | Option | Type | Default | Notes |
58
+ | ------------ | ---------------------------- | -------- | -------------------------------------------------------- |
59
+ | `apiKey` | `string` | none | Workspace key or a [scoped key](#scoped-keys). Server only. |
60
+ | `clientCode` | `string` | none | Your workspace code, e.g. `ERIX_CLNT_XXXXXX`. |
61
+ | `version` | `"v1"` | `"v1"` | API route version. |
62
+ | `timeout` | `number` (ms) | `30000` | Per request. `0` disables. |
63
+ | `proxy` | `{ url, ticket? }` | none | Browser mode. Replaces `apiKey`. |
185
64
 
186
- ```typescript
187
- await ecod.whatsapp.messages.markRead("message_id");
188
- ```
65
+ ## How the client is laid out
189
66
 
190
- ---
67
+ The client follows the API's route tree:
191
68
 
192
- #### `ecod.whatsapp.conversations`
69
+ | Namespace | What it covers |
70
+ | ---------------------------- | ------------------------------------------------------------------- |
71
+ | `ecod.product.erix.*` | CRM (`leads`, `pipelines`, `activities`, `analytics`), `events` (Flow triggers), `chat` (inbox), `invoices`, `payments`, `ai` |
72
+ | `ecod.infra.connect.*` | Channels: `whatsapp.templates`, `email`, `meet`, `connections` |
73
+ | `ecod.infra.storage.*` | Files: `media`, `files`, `folders` |
74
+ | `ecod.platform.*` | Workspace: `knowledge` (AI knowledge base), `users.notifications` |
75
+ | `ecod.webhooks` | Webhook signature verification |
76
+ | `ecod.on()` / `ecod.off()` | Realtime events |
77
+ | `ecod.request()` | Authenticated call to any endpoint without a typed method yet |
193
78
 
194
- ```typescript
195
- // List conversations (cursor-based pagination)
196
- const { data } = await ecod.whatsapp.conversations.list({
197
- limit: 20,
198
- status: "open",
199
- after: "cursor_token",
200
- });
79
+ ## Browser use (proxy mode)
201
80
 
202
- // Get a specific conversation
203
- const conv = await ecod.whatsapp.conversations.retrieve("conversation_id");
81
+ Never put a workspace key in the browser. That includes `NEXT_PUBLIC_*`,
82
+ `VITE_*` or any other variable that ends up in a client bundle. Anyone who
83
+ opens dev tools can read it, and a workspace key has full access.
204
84
 
205
- // Get messages in a conversation
206
- const { data: msgs } = await ecod.whatsapp.conversations.messages(
207
- "conversation_id",
208
- {
209
- limit: 50,
210
- },
211
- );
85
+ In the browser, run the SDK in proxy mode:
212
86
 
213
- // Create a conversation explicitly
214
- await ecod.whatsapp.conversations.create({
215
- phone: "+919876543210",
216
- name: "Priya",
217
- });
87
+ 1. Your server exposes `/api/erix/*`. It checks your signed-in user, adds the
88
+ key, and forwards the request to ECODrIx.
89
+ 2. For realtime, your server mints a 5-minute socket ticket. The browser gets
90
+ the ticket, never the key.
218
91
 
219
- // Link a conversation to a CRM lead
220
- await ecod.whatsapp.conversations.linkLead("conversation_id", "lead_id");
92
+ Proxy route (Next.js App Router, `app/api/erix/[...path]/route.ts`):
221
93
 
222
- // Mark a conversation as read (clears unread badge)
223
- await ecod.whatsapp.conversations.markRead("conversation_id");
94
+ ```ts
95
+ const API = "https://api.ecodrix.com";
96
+ const PASS_HEADERS = ["content-type", "accept", "idempotency-key"];
97
+ const DROP_HEADERS = ["content-encoding", "content-length", "transfer-encoding", "connection"];
224
98
 
225
- // Delete / archive a conversation
226
- await ecod.whatsapp.conversations.delete("conversation_id");
99
+ async function proxy(req: Request, ctx: { params: Promise<{ path: string[] }> }) {
100
+ const user = await requireUser(req); // your own session check
101
+ if (!user) {
102
+ return Response.json({ success: false, error: "Unauthorized" }, { status: 401 });
103
+ }
227
104
 
228
- // Bulk delete conversations
229
- await ecod.whatsapp.conversations.bulkDelete(["conv_1", "conv_2"]);
230
- ```
105
+ const { path } = await ctx.params;
106
+ const rel = `/${path.map(encodeURIComponent).join("/")}`;
107
+ if (!/^\/v\d+\/api\//.test(rel)) {
108
+ return Response.json({ success: false, error: "Not found" }, { status: 404 });
109
+ }
231
110
 
232
- ---
111
+ const headers = new Headers({
112
+ "x-api-key": process.env.ECOD_API_KEY!,
113
+ "x-client-code": "ERIX_CLNT_XXXXXX",
114
+ });
115
+ for (const name of PASS_HEADERS) {
116
+ const value = req.headers.get(name);
117
+ if (value) headers.set(name, value);
118
+ }
233
119
 
234
- #### `ecod.whatsapp.templates`
120
+ const hasBody = req.method !== "GET" && req.method !== "HEAD";
121
+ const upstream = await fetch(`${API}${rel}${new URL(req.url).search}`, {
122
+ method: req.method,
123
+ headers,
124
+ body: hasBody ? await req.arrayBuffer() : undefined,
125
+ redirect: "manual",
126
+ });
235
127
 
236
- ```typescript
237
- // List pre-approved Meta templates
238
- const { data } = await ecod.whatsapp.templates.list();
128
+ const out = new Headers(upstream.headers);
129
+ for (const name of DROP_HEADERS) out.delete(name);
130
+ return new Response(upstream.body, { status: upstream.status, headers: out });
131
+ }
239
132
 
240
- // Sync templates from the Meta Business Account
241
- await ecod.whatsapp.templates.sync();
133
+ export { proxy as GET, proxy as POST, proxy as PUT, proxy as PATCH, proxy as DELETE };
242
134
  ```
243
135
 
244
- ---
245
-
246
- #### `ecod.whatsapp.broadcasts`
136
+ Ticket route (`app/api/erix/ticket/route.ts`):
247
137
 
248
- Send a personalised WhatsApp template message to multiple recipients at once.
138
+ ```ts
139
+ export async function POST(req: Request) {
140
+ const user = await requireUser(req); // your own session check
141
+ if (!user) return new Response("Unauthorized", { status: 401 });
249
142
 
250
- ```typescript
251
- await ecod.whatsapp.broadcasts.create({
252
- name: "April Promo",
253
- templateName: "discount_offer",
254
- templateLanguage: "en_US",
255
- recipients: [
256
- { phone: "+919876543210", variables: ["Priya", "20%"] },
257
- { phone: "+919123456789", variables: ["Ravi", "30%"] },
258
- ],
259
- });
143
+ const res = await fetch("https://api.ecodrix.com/v1/api/platform/realtime/ticket", {
144
+ method: "POST",
145
+ headers: {
146
+ "x-api-key": process.env.ECOD_API_KEY!,
147
+ "x-client-code": "ERIX_CLNT_XXXXXX",
148
+ },
149
+ cache: "no-store",
150
+ });
151
+ const body = (await res.json().catch(() => null)) as { data?: { ticket?: string } } | null;
152
+ const ticket = body?.data?.ticket;
153
+ if (!res.ok || !ticket) return new Response("Ticket unavailable", { status: 502 });
260
154
 
261
- // List past broadcasts
262
- const { data } = await ecod.whatsapp.broadcasts.list({ status: "completed" });
155
+ return new Response(ticket, {
156
+ headers: { "content-type": "text/plain", "cache-control": "no-store" },
157
+ });
158
+ }
263
159
  ```
264
160
 
265
- ---
266
-
267
- #### `ecod.whatsapp.sendTemplate` — Direct Template (Bypass Queue)
161
+ Browser client:
268
162
 
269
- Use this for high-priority utility templates that must deliver immediately outside the automation queue.
163
+ ```ts
164
+ import { ECODrIxAPI } from "@ecodrix/erix-api";
270
165
 
271
- ```typescript
272
- const result = await ecod.whatsapp.sendTemplate({
273
- phone: "+919876543210",
274
- templateName: "payment_confirmation",
275
- variables: {
276
- name: "Priya",
277
- amount: "₹1,500",
166
+ export const ecod = new ECODrIxAPI({
167
+ proxy: {
168
+ url: "/api/erix",
169
+ ticket: () =>
170
+ fetch("/api/erix/ticket", { method: "POST" }).then((r) =>
171
+ r.ok ? r.text() : Promise.reject(new Error("No realtime ticket")),
172
+ ),
278
173
  },
279
174
  });
280
- // result.messageId → WA message ID
281
175
  ```
282
176
 
283
- ---
284
-
285
- ### CRM
286
-
287
- Access via `ecod.crm`.
288
-
289
- ---
177
+ The SDK fetches a fresh ticket on every reconnect. Without `ticket`, REST
178
+ calls work and no socket opens.
290
179
 
291
- #### `ecod.crm.leads`
180
+ Anyone signed in to your app can now do whatever the workspace key can.
181
+ If your users shouldn't have all of that, allow-list the paths they need in the
182
+ proxy route.
292
183
 
293
- **Create**
184
+ ## Website chat visitor client
294
185
 
295
- ```typescript
296
- const { data: lead } = await ecod.crm.leads.create({
297
- firstName: "Priya",
298
- lastName: "Sharma",
299
- phone: "+919876543210",
300
- email: "priya@example.com",
301
- source: "website",
302
- pipelineId: "pipeline_id",
303
- stageId: "stage_id",
304
- metadata: { utmSource: "google" },
305
- });
306
- ```
186
+ `@ecodrix/erix-api/visitor` is a separate entry for an anonymous website
187
+ visitor chatting with a workspace. It takes no key and reaches only that
188
+ visitor's own thread. It uses `fetch` and loads `socket.io-client` only when
189
+ you go live, so a chat widget doesn't ship the full SDK. The ERIX chat widget
190
+ runs on it.
307
191
 
308
- **Upsert by phone** — Creates if absent, updates if exists
192
+ ```ts
193
+ import { WebchatVisitor, WebchatVisitorError } from "@ecodrix/erix-api/visitor";
309
194
 
310
- ```typescript
311
- await ecod.crm.leads.upsert({
312
- leadData: { phone: "+919876543210", firstName: "Priya" },
313
- trigger: "webinar_joined",
195
+ const chat = new WebchatVisitor({
196
+ clientCode: "ERIX_CLNT_XXXXXX",
197
+ // The visitor's only secret: random, 20–80 characters of A-Z a-z 0-9 _ -.
198
+ // Keep it (e.g. in localStorage) to continue the same thread next visit.
199
+ sessionId,
200
+ // The site the chat runs on. It must be on the workspace's allowed domains.
201
+ parent: "https://www.example.in",
314
202
  });
315
- ```
316
-
317
- **List with filters**
318
203
 
319
- ```typescript
320
- const { data } = await ecod.crm.leads.list({
321
- status: "new",
322
- source: "whatsapp",
323
- pipelineId: "pipeline_id",
324
- page: 1,
325
- limit: 25,
326
- });
327
- ```
204
+ const { data: messages, widget, consentNotice } = await chat.messages();
328
205
 
329
- **Retrieve**
206
+ try {
207
+ await chat.send("Do you open on Sunday?", {
208
+ messageId: crypto.randomUUID(), // resend with the same id: stored once
209
+ page: { path: location.pathname, title: document.title },
210
+ });
211
+ } catch (err) {
212
+ if (err instanceof WebchatVisitorError) {
213
+ console.warn(err.code, err.status, err.field, err.message);
214
+ }
215
+ }
330
216
 
331
- ```typescript
332
- const { data: lead } = await ecod.crm.leads.retrieve("lead_id");
333
- const { data } = await ecod.crm.leads.retrieveByPhone("+919876543210");
334
- const { data } = await ecod.crm.leads.retrieveByRef("orderId", "ORD-123");
217
+ await chat.contact({ name: "Ravi", phone: "98765 43210" });
335
218
  ```
336
219
 
337
- **Update**
220
+ Live replies and typing (`/visitor` socket):
338
221
 
339
- ```typescript
340
- await ecod.crm.leads.update("lead_id", { email: "new@email.com" });
341
- await ecod.crm.leads.updateMetadata("lead_id", {
342
- refs: { orderId: "ORD-456" },
343
- extra: { plan: "pro" },
222
+ ```ts
223
+ chat.connect({
224
+ onMessage: (message) => upsert(message), // `from`: visitor | ai | team | system
225
+ onTyping: ({ on, waiting }) => showTyping(on, waiting), // waiting: the team will reply
226
+ onError: () => startPolling(), // live is down: poll messages()
227
+ onReconnect: () => {
228
+ stopPolling();
229
+ chat.messages().then(redraw); // nothing missed is replayed
230
+ },
344
231
  });
345
- ```
346
-
347
- **Move & Convert**
348
-
349
- ```typescript
350
- await ecod.crm.leads.move("lead_id", "target_stage_id");
351
- await ecod.crm.leads.convert("lead_id", "won", "Signed contract");
352
- ```
353
232
 
354
- **Tags**
355
-
356
- ```typescript
357
- await ecod.crm.leads.tags("lead_id", { add: ["vip", "hot"], remove: ["cold"] });
233
+ chat.disconnect();
358
234
  ```
359
235
 
360
- **Score**
236
+ | `err.code` | Meaning |
237
+ | -------------------- | ----------------------------------------------------------------- |
238
+ | `VALIDATION_ERROR` | Bad input. `err.field` names it, `err.message` is visitor copy |
239
+ | `ORIGIN_NOT_ALLOWED` | The site isn't on the workspace's allowed domains |
240
+ | `NOT_FOUND` | Unknown client code |
241
+ | `RATE_LIMITED` | Too many messages or connects. Wait a minute |
242
+ | `MALICIOUS_INPUT` | The text was refused as a script or injection |
243
+ | `INTERNAL_ERROR` | Server error. Try again |
244
+ | `NETWORK_ERROR` | No answer (offline, 30 s timeout). Try again |
361
245
 
362
- ```typescript
363
- await ecod.crm.leads.recalculateScore("lead_id");
364
- ```
246
+ The live socket only accepts the ERIX widget frame's origin. A chat UI on your
247
+ own page works over HTTP when that site is the workspace's website (browser
248
+ CORS), and polls `messages()` for replies.
365
249
 
366
- **Bulk**
250
+ ## Scoped keys
367
251
 
368
- ```typescript
369
- await ecod.crm.leads.import(leadArray); // Bulk import
370
- await ecod.crm.leads.delete("lead_id");
371
- await ecod.crm.leads.bulkDelete(["id_1", "id_2"]);
372
- ```
252
+ For an integration that does one job (a booking form that fires a Flow event,
253
+ say), create a scoped key under **Settings → Developer → Scoped API keys**.
254
+ Scoped keys can be revoked, can expire and have usage tracking. Each one
255
+ carries a single grant:
373
256
 
374
- ---
375
-
376
- #### `ecod.crm.pipelines`
377
-
378
- ```typescript
379
- // CRUD
380
- const { data } = await ecod.crm.pipelines.list();
381
- await ecod.crm.pipelines.create({ name: "Sales", isDefault: true });
382
- await ecod.crm.pipelines.retrieve("pipeline_id");
383
- await ecod.crm.pipelines.update("pipeline_id", { name: "Deals" });
384
- await ecod.crm.pipelines.delete("pipeline_id");
385
-
386
- // Advanced
387
- await ecod.crm.pipelines.setDefault("pipeline_id");
388
- await ecod.crm.pipelines.duplicate("pipeline_id", "Sales Copy");
389
- await ecod.crm.pipelines.archive("pipeline_id");
390
- const { data: board } = await ecod.crm.pipelines.board("pipeline_id");
391
- const { data: forecast } = await ecod.crm.pipelines.forecast("pipeline_id");
392
-
393
- // Stage management
394
- await ecod.crm.pipelines.addStage("pipeline_id", {
395
- name: "Negotiation",
396
- color: "#f59e0b",
397
- });
398
- await ecod.crm.pipelines.reorderStages("pipeline_id", [
399
- "stage_1",
400
- "stage_3",
401
- "stage_2",
402
- ]);
403
- await ecod.crm.pipelines.updateStage("stage_id", { probability: 80 });
404
- await ecod.crm.pipelines.deleteStage("stage_id", "fallback_stage_id");
405
- ```
257
+ | Purpose | Grant | Works for |
258
+ | ---------------- | ------------------ | --------------------------------------------- |
259
+ | Trigger events | `flow:events` | `product.erix.events.trigger()` |
260
+ | Send WhatsApp | `connect:whatsapp` | `chat.messages.send({ to })`, `sendTemplate()`|
261
+ | Send email | `connect:email` | `POST /infra/connect/send/email` |
262
+ | Send Telegram | `connect:telegram` | `POST /infra/connect/send/telegram` |
406
263
 
407
- ---
264
+ Keys look like `erix_<scope>_<secret>`. Pass one as `apiKey`. The server
265
+ enforces its grant, so any call outside the grant fails with
266
+ `AuthenticationError` (HTTP 403). Over plain HTTP you can also send it as
267
+ `Authorization: Bearer erix_<scope>_…`.
408
268
 
409
- #### `ecod.crm.automations`
269
+ ```ts
270
+ import { ECODrIxAPI } from "@ecodrix/erix-api";
410
271
 
411
- ```typescript
412
- // CRUD
413
- const { data } = await ecod.crm.automations.list();
414
- await ecod.crm.automations.create({
415
- name: "Follow-up",
416
- trigger: "lead_created",
417
- nodes: [],
418
- edges: [],
272
+ const events = new ECODrIxAPI({
273
+ apiKey: process.env.ECOD_EVENTS_KEY!, // erix_flow_… with flow:events
274
+ clientCode: "ERIX_CLNT_XXXXXX",
419
275
  });
420
- await ecod.crm.automations.update("rule_id", { isActive: false });
421
- await ecod.crm.automations.toggle("rule_id");
422
- await ecod.crm.automations.deleteRule("rule_id");
423
- await ecod.crm.automations.bulkDelete(["rule_1", "rule_2"]);
424
-
425
- // Testing & events
426
- await ecod.crm.automations.test("rule_id", "lead_id");
427
- const { data: events } = await ecod.crm.automations.getAvailableEvents();
428
-
429
- // Enrollment management
430
- const { data } = await ecod.crm.automations.enrollments("rule_id", {
431
- status: "active",
432
- page: 1,
433
- });
434
- await ecod.crm.automations.getEnrollment("enrollment_id");
435
- await ecod.crm.automations.pauseEnrollment("rule_id", "enrollment_id");
436
- await ecod.crm.automations.resumeEnrollment("rule_id", "enrollment_id");
437
-
438
- // Run inspection
439
- const { data: runs } = await ecod.crm.automations.runs("rule_id");
440
- await ecod.crm.automations.getRun("run_id");
441
- await ecod.crm.automations.resumeRun("run_id");
442
- await ecod.crm.automations.abortRun("run_id");
443
-
444
- // Unlock a wait_event node from external tool (e.g. Zapier, payment gateway callback)
445
- await ecod.crm.automations.webhookEvent("rule_id", "payment_received", {
446
- amount: 1500,
447
- currency: "INR",
448
- });
449
- ```
450
-
451
- ---
452
-
453
- #### `ecod.crm.sequences`
454
-
455
- Manually enroll / unenroll leads in drip automation sequences.
456
276
 
457
- ```typescript
458
- await ecod.crm.sequences.enroll({
459
- leadId: "lead_id",
460
- ruleId: "rule_id",
461
- variables: { discount: "20%" },
462
- });
463
- await ecod.crm.sequences.unenroll("enrollment_id");
464
- const { data } = await ecod.crm.sequences.listForLead("lead_id");
277
+ await events.product.erix.events.trigger({ trigger: "booking_created", phone: "+91XXXXXXXXXX" });
465
278
  ```
466
279
 
467
- ---
468
-
469
- #### `ecod.crm.activities`
470
-
471
- ```typescript
472
- // Lead timeline (all CRM events in order)
473
- const { data } = await ecod.crm.activities.timeline("lead_id", {
474
- page: 1,
475
- limit: 50,
476
- });
477
-
478
- // Filtered activity list
479
- const { data } = await ecod.crm.activities.list("lead_id", { type: "call" });
280
+ ## CRM: leads and pipelines
480
281
 
481
- // Log a call outcome
482
- await ecod.crm.activities.logCall("lead_id", {
483
- outcome: "answered",
484
- duration: 120,
485
- notes: "Interested in Pro plan",
282
+ ```ts
283
+ // Create or update by phone. `refs` are your own IDs, kept on the lead.
284
+ await ecod.product.erix.leads.upsert({
285
+ leadData: { phone: "+91XXXXXXXXXX", firstName: "Test", source: "website" },
286
+ refs: { orderId: "ORD-1042" },
486
287
  });
487
288
 
488
- // Log a generic activity
489
- await ecod.crm.activities.log({
490
- leadId: "lead_id",
491
- type: "email_opened",
492
- title: "Campaign Email Opened",
493
- });
494
-
495
- // --- Notes ---
496
- const { data: notes } = await ecod.crm.activities.notes.list("lead_id");
497
- await ecod.crm.activities.notes.create("lead_id", {
498
- content: "Call scheduled for Monday",
289
+ const { data: leads, pagination } = await ecod.product.erix.leads.list({
290
+ status: "open", // open | won | lost | archived
291
+ search: "test",
292
+ limit: 25,
499
293
  });
500
- await ecod.crm.activities.notes.update("note_id", "Updated note text");
501
- await ecod.crm.activities.notes.pin("note_id"); // pin to top
502
- await ecod.crm.activities.notes.pin("note_id", false); // unpin
503
- ```
504
294
 
505
- ---
506
-
507
- #### `ecod.crm.analytics`
508
-
509
- ```typescript
510
- const { data: overview } = await ecod.crm.analytics.overview("pipeline_id");
511
- const { data: conversion } =
512
- await ecod.crm.analytics.conversionRate("pipeline_id");
513
- const { data: velocity } = await ecod.crm.analytics.dealVelocity("pipeline_id");
514
- const { data: stageTime } = await ecod.crm.analytics.stageTime("pipeline_id");
295
+ await ecod.product.erix.leads.move("LEAD_ID", "STAGE_ID"); // pipeline column
296
+ await ecod.product.erix.leads.setFollowUp("LEAD_ID", "2026-10-20T15:00:00+05:30", "Share the quote");
297
+ await ecod.product.erix.leads.tags("LEAD_ID", { add: ["hot"], remove: ["cold"] });
298
+ await ecod.product.erix.leads.convert("LEAD_ID", "won", "Advance received");
299
+ await ecod.product.erix.leads.delete("LEAD_ID"); // archives; history stays
515
300
  ```
516
301
 
517
- ---
518
-
519
- #### `ecod.crm.scoring`
302
+ Other lookups: `retrieve(id)`, `retrieveByPhone(phone)`,
303
+ `retrieveByRef(key, value)`, `update(id, patch)`, `notes(id)`,
304
+ `createNote(id, { content })`, `activities(id)`, `profile(id)`,
305
+ `import(leads)` and `merge(primaryId, secondaryId)`.
520
306
 
521
- ```typescript
522
- const { data } = await ecod.crm.scoring.getConfig();
523
- await ecod.crm.scoring.updateConfig({ rules: [...] });
524
- ```
525
-
526
- ---
307
+ Data protection (DPDP). Only owners and admins can do this:
527
308
 
528
- #### `ecod.crm.payments`
309
+ ```ts
310
+ const { data: everything } = await ecod.product.erix.leads.export("LEAD_ID"); // (3.2.0)
529
311
 
530
- ```typescript
531
- const { data } = await ecod.crm.payments.list("lead_id");
312
+ // Irreversible. Wipes personal data across CRM, inbox and automations.
313
+ // Invoices are kept for GST records.
314
+ const { data: result } = await ecod.product.erix.leads.erase("LEAD_ID"); // (3.2.0)
315
+ console.log(result.invoicesKept);
532
316
  ```
533
317
 
534
- ---
535
-
536
- #### `ecod.crm.automationDashboard`
318
+ Pipelines:
537
319
 
538
- ```typescript
539
- const { data: stats } = await ecod.crm.automationDashboard.stats();
540
- const { data: logs } = await ecod.crm.automationDashboard.logs({
541
- limit: 10,
542
- status: "failed",
320
+ ```ts
321
+ const { data: pipeline } = await ecod.product.erix.pipelines.create({
322
+ name: "Admissions",
323
+ stages: ["Enquiry", "Visit booked", "Fee paid", "Admitted"],
543
324
  });
544
- await ecod.crm.automationDashboard.retryFailedEvent("log_id");
545
- ```
546
-
547
- ---
548
-
549
- ### Events & Workflows
550
325
 
551
- Access via `ecod.events`. Connect your external applications to the CRM automation engine.
326
+ const { data: pipelines } = await ecod.product.erix.pipelines.list();
327
+ const { data: board } = await ecod.product.erix.pipelines.board("PIPELINE_ID");
552
328
 
553
- **List all available triggers**
554
-
555
- ```typescript
556
- const { data: triggers } = await ecod.events.list();
329
+ await ecod.product.erix.pipelines.addStage("PIPELINE_ID", { name: "Follow-up", color: "#f59e0b" });
330
+ // Moves the stage's leads to another stage of the same pipeline, then deletes it.
331
+ await ecod.product.erix.pipelines.deleteStage("STAGE_ID", "TARGET_STAGE_ID");
557
332
  ```
558
333
 
559
- **Register a custom event**
560
-
561
- ```typescript
562
- await ecod.events.assign({
563
- name: "cart_abandoned",
564
- displayName: "Shopping Cart Abandoned",
565
- pipelineId: "pipeline_123",
566
- });
567
-
568
- // Deactivate
569
- await ecod.events.unassign("cart_abandoned");
570
- await ecod.events.unassignBulk(["event_a", "event_b"]);
571
- ```
334
+ ## Flow: events and custom triggers
572
335
 
573
- **Fire an event / trigger workflows**
336
+ `events.trigger()` fires an event. Every active flow with a matching trigger
337
+ starts a run for that contact. You need `phone` or `email`.
574
338
 
575
- ```typescript
576
- await ecod.events.trigger({
577
- trigger: "cart_abandoned",
578
- phone: "+919876543210",
579
- variables: { items: "T-Shirt, Mug", total: "₹850" },
339
+ ```ts
340
+ const res = await ecod.product.erix.events.trigger({
341
+ trigger: "appointment_booked",
342
+ phone: "+91XXXXXXXXXX",
343
+ variables: { slot: "Mon 11:00", branch: "Indore" },
580
344
  createLeadIfMissing: true,
581
- delayMinutes: 30,
582
- // Optional: auto-book a Google Meet during the workflow
583
- requiresMeet: true,
584
- meetConfig: {
585
- summary: "Follow-up call",
586
- duration: 30,
587
- timezone: "Asia/Kolkata",
588
- },
345
+ leadData: { firstName: "Test", source: "website" },
346
+ delayMinutes: 30, // or delaySeconds, or runAt (ISO with offset). Use one.
347
+ idempotencyKey: "booking-8812", // a retry with the same key won't start flows twice
589
348
  });
349
+ console.log(res.data?.idempotencyKey, res.data?.leadId);
590
350
  ```
591
351
 
592
- **Custom event definitions (CRM-managed)**
593
-
594
- ```typescript
595
- const { data } = await ecod.events.listCustomEvents();
596
- await ecod.events.createCustomEvent({
597
- name: "webinar_attended",
598
- displayName: "Webinar Attended",
599
- });
600
- await ecod.events.deleteCustomEvent("event_id");
601
- await ecod.events.emit({ eventName: "webinar_attended", leadId: "lead_id" });
602
- ```
603
-
604
- ---
605
-
606
- ### Marketing
607
-
608
- Access via `ecod.marketing`.
352
+ `createLeadIfMissing`, the delays and `runAt` are released per workspace.
353
+ Without the release the server ignores them and says so in the response.
609
354
 
610
- **Email campaigns**
355
+ Define your own triggers so flows can start on them:
611
356
 
612
- ```typescript
613
- await ecod.marketing.emails.sendCampaign({
614
- recipients: ["user@example.com"],
615
- subject: "Summer Sale!",
616
- html: "<h1>Get 20% off!</h1>",
357
+ ```ts
358
+ await ecod.product.erix.events.createCustomEvent({
359
+ name: "appointment_confirm", // lowercase letters, digits, _ . -
360
+ displayName: "Appointment confirmed",
617
361
  });
618
- await ecod.marketing.emails.sendTest("dev@example.com");
619
- ```
620
-
621
- **Campaigns (Email + SMS)**
622
362
 
623
- ```typescript
624
- await ecod.marketing.campaigns.create({ name: "Q2 Push", type: "email" });
625
- await ecod.marketing.campaigns.list({ status: "active" });
626
- await ecod.marketing.campaigns.retrieve("campaign_id");
627
- await ecod.marketing.campaigns.update("campaign_id", {
628
- subject: "New subject",
629
- });
630
- await ecod.marketing.campaigns.send("campaign_id", {
631
- scheduledAt: "2026-05-01T09:00:00Z",
632
- });
633
- await ecod.marketing.campaigns.stats("campaign_id");
634
- await ecod.marketing.campaigns.delete("campaign_id");
363
+ const { data } = await ecod.product.erix.events.listCustomEvents(); // data.triggers
364
+ await ecod.product.erix.events.deleteCustomEvent("TRIGGER_ID");
635
365
  ```
636
366
 
637
- **WhatsApp marketing (CRM-integrated template dispatch)**
638
-
639
- Unlike the direct `ecod.whatsapp.sendTemplate`, this endpoint resolves CRM field variables automatically.
367
+ Some runs stop at an approval step and wait for a person. You can list those
368
+ and decide on them:
640
369
 
641
- ```typescript
642
- await ecod.marketing.whatsapp.sendTemplate({
643
- phone: "+919876543210",
644
- templateName: "seasonal_offer",
645
- variables: { discount: "40%" },
646
- });
370
+ ```ts
371
+ const { data } = await ecod.product.erix.ai.approvals(); // { drafts, runs }
372
+ for (const run of data.runs) {
373
+ // Pass run.nodeId (3.2.0) so a stale view can't decide a different step.
374
+ await ecod.product.erix.ai.decideRun(run.id, "approve", run.nodeId);
375
+ }
647
376
  ```
648
377
 
649
- ---
650
-
651
- ### Meetings
378
+ ## Channels: inbox and direct send
652
379
 
653
- Access via `ecod.meet`. Backed by **Google Meet**.
380
+ All channels share one inbox. Any thread can be answered with
381
+ `chat.messages.send({ conversationId })`.
654
382
 
655
- ```typescript
656
- const { data: meeting } = await ecod.meet.create({
657
- leadId: "lead_id",
658
- participantName: "Priya Sharma",
659
- participantPhone: "+919876543210",
660
- startTime: "2026-04-10T10:00:00.000Z",
661
- endTime: "2026-04-10T10:30:00.000Z",
383
+ ```ts
384
+ const { data: threads } = await ecod.product.erix.chat.conversations.list({
385
+ channel: "whatsapp",
386
+ limit: 20,
662
387
  });
663
- console.log(meeting.meetLink); // → https://meet.google.com/abc-defg-hij
664
-
665
- const { data: meetings } = await ecod.meet.list({ status: "scheduled" });
666
- const { data } = await ecod.meet.retrieve("meeting_id");
667
- await ecod.meet.update("meeting_id", { startTime: "2026-04-11T11:00:00.000Z" });
668
- await ecod.meet.delete("meeting_id");
669
- ```
670
-
671
- ---
672
388
 
673
- ### Storage
674
-
675
- Access via `ecod.storage`. Powered by **Cloudflare R2**.
676
-
677
- This handles the full presigned-URL orchestration transparently:
678
-
679
- 1. Requests a presigned PUT URL from the backend.
680
- 2. Uploads directly to R2 (no proxy overhead).
681
- 3. Confirms the upload.
682
-
683
- ```typescript
684
- // Node.js: from a Buffer
685
- import { readFileSync } from "fs";
389
+ const { data: messages, hasMore } = await ecod.product.erix.chat.conversations.messages(
390
+ "CONVERSATION_ID",
391
+ { limit: 50 }, // page back with { before: oldestMessage.createdAt }
392
+ );
686
393
 
687
- const fileBuffer = readFileSync("./contract.pdf");
688
- const { data } = await ecod.storage.upload(fileBuffer, {
689
- folder: "customer_documents",
690
- filename: "contract.pdf",
691
- contentType: "application/pdf",
394
+ await ecod.product.erix.chat.messages.send({
395
+ conversationId: "CONVERSATION_ID",
396
+ text: "Your order is packed and ships today.",
692
397
  });
693
- console.log(data.url); // → https://cdn.ecodrix.com/customer_documents/contract.pdf
694
-
695
- // Browser: from an <input> element
696
- const file = document.getElementById("file-input").files[0];
697
- const { data } = await ecod.storage.upload(file, {
698
- folder: "avatars",
699
- filename: file.name,
700
- contentType: file.type,
398
+ await ecod.product.erix.chat.conversations.markRead("CONVERSATION_ID");
399
+
400
+ // Open (or find) a thread for a number and link it to a lead.
401
+ const { data: thread } = await ecod.product.erix.chat.conversations.create({
402
+ phone: "+91XXXXXXXXXX",
403
+ channel: "whatsapp",
701
404
  });
405
+ await ecod.product.erix.chat.conversations.linkLead(thread.id, "LEAD_ID");
702
406
  ```
703
407
 
704
- ---
705
-
706
- ### Media
707
-
708
- Access via `ecod.media`. Manage files stored in R2.
709
-
710
- ```typescript
711
- // Get a presigned download URL for a private file
712
- const { data } = await ecod.media.getDownloadUrl("confidential/contract.pdf");
713
-
714
- // Monitor storage quota
715
- const { data: usage } = await ecod.media.getUsage();
716
- console.log(`${usage.usedMB} MB of ${usage.limitMB} MB used`);
408
+ Send a file. Upload it to your storage first, then attach it:
717
409
 
718
- // List files in a folder
719
- await ecod.media.list({ folder: "invoices" });
410
+ ```ts
411
+ const input = document.querySelector<HTMLInputElement>("#attachment")!;
412
+ const media = await ecod.product.erix.chat.messages.upload(input.files![0]);
720
413
 
721
- // Delete a file
722
- await ecod.media.delete("invoices/old_invoice.pdf");
414
+ await ecod.product.erix.chat.messages.send({
415
+ conversationId: "CONVERSATION_ID",
416
+ media,
417
+ text: "Invoice attached",
418
+ });
723
419
  ```
724
420
 
725
- ---
726
-
727
- ### Email
421
+ Direct WhatsApp send, without an inbox thread ID:
728
422
 
729
- Access via `ecod.email`. Backed by SMTP or AWS SES depending on tenant configuration.
730
-
731
- ```typescript
732
- await ecod.email.sendEmailCampaign({
733
- subject: "Summer Discount!",
734
- recipients: ["user@example.com"],
735
- html: "<h1>Get 20% off all plans!</h1>",
423
+ ```ts
424
+ // An approved template. Use this to start a conversation.
425
+ await ecod.product.erix.chat.messages.sendTemplate({
426
+ to: "+91XXXXXXXXXX",
427
+ templateName: "order_update",
428
+ language: "en",
429
+ variables: ["ORD-1042", "₹1,499"],
736
430
  });
737
431
 
738
- // Verify SMTP configuration
739
- await ecod.email.sendTestEmail("admin@example.com");
432
+ // Free text. Only works inside WhatsApp's 24-hour customer service window.
433
+ await ecod.product.erix.chat.messages.send({ to: "+91XXXXXXXXXX", text: "Thanks, we got your enquiry." });
740
434
  ```
741
435
 
742
- ---
436
+ Manage templates with `ecod.infra.connect.whatsapp.templates`: `list()`,
437
+ `sync()` and `retrieve(name)`.
743
438
 
744
- ### Notifications & Logs
439
+ ## Invoices and payment links
745
440
 
746
- Access via `ecod.notifications`.
441
+ Amounts are in rupees. This invoice comes to ₹25,000 + 18% GST = ₹29,500:
747
442
 
748
- ```typescript
749
- // Automation execution logs
750
- const { data: logs } = await ecod.notifications.listLogs({
751
- trigger: "lead_created",
752
- status: "failed",
753
- startDate: "2026-04-01",
754
- endDate: "2026-04-30",
443
+ ```ts
444
+ const { data: invoice } = await ecod.product.erix.invoices.create({
445
+ clientName: "Test Customer",
446
+ clientPhone: "+91XXXXXXXXXX",
447
+ leadId: "LEAD_ID",
448
+ items: [{ description: "Website design", quantity: 1, rate: 25000, hsnSac: "998314" }],
449
+ taxRate: 18,
450
+ dueDate: "2026-11-15",
755
451
  });
756
452
 
757
- await ecod.notifications.retrieveLog("log_id");
758
-
759
- // Aggregate stats
760
- const { data: stats } = await ecod.notifications.getStats({
761
- startDate: "2026-04-01",
762
- endDate: "2026-04-30",
763
- });
453
+ // A link on your connected gateway (Razorpay or Stripe). Calling again returns the same link.
454
+ const { data: link } = await ecod.product.erix.invoices.paymentLink(invoice.id); // (3.2.0)
455
+ console.log(link.paymentLinkUrl);
764
456
 
765
- // Provider webhook callback logs
766
- const { data } = await ecod.notifications.listCallbacks({ limit: 20 });
457
+ await ecod.product.erix.invoices.sendWhatsApp(invoice.id); // (3.2.0) with the payment link
458
+ await ecod.product.erix.invoices.sendEmail(invoice.id); // (3.2.0) PDF + payment link
767
459
  ```
768
460
 
769
- ---
461
+ Invoices can't be deleted, only voided (`invoices.void(id)`), so GST records
462
+ stay intact. Other calls: `list({ status })`, `getInvoice(id)` and
463
+ `update(id, patch)`. Accounting exports live under
464
+ `product.erix.invoiceExport`: `csv()`, `tally()` and `zoho()`.
770
465
 
771
- ### Queue Management
466
+ A one-off payment link that isn't tied to an invoice, or a payment you took
467
+ offline:
772
468
 
773
- Access via `ecod.queue`. Inspect and manage background automation jobs.
774
-
775
- ```typescript
776
- // View failed jobs
777
- const { data: failed } = await ecod.queue.listFailed();
778
-
779
- // Get queue health by status
780
- const stats = await ecod.queue.getStats();
781
- // stats → { waiting: 12, active: 3, completed: 482, failed: 7, delayed: 0 }
782
-
783
- // Retry a failed job
784
- await ecod.queue.retryJob("job_id");
469
+ ```ts
470
+ await ecod.infra.connect.connections.createPaymentLink({
471
+ amount: 1499, // ₹1,499
472
+ description: "Consultation fee",
473
+ customer: { phone: "+91XXXXXXXXXX" },
474
+ });
785
475
 
786
- // Remove a job permanently
787
- await ecod.queue.deleteJob("job_id");
476
+ // Cash, UPI or bank transfer. With orderId, a retry is counted once.
477
+ await ecod.product.erix.payments.capture({ leadId: "LEAD_ID", amount: 5000, orderId: "UPI-2026-0042" });
788
478
  ```
789
479
 
790
- ---
791
-
792
- ### Health & Diagnostics
480
+ ## Storage uploads
793
481
 
794
- Access via `ecod.health`.
482
+ `media.upload()` gets a presigned URL, uploads straight to storage and then
483
+ confirms the upload, all in one call. The file bytes never pass through the
484
+ API, and your key never goes to the storage host.
795
485
 
796
- ```typescript
797
- // Global platform status
798
- const health = await ecod.health.system();
799
- // { status: "ok", version: "...", env: "production", uptime: 3600, db: "connected", queueDepth: 5 }
486
+ ```ts
487
+ import { readFile } from "node:fs/promises";
800
488
 
801
- // Tenant-scoped service readiness
802
- const clientHealth = await ecod.health.clientHealth();
803
- // { clientCode: "...", services: { whatsapp: "connected", email: "configured", googleMeet: "configured" }, ... }
489
+ const buffer = await readFile("./brochure.pdf");
490
+ const { data: file } = await ecod.infra.storage.media.upload(buffer, {
491
+ folder: "documents",
492
+ filename: "brochure.pdf",
493
+ contentType: "application/pdf",
494
+ });
804
495
 
805
- // Background job status lookup
806
- const job = await ecod.health.jobStatus("job_id");
496
+ const { data: download } = await ecod.infra.storage.files.getDownloadUrl<{
497
+ data: { downloadUrl: string };
498
+ }>(file.key);
499
+ console.log(download.downloadUrl); // short-lived URL
807
500
  ```
808
501
 
809
- ---
810
-
811
- ### Webhooks
812
-
813
- Access via `ecod.webhooks`.
502
+ The same call takes a browser `File`. Other calls: `files.list(folder)` and
503
+ `files.delete(key)`.
814
504
 
815
- > ⚠️ **Node.js only** — Requires `node:crypto`. Not available in browser bundles.
505
+ ## Realtime events
816
506
 
817
- ---
507
+ Events stream over a socket that's scoped to your workspace. You only receive
508
+ your own workspace's events.
818
509
 
819
- ## Enterprise Capabilities
510
+ ```ts
511
+ import type { Message } from "@ecodrix/erix-api";
820
512
 
821
- ### Auto-Paginating Iterators
513
+ const onMessage = ({ conversationId, message }: { conversationId: string; message: Message }) => {
514
+ console.log(conversationId, message.direction, message.text);
515
+ };
822
516
 
823
- Stream all records without manual page management:
517
+ ecod
518
+ .on("inbox.message", onMessage)
519
+ .on("message_status_update", ({ messageId, status }) => console.log(messageId, status))
520
+ .on("resync", () => {
521
+ // Reconnected after a gap too large to replay. Refetch your data.
522
+ });
824
523
 
825
- ```typescript
826
- // With optional type parameter for full type safety
827
- for await (const lead of ecod.crm.leads.listAutoPaging<Lead>({
828
- status: "won",
829
- })) {
830
- await syncToMyDatabase(lead);
524
+ // Later, e.g. in a React effect cleanup:
525
+ ecod.off("inbox.message", onMessage);
526
+ ```
527
+
528
+ | Event | Payload / meaning |
529
+ | --------------------------------------------- | ----------------------------------------------------------------------- |
530
+ | `inbox.message` | `{ conversationId, message }`. Every stored message: any channel, inbound or outbound |
531
+ | `message_status_update` | `{ messageId, conversationId, status }`. Sent, delivered, read or failed |
532
+ | `ai_suggestion` / `ai_handoff` | An AI draft is waiting / the AI handed a chat to a person |
533
+ | `workflow-run-update` | Flow run progress |
534
+ | `platform:notification` | A new in-app notification |
535
+ | `knowledge.source_updated` | A knowledge base source changed status |
536
+ | `resync` | Missed events couldn't be replayed. Refetch |
537
+
538
+ Each event's last argument is `{ seq }`. After a reconnect the SDK sends back
539
+ the last `seq` it saw and the server replays what was missed. A replay can
540
+ repeat an event, so write handlers that are safe to run twice (upsert by ID).
541
+
542
+ ## Webhook signature verification
543
+
544
+ Flow's webhook step sends JSON to your URL. If you set a secret on the
545
+ Webhook connection, the request is signed with it in
546
+ `x-ecodrix-signature: sha256=<hex>`. Verify the signature against the **raw**
547
+ body, before you parse it. Node only.
548
+
549
+ ```ts
550
+ import { APIError } from "@ecodrix/erix-api";
551
+ import { ecod } from "./ecodrix"; // your server-side client from Quick start
552
+
553
+ export async function POST(req: Request) {
554
+ const raw = await req.text();
555
+ try {
556
+ const event = await ecod.webhooks.constructEvent(
557
+ raw,
558
+ req.headers.get("x-ecodrix-signature") ?? undefined,
559
+ process.env.ECOD_WEBHOOK_SECRET!,
560
+ );
561
+ console.log("verified", event);
562
+ return Response.json({ received: true });
563
+ } catch (err) {
564
+ // WebhookSignatureError: APIError with code "invalid_signature"
565
+ const message = err instanceof APIError ? err.message : "Bad request";
566
+ return new Response(message, { status: 400 });
567
+ }
831
568
  }
832
569
  ```
833
570
 
834
- ### Bulk Data Chunking
571
+ ## Raw requests
835
572
 
836
- Insert thousands of leads without hitting rate limits. `createMany` automatically chunks and concurrently streams batches:
573
+ Use `ecod.request()` for an endpoint that has no typed method yet. You still
574
+ get auth, retries and the timeout. Paths are relative to `/v1/api`:
837
575
 
838
- ```typescript
839
- const results = await ecod.crm.leads.createMany(massiveArrayOfLeads, 100);
840
- console.log(`Ingested ${results.length} leads.`);
841
- ```
842
-
843
- ### Idempotency Keys
844
-
845
- Prevent duplicate requests caused by automatic retries:
846
-
847
- ```typescript
848
- await ecod.email.sendEmailCampaign(
849
- { subject: "Promo", recipients: ["user@example.com"], html: "..." },
850
- { idempotencyKey: "promo_campaign_uuid_2026_q2" },
851
- );
852
- ```
853
-
854
- ### Webhook Signature Verification
855
-
856
- Cryptographically verify that webhook payloads originated from ECODrIx:
857
-
858
- ```typescript
859
- app.post(
860
- "/api/webhooks",
861
- express.raw({ type: "application/json" }),
862
- async (req, res) => {
863
- try {
864
- const event = await ecod.webhooks.constructEvent(
865
- req.body.toString(),
866
- req.headers["x-ecodrix-signature"],
867
- "whsec_your_webhook_secret",
868
- );
869
- console.log("Verified event:", event);
870
- res.json({ received: true });
871
- } catch (err) {
872
- return res.status(400).send(`Invalid signature: ${err.message}`);
873
- }
874
- },
875
- );
876
- ```
877
-
878
- ### Raw Execution Engine
879
-
880
- Access any API endpoint — including experimental or custom ones — with full authentication and retry benefits:
881
-
882
- ```typescript
883
- const { data } = await ecod.request("POST", "/api/saas/beta-feature", {
884
- experimentalFlag: true,
576
+ ```ts
577
+ await ecod.request("POST", "/infra/connect/send/email", {
578
+ to: "customer@example.com",
579
+ subject: "Your booking is confirmed",
580
+ html: "<p>See you on Monday at 11:00.</p>",
885
581
  });
886
582
  ```
887
583
 
888
- ---
889
-
890
- ## Real-time Events
584
+ ## Errors
891
585
 
892
- The SDK maintains a persistent Socket.io connection, scoped to your `clientCode` tenant. Use `ecod.on()` to subscribe:
586
+ Failed calls throw. The API answers with an envelope like
587
+ `{ success: false, error, message, code? }`, and the SDK turns it into one of
588
+ these classes:
893
589
 
894
- ```typescript
895
- ecod
896
- .on("whatsapp.message_received", (msg) => {
897
- console.log(`New message from ${msg.from}: ${msg.body}`);
898
- })
899
- .on("crm.lead_created", ({ leadId }) => {
900
- console.log(`New lead: ${leadId}`);
901
- })
902
- .on("crm.lead_updated", ({ leadId, stageName }) => {
903
- console.log(`${leadId} moved to ${stageName}`);
904
- })
905
- .on("meet.scheduled", ({ meetLink }) => {
906
- console.log(`Meeting booked: ${meetLink}`);
907
- })
908
- .on("automation.failed", ({ trigger, reason }) => {
909
- alertTeam(`Automation ${trigger} failed: ${reason}`);
910
- });
590
+ | Class | When |
591
+ | --------------------- | -------------------------------------------------------------------- |
592
+ | `EcodrixError` | Base class of everything below |
593
+ | `APIError` | Any other failure. Has `.status`, `.code` (when sent) and `.message` |
594
+ | `AuthenticationError` | HTTP 401 or 403: bad key or client code, missing scope, or a role that isn't allowed |
595
+ | `RateLimitError` | HTTP 429, after the SDK's own retries run out |
911
596
 
912
- // Graceful shutdown
913
- process.on("SIGTERM", () => ecod.disconnect());
914
- ```
915
-
916
- ### Standard Event Reference
917
-
918
- | Event | Payload | Description |
919
- | --------------------------- | -------------------------------- | ------------------------------ |
920
- | `whatsapp.message_received` | `{ from, body, conversationId }` | Inbound WhatsApp message |
921
- | `whatsapp.message_sent` | `{ to, messageId }` | Outbound message delivered |
922
- | `crm.lead_created` | `{ leadId, phone }` | New CRM lead created |
923
- | `crm.lead_updated` | `{ leadId, stageName }` | Lead stage or fields updated |
924
- | `meet.scheduled` | `{ meetingId, meetLink }` | Google Meet appointment booked |
925
- | `storage.upload_confirmed` | `{ key, url, sizeBytes }` | File upload confirmed |
926
- | `automation.failed` | `{ trigger, reason, leadId }` | Automation execution failed |
927
-
928
- ---
929
-
930
- ## Error Handling
931
-
932
- All methods throw typed errors you can catch and inspect:
933
-
934
- ```typescript
935
- import {
936
- APIError,
937
- AuthenticationError,
938
- RateLimitError,
939
- Ecodrix,
940
- } from "@ecodrix/erix-api";
597
+ ```ts
598
+ import { APIError, AuthenticationError, RateLimitError } from "@ecodrix/erix-api";
941
599
 
942
600
  try {
943
- const { data } = await ecod.crm.leads.retrieve("non_existent_id");
601
+ await ecod.product.erix.leads.retrieve("LEAD_ID");
944
602
  } catch (err) {
945
603
  if (err instanceof AuthenticationError) {
946
- // 401 — invalid API key or client code
947
- console.error("Check your credentials.");
604
+ console.error("Check the key, client code, scope or role:", err.message);
948
605
  } else if (err instanceof RateLimitError) {
949
- // 429 — too many requests (SDK auto-retries up to 3×)
950
- console.warn("Rate limit hit.");
606
+ console.warn("Rate limited, back off and retry later");
951
607
  } else if (err instanceof APIError) {
952
- console.error(`API Error [${err.status}]: ${err.message}`);
608
+ console.error(`API error ${err.status ?? "network"}: ${err.message}`, err.code);
953
609
  } else {
954
610
  throw err;
955
611
  }
956
612
  }
957
613
  ```
958
614
 
959
- ### Error Classes
615
+ `ecod.request()` throws a plain `APIError` for every status.
960
616
 
961
- | Class | HTTP Status | Description |
962
- | --------------------- | ----------- | -------------------------------------------- |
963
- | `EcodrixError` | — | Base error class |
964
- | `APIError` | varies | Generic API error with `.status` and `.code` |
965
- | `AuthenticationError` | 401 | Invalid API key or client code |
966
- | `RateLimitError` | 429 | Too many requests |
617
+ **Retries and timeouts.** The SDK retries up to 3 times with exponential
618
+ backoff. It retries network errors (any method), 5xx responses on idempotent
619
+ methods (GET, PUT, DELETE) and 429 responses. Each request times out after
620
+ 30 s; change that with the `timeout` option. A timed-out request isn't retried.
621
+ A network error can also hit a POST whose response was lost, and the retry
622
+ then sends it a second time. Pass your own reference where a method takes one
623
+ (`refs`, `orderId`) so a duplicate is safe.
967
624
 
968
- ---
625
+ ## TypeScript
969
626
 
970
- ## Browser / CDN Usage
627
+ Methods return the response body, so pass a type argument to get it typed.
628
+ The SDK exports the main entity and parameter types (`Lead`,
629
+ `CreateLeadParams`, `ListLeadsParams`, `Pipeline`, `PipelineStage`,
630
+ `Conversation`, `Message`, `SendMessageParams`, `PausedRun`, `AiSuggestion`,
631
+ `KnowledgeSource`, `PaginatedResponse` and more).
971
632
 
972
- For usage without a bundler:
633
+ ```ts
634
+ import type { Lead, PaginatedResponse } from "@ecodrix/erix-api";
973
635
 
974
- ```html
975
- <!-- Via CDN (jsDelivr) -->
976
- <script src="https://cdn.jsdelivr.net/npm/@ecodrix/erix-api/dist/ts/browser/index.global.js"></script>
977
- <script>
978
- const ecod = new Ecodrix.Ecodrix({
979
- apiKey: "your_api_key",
980
- clientCode: "YOUR_CLIENT_CODE",
981
- });
636
+ const page = await ecod.product.erix.leads.list<PaginatedResponse<Lead>>({ limit: 50 });
637
+ console.log(page.pagination.total, page.data[0]?.firstName);
982
638
 
983
- ecod.whatsapp.messages.send({
984
- to: "+919876543210",
985
- text: "Hello from the browser!",
986
- });
987
- </script>
639
+ // Page through every lead without handling pages yourself.
640
+ for await (const lead of ecod.product.erix.leads.listAutoPaging<Lead>({ status: "open" })) {
641
+ console.log(lead.phone);
642
+ }
988
643
  ```
989
644
 
990
- > ⚠️ **Note**: `ecod.webhooks.constructEvent` and `ecod.storage.upload` (from `Buffer`) are Node.js only and will not function in browser environments.
991
-
992
- ---
993
-
994
- ## Contributing
645
+ ## Versioning
995
646
 
996
- 1. Fork the repository.
997
- 2. Create a feature branch: `git checkout -b feat/my-feature`
998
- 3. Make changes, then run `pnpm check` to validate formatting and lint.
999
- 4. Run `pnpm build` to verify the output compiles cleanly.
1000
- 5. Submit a Pull Request.
647
+ - The SDK follows [semver](https://semver.org) on its own line (3.x). Breaking
648
+ changes only land in a major release. See [CHANGELOG.md](CHANGELOG.md).
649
+ - The API server has its own version numbers: platform v1.0 is server 1.0.0.
650
+ The SDK calls the `/v1` routes, and the `version` option chooses the route
651
+ version.
652
+ - A method marked **(3.2.0)** needs SDK 3.2.0 or later.
1001
653
 
1002
- ---
654
+ Guides: https://api.ecodrix.com/docs. Endpoint reference:
655
+ https://api.ecodrix.com/reference.
1003
656
 
1004
657
  ## License
1005
658
 
1006
- [MIT](LICENSE) © 2026 [ECODrIx Team](https://ecodrix.com)
659
+ [MIT](LICENSE)