@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 +413 -828
- package/dist/cli.js +4 -4
- package/dist/ts/cjs/index.cjs +1 -1
- package/dist/ts/esm/index.d.ts +337 -171
- package/dist/ts/esm/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,1006 +1,591 @@
|
|
|
1
1
|
# @ecodrix/erix-api
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@ecodrix/erix-api)
|
|
4
|
+
[](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
|
-
|
|
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
|
-
|
|
29
|
+
Run this on your server. Keep the key in a server-only environment variable.
|
|
77
30
|
|
|
78
|
-
```
|
|
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: "
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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: "
|
|
110
|
-
phone: "+
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
65
|
+
The client follows the API's route tree:
|
|
123
66
|
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
+
Proxy route (Next.js App Router, `app/api/erix/[...path]/route.ts`):
|
|
146
91
|
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
await
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
+
Ticket route (`app/api/erix/ticket/route.ts`):
|
|
174
135
|
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
203
|
-
|
|
161
|
+
```ts
|
|
162
|
+
import { ECODrIxAPI } from "@ecodrix/erix-api";
|
|
204
163
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
+
```ts
|
|
202
|
+
import { ECODrIxAPI } from "@ecodrix/erix-api";
|
|
270
203
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
+
## CRM: leads and pipelines
|
|
309
213
|
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
+
```ts
|
|
242
|
+
const { data: everything } = await ecod.product.erix.leads.export("LEAD_ID"); // (3.2.0)
|
|
361
243
|
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
250
|
+
Pipelines:
|
|
367
251
|
|
|
368
|
-
```
|
|
369
|
-
await ecod.
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
458
|
-
await ecod.
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
variables: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
+
All channels share one inbox. Any thread can be answered with
|
|
313
|
+
`chat.messages.send({ conversationId })`.
|
|
593
314
|
|
|
594
|
-
```
|
|
595
|
-
const { data } = await ecod.
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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.
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
**Campaigns (Email + SMS)**
|
|
330
|
+
await ecod.product.erix.chat.conversations.markRead("CONVERSATION_ID");
|
|
622
331
|
|
|
623
|
-
|
|
624
|
-
await ecod.
|
|
625
|
-
|
|
626
|
-
|
|
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.
|
|
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
|
-
|
|
340
|
+
Send a file. Upload it to your storage first, then attach it:
|
|
638
341
|
|
|
639
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
666
|
-
|
|
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
|
-
|
|
371
|
+
## Invoices and payment links
|
|
678
372
|
|
|
679
|
-
|
|
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
|
-
```
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
//
|
|
719
|
-
await ecod.
|
|
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
|
-
//
|
|
722
|
-
await ecod.
|
|
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
|
-
|
|
398
|
+
A one-off payment link that isn't tied to an invoice, or a payment you took
|
|
399
|
+
offline:
|
|
728
400
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
//
|
|
739
|
-
await ecod.
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
418
|
+
```ts
|
|
419
|
+
import { readFile } from "node:fs/promises";
|
|
758
420
|
|
|
759
|
-
|
|
760
|
-
const { data:
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
766
|
-
|
|
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
|
-
|
|
437
|
+
## Realtime events
|
|
774
438
|
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
780
|
-
|
|
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
|
-
|
|
784
|
-
|
|
445
|
+
const onMessage = ({ conversationId, message }: { conversationId: string; message: Message }) => {
|
|
446
|
+
console.log(conversationId, message.direction, message.text);
|
|
447
|
+
};
|
|
785
448
|
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
533
|
+
await ecod.product.erix.leads.retrieve("LEAD_ID");
|
|
944
534
|
} catch (err) {
|
|
945
535
|
if (err instanceof AuthenticationError) {
|
|
946
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
547
|
+
`ecod.request()` throws a plain `APIError` for every status.
|
|
960
548
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
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
|
-
|
|
565
|
+
```ts
|
|
566
|
+
import type { Lead, PaginatedResponse } from "@ecodrix/erix-api";
|
|
973
567
|
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
---
|
|
993
|
-
|
|
994
|
-
## Contributing
|
|
577
|
+
## Versioning
|
|
995
578
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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)
|
|
591
|
+
[MIT](LICENSE)
|