Client SDK
@egox/client for Node.js — ask, streaming, threading, header forwarding, webhooks.
@egox/client is the official EgoX SDK for Node.js. One method — ask — turns
your project's configuration (model, prompts, tools, knowledge) into an answer.
Streaming, conversation threading, trusted-header forwarding, and webhook
verification are built in. Wire types come from @egox/contracts — the same package
the backend uses — so your decoded responses can't silently drift from the wire.
npm install @egox/clientQuick start
import { EgoX } from '@egox/client';
const egox = new EgoX({
apiKey: process.env.EGOX_API_KEY, // egox_live_xxx or egox_test_xxx
tenantId: process.env.EGOX_TENANT_ID, // your project's tenant id
});
const response = await egox.ask({ message: 'What is the refund policy?' });
console.log(response.answer);
console.log(`Tokens used: ${response.usage.totalTokens}`);Configuration
const egox = new EgoX({
apiKey: 'egox_live_xxx', // required
tenantId: 'your-tenant-id', // required
timeout: 30000, // optional — request timeout (ms)
retries: 2, // optional — retry attempts (exponential backoff)
});Threading
Two ways to keep a conversation stateful:
// 1. Use EgoX's thread id — round-trip it back.
const first = await egox.ask({ message: 'What is your return policy?', externalUserId: 'user-123' });
const second = await egox.ask({ message: 'What about electronics?', threadId: first.threadId, externalUserId: 'user-123' });
// 2. Bring your own conversation id — never track EgoX's UUID.
const r1 = await egox.ask({ message: 'Hello', externalThreadId: 'conv:abc123', externalUserId: 'user:42' });
const r2 = await egox.ask({ message: 'Continue please', externalThreadId: 'conv:abc123' });
// r1.threadId === r2.threadIdexternalThreadId is any string ≤ 255 chars, opaque to EgoX, unique per-tenant.
Failure modes are explicit:
| You send | Behaviour |
|---|---|
externalThreadId (first time) | New thread created with this id attached |
externalThreadId (subsequent) | Same thread resolved every time |
threadId = unknown EgoX UUID | 404 NotFound — no silent new thread |
threadId = not a UUID | 400 InvalidInput — use externalThreadId for your own ids |
| Both | threadId wins (back-compat); debug warning logged |
Forwarding context to your tools
Three channels, each for a different job:
| Field | Channel | Use for |
|---|---|---|
authToken | forwarded as Authorization: Bearer … to your tool | the caller's user token |
headers | inbound HTTP headers, passed through per the tool's forwardHeaders allowlist | trusted, server-set context — locale, app version, user/tenant id behind a gateway |
toolHeaders | request body → merged into the tool call | per-end-user secrets whose name collides with EgoX's own auth (Authorization, X-API-Key) |
// Trusted context (server-side, caller already authenticated):
await egox.ask({
message: 'Show my recent orders',
externalUserId: ctx.user.id,
headers: { 'x-user-id': ctx.user.id, 'x-locale': ctx.user.locale },
});
// Per-end-user secret destined for the tool, not for EgoX:
await egox.ask({
message: 'Show my recent invoices',
toolHeaders: { Authorization: `Bearer ${ctx.user.tenantApiToken}` },
});Reserved header names
For headers to reach a tool, add the header name to that tool's forwardHeaders
allowlist in the Console. SDK-reserved names are
stripped (Authorization, Content-Type, Accept, X-API-Key, X-Tenant-Id,
X-Request-Id, plus HTTP framing headers) — the full list is exported as
SDK_RESERVED_HEADERS.
Streaming
EgoX streams events as it thinks, retrieves knowledge, calls tools, and generates text. Pick the consumption style that fits the call site.
await egox.askStream(
{ message: 'Walk me through resetting my password.' },
{
onEvent: (event) => {
if (event.type === 'delta') process.stdout.write(event.content);
if (event.type === 'tool.calling') console.log('\n→', event.name);
if (event.type === 'done') console.log('\n[done]', event.usage);
},
},
);Natural for GraphQL Subscription resolvers and anywhere for await beats a callback.
Breaking the loop aborts the SSE connection.
for await (const ev of egox.askStreamIter({ message: 'Reset my password.' })) {
switch (ev.type) {
case 'delta': yield { token: ev.content }; break;
case 'tool.calling': yield { toolStart: ev.name }; break;
case 'done': yield { final: ev }; return;
case 'error': throw new Error(ev.message);
}
}Pass an AbortSignal for external cancellation (WebSocket close, aborted request):
const ctrl = new AbortController();
req.on('close', () => ctrl.abort());
for await (const ev of egox.askStreamIter(req.body, { signal: ctrl.signal })) { /* … */ }Response object
interface AskResponse {
threadId: string; // conversation thread id
answer: string; // the AI's response text
intent: 'vanilla' | 'rag' | 'tools' | 'rag_tools'; // how the request was classified
toolUsed: string | null; // tool name if one was called
ragChunksUsed: number; // # knowledge chunks used
usage: { promptTokens: number; completionTokens: number; totalTokens: number };
model: string; // e.g. 'gpt-4.1'
}Webhook signature verification
When EgoX calls your tool endpoints it signs the request. Verify the signature so only EgoX can invoke your tools.
import { createEgoXMiddleware } from '@egox/client/express';
const verifyEgoX = createEgoXMiddleware({
secret: process.env.EGOX_WEBHOOK_SECRET,
tolerance: 300, // max age in seconds (default 5 min)
});
app.post('/api/tools/check-order', verifyEgoX, (req, res) => {
req.egox?.requestId; req.egox?.tenantId; // verified metadata
res.json({ status: 'Order shipped' });
});import { verifySignature, validateSignature } from '@egox/client';
// throws on invalid:
verifySignature({
signature: req.headers['x-egox-signature'],
timestamp: req.headers['x-egox-timestamp'],
body: JSON.stringify(req.body),
secret: process.env.EGOX_WEBHOOK_SECRET,
});
// or boolean:
const ok = validateSignature({ /* same params */ });Headers EgoX sends to your endpoints:
| Header | Meaning |
|---|---|
X-EgoX-Signature | HMAC-SHA256 (sha256=…) |
X-EgoX-Timestamp | Unix timestamp |
X-EgoX-Request-Id | Request id for debugging |
X-EgoX-Tenant-Id | Your tenant/project id |
Error handling
import {
EgoXError, AuthenticationError, ValidationError,
NetworkError, TimeoutError, RateLimitError,
} from '@egox/client';
try {
await egox.ask({ message: 'Hello' });
} catch (error) {
if (error instanceof AuthenticationError) { /* invalid API key */ }
else if (error instanceof ValidationError) { /* bad request */ }
else if (error instanceof RateLimitError) { /* retry after error.retryAfter s */ }
else if (error instanceof TimeoutError) { /* timed out */ }
else if (error instanceof NetworkError) { /* network */ }
else if (error instanceof EgoXError) { /* error.code / error.message */ }
}Health check & types
const healthy = await egox.ping();import type {
EgoXConfig, AskRequest, AskResponse,
AskStreamRequest, AskStreamEvent,
SignatureParams, ExpressMiddlewareOptions,
} from '@egox/client';Calling /ask without the SDK
The SDK is a thin wrapper over one HTTP call. Any language can hit it directly:
POST /egox/ask
Content-Type: application/json
X-API-Key: {your-api-key}
{ "tenantId": "your-tenant-id", "message": "Generate a product description…",
"responseFormat": "json_object", "jsonSchema": { /* … */ },
"metadata": { "requestType": "product-description" } }Responses use one envelope — see Core concepts.
The SDK is licensed Apache-2.0.