@nessyapi/sdk - v0.1.0

@nessyapi/sdk

Official Node.js / TypeScript SDK for NessyAPI — clinical decision support API for healthcare applications.

  • Zero runtime dependencies (uses native fetch + node:crypto)
  • TypeScript-first — full types for every request and response
  • Security-hardened — API keys never leak to logs, HMAC webhook verification with replay protection
  • Resilient — exponential backoff with jitter, automatic idempotency keys on retries
  • Runs anywhere — Node 18.17+, Bun, Deno, Cloudflare Workers
  • Server-only — refuses to run in browsers (API keys would leak to end users)
npm install @nessyapi/sdk
# or
pnpm add @nessyapi/sdk
# or
yarn add @nessyapi/sdk
import { NessyClient } from "@nessyapi/sdk";

const client = new NessyClient({ apiKey: process.env.NESSYAPI_KEY! });

// Create a session
const session = await client.sessions.create({
chiefComplaint: "headache",
age: 35,
sex: "male",
});

// Answer questions
let current = session.current_question;
while (current) {
const response = await client.sessions.answer(session.session_id, current.question_id, {
rawText: "since yesterday, pressure behind eyes",
});
if (response.is_complete) break;
current = response.current_question;
}

// Finalize and read results
const results = await client.sessions.finalize(session.session_id);
console.log(results.triage_level, results.differentials);

Or use the convenience method that does the full loop:

const results = await client.sessions.runAssessment({
chiefComplaint: "headache",
age: 35,
answers: { q_onset: "yesterday", q_severity: "8" },
});
Group Methods
client.sessions create, answer, route, getResults, getState, finalize, getQuestions, updateDemographics, submitFeedback, rectify, delete, exportSession, list, runAssessment
client.patients get, update, listSessions, delete
client.admin balance, usage, usageDetails, stats, rateLimit, auditLog, getTier, listTiers, setTier, listKeys, createKey, rotateKey, revokeKey, getNlpSettings, setNlpSettings
client.webhooks create, list, delete, rotate, test, deliveries
client.team list, invite, changeRole, remove
client.schema chiefComplaints, branches

List methods return an AsyncIterable that auto-paginates:

for await (const session of client.sessions.list({ status: "finalized" })) {
console.log(session.session_id, session.triage_level);
}

// Or fetch one page at a time
const page = await client.sessions.list({ limit: 50 }).page(0);
console.log(`Showing ${page.data.length} of ${page.total}`);

// Or materialize to array (capped at 10_000 by default)
const all = await client.sessions.list().toArray(500);

All errors extend typed subclasses. Use instanceof instead of checking status codes:

import {
NessyAuthenticationError,
NessyRateLimitError,
NessyValidationError,
NessyAPIError,
} from "@nessyapi/sdk";

try {
await client.sessions.create({ chiefComplaint: "headache" });
} catch (err) {
if (err instanceof NessyRateLimitError) {
console.warn(`Rate limited. Retry after ${err.retryAfterMs}ms`);
} else if (err instanceof NessyValidationError) {
console.error(`Invalid input:`, err.fieldErrors);
} else if (err instanceof NessyAuthenticationError) {
console.error("Check your API key");
} else if (err instanceof NessyAPIError) {
console.error(`API error ${err.status} (${err.errorCode}) — request_id=${err.requestId}`);
} else {
throw err;
}
}

Webhook signatures use HMAC-SHA256 with a timestamp to prevent replay attacks. Use the dedicated subpath import so webhook receivers don't bundle the full client:

import { verifyWebhookSignature, NessyWebhookSignatureError } from "@nessyapi/sdk/webhook";

// Express
import express from "express";
const app = express();
app.post("/webhooks/nessy", express.raw({ type: "application/json" }), (req, res) => {
try {
verifyWebhookSignature({
payload: req.body, // Buffer, NOT parsed JSON
secret: process.env.NESSY_WEBHOOK_SECRET!,
signatureHeader: req.header("x-nessyapi-signature-256")!,
timestampHeader: req.header("x-nessyapi-timestamp")!,
});
} catch (err) {
if (err instanceof NessyWebhookSignatureError) return res.status(401).end();
throw err;
}
const event = JSON.parse(req.body.toString("utf8"));
// handle event...
res.status(200).end();
});
// Next.js App Router
export async function POST(req: Request) {
const payload = Buffer.from(await req.arrayBuffer());
verifyWebhookSignature({
payload,
secret: process.env.NESSY_WEBHOOK_SECRET!,
signatureHeader: req.headers.get("x-nessyapi-signature-256")!,
timestampHeader: req.headers.get("x-nessyapi-timestamp")!,
});
// ...
}
// Fastify
fastify.addContentTypeParser(
"application/json",
{ parseAs: "buffer" },
(_req, body, done) => done(null, body),
);
fastify.post("/webhooks/nessy", async (req, reply) => {
verifyWebhookSignature({
payload: req.body as Buffer,
secret: process.env.NESSY_WEBHOOK_SECRET!,
signatureHeader: req.headers["x-nessyapi-signature-256"] as string,
timestampHeader: req.headers["x-nessyapi-timestamp"] as string,
});
// ...
});

Timestamp tolerance defaults to 300 seconds. If you don't want replay protection (not recommended):

verifyWebhookSignature({ /* ... */, toleranceSeconds: 0 });
const client = new NessyClient({
apiKey: "nsy_live_...", // required
baseUrl: "https://api.custom.com", // optional, defaults to prod
timeoutMs: 30_000, // per-attempt, default 30s
retries: 3, // or { maxRetries, initialDelayMs, maxDelayMs }
userAgent: "my-app/1.0", // appended to built-in UA
logger: console, // or pino/winston/bunyan (duck-typed)
fetch: undici.fetch.bind(agent), // DI for connection pooling, testing
defaultHeaders: { "x-custom-trace": "..." },
});

The SDK retries on 408, 429, and 5xx responses plus network errors. It never retries on other 4xx (idempotency is preserved via stable Idempotency-Key across attempts).

Backoff is exponential with full jitter:

delay = min(initialDelayMs * 2^attempt, maxDelayMs) * (0.5 + random() * 0.5)

Capped at 60 seconds per step. Defaults to 3 retries. Retry-After header is respected on 429.

Threat Defense
API key in logs Symbol-keyed private storage; JSON.stringify(client) and util.inspect(client) return [REDACTED]
API key in browser bundle Constructor throws if window is defined (escape hatch: allowBrowser: true)
Webhook replay Timestamp included in HMAC; 300s tolerance window; rejected if stale
Webhook signature tampering crypto.timingSafeEqual on equal-length Buffers
MITM HTTPS-only (except localhost for dev); TLS 1.2+
Path traversal via IDs Regex validation on session_id / patient_id
Prototype pollution Native JSON.parse only
Double-billing on retry Auto Idempotency-Key, reused across attempts

For corporate proxies with custom root CAs, set NODE_EXTRA_CA_CERTS=path/to/ca.pem. The SDK does not expose a flag to disable certificate validation.

Runtime Supported Notes
Node.js 18.17+ Primary target
Node.js 20+ Recommended
Bun 1.0+ Best-effort
Deno 1.40+ Best-effort
Cloudflare Workers Use @nessyapi/sdk/webhook for receivers
Browser API keys must stay on the server
  • docs/ERROR_CODES.md — every error class, status code, and server errorCode with how to fix it.
  • docs/TROUBLESHOOTING.md — common pitfalls (webhook signature mismatches, rate limiting, timeouts, browser refusal, etc.).
  • examples/ — runnable snippets for Express, Fastify, Next.js App Router, Hono, Cloudflare Workers, AWS Lambda, plus pagination and error-handling patterns.
  • In-IDE JSDoc — every public method carries a summary, @param descriptions, cost annotation, and @throws pointers. Your editor should show all of it on hover.

MIT © HealthyNess.cz

Please report security issues to security@healthyness.cz or via GitHub Security Advisories. We disclose responsibly within 90 days.