Official Node.js SDK for the Elaras Chat developer API.
Server-side TypeScript library for building backends, webhooks, and integrations against the /api/developer/v1/* endpoints.
- Dual-published (ESM + CJS)
- First-class TypeScript types
- Built-in retry with exponential backoff + jitter
- Typed error hierarchy
- Timing-safe webhook signature verification
Requires Node 18+ (uses the global fetch and crypto built-ins).
Install
npm install @elaras/sdk-node
Authentication
Create a sk_ secret key from the Elaras dashboard (Settings → Developer → API Keys) and pass it to the client:
import { ElarasClient } from '@elaras/sdk-node'; const elaras = new ElarasClient({ apiKey: process.env.ELARAS_API_KEY!, });
Full options:
new ElarasClient({ apiKey: 'sk_...', // required baseUrl: 'https://ask-api.elaras.ai', // default timeout: 30_000, // default (ms, per-request) maxRetries: 3, // default (429 + 5xx + network only) });
Quickstart: build + deploy an agent
The full chain — from empty workspace to an embeddable chatbot — in six calls.
import { ElarasClient } from '@elaras/sdk-node'; const elaras = new ElarasClient({ apiKey: process.env.ELARAS_API_KEY! }); // 1. Create a chatbot (the thing you embed on your site). const bot = await elaras.chatbots.create({ name: 'Support bot', aiProvider: 'anthropic', greetingMessage: 'Hi! Ask me anything about our pricing or returns.', }); // 2. Ingest some knowledge (workspace-wide, reusable across chatbots). const pricing = await elaras.knowledge.createFromUrl({ url: 'https://acme.com/pricing' }); const faq = await elaras.knowledge.createQa({ question: 'What is your refund window?', answer: '30 days, no questions asked.', }); // Wait for the URL doc to finish embedding. await elaras.knowledge.waitForIndex(pricing.id); // 3. Create a personality — the versioned behaviour layer. Fresh personalities // come with a v1 draft already created, so we patch that draft directly. const personality = await elaras.personalities.create({ name: 'Support bot personality', description: 'Friendly, concise, defers refunds to a human.', }); // 4. Patch the draft (v1) with the system prompt + attached knowledge. await elaras.personalities.versions.update(personality.id, 1, { snapshot: { system_prompt: 'You are a friendly support agent for Acme. Keep answers short.', knowledge_source_ids: [pricing.id, faq.id], ai_provider: 'anthropic', ai_model: 'claude-haiku-4-5-20251001', }, }); // 5. Publish the version (freezes it + makes it assignable). const published = await elaras.personalities.versions.publish(personality.id, 1); // 6. Activate the published version on the chatbot. await elaras.chatbots.assignModel(bot.id, { versionId: published.id }); // Done — `bot.public_key` is the X-Chatbot-Key the browser widget uses. console.log('Embed with key:', bot.public_key);
Account usage
Snapshot of the four metrics on the dashboard billing page for the team the API key is scoped to.
const usage = await elaras.account.usage(); // { // chatbots: { current: 3, limit: 10 }, // credits: { current: 1500, limit: 2000, used: 500 }, // team_members: { current: 4, limit: 20 }, // knowledge_chunks: { current: 2187, limit: 10000 }, // }
currentis what you're using right now;limitcomes from the plan (nullmeans unlimited or no plan).credits.usedis lifetime usage this billing period, independent ofcurrent(which is credits remaining).- Point-in-time only — no history or time-series. For deltas, poll and diff.
Maps to GET /api/developer/v1/account/usage. Requires the read scope.
Sessions
// Create a session. Idempotent on sessionToken — passing the same token // twice returns the existing session instead of erroring. Omit the token // to let the server generate a UUID. const session = await elaras.sessions.create({ chatbotId: 'cbt_live_xxx', sessionToken: crypto.randomUUID(), // optional externalUserId: 'user_42', // optional identity passthrough externalProfileId: 'tenant:acme', // optional externalOrgId: 'org_xyz', // optional }); // → { id, chatbot_id, external_user_id, started_at, message_count, ... } // Fetch session detail (includes messages[]). const detail = await elaras.sessions.get(session.id); const transcript = await elaras.sessions.getTranscript(session.id); // same endpoint, named alias // Paginated list. const page = await elaras.sessions.list({ chatbotId: 'cbt_live_xxx', externalUserId: 'user_42', limit: 50, cursor: '2', // Laravel page number });
stream(sessionId, message)
Sends a message over SSE. Token chunks arrive as delta events as the AI types, followed by a final done event containing the full assembled response.
for await (const event of elaras.sessions.stream(session.id, 'What are your opening hours?')) { if (event.type === 'delta') process.stdout.write(event.content); // stream to your frontend if (event.type === 'done') console.log('\nDone:', event.response); if (event.type === 'error') console.error(event.message); }
| Event | Fields | Description |
|---|---|---|
delta | content: string | Token chunk — forward to your frontend to display as the AI types |
done | response: string, message_id: number | AI response complete — response is the full assembled text |
error | message: string, code?: string | An error occurred |
Knowledge
Knowledge in Elaras is workspace-scoped, not per-chatbot. Attach a doc to a chatbot by adding its id to a personality version's snapshot.knowledge_source_ids and publishing.
Breaking in 0.2.0:
chatbotIdwas removed fromcreateFromUrl,createQa, andlist. The field was previously accepted and silently ignored.
// Add a URL — content is fetched and chunked server-side. const doc = await elaras.knowledge.createFromUrl({ url: 'https://example.com/pricing', name: 'Pricing page', // optional }); // Add a Q&A pair. await elaras.knowledge.createQa({ question: 'What are your office hours?', answer: 'We are open 9am–5pm, Monday to Friday.', }); // Fetch / list. const detail = await elaras.knowledge.get(doc.id); const list = await elaras.knowledge.list(); // → { data: KnowledgeDocument[], locked: boolean } // Block until the doc is indexed (useful after createFromUrl which is async). const indexed = await elaras.knowledge.waitForIndex(doc.id, { timeoutMs: 60_000, pollIntervalMs: 2_000, }); // Mutations. await elaras.knowledge.delete(doc.id); const refreshed = await elaras.knowledge.resync(doc.id); const cloned = await elaras.knowledge.clone(doc.id); const chunks = await elaras.knowledge.getChunks(doc.id); // → { data: KnowledgeChunk[] }
Leads
// A lead in Elaras is a session with `lead_email` captured. const leads = await elaras.leads.list({ chatbotId: 'cbt_live_xxx', email: 'jane@acme.com', // optional partial match limit: 50, }); // Leads are sessions with lead_email populated — there's no separate lead ID. // Fetch by the session the lead was captured on: const lead = await elaras.leads.getBySession('sess_abc');
Chatbots
// CRUD const bot = await elaras.chatbots.create({ name: 'Support bot' }); const list = await elaras.chatbots.list(); const fetched = await elaras.chatbots.get('cbt_live_xxx'); await elaras.chatbots.update('cbt_live_xxx', { status: 'inactive' }); await elaras.chatbots.delete('cbt_live_xxx'); // Activate a published personality version on this chatbot. `versionId` is the // numeric `id` returned by elaras.personalities.versions.list(modelId). await elaras.chatbots.assignModel('cbt_live_xxx', { versionId: 42 }); // Attach a Flow (deterministic conversation branch) to the chatbot. await elaras.chatbots.attachFlow('cbt_live_xxx', 7, { triggerPhrase: 'book a call', // optional — forces this flow when matched enabled: true, }); // Analytics const stats = await elaras.chatbots.getStats('cbt_live_xxx', { period: '7d' }); const flagged = await elaras.chatbots.listFlaggedSessions('cbt_live_xxx', { category: 'cannot_answer', limit: 50, }); // `public_key` is exposed on get() — this is the X-Chatbot-Key the browser // widget / JS SDK uses. Safe to ship client-side; it's not a secret. console.log(bot.public_key);
Personalities + Versions
A personality is a versioned behaviour container. Draft versions are mutable; published versions are frozen. Attach knowledge, skills, and flows to a version via its snapshot, then publish and assign the published version to a chatbot.
Identifiers: personality.id is a UUID string (the public identifier). Pass it to every .get/.update/.delete method and as the first arg to versions.*. Integer PKs are internal — passing one to the SDK will 404 against the backend. Versions within a personality are identified by their sequential version_number (1, 2, 3, …), not an ID.
// Personality CRUD const list = await elaras.personalities.list(); const created = await elaras.personalities.create({ name: 'Support bot' }); const personaId = created.id; // UUID const detail = await elaras.personalities.get(personaId); const renamed = await elaras.personalities.update(personaId, { name: 'Renamed' }); await elaras.personalities.delete(personaId); // Versions — nested resource under elaras.personalities.versions const versions = await elaras.personalities.versions.list(personaId); const v2 = await elaras.personalities.versions.get(personaId, 2); // by version_number const draft = await elaras.personalities.versions.create(personaId, { changeNotes: 'Tightening the system prompt', }); // Patch the draft snapshot. Deep-merged server-side. await elaras.personalities.versions.update(personaId, draft.version_number, { snapshot: { system_prompt: 'You are helpful.', knowledge_source_ids: [7, 9], skill_ids: [1, 2], ai_provider: 'anthropic', ai_model: 'claude-haiku-4-5-20251001', rules: ['Never discuss competitor pricing.'], }, }); // Publish → freezes the version. Returns the published row. const published = await elaras.personalities.versions.publish(personaId, draft.version_number); // Clone a published version into a new editable draft. const clone = await elaras.personalities.versions.clone(personaId, 2, { changeNotes: 'v2 → v3 iteration', }); await elaras.personalities.versions.delete(personaId, draft.version_number);
Flows
A Flow is a deterministic conversation branch (step graph) that the LLM can hand off to. Flows are workspace-scoped; attach to a chatbot with elaras.chatbots.attachFlow().
const flows = await elaras.flows.list(); const flow = await elaras.flows.get(7); const created = await elaras.flows.create({ name: 'Book a slot', description: 'Walks the user through calendar availability.', definition: { steps: [/* ... */] }, }); await elaras.flows.update(7, { name: 'Renamed', enabled: false }); await elaras.flows.delete(7); // Ask the AI to draft a Flow definition from a plain-text description. The // returned `definition` is a draft only — pass it to flows.create to persist. const draft = await elaras.flows.generate('book a 30-minute discovery call'); await elaras.flows.create({ name: 'Discovery', definition: draft.definition });
Scenarios
A scenario is a stored multi-turn test case bound to a personality. A scenario run replays one or more scenarios against a specific version of the personality and judges each bot reply against the configured criteria. The typical pattern is to gate publishing a draft version on a passing run.
Identifiers: scenarios are identified by integer within their personality. Runs are identified by UUID — the run id you get back is the public identifier; pass it to getRun, cancelRun, and waitForRun.
// Seed scenarios on the personality const seeded = await elaras.scenarios.create(personaId, { name: 'Return policy — 30 days', initialMessage: 'Can I return something after 2 weeks?', overallCriteria: { should_resolve: true }, turns: [ { bot_criteria: { mentions_30_days: true }, user_reply: 'What about damaged items?' }, ], }); // Or let the AI draft a batch const drafts = await elaras.scenarios.draft(personaId, { intent: 'returns + exchanges', count: 5 }); // Run them against a draft version you want to publish const run = await elaras.scenarios.run(personaId, { versionNumber: 2 }); // Wait for it to finish, then gate publish on the result const final = await elaras.scenarios.waitForRun(personaId, run.id); if (final.status === 'completed' && (final.failed ?? 0) === 0) { await elaras.personalities.versions.publish(personaId, 2); } else { console.warn('Scenario run did not pass — not publishing', final); } // Other operations const all = await elaras.scenarios.list(personaId); // Scenario[] await elaras.scenarios.get(personaId, seeded.id); await elaras.scenarios.update(personaId, seeded.id, { name: 'Returns — 30 days (v2)' }); await elaras.scenarios.delete(personaId, seeded.id); await elaras.scenarios.listRuns(personaId, { limit: 10 }); await elaras.scenarios.cancelRun(personaId, run.id);
Run identifiers (
run.id) are UUID strings — feed them straight back intogetRun,cancelRun, andwaitForRun. The SDK normalises every run shape it returns, even though the backend sometimes serves a raw Eloquent row with a separateuuidfield.
waitForRun polls getRun on an interval (default: 2s) and resolves when the run's status leaves queued/running. The default timeout is 3 minutes — override with waitForRun(…, { timeoutMs, pollIntervalMs }).
Error handling
Every failure thrown by the SDK extends ElarasError. Match on the specific subclass for ergonomic handling:
import { ElarasAuthError, ElarasValidationError, ElarasRateLimitError, ElarasServerError, ElarasNetworkError, } from '@elaras/sdk-node'; try { await elaras.knowledge.createFromUrl({ url: 'nope' }); } catch (err) { if (err instanceof ElarasValidationError) { console.log(err.errors); // { url: ['The url field is required.'] } } else if (err instanceof ElarasAuthError) { // 401 / 403 — bad key or missing scope } else if (err instanceof ElarasRateLimitError) { console.log(err.retryAfterSeconds); // honour this before retrying } else if (err instanceof ElarasServerError) { // 5xx — SDK already retried per `maxRetries` } else if (err instanceof ElarasNetworkError) { // DNS, TCP, timeout — SDK already retried } }
Retry policy
429and5xxresponses are retried.- Backoff:
500ms * 2^attempt + jitter(0..100ms). 429responses honour theRetry-Afterheader when present.4xxresponses other than429are never retried.- Default retries:
3. SetmaxRetries: 0to opt out.
Webhook signature verification
When Elaras calls your skill endpoint it signs the request body with HMAC-SHA256 using the skill's API key. Verify every incoming request before acting on it:
import { verifyWebhookSignature } from '@elaras/sdk-node'; import type { SkillPayload } from '@elaras/sdk-node'; import express from 'express'; const app = express(); // IMPORTANT: use the raw body — re-serialising via JSON.stringify can reorder // keys and break the signature. app.post( '/webhooks/elaras/book-slot', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.get('X-Elaras-Signature'); const ok = verifyWebhookSignature(req.body, sig, process.env.ELARAS_SKILL_SECRET!); if (!ok) return res.status(401).send('invalid signature'); const payload = JSON.parse(req.body.toString('utf8')) as SkillPayload; // payload.skill, payload.params, payload.session.{id, lead_name, lead_email} res.json({ booked: true }); }, );
The signature header format is X-Elaras-Signature: sha256=<hex>. Comparison is done with crypto.timingSafeEqual.
Partial coverage in 0.2.0
A handful of surface-level helpers from the planned 0.2.0 API are not shipped because the matching developer-API routes do not exist yet. Call out explicitly:
chatbots.attachSkill(chatbotId, skillId)/detachSkill— the developer namespace exposes CREATE/UPDATE/DELETE for chatbot-scoped skills, not attach/detach of existing workspace skills. Useelaras.personalities.versions.update(modelId, n, { snapshot: { skill_ids: [...] } })and publish to attach a workspace skill to a chatbot via its personality.chatbots.detachFlow(chatbotId, flowId)— onlyPOST /chatbots/{chatbot}/flows/{flow}/attachis routed in developer scope; no DELETE pair exists.knowledge.update(id, {...})—PATCH /knowledge/{id}is only exposed on the dashboard route, not the developer namespace.personalities.versions.get(modelId, versionNumber)is implemented as alist(...).find(...)client-side because the server has no single-version GET route.
These will land as soon as the backend adds the missing routes.
Development
npm install npm test # vitest run npm run typecheck # tsc --noEmit npm run build # tsup — emits dist/index.{js,cjs,d.ts}
License
MIT © Elaras
