@ecodrix/erix-api 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/core.ts ADDED
@@ -0,0 +1,262 @@
1
+ import axios, { type AxiosInstance, type Method } from "axios";
2
+ import axiosRetry from "axios-retry";
3
+ import { AuthenticationError, APIError } from "./error";
4
+ import { WhatsApp } from "./resources/whatsapp";
5
+ import { CRM } from "./resources/crm";
6
+ import { MediaResource } from "./resources/media";
7
+ import { Meetings } from "./resources/meet";
8
+ import { Notifications } from "./resources/notifications";
9
+ import { EmailResource } from "./resources/email";
10
+ import { EventsResource } from "./resources/events";
11
+ import { Webhooks } from "./resources/webhooks";
12
+ import { io, type Socket } from "socket.io-client";
13
+
14
+ declare const process: any;
15
+
16
+ /**
17
+ * Configuration options for the Ecodrix client.
18
+ */
19
+ export interface EcodrixOptions {
20
+ /**
21
+ * Your ECODrIx Platform API key.
22
+ * Obtain this from the ECODrIx dashboard under Settings → API Keys.
23
+ * @example "ecod_live_sk_..."
24
+ */
25
+ apiKey: string;
26
+
27
+ /**
28
+ * Your tenant ID (Client Code).
29
+ * This scopes all API requests to your specific organisation.
30
+ * Required for most operations.
31
+ * @example "ERIX_CLNT_JHBJHF"
32
+ */
33
+ clientCode?: string;
34
+
35
+ /**
36
+ * Override the base URL of the ECODrIx API.
37
+ * Useful for pointing to a local development or staging server.
38
+ * @default "https://api.ecodrix.com"
39
+ */
40
+ baseUrl?: string;
41
+
42
+ /**
43
+ * Override the Socket.io server URL for real-time events.
44
+ * Defaults to the same value as `baseUrl`.
45
+ * @default "https://api.ecodrix.com"
46
+ */
47
+ socketUrl?: string;
48
+ }
49
+
50
+ /**
51
+ * The primary entry point for the ECODrIx SDK.
52
+ *
53
+ * Initialise once with your credentials and use the namespaced resources
54
+ * to interact with every part of the platform.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * import { Ecodrix } from "@ecodrix/erix-api";
59
+ *
60
+ * const ecod = new Ecodrix({
61
+ * apiKey: process.env.ECOD_API_KEY!,
62
+ * clientCode: "ERIX_CLNT_JHBJHF",
63
+ * });
64
+ *
65
+ * await ecod.whatsapp.messages.send({ to: "+91...", text: "Hello!" });
66
+ * const lead = await ecod.crm.leads.create({ firstName: "Alice", phone: "+91..." });
67
+ * ```
68
+ */
69
+ export class Ecodrix {
70
+ private readonly client: AxiosInstance;
71
+ private readonly socket: Socket;
72
+
73
+ /** WhatsApp messaging and conversation management. */
74
+ public readonly whatsapp: WhatsApp;
75
+
76
+ /** CRM resources — Leads and related sub-resources. */
77
+ public readonly crm: CRM;
78
+
79
+ /** Cloudflare R2-backed media storage. */
80
+ public readonly media: MediaResource;
81
+
82
+ /** Google Meet appointment scheduling. */
83
+ public readonly meet: Meetings;
84
+
85
+ /** Automation execution logs and provider webhook callbacks. */
86
+ public readonly notifications: Notifications;
87
+
88
+ /** Outbound email marketing engine. */
89
+ public readonly email: EmailResource;
90
+
91
+ /** Lead events and workflow automation triggers. */
92
+ public readonly events: EventsResource;
93
+
94
+ /** Cryptographic webhook signature verification. */
95
+ public readonly webhooks: Webhooks;
96
+
97
+ constructor(options: EcodrixOptions) {
98
+ if (!options.apiKey) {
99
+ throw new AuthenticationError("API Key is required");
100
+ }
101
+
102
+ const baseUrl = options.baseUrl || "https://api.ecodrix.com";
103
+ const socketUrl = options.socketUrl || baseUrl;
104
+
105
+ const isBrowser =
106
+ typeof window !== "undefined" && typeof window.document !== "undefined";
107
+ const runtime = isBrowser
108
+ ? "browser"
109
+ : typeof process !== "undefined"
110
+ ? `node ${process.version}`
111
+ : "unknown";
112
+ const os = isBrowser
113
+ ? globalThis.navigator?.userAgent || "browser"
114
+ : typeof process !== "undefined"
115
+ ? process.platform
116
+ : "unknown";
117
+
118
+ this.client = axios.create({
119
+ baseURL: baseUrl,
120
+ headers: {
121
+ "x-api-key": options.apiKey,
122
+ "x-client-code": options.clientCode?.toUpperCase(),
123
+ "Content-Type": "application/json",
124
+ "x-ecodrix-client-agent": JSON.stringify({
125
+ sdk_version: "1.0.0", // Can be auto-injected during build in future
126
+ runtime,
127
+ os,
128
+ }),
129
+ },
130
+ });
131
+
132
+ // Make the client completely bulletproof for execution from external projects.
133
+ // It will automatically handle network blips, 502 Bad Gateways, and 429 Rate Limits.
134
+ axiosRetry(this.client, {
135
+ retries: 3,
136
+ retryDelay: axiosRetry.exponentialDelay,
137
+ retryCondition: (error) => {
138
+ return (
139
+ axiosRetry.isNetworkOrIdempotentRequestError(error) ||
140
+ error.response?.status === 429
141
+ );
142
+ },
143
+ });
144
+
145
+ // Initialise resources
146
+ this.whatsapp = new WhatsApp(this.client);
147
+ this.crm = new CRM(this.client);
148
+ this.media = new MediaResource(this.client);
149
+ this.meet = new Meetings(this.client);
150
+ this.notifications = new Notifications(this.client);
151
+ this.email = new EmailResource(this.client);
152
+ this.events = new EventsResource(this.client);
153
+ this.webhooks = new Webhooks();
154
+
155
+ // Establish persistent Socket.io connection
156
+ this.socket = io(socketUrl, {
157
+ extraHeaders: {
158
+ "x-api-key": options.apiKey,
159
+ "x-client-code": options.clientCode?.toUpperCase() || "",
160
+ },
161
+ });
162
+
163
+ this.setupSocket(options.clientCode);
164
+ }
165
+
166
+ private setupSocket(clientCode?: string) {
167
+ this.socket.on("connect", () => {
168
+ if (clientCode) {
169
+ // Join the tenant-scoped room to receive only relevant events.
170
+ this.socket.emit("join-room", clientCode.toUpperCase());
171
+ }
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Subscribe to a real-time event emitted by the ECODrIx platform.
177
+ *
178
+ * The SDK maintains a persistent Socket.io connection. Events are
179
+ * scoped to your `clientCode` — you will only receive events for
180
+ * your own tenant.
181
+ *
182
+ * **Standard events:**
183
+ * - `whatsapp.message_received` — inbound WhatsApp message
184
+ * - `whatsapp.message_sent` — outbound message delivered
185
+ * - `crm.lead_created` — new CRM lead ingested
186
+ * - `crm.lead_updated` — lead details or stage changed
187
+ * - `meet.scheduled` — Google Meet appointment booked
188
+ * - `storage.upload_confirmed` — file upload completed
189
+ * - `automation.failed` — automation execution error
190
+ *
191
+ * @param event - The event name to subscribe to.
192
+ * @param callback - The handler function invoked when the event fires.
193
+ * @returns `this` for method chaining.
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * ecod
198
+ * .on("whatsapp.message_received", (msg) => console.log(msg.body))
199
+ * .on("automation.failed", (err) => alertTeam(err));
200
+ * ```
201
+ */
202
+ public on(event: string, callback: (...args: any[]) => void): this {
203
+ this.socket.on(event, callback);
204
+ return this;
205
+ }
206
+
207
+ /**
208
+ * Gracefully disconnect the real-time Socket.io connection.
209
+ * Call this when shutting down your server or when the client is
210
+ * no longer needed to free up resources.
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * process.on("SIGTERM", () => ecod.disconnect());
215
+ * ```
216
+ */
217
+ public disconnect() {
218
+ this.socket.disconnect();
219
+ }
220
+
221
+ /**
222
+ * Raw Execution Escape-Hatch.
223
+ * Send an authenticated HTTP request directly to the ECODrIx backend from ANY external project.
224
+ *
225
+ * This is extremely powerful giving you full unrestricted access to make calls
226
+ * against new, experimental, or completely custom backend APIs while still benefitting
227
+ * from the SDK's built-in authentication and automatic `axios-retry` logic.
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * const { data } = await ecod.request("POST", "/api/saas/experimental-feature", { flag: true });
232
+ * console.log(data);
233
+ * ```
234
+ */
235
+ public async request<T = any>(
236
+ method: Method,
237
+ path: string,
238
+ data?: any,
239
+ params?: any,
240
+ ): Promise<T> {
241
+ try {
242
+ const response = await this.client.request<T>({
243
+ method,
244
+ url: path,
245
+ data,
246
+ params,
247
+ });
248
+ return response.data;
249
+ } catch (error: any) {
250
+ if (error.response) {
251
+ throw new APIError(
252
+ error.response.data?.message ||
253
+ error.response.data?.error ||
254
+ "Raw Execution Failed",
255
+ error.response.status,
256
+ error.response.data?.code,
257
+ );
258
+ }
259
+ throw new APIError(error.message || "Network Error");
260
+ }
261
+ }
262
+ }
package/src/error.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Typed error hierarchy for the @ecodrix/erix-api SDK.
3
+ *
4
+ * All errors thrown by the SDK extend `EcodrixError`, so a single
5
+ * `catch` block can handle them all, while still allowing granular
6
+ * differentiation between auth failures, rate limits, and generic API errors.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { APIError, AuthenticationError, RateLimitError } from "@ecodrix/erix-api";
11
+ *
12
+ * try {
13
+ * await ecod.crm.leads.retrieve("invalid_id");
14
+ * } catch (err) {
15
+ * if (err instanceof AuthenticationError) {
16
+ * console.error("Invalid API key or client code.");
17
+ * } else if (err instanceof RateLimitError) {
18
+ * console.warn("Slow down — rate limit hit.");
19
+ * } else if (err instanceof APIError) {
20
+ * console.error(`API error ${err.status}: ${err.message}`);
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+
26
+ /** Base class for all ECODrIx SDK errors. */
27
+ export class EcodrixError extends Error {
28
+ constructor(message: string) {
29
+ super(message);
30
+ this.name = "EcodrixError";
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Represents an error returned by the ECODrIx API.
36
+ * Carries the HTTP `status` code and an optional `code` string.
37
+ */
38
+ export class APIError extends EcodrixError {
39
+ /** HTTP status code returned by the API (e.g. 400, 404, 500). */
40
+ public status?: number;
41
+ /** Machine-readable error code (e.g. `"NOT_FOUND"`, `"VALIDATION_ERROR"`). */
42
+ public code?: string;
43
+
44
+ constructor(message: string, status?: number, code?: string) {
45
+ super(message);
46
+ this.name = "APIError";
47
+ this.status = status;
48
+ this.code = code;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Thrown when the API key or client code is missing, invalid, or expired.
54
+ * HTTP 401.
55
+ */
56
+ export class AuthenticationError extends APIError {
57
+ constructor(message = "Invalid API Key or Client Code") {
58
+ super(message, 401, "AUTH_FAILED");
59
+ this.name = "AuthenticationError";
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Thrown when the rate limit for the API key has been exceeded.
65
+ * HTTP 429. Implement exponential backoff before retrying.
66
+ */
67
+ export class RateLimitError extends APIError {
68
+ constructor(message = "Too many requests. Please slow down.") {
69
+ super(message, 429, "RATE_LIMIT_EXCEEDED");
70
+ this.name = "RateLimitError";
71
+ }
72
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ export { Ecodrix, type EcodrixOptions } from "./core";
2
+ export * from "./error";
3
+
4
+ // Resource Type Exports
5
+ export * from "./resources/whatsapp/messages";
6
+ export * from "./resources/whatsapp/conversations";
7
+ export * from "./resources/whatsapp/index";
8
+ export * from "./resources/crm/leads";
9
+ export * from "./resources/meet";
10
+ export * from "./resources/media";
11
+ export * from "./resources/notifications";
12
+ export * from "./resources/email";
13
+ export * from "./resources/events";
14
+ export * from "./resources/webhooks";
15
+
16
+ // Export the main client also as default for better ergonomics
17
+ import { Ecodrix } from "./core";
18
+ export default Ecodrix;
@@ -0,0 +1,73 @@
1
+ import type { AxiosInstance, AxiosRequestConfig } from "axios";
2
+ import { APIError } from "./error";
3
+
4
+ export interface RequestOptions extends AxiosRequestConfig {
5
+ /**
6
+ * If true, will not add the x-client-code header.
7
+ */
8
+ ignoreClientCode?: boolean;
9
+
10
+ /**
11
+ * Safe execution idempotency key.
12
+ * If provided, the backend will safely ignore duplicate requests retried with the same key.
13
+ */
14
+ idempotencyKey?: string;
15
+ }
16
+
17
+ export abstract class APIResource {
18
+ public constructor(protected readonly client: AxiosInstance) {}
19
+
20
+ protected async post<T>(url: string, data?: any, options?: RequestOptions): Promise<T> {
21
+ try {
22
+ const config = this.buildConfig(options);
23
+ const response = await this.client.post(url, data, config);
24
+ return response.data;
25
+ } catch (error: any) {
26
+ this.handleError(error);
27
+ }
28
+ }
29
+
30
+ protected async get<T>(url: string, options?: RequestOptions): Promise<T> {
31
+ try {
32
+ const config = this.buildConfig(options);
33
+ const response = await this.client.get(url, config);
34
+ return response.data;
35
+ } catch (error: any) {
36
+ this.handleError(error);
37
+ }
38
+ }
39
+
40
+ protected async deleteRequest<T>(url: string, options?: RequestOptions): Promise<T> {
41
+ try {
42
+ const config = this.buildConfig(options);
43
+ const response = await this.client.delete(url, config);
44
+ return response.data;
45
+ } catch (error: any) {
46
+ this.handleError(error);
47
+ }
48
+ }
49
+
50
+ private buildConfig(options?: RequestOptions): AxiosRequestConfig | undefined {
51
+ if (!options) return undefined;
52
+
53
+ const config: AxiosRequestConfig = { ...options };
54
+ if (options.idempotencyKey) {
55
+ config.headers = {
56
+ ...config.headers,
57
+ "Idempotency-Key": options.idempotencyKey,
58
+ };
59
+ }
60
+ return config;
61
+ }
62
+
63
+ private handleError(error: any): never {
64
+ if (error.response) {
65
+ throw new APIError(
66
+ error.response.data?.message || error.response.data?.error || "API Request Failed",
67
+ error.response.status,
68
+ error.response.data?.code
69
+ );
70
+ }
71
+ throw new APIError(error.message || "Network Error");
72
+ }
73
+ }
@@ -0,0 +1,10 @@
1
+ import { Leads } from "./leads";
2
+ import type { AxiosInstance } from "axios";
3
+
4
+ export class CRM {
5
+ public leads: Leads;
6
+
7
+ constructor(client: AxiosInstance) {
8
+ this.leads = new Leads(client);
9
+ }
10
+ }
@@ -0,0 +1,186 @@
1
+ import { APIResource } from "../../resource";
2
+
3
+ /**
4
+ * Parameters for creating a new CRM Lead.
5
+ */
6
+ export interface CreateLeadParams {
7
+ /** Lead's first name. Required. */
8
+ firstName: string;
9
+ /** Lead's last name. */
10
+ lastName?: string;
11
+ /** Lead's email address. */
12
+ email?: string;
13
+ /** Lead's phone number in E.164 format (e.g. "+919876543210"). */
14
+ phone?: string;
15
+ /**
16
+ * Acquisition channel.
17
+ * @example "website" | "whatsapp" | "direct" | "referral"
18
+ */
19
+ source?: string;
20
+ /** Arbitrary key-value metadata (UTM params, order IDs, etc.). */
21
+ metadata?: Record<string, any>;
22
+ }
23
+
24
+ /**
25
+ * CRM Lead resource — full lifecycle management.
26
+ *
27
+ * Access via `ecod.crm.leads`.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const { data: lead } = await ecod.crm.leads.create({
32
+ * firstName: "Alice",
33
+ * phone: "+919876543210",
34
+ * source: "website",
35
+ * });
36
+ * ```
37
+ */
38
+ export class Leads extends APIResource {
39
+ /**
40
+ * Create a new lead in the CRM pipeline.
41
+ *
42
+ * @param params - Lead creation parameters. `firstName` is required.
43
+ * @returns The newly created Lead document.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const { data } = await ecod.crm.leads.create({
48
+ * firstName: "Alice",
49
+ * phone: "+919876543210",
50
+ * });
51
+ * ```
52
+ */
53
+ async create<T = any>(params: CreateLeadParams) {
54
+ return this.post<T>("/api/services/leads", params);
55
+ }
56
+
57
+ /**
58
+ * Bulk ingest leads efficiently in parallel using automatic chunking.
59
+ * Prevents rate limit exhaustion by executing `chunkSize` requests at a time.
60
+ *
61
+ * @param leads - Array of leads to create.
62
+ * @param chunkSize - Number of leads to send concurrently (default: 50)
63
+ * @returns Array of created lead results.
64
+ */
65
+ async createMany(leads: CreateLeadParams[], chunkSize = 50): Promise<any[]> {
66
+ const results: any[] = [];
67
+
68
+ for (let i = 0; i < leads.length; i += chunkSize) {
69
+ const chunk = leads.slice(i, i + chunkSize);
70
+
71
+ const chunkPromises = chunk.map((lead) => this.create(lead));
72
+ const chunkResults = await Promise.allSettled(chunkPromises);
73
+
74
+ for (const res of chunkResults) {
75
+ if (res.status === "fulfilled") {
76
+ results.push(res.value);
77
+ } else {
78
+ throw res.reason;
79
+ }
80
+ }
81
+ }
82
+
83
+ return results;
84
+ }
85
+
86
+ /**
87
+ * List leads with optional filtering and pagination.
88
+ *
89
+ * @param params - Filter options (status, source, pipelineId, page, limit, etc.)
90
+ * @returns Paginated list of Lead documents.
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * const { data } = await ecod.crm.leads.list({ status: "new", limit: 25 });
95
+ * ```
96
+ */
97
+ async list<T = any>(params?: Record<string, any>) {
98
+ return this.get<T>("/api/services/leads", { params } as any);
99
+ }
100
+
101
+ /**
102
+ * Auto-paginating iterator for leads.
103
+ * Seamlessly fetches leads page by page as you iterate.
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * for await (const lead of ecod.crm.leads.listAutoPaging({ status: "won" })) {
108
+ * console.log(lead.firstName);
109
+ * }
110
+ * ```
111
+ */
112
+ async *listAutoPaging(params?: Record<string, any>): AsyncGenerator<any, void, unknown> {
113
+ let currentPage = params?.page || 1;
114
+ let hasMore = true;
115
+
116
+ while (hasMore) {
117
+ const response: any = await this.list({ ...params, page: currentPage });
118
+
119
+ const items = Array.isArray(response.data) ? response.data : response || [];
120
+
121
+ if (items.length === 0) {
122
+ hasMore = false;
123
+ break;
124
+ }
125
+
126
+ for (const item of items) {
127
+ yield item;
128
+ }
129
+
130
+ if (response.pagination && currentPage < response.pagination.pages) {
131
+ currentPage++;
132
+ } else if (!response.pagination && items.length > 0) {
133
+ // Fallback: If no explicit pagination struct, assume simple array and just keep pulling until empty
134
+ currentPage++;
135
+ } else {
136
+ hasMore = false;
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Retrieve a single lead by its unique ID.
143
+ *
144
+ * @param leadId - The MongoDB ObjectId of the lead.
145
+ * @returns The Lead document, or a 404 error if not found.
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * const { data } = await ecod.crm.leads.retrieve("64abc...");
150
+ * ```
151
+ */
152
+ async retrieve(leadId: string) {
153
+ return this.get(`/api/services/leads/${leadId}`);
154
+ }
155
+
156
+ /**
157
+ * Update the fields of an existing lead.
158
+ *
159
+ * @param leadId - The ID of the lead to update.
160
+ * @param params - Partial lead fields to update.
161
+ * @returns The updated Lead document.
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * await ecod.crm.leads.update("64abc...", { email: "new@email.com" });
166
+ * ```
167
+ */
168
+ async update(leadId: string, params: Partial<CreateLeadParams>) {
169
+ return this.post(`/api/services/leads/${leadId}`, params);
170
+ }
171
+
172
+ /**
173
+ * Archive (soft-delete) a lead. The record is retained in the database
174
+ * for audit purposes but is excluded from all standard list views.
175
+ *
176
+ * @param leadId - The ID of the lead to archive.
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * await ecod.crm.leads.delete("64abc...");
181
+ * ```
182
+ */
183
+ async delete(leadId: string) {
184
+ return this.deleteRequest(`/api/services/leads/${leadId}`);
185
+ }
186
+ }
@@ -0,0 +1,51 @@
1
+ import type { AxiosInstance } from "axios";
2
+ import { APIResource } from "../resource";
3
+ import { APIError } from "../error";
4
+
5
+ /**
6
+ * Payload to send a high-throughput email campaign.
7
+ */
8
+ export interface SendCampaignPayload {
9
+ /** Array of recipient email addresses. */
10
+ recipients: string[];
11
+ /** Subject line of the email. */
12
+ subject: string;
13
+ /** HTML body of the email. */
14
+ html: string;
15
+ }
16
+
17
+
18
+ /**
19
+ * Interface representing the result of a campaign dispatch.
20
+ */
21
+ export interface CampaignResult {
22
+ success: boolean;
23
+ message?: string;
24
+ [key: string]: any;
25
+ }
26
+
27
+ export class EmailResource extends APIResource {
28
+ /**
29
+ * Send an HTML email campaign to a list of recipients.
30
+ *
31
+ * @param payload - The campaign details (recipients, subject, html).
32
+ * @returns The dispatch result.
33
+ */
34
+ async sendEmailCampaign(
35
+ payload: SendCampaignPayload,
36
+ ): Promise<CampaignResult> {
37
+ return this.post<CampaignResult>("/api/saas/emails/campaign", payload);
38
+ }
39
+
40
+ /**
41
+ * Send a system verification/test email to validate SMTP configuration.
42
+ *
43
+ * @param to - The recipient's email address.
44
+ * @returns The dispatch result.
45
+ */
46
+ async sendTestEmail(to: string): Promise<CampaignResult> {
47
+ return this.post<CampaignResult>("/api/saas/emails/test", { to });
48
+ }
49
+
50
+ // WhatsApp moved to native wrapper
51
+ }