@ecodrix/erix-api 3.1.0 → 3.1.2

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,591 @@
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. There is no CDN build.
73
26
 
74
- ---
27
+ ## Quick start
75
28
 
76
- ## Quick Start
29
+ Run this on your server. Keep the key in a server-only environment variable.
77
30
 
78
- ```typescript
31
+ ```ts
79
32
  import { ECODrIxAPI } from "@ecodrix/erix-api";
80
33
 
81
34
  const ecod = new ECODrIxAPI({
82
35
  apiKey: process.env.ECOD_API_KEY!,
83
- clientCode: "ERIX_CLNT_JHBJHF",
84
- // Optional — pick the API version. Defaults to "v1".
85
- version: "v1",
36
+ clientCode: "ERIX_CLNT_XXXXXX", // Settings → Developer in the dashboard
86
37
  });
87
38
 
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!",
98
- });
99
-
100
- // Create a CRM lead
101
- const lead = await ecod.product.erix.leads.create({
102
- firstName: "Priya",
103
- phone: "+919876543210",
39
+ const { data: lead } = await ecod.product.erix.leads.create({
40
+ firstName: "Test",
41
+ lastName: "Customer",
42
+ phone: "+91XXXXXXXXXX",
104
43
  source: "website",
105
44
  });
106
45
 
107
- // Fire an automation trigger
108
46
  await ecod.product.erix.events.trigger({
109
- trigger: "trial_started",
110
- phone: "+919876543210",
111
- createLeadIfMissing: true,
47
+ trigger: "enquiry_received",
48
+ phone: "+91XXXXXXXXXX",
112
49
  });
50
+
51
+ // The client keeps a realtime socket open. Close it when your process stops.
52
+ process.on("SIGTERM", () => ecod.disconnect());
113
53
  ```
114
54
 
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`.
55
+ | Option | Type | Default | Notes |
56
+ | ------------ | ---------------------------- | -------- | -------------------------------------------------------- |
57
+ | `apiKey` | `string` | none | Workspace key or a [scoped key](#scoped-keys). Server only. |
58
+ | `clientCode` | `string` | none | Your workspace code, e.g. `ERIX_CLNT_XXXXXX`. |
59
+ | `version` | `"v1"` | `"v1"` | API route version. |
60
+ | `timeout` | `number` (ms) | `30000` | Per request. `0` disables. |
61
+ | `proxy` | `{ url, ticket? }` | none | Browser mode. Replaces `apiKey`. |
119
62
 
120
- ---
63
+ ## How the client is laid out
121
64
 
122
- ## Configuration
65
+ The client follows the API's route tree:
123
66
 
124
- Pass an options object to `new Ecodrix(options)`:
67
+ | Namespace | What it covers |
68
+ | ---------------------------- | ------------------------------------------------------------------- |
69
+ | `ecod.product.erix.*` | CRM (`leads`, `pipelines`, `activities`, `analytics`), `events` (Flow triggers), `chat` (inbox), `invoices`, `payments`, `ai` |
70
+ | `ecod.infra.connect.*` | Channels: `whatsapp.templates`, `email`, `meet`, `connections` |
71
+ | `ecod.infra.storage.*` | Files: `media`, `files`, `folders` |
72
+ | `ecod.platform.*` | Workspace: `knowledge` (AI knowledge base), `users.notifications` |
73
+ | `ecod.webhooks` | Webhook signature verification |
74
+ | `ecod.on()` / `ecod.off()` | Realtime events |
75
+ | `ecod.request()` | Authenticated call to any endpoint without a typed method yet |
125
76
 
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.*`) |
77
+ ## Browser use (proxy mode)
132
78
 
133
- ```typescript
134
- const ecod = new ECODrIxAPI({
135
- apiKey: process.env.ECOD_API_KEY!,
136
- clientCode: "ERIX_CLNT_JHBJHF",
137
- version: "v1",
138
- });
139
- ```
79
+ Never put a workspace key in the browser. That includes `NEXT_PUBLIC_*`,
80
+ `VITE_*` or any other variable that ends up in a client bundle. Anyone who
81
+ opens dev tools can read it, and a workspace key has full access.
140
82
 
141
- ---
83
+ In the browser, run the SDK in proxy mode:
142
84
 
143
- ## Resources
85
+ 1. Your server exposes `/api/erix/*`. It checks your signed-in user, adds the
86
+ key, and forwards the request to ECODrIx.
87
+ 2. For realtime, your server mints a 5-minute socket ticket. The browser gets
88
+ the ticket, never the key.
144
89
 
145
- ### WhatsApp
90
+ Proxy route (Next.js App Router, `app/api/erix/[...path]/route.ts`):
146
91
 
147
- Access via `ecod.whatsapp`.
92
+ ```ts
93
+ const API = "https://api.ecodrix.com";
94
+ const PASS_HEADERS = ["content-type", "accept", "idempotency-key"];
95
+ const DROP_HEADERS = ["content-encoding", "content-length", "transfer-encoding", "connection"];
148
96
 
149
- ---
97
+ async function proxy(req: Request, ctx: { params: Promise<{ path: string[] }> }) {
98
+ const user = await requireUser(req); // your own session check
99
+ if (!user) {
100
+ return Response.json({ success: false, error: "Unauthorized" }, { status: 401 });
101
+ }
150
102
 
151
- #### `ecod.whatsapp.messages`
103
+ const { path } = await ctx.params;
104
+ const rel = `/${path.map(encodeURIComponent).join("/")}`;
105
+ if (!/^\/v\d+\/api\//.test(rel)) {
106
+ return Response.json({ success: false, error: "Not found" }, { status: 404 });
107
+ }
152
108
 
153
- **Send a text message**
109
+ const headers = new Headers({
110
+ "x-api-key": process.env.ECOD_API_KEY!,
111
+ "x-client-code": "ERIX_CLNT_XXXXXX",
112
+ });
113
+ for (const name of PASS_HEADERS) {
114
+ const value = req.headers.get(name);
115
+ if (value) headers.set(name, value);
116
+ }
154
117
 
155
- ```typescript
156
- await ecod.whatsapp.messages.send({
157
- to: "+919876543210",
158
- text: "Your appointment is confirmed!",
159
- });
160
- ```
118
+ const hasBody = req.method !== "GET" && req.method !== "HEAD";
119
+ const upstream = await fetch(`${API}${rel}${new URL(req.url).search}`, {
120
+ method: req.method,
121
+ headers,
122
+ body: hasBody ? await req.arrayBuffer() : undefined,
123
+ redirect: "manual",
124
+ });
161
125
 
162
- **Send a media message (image, video, document, audio)**
126
+ const out = new Headers(upstream.headers);
127
+ for (const name of DROP_HEADERS) out.delete(name);
128
+ return new Response(upstream.body, { status: upstream.status, headers: out });
129
+ }
163
130
 
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",
170
- });
131
+ export { proxy as GET, proxy as POST, proxy as PUT, proxy as PATCH, proxy as DELETE };
171
132
  ```
172
133
 
173
- **Send a template message via queue**
134
+ Ticket route (`app/api/erix/ticket/route.ts`):
174
135
 
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
- });
182
- ```
136
+ ```ts
137
+ export async function POST(req: Request) {
138
+ const user = await requireUser(req); // your own session check
139
+ if (!user) return new Response("Unauthorized", { status: 401 });
183
140
 
184
- **Mark messages as read**
141
+ const res = await fetch("https://api.ecodrix.com/v1/api/platform/realtime/ticket", {
142
+ method: "POST",
143
+ headers: {
144
+ "x-api-key": process.env.ECOD_API_KEY!,
145
+ "x-client-code": "ERIX_CLNT_XXXXXX",
146
+ },
147
+ cache: "no-store",
148
+ });
149
+ const body = (await res.json().catch(() => null)) as { data?: { ticket?: string } } | null;
150
+ const ticket = body?.data?.ticket;
151
+ if (!res.ok || !ticket) return new Response("Ticket unavailable", { status: 502 });
185
152
 
186
- ```typescript
187
- await ecod.whatsapp.messages.markRead("message_id");
153
+ return new Response(ticket, {
154
+ headers: { "content-type": "text/plain", "cache-control": "no-store" },
155
+ });
156
+ }
188
157
  ```
189
158
 
190
- ---
191
-
192
- #### `ecod.whatsapp.conversations`
193
-
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
- });
159
+ Browser client:
201
160
 
202
- // Get a specific conversation
203
- const conv = await ecod.whatsapp.conversations.retrieve("conversation_id");
161
+ ```ts
162
+ import { ECODrIxAPI } from "@ecodrix/erix-api";
204
163
 
205
- // Get messages in a conversation
206
- const { data: msgs } = await ecod.whatsapp.conversations.messages(
207
- "conversation_id",
208
- {
209
- limit: 50,
164
+ export const ecod = new ECODrIxAPI({
165
+ proxy: {
166
+ url: "/api/erix",
167
+ ticket: () =>
168
+ fetch("/api/erix/ticket", { method: "POST" }).then((r) =>
169
+ r.ok ? r.text() : Promise.reject(new Error("No realtime ticket")),
170
+ ),
210
171
  },
211
- );
212
-
213
- // Create a conversation explicitly
214
- await ecod.whatsapp.conversations.create({
215
- phone: "+919876543210",
216
- name: "Priya",
217
172
  });
218
-
219
- // Link a conversation to a CRM lead
220
- await ecod.whatsapp.conversations.linkLead("conversation_id", "lead_id");
221
-
222
- // Mark a conversation as read (clears unread badge)
223
- await ecod.whatsapp.conversations.markRead("conversation_id");
224
-
225
- // Delete / archive a conversation
226
- await ecod.whatsapp.conversations.delete("conversation_id");
227
-
228
- // Bulk delete conversations
229
- await ecod.whatsapp.conversations.bulkDelete(["conv_1", "conv_2"]);
230
- ```
231
-
232
- ---
233
-
234
- #### `ecod.whatsapp.templates`
235
-
236
- ```typescript
237
- // List pre-approved Meta templates
238
- const { data } = await ecod.whatsapp.templates.list();
239
-
240
- // Sync templates from the Meta Business Account
241
- await ecod.whatsapp.templates.sync();
242
173
  ```
243
174
 
244
- ---
245
-
246
- #### `ecod.whatsapp.broadcasts`
175
+ The SDK fetches a fresh ticket on every reconnect. Without `ticket`, REST
176
+ calls work and no socket opens.
247
177
 
248
- Send a personalised WhatsApp template message to multiple recipients at once.
178
+ Anyone signed in to your app can now do whatever the workspace key can.
179
+ If your users shouldn't have all of that, allow-list the paths they need in the
180
+ proxy route.
249
181
 
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
- });
182
+ ## Scoped keys
260
183
 
261
- // List past broadcasts
262
- const { data } = await ecod.whatsapp.broadcasts.list({ status: "completed" });
263
- ```
184
+ For an integration that does one job (a booking form that fires a Flow event,
185
+ say), create a scoped key under **Settings → Developer → Scoped API keys**.
186
+ Scoped keys can be revoked, can expire and have usage tracking. Each one
187
+ carries a single grant:
264
188
 
265
- ---
189
+ | Purpose | Grant | Works for |
190
+ | ---------------- | ------------------ | --------------------------------------------- |
191
+ | Trigger events | `flow:events` | `product.erix.events.trigger()` |
192
+ | Send WhatsApp | `connect:whatsapp` | `chat.messages.send({ to })`, `sendTemplate()`|
193
+ | Send email | `connect:email` | `POST /infra/connect/send/email` |
194
+ | Send Telegram | `connect:telegram` | `POST /infra/connect/send/telegram` |
266
195
 
267
- #### `ecod.whatsapp.sendTemplate` — Direct Template (Bypass Queue)
196
+ Keys look like `erix_<scope>_<secret>`. Pass one as `apiKey`. The server
197
+ enforces its grant, so any call outside the grant fails with
198
+ `AuthenticationError` (HTTP 403). Over plain HTTP you can also send it as
199
+ `Authorization: Bearer erix_<scope>_…`.
268
200
 
269
- Use this for high-priority utility templates that must deliver immediately outside the automation queue.
201
+ ```ts
202
+ import { ECODrIxAPI } from "@ecodrix/erix-api";
270
203
 
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",
278
- },
204
+ const events = new ECODrIxAPI({
205
+ apiKey: process.env.ECOD_EVENTS_KEY!, // erix_flow_… with flow:events
206
+ clientCode: "ERIX_CLNT_XXXXXX",
279
207
  });
280
- // result.messageId → WA message ID
281
- ```
282
-
283
- ---
284
-
285
- ### CRM
286
-
287
- Access via `ecod.crm`.
288
-
289
- ---
290
-
291
- #### `ecod.crm.leads`
292
208
 
293
- **Create**
294
-
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
- });
209
+ await events.product.erix.events.trigger({ trigger: "booking_created", phone: "+91XXXXXXXXXX" });
306
210
  ```
307
211
 
308
- **Upsert by phone** — Creates if absent, updates if exists
212
+ ## CRM: leads and pipelines
309
213
 
310
- ```typescript
311
- await ecod.crm.leads.upsert({
312
- leadData: { phone: "+919876543210", firstName: "Priya" },
313
- trigger: "webinar_joined",
214
+ ```ts
215
+ // Create or update by phone. `refs` are your own IDs, kept on the lead.
216
+ await ecod.product.erix.leads.upsert({
217
+ leadData: { phone: "+91XXXXXXXXXX", firstName: "Test", source: "website" },
218
+ refs: { orderId: "ORD-1042" },
314
219
  });
315
- ```
316
-
317
- **List with filters**
318
220
 
319
- ```typescript
320
- const { data } = await ecod.crm.leads.list({
321
- status: "new",
322
- source: "whatsapp",
323
- pipelineId: "pipeline_id",
324
- page: 1,
221
+ const { data: leads, pagination } = await ecod.product.erix.leads.list({
222
+ status: "open", // open | won | lost | archived
223
+ search: "test",
325
224
  limit: 25,
326
225
  });
327
- ```
328
226
 
329
- **Retrieve**
330
-
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");
335
- ```
336
-
337
- **Update**
338
-
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" },
344
- });
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");
227
+ await ecod.product.erix.leads.move("LEAD_ID", "STAGE_ID"); // pipeline column
228
+ await ecod.product.erix.leads.setFollowUp("LEAD_ID", "2026-10-20T15:00:00+05:30", "Share the quote");
229
+ await ecod.product.erix.leads.tags("LEAD_ID", { add: ["hot"], remove: ["cold"] });
230
+ await ecod.product.erix.leads.convert("LEAD_ID", "won", "Advance received");
231
+ await ecod.product.erix.leads.delete("LEAD_ID"); // archives; history stays
352
232
  ```
353
233
 
354
- **Tags**
234
+ Other lookups: `retrieve(id)`, `retrieveByPhone(phone)`,
235
+ `retrieveByRef(key, value)`, `update(id, patch)`, `notes(id)`,
236
+ `createNote(id, { content })`, `activities(id)`, `profile(id)`,
237
+ `import(leads)` and `merge(primaryId, secondaryId)`.
355
238
 
356
- ```typescript
357
- await ecod.crm.leads.tags("lead_id", { add: ["vip", "hot"], remove: ["cold"] });
358
- ```
239
+ Data protection (DPDP). Only owners and admins can do this:
359
240
 
360
- **Score**
241
+ ```ts
242
+ const { data: everything } = await ecod.product.erix.leads.export("LEAD_ID"); // (3.2.0)
361
243
 
362
- ```typescript
363
- await ecod.crm.leads.recalculateScore("lead_id");
244
+ // Irreversible. Wipes personal data across CRM, inbox and automations.
245
+ // Invoices are kept for GST records.
246
+ const { data: result } = await ecod.product.erix.leads.erase("LEAD_ID"); // (3.2.0)
247
+ console.log(result.invoicesKept);
364
248
  ```
365
249
 
366
- **Bulk**
250
+ Pipelines:
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
- ```
373
-
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",
252
+ ```ts
253
+ const { data: pipeline } = await ecod.product.erix.pipelines.create({
254
+ name: "Admissions",
255
+ stages: ["Enquiry", "Visit booked", "Fee paid", "Admitted"],
397
256
  });
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
- ```
406
257
 
407
- ---
258
+ const { data: pipelines } = await ecod.product.erix.pipelines.list();
259
+ const { data: board } = await ecod.product.erix.pipelines.board("PIPELINE_ID");
408
260
 
409
- #### `ecod.crm.automations`
410
-
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: [],
419
- });
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
- });
261
+ await ecod.product.erix.pipelines.addStage("PIPELINE_ID", { name: "Follow-up", color: "#f59e0b" });
262
+ // Moves the stage's leads to another stage of the same pipeline, then deletes it.
263
+ await ecod.product.erix.pipelines.deleteStage("STAGE_ID", "TARGET_STAGE_ID");
449
264
  ```
450
265
 
451
- ---
452
-
453
- #### `ecod.crm.sequences`
266
+ ## Flow: events and custom triggers
454
267
 
455
- Manually enroll / unenroll leads in drip automation sequences.
268
+ `events.trigger()` fires an event. Every active flow with a matching trigger
269
+ starts a run for that contact. You need `phone` or `email`.
456
270
 
457
- ```typescript
458
- await ecod.crm.sequences.enroll({
459
- leadId: "lead_id",
460
- ruleId: "rule_id",
461
- variables: { discount: "20%" },
271
+ ```ts
272
+ const res = await ecod.product.erix.events.trigger({
273
+ trigger: "appointment_booked",
274
+ phone: "+91XXXXXXXXXX",
275
+ variables: { slot: "Mon 11:00", branch: "Indore" },
276
+ createLeadIfMissing: true,
277
+ leadData: { firstName: "Test", source: "website" },
278
+ delayMinutes: 30, // or delaySeconds, or runAt (ISO with offset). Use one.
279
+ idempotencyKey: "booking-8812", // a retry with the same key won't start flows twice
462
280
  });
463
- await ecod.crm.sequences.unenroll("enrollment_id");
464
- const { data } = await ecod.crm.sequences.listForLead("lead_id");
281
+ console.log(res.data?.idempotencyKey, res.data?.leadId);
465
282
  ```
466
283
 
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" });
480
-
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",
486
- });
284
+ `createLeadIfMissing`, the delays and `runAt` are released per workspace.
285
+ Without the release the server ignores them and says so in the response.
487
286
 
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
- });
287
+ Define your own triggers so flows can start on them:
494
288
 
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
+ ```ts
290
+ await ecod.product.erix.events.createCustomEvent({
291
+ name: "appointment_confirm", // lowercase letters, digits, _ . -
292
+ displayName: "Appointment confirmed",
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
-
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");
515
- ```
516
-
517
- ---
518
-
519
- #### `ecod.crm.scoring`
520
-
521
- ```typescript
522
- const { data } = await ecod.crm.scoring.getConfig();
523
- await ecod.crm.scoring.updateConfig({ rules: [...] });
524
- ```
525
-
526
- ---
527
-
528
- #### `ecod.crm.payments`
529
294
 
530
- ```typescript
531
- const { data } = await ecod.crm.payments.list("lead_id");
295
+ const { data } = await ecod.product.erix.events.listCustomEvents(); // data.triggers
296
+ await ecod.product.erix.events.deleteCustomEvent("TRIGGER_ID");
532
297
  ```
533
298
 
534
- ---
299
+ Some runs stop at an approval step and wait for a person. You can list those
300
+ and decide on them:
535
301
 
536
- #### `ecod.crm.automationDashboard`
537
-
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",
543
- });
544
- await ecod.crm.automationDashboard.retryFailedEvent("log_id");
545
- ```
546
-
547
- ---
548
-
549
- ### Events & Workflows
550
-
551
- Access via `ecod.events`. Connect your external applications to the CRM automation engine.
552
-
553
- **List all available triggers**
554
-
555
- ```typescript
556
- const { data: triggers } = await ecod.events.list();
557
- ```
558
-
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"]);
302
+ ```ts
303
+ const { data } = await ecod.product.erix.ai.approvals(); // { drafts, runs }
304
+ for (const run of data.runs) {
305
+ // Pass run.nodeId (3.2.0) so a stale view can't decide a different step.
306
+ await ecod.product.erix.ai.decideRun(run.id, "approve", run.nodeId);
307
+ }
571
308
  ```
572
309
 
573
- **Fire an event / trigger workflows**
574
-
575
- ```typescript
576
- await ecod.events.trigger({
577
- trigger: "cart_abandoned",
578
- phone: "+919876543210",
579
- variables: { items: "T-Shirt, Mug", total: "₹850" },
580
- 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
- },
589
- });
590
- ```
310
+ ## Channels: inbox and direct send
591
311
 
592
- **Custom event definitions (CRM-managed)**
312
+ All channels share one inbox. Any thread can be answered with
313
+ `chat.messages.send({ conversationId })`.
593
314
 
594
- ```typescript
595
- const { data } = await ecod.events.listCustomEvents();
596
- await ecod.events.createCustomEvent({
597
- name: "webinar_attended",
598
- displayName: "Webinar Attended",
315
+ ```ts
316
+ const { data: threads } = await ecod.product.erix.chat.conversations.list({
317
+ channel: "whatsapp",
318
+ limit: 20,
599
319
  });
600
- await ecod.events.deleteCustomEvent("event_id");
601
- await ecod.events.emit({ eventName: "webinar_attended", leadId: "lead_id" });
602
- ```
603
-
604
- ---
605
320
 
606
- ### Marketing
607
-
608
- Access via `ecod.marketing`.
609
-
610
- **Email campaigns**
321
+ const { data: messages, hasMore } = await ecod.product.erix.chat.conversations.messages(
322
+ "CONVERSATION_ID",
323
+ { limit: 50 }, // page back with { before: oldestMessage.createdAt }
324
+ );
611
325
 
612
- ```typescript
613
- await ecod.marketing.emails.sendCampaign({
614
- recipients: ["user@example.com"],
615
- subject: "Summer Sale!",
616
- html: "<h1>Get 20% off!</h1>",
326
+ await ecod.product.erix.chat.messages.send({
327
+ conversationId: "CONVERSATION_ID",
328
+ text: "Your order is packed and ships today.",
617
329
  });
618
- await ecod.marketing.emails.sendTest("dev@example.com");
619
- ```
620
-
621
- **Campaigns (Email + SMS)**
330
+ await ecod.product.erix.chat.conversations.markRead("CONVERSATION_ID");
622
331
 
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",
332
+ // Open (or find) a thread for a number and link it to a lead.
333
+ const { data: thread } = await ecod.product.erix.chat.conversations.create({
334
+ phone: "+91XXXXXXXXXX",
335
+ channel: "whatsapp",
629
336
  });
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");
337
+ await ecod.product.erix.chat.conversations.linkLead(thread.id, "LEAD_ID");
635
338
  ```
636
339
 
637
- **WhatsApp marketing (CRM-integrated template dispatch)**
340
+ Send a file. Upload it to your storage first, then attach it:
638
341
 
639
- Unlike the direct `ecod.whatsapp.sendTemplate`, this endpoint resolves CRM field variables automatically.
342
+ ```ts
343
+ const input = document.querySelector<HTMLInputElement>("#attachment")!;
344
+ const media = await ecod.product.erix.chat.messages.upload(input.files![0]);
640
345
 
641
- ```typescript
642
- await ecod.marketing.whatsapp.sendTemplate({
643
- phone: "+919876543210",
644
- templateName: "seasonal_offer",
645
- variables: { discount: "40%" },
346
+ await ecod.product.erix.chat.messages.send({
347
+ conversationId: "CONVERSATION_ID",
348
+ media,
349
+ text: "Invoice attached",
646
350
  });
647
351
  ```
648
352
 
649
- ---
650
-
651
- ### Meetings
353
+ Direct WhatsApp send, without an inbox thread ID:
652
354
 
653
- Access via `ecod.meet`. Backed by **Google Meet**.
654
-
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",
355
+ ```ts
356
+ // An approved template. Use this to start a conversation.
357
+ await ecod.product.erix.chat.messages.sendTemplate({
358
+ to: "+91XXXXXXXXXX",
359
+ templateName: "order_update",
360
+ language: "en",
361
+ variables: ["ORD-1042", "₹1,499"],
662
362
  });
663
- console.log(meeting.meetLink); // → https://meet.google.com/abc-defg-hij
664
363
 
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");
364
+ // Free text. Only works inside WhatsApp's 24-hour customer service window.
365
+ await ecod.product.erix.chat.messages.send({ to: "+91XXXXXXXXXX", text: "Thanks, we got your enquiry." });
669
366
  ```
670
367
 
671
- ---
672
-
673
- ### Storage
674
-
675
- Access via `ecod.storage`. Powered by **Cloudflare R2**.
368
+ Manage templates with `ecod.infra.connect.whatsapp.templates`: `list()`,
369
+ `sync()` and `retrieve(name)`.
676
370
 
677
- This handles the full presigned-URL orchestration transparently:
371
+ ## Invoices and payment links
678
372
 
679
- 1. Requests a presigned PUT URL from the backend.
680
- 2. Uploads directly to R2 (no proxy overhead).
681
- 3. Confirms the upload.
373
+ Amounts are in rupees. This invoice comes to ₹25,000 + 18% GST = ₹29,500:
682
374
 
683
- ```typescript
684
- // Node.js: from a Buffer
685
- import { readFileSync } from "fs";
686
-
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",
692
- });
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,
375
+ ```ts
376
+ const { data: invoice } = await ecod.product.erix.invoices.create({
377
+ clientName: "Test Customer",
378
+ clientPhone: "+91XXXXXXXXXX",
379
+ leadId: "LEAD_ID",
380
+ items: [{ description: "Website design", quantity: 1, rate: 25000, hsnSac: "998314" }],
381
+ taxRate: 18,
382
+ dueDate: "2026-11-15",
701
383
  });
702
- ```
703
-
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`);
717
384
 
718
- // List files in a folder
719
- await ecod.media.list({ folder: "invoices" });
385
+ // A link on your connected gateway (Razorpay or Stripe). Calling again returns the same link.
386
+ const { data: link } = await ecod.product.erix.invoices.paymentLink(invoice.id); // (3.2.0)
387
+ console.log(link.paymentLinkUrl);
720
388
 
721
- // Delete a file
722
- await ecod.media.delete("invoices/old_invoice.pdf");
389
+ await ecod.product.erix.invoices.sendWhatsApp(invoice.id); // (3.2.0) with the payment link
390
+ await ecod.product.erix.invoices.sendEmail(invoice.id); // (3.2.0) PDF + payment link
723
391
  ```
724
392
 
725
- ---
393
+ Invoices can't be deleted, only voided (`invoices.void(id)`), so GST records
394
+ stay intact. Other calls: `list({ status })`, `getInvoice(id)` and
395
+ `update(id, patch)`. Accounting exports live under
396
+ `product.erix.invoiceExport`: `csv()`, `tally()` and `zoho()`.
726
397
 
727
- ### Email
398
+ A one-off payment link that isn't tied to an invoice, or a payment you took
399
+ offline:
728
400
 
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>",
401
+ ```ts
402
+ await ecod.infra.connect.connections.createPaymentLink({
403
+ amount: 1499, // ₹1,499
404
+ description: "Consultation fee",
405
+ customer: { phone: "+91XXXXXXXXXX" },
736
406
  });
737
407
 
738
- // Verify SMTP configuration
739
- await ecod.email.sendTestEmail("admin@example.com");
408
+ // Cash, UPI or bank transfer. With orderId, a retry is counted once.
409
+ await ecod.product.erix.payments.capture({ leadId: "LEAD_ID", amount: 5000, orderId: "UPI-2026-0042" });
740
410
  ```
741
411
 
742
- ---
743
-
744
- ### Notifications & Logs
745
-
746
- Access via `ecod.notifications`.
412
+ ## Storage uploads
747
413
 
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",
755
- });
414
+ `media.upload()` gets a presigned URL, uploads straight to storage and then
415
+ confirms the upload, all in one call. The file bytes never pass through the
416
+ API, and your key never goes to the storage host.
756
417
 
757
- await ecod.notifications.retrieveLog("log_id");
418
+ ```ts
419
+ import { readFile } from "node:fs/promises";
758
420
 
759
- // Aggregate stats
760
- const { data: stats } = await ecod.notifications.getStats({
761
- startDate: "2026-04-01",
762
- endDate: "2026-04-30",
421
+ const buffer = await readFile("./brochure.pdf");
422
+ const { data: file } = await ecod.infra.storage.media.upload(buffer, {
423
+ folder: "documents",
424
+ filename: "brochure.pdf",
425
+ contentType: "application/pdf",
763
426
  });
764
427
 
765
- // Provider webhook callback logs
766
- const { data } = await ecod.notifications.listCallbacks({ limit: 20 });
428
+ const { data: download } = await ecod.infra.storage.files.getDownloadUrl<{
429
+ data: { downloadUrl: string };
430
+ }>(file.key);
431
+ console.log(download.downloadUrl); // short-lived URL
767
432
  ```
768
433
 
769
- ---
770
-
771
- ### Queue Management
434
+ The same call takes a browser `File`. Other calls: `files.list(folder)` and
435
+ `files.delete(key)`.
772
436
 
773
- Access via `ecod.queue`. Inspect and manage background automation jobs.
437
+ ## Realtime events
774
438
 
775
- ```typescript
776
- // View failed jobs
777
- const { data: failed } = await ecod.queue.listFailed();
439
+ Events stream over a socket that's scoped to your workspace. You only receive
440
+ your own workspace's events.
778
441
 
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 }
442
+ ```ts
443
+ import type { Message } from "@ecodrix/erix-api";
782
444
 
783
- // Retry a failed job
784
- await ecod.queue.retryJob("job_id");
445
+ const onMessage = ({ conversationId, message }: { conversationId: string; message: Message }) => {
446
+ console.log(conversationId, message.direction, message.text);
447
+ };
785
448
 
786
- // Remove a job permanently
787
- await ecod.queue.deleteJob("job_id");
788
- ```
789
-
790
- ---
791
-
792
- ### Health & Diagnostics
793
-
794
- Access via `ecod.health`.
795
-
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 }
800
-
801
- // Tenant-scoped service readiness
802
- const clientHealth = await ecod.health.clientHealth();
803
- // { clientCode: "...", services: { whatsapp: "connected", email: "configured", googleMeet: "configured" }, ... }
804
-
805
- // Background job status lookup
806
- const job = await ecod.health.jobStatus("job_id");
807
- ```
808
-
809
- ---
810
-
811
- ### Webhooks
812
-
813
- Access via `ecod.webhooks`.
814
-
815
- > ⚠️ **Node.js only** — Requires `node:crypto`. Not available in browser bundles.
816
-
817
- ---
818
-
819
- ## Enterprise Capabilities
820
-
821
- ### Auto-Paginating Iterators
822
-
823
- Stream all records without manual page management:
449
+ ecod
450
+ .on("inbox.message", onMessage)
451
+ .on("message_status_update", ({ messageId, status }) => console.log(messageId, status))
452
+ .on("resync", () => {
453
+ // Reconnected after a gap too large to replay. Refetch your data.
454
+ });
824
455
 
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);
456
+ // Later, e.g. in a React effect cleanup:
457
+ ecod.off("inbox.message", onMessage);
458
+ ```
459
+
460
+ | Event | Payload / meaning |
461
+ | --------------------------------------------- | ----------------------------------------------------------------------- |
462
+ | `inbox.message` | `{ conversationId, message }`. Every stored message: any channel, inbound or outbound |
463
+ | `message_status_update` | `{ messageId, conversationId, status }`. Sent, delivered, read or failed |
464
+ | `ai_suggestion` / `ai_handoff` | An AI draft is waiting / the AI handed a chat to a person |
465
+ | `workflow-run-update` | Flow run progress |
466
+ | `platform:notification` | A new in-app notification |
467
+ | `knowledge.source_updated` | A knowledge base source changed status |
468
+ | `resync` | Missed events couldn't be replayed. Refetch |
469
+
470
+ Each event's last argument is `{ seq }`. After a reconnect the SDK sends back
471
+ the last `seq` it saw and the server replays what was missed. A replay can
472
+ repeat an event, so write handlers that are safe to run twice (upsert by ID).
473
+
474
+ ## Webhook signature verification
475
+
476
+ Flow's webhook step sends JSON to your URL. If you set a secret on the
477
+ Webhook connection, the request is signed with it in
478
+ `x-ecodrix-signature: sha256=<hex>`. Verify the signature against the **raw**
479
+ body, before you parse it. Node only.
480
+
481
+ ```ts
482
+ import { APIError } from "@ecodrix/erix-api";
483
+ import { ecod } from "./ecodrix"; // your server-side client from Quick start
484
+
485
+ export async function POST(req: Request) {
486
+ const raw = await req.text();
487
+ try {
488
+ const event = await ecod.webhooks.constructEvent(
489
+ raw,
490
+ req.headers.get("x-ecodrix-signature") ?? undefined,
491
+ process.env.ECOD_WEBHOOK_SECRET!,
492
+ );
493
+ console.log("verified", event);
494
+ return Response.json({ received: true });
495
+ } catch (err) {
496
+ // WebhookSignatureError: APIError with code "invalid_signature"
497
+ const message = err instanceof APIError ? err.message : "Bad request";
498
+ return new Response(message, { status: 400 });
499
+ }
831
500
  }
832
501
  ```
833
502
 
834
- ### Bulk Data Chunking
835
-
836
- Insert thousands of leads without hitting rate limits. `createMany` automatically chunks and concurrently streams batches:
837
-
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
503
+ ## Raw requests
879
504
 
880
- Access any API endpoint — including experimental or custom ones — with full authentication and retry benefits:
505
+ Use `ecod.request()` for an endpoint that has no typed method yet. You still
506
+ get auth, retries and the timeout. Paths are relative to `/v1/api`:
881
507
 
882
- ```typescript
883
- const { data } = await ecod.request("POST", "/api/saas/beta-feature", {
884
- experimentalFlag: true,
508
+ ```ts
509
+ await ecod.request("POST", "/infra/connect/send/email", {
510
+ to: "customer@example.com",
511
+ subject: "Your booking is confirmed",
512
+ html: "<p>See you on Monday at 11:00.</p>",
885
513
  });
886
514
  ```
887
515
 
888
- ---
889
-
890
- ## Real-time Events
891
-
892
- The SDK maintains a persistent Socket.io connection, scoped to your `clientCode` tenant. Use `ecod.on()` to subscribe:
893
-
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
- });
911
-
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 |
516
+ ## Errors
927
517
 
928
- ---
518
+ Failed calls throw. The API answers with an envelope like
519
+ `{ success: false, error, message, code? }`, and the SDK turns it into one of
520
+ these classes:
929
521
 
930
- ## Error Handling
522
+ | Class | When |
523
+ | --------------------- | -------------------------------------------------------------------- |
524
+ | `EcodrixError` | Base class of everything below |
525
+ | `APIError` | Any other failure. Has `.status`, `.code` (when sent) and `.message` |
526
+ | `AuthenticationError` | HTTP 401 or 403: bad key or client code, missing scope, or a role that isn't allowed |
527
+ | `RateLimitError` | HTTP 429, after the SDK's own retries run out |
931
528
 
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";
529
+ ```ts
530
+ import { APIError, AuthenticationError, RateLimitError } from "@ecodrix/erix-api";
941
531
 
942
532
  try {
943
- const { data } = await ecod.crm.leads.retrieve("non_existent_id");
533
+ await ecod.product.erix.leads.retrieve("LEAD_ID");
944
534
  } catch (err) {
945
535
  if (err instanceof AuthenticationError) {
946
- // 401 — invalid API key or client code
947
- console.error("Check your credentials.");
536
+ console.error("Check the key, client code, scope or role:", err.message);
948
537
  } else if (err instanceof RateLimitError) {
949
- // 429 — too many requests (SDK auto-retries up to 3×)
950
- console.warn("Rate limit hit.");
538
+ console.warn("Rate limited, back off and retry later");
951
539
  } else if (err instanceof APIError) {
952
- console.error(`API Error [${err.status}]: ${err.message}`);
540
+ console.error(`API error ${err.status ?? "network"}: ${err.message}`, err.code);
953
541
  } else {
954
542
  throw err;
955
543
  }
956
544
  }
957
545
  ```
958
546
 
959
- ### Error Classes
547
+ `ecod.request()` throws a plain `APIError` for every status.
960
548
 
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 |
549
+ **Retries and timeouts.** The SDK retries up to 3 times with exponential
550
+ backoff. It retries network errors (any method), 5xx responses on idempotent
551
+ methods (GET, PUT, DELETE) and 429 responses. Each request times out after
552
+ 30 s; change that with the `timeout` option. A timed-out request isn't retried.
553
+ A network error can also hit a POST whose response was lost, and the retry
554
+ then sends it a second time. Pass your own reference where a method takes one
555
+ (`refs`, `orderId`) so a duplicate is safe.
967
556
 
968
- ---
557
+ ## TypeScript
969
558
 
970
- ## Browser / CDN Usage
559
+ Methods return the response body, so pass a type argument to get it typed.
560
+ The SDK exports the main entity and parameter types (`Lead`,
561
+ `CreateLeadParams`, `ListLeadsParams`, `Pipeline`, `PipelineStage`,
562
+ `Conversation`, `Message`, `SendMessageParams`, `PausedRun`, `AiSuggestion`,
563
+ `KnowledgeSource`, `PaginatedResponse` and more).
971
564
 
972
- For usage without a bundler:
565
+ ```ts
566
+ import type { Lead, PaginatedResponse } from "@ecodrix/erix-api";
973
567
 
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
- });
568
+ const page = await ecod.product.erix.leads.list<PaginatedResponse<Lead>>({ limit: 50 });
569
+ console.log(page.pagination.total, page.data[0]?.firstName);
982
570
 
983
- ecod.whatsapp.messages.send({
984
- to: "+919876543210",
985
- text: "Hello from the browser!",
986
- });
987
- </script>
571
+ // Page through every lead without handling pages yourself.
572
+ for await (const lead of ecod.product.erix.leads.listAutoPaging<Lead>({ status: "open" })) {
573
+ console.log(lead.phone);
574
+ }
988
575
  ```
989
576
 
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
577
+ ## Versioning
995
578
 
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.
579
+ - The SDK follows [semver](https://semver.org) on its own line (3.x). Breaking
580
+ changes only land in a major release. See [CHANGELOG.md](CHANGELOG.md).
581
+ - The API server has its own version numbers: platform v1.0 is server 1.0.0.
582
+ The SDK calls the `/v1` routes, and the `version` option chooses the route
583
+ version.
584
+ - A method marked **(3.2.0)** needs SDK 3.2.0 or later.
1001
585
 
1002
- ---
586
+ Guides: https://api.ecodrix.com/docs. Endpoint reference:
587
+ https://api.ecodrix.com/reference.
1003
588
 
1004
589
  ## License
1005
590
 
1006
- [MIT](LICENSE) © 2026 [ECODrIx Team](https://ecodrix.com)
591
+ [MIT](LICENSE)