@eka-care/webhook-sdk
v1.0.6
Published
Eka Care Webhook SDK for Node.js - Verify webhook signatures and process appointment events with standalone server or framework integration
Readme
@eka-care/webhook-sdk
Eka Care Webhook SDK for Node.js -- process incoming webhook events from the Eka Care platform with signature verification, automatic appointment data enrichment, schedule persistence, and one-shot reminder scheduling.
Features
- Signature verification -- HMAC-SHA256 verification of incoming webhook requests (optional, but recommended)
- Automatic data enrichment -- fetches full appointment, patient, doctor, and clinic details from Eka Care API for appointment events
- Multiple integration modes:
- Standalone server -- starts its own HTTP server, no framework needed
- Express middleware -- drop-in middleware for Express apps
- Framework-agnostic handler -- works with any Node.js framework (Fastify, Koa, NestJS, tsoa, Hapi, etc.)
- Schedule persistence -- pluggable backend (MySQL or DynamoDB) that auto-creates a table on first use, writes a row for every bookable / followup appointment, and migrates the schema on subsequent runs
- Range queries with enrichment --
findAppointmentsBetween()returns full appointment + patient + doctor + clinic details for every booking in a given time window, split into normal vs followup - Reminder scheduling --
scheduleReminder()schedules a one-shot reminder vianode-scheduleand fires the callback with freshly fetched appointment context - TypeScript-first -- full type definitions included
- Dual module format -- supports both CommonJS (
require) and ESM (import) - Silent by default --
debug: trueopt-in to surface internal warnings/errors; otherwise the SDK runs silent - Node.js 18+ -- uses built-in
cryptomodule; required external code is@eka-care/eka-care-core. Persistence and reminder backends are optional peer dependencies that you only install if you opt in
Supported Events
| Event | Enrichment | Persistence behavior |
|---|---|---|
| appointment.created | Full appointment + patient + doctor + clinic details fetched | Inserts a schedule row when status = "BK" (booking) or (status = "IN" AND visit_type = "FLW") (followup) |
| appointment.updated | Full appointment + patient + doctor + clinic details fetched | Updates the existing row's status to whatever the new status is (cancellation, completion, no-show, anything) |
| prescription.created | Raw payload passed through | -- |
| prescription.updated | Raw payload passed through | -- |
Installation
npm install @eka-care/webhook-sdkOptional peer dependencies
The SDK loads each of the packages below dynamically at runtime, so you only install what your code path actually uses:
| Feature you use | Install |
|---|---|
| Standalone server, handleRequest(), framework integrations (Fastify/Koa/NestJS/tsoa/plain http) | nothing extra |
| sdk.expressMiddleware() | npm install express |
| persistence: { type: "mysql", ... } | npm install mysql2 |
| persistence: { type: "dynamodb", ... } | npm install @aws-sdk/client-dynamodb |
| sdk.scheduleReminder(...) | npm install node-schedule |
If you forget one, the relevant SDK method throws a clear error message that names the missing package.
Quick Start
Option 1: Standalone Server (No Framework Needed)
The simplest way to start receiving webhooks. The SDK runs its own HTTP server.
import { WebhookSDK } from "@eka-care/webhook-sdk";
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
apiKey: "your-api-key", // optional
signingKey: "your-signing-key", // optional, enables signature verification
onEvent: async (event) => {
console.log("Event type:", event.type);
console.log("Appointment:", event.appointmentDetails);
// Your business logic here:
// - Save to database
// - Send notifications
// - Trigger workflows
},
});
const server = sdk.listen(3000, () => {
console.log("Webhook server listening on port 3000");
});
// Graceful shutdown
process.on("SIGTERM", () => server.close());The standalone server provides:
POST /-- webhook endpointGET /eka-webhook-health-- health check endpoint ({ "status": "ok" })
Option 2: Express Middleware
Drop into an existing Express application. Use onEvent for your business logic, or access result.event after the middleware (see Option 3).
import express from "express";
import { WebhookSDK } from "@eka-care/webhook-sdk";
const app = express();
app.use(express.json()); // Required: body must be parsed before SDK middleware
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
signingKey: "your-signing-key",
onEvent: async (event) => {
console.log("Received:", event.type, event.appointmentDetails);
// Save to DB, send notifications, etc.
},
});
app.post("/webhook/eka", sdk.expressMiddleware());
app.listen(3000);Option 3: Generic Handler (Any Framework)
Use handleRequest() directly in any Node.js framework. The result includes the enriched event object -- no onEvent callback needed.
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
signingKey: "your-signing-key",
// No onEvent callback -- use result.event instead
});
// In your route handler:
const result = await sdk.handleRequest(requestBody, requestHeaders);
if (result.event) {
// Access the enriched data directly
console.log(result.event.type); // "appointment.created"
console.log(result.event.appointmentDetails); // full appointment object
console.log(result.event.payload); // raw webhook payload
// Your business logic here
await saveToDatabase(result.event.appointmentDetails);
}
res.status(result.status).send(result.body);Framework Integration Examples
Fastify
import Fastify from "fastify";
import { WebhookSDK } from "@eka-care/webhook-sdk";
const fastify = Fastify();
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
signingKey: "your-signing-key",
});
fastify.post("/webhook/eka", async (request, reply) => {
const result = await sdk.handleRequest(
request.body,
request.headers as Record<string, string | string[] | undefined>
);
if (result.event) {
console.log(result.event.type, result.event.appointmentDetails);
// Your business logic here
}
reply.status(result.status).send(result.body);
});
fastify.listen({ port: 3000 });tsoa
import { Controller, Post, Route, Request, SuccessResponse, Response } from "tsoa";
import type { Request as ExpressRequest } from "express";
import { WebhookSDK } from "@eka-care/webhook-sdk";
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
signingKey: "your-signing-key",
});
@Route("webhook")
export class EkaWebhookController extends Controller {
@Post("eka")
@SuccessResponse(200, "OK")
@Response(403, "Signature verification failed")
public async handleWebhook(
@Request() request: ExpressRequest
): Promise<{ success: boolean; event?: string; data?: unknown }> {
const result = await sdk.handleRequest(request.body, request.headers);
this.setStatus(result.status);
if (result.event) {
// Your business logic here
console.log(result.event.type, result.event.appointmentDetails);
}
return JSON.parse(result.body);
}
}NestJS
import { Controller, Post, Req, Res, Injectable } from "@nestjs/common";
import type { Request, Response } from "express";
import { WebhookSDK } from "@eka-care/webhook-sdk";
@Injectable()
export class EkaWebhookService {
public readonly sdk: WebhookSDK;
constructor() {
this.sdk = new WebhookSDK({
clientId: process.env.EKA_CLIENT_ID!,
clientSecret: process.env.EKA_CLIENT_SECRET!,
signingKey: process.env.EKA_SIGNING_KEY,
});
}
}
@Controller("webhook")
export class EkaWebhookController {
constructor(private readonly service: EkaWebhookService) {}
@Post("eka")
async handle(@Req() req: Request, @Res() res: Response) {
const result = await this.service.sdk.handleRequest(req.body, req.headers);
if (result.event) {
switch (result.event.type) {
case "appointment.created":
// Save new appointment
break;
case "appointment.updated":
// Update appointment
break;
case "prescription.created":
case "prescription.updated":
// Handle prescription events
break;
}
}
res.status(result.status).json(JSON.parse(result.body));
}
}Koa
import Koa from "koa";
import Router from "@koa/router";
import bodyParser from "koa-bodyparser";
import { WebhookSDK } from "@eka-care/webhook-sdk";
const app = new Koa();
const router = new Router();
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
signingKey: "your-signing-key",
});
router.post("/webhook/eka", async (ctx) => {
const result = await sdk.handleRequest(ctx.request.body, ctx.headers);
if (result.event) {
console.log(result.event.type, result.event.appointmentDetails);
}
ctx.status = result.status;
ctx.body = result.body;
});
app.use(bodyParser());
app.use(router.routes());
app.listen(3000);Plain Node.js http
import http from "node:http";
import { WebhookSDK } from "@eka-care/webhook-sdk";
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
signingKey: "your-signing-key",
});
const server = http.createServer(async (req, res) => {
if (req.method === "POST" && req.url === "/webhook/eka") {
// Read and parse body
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk as Buffer);
const body = JSON.parse(Buffer.concat(chunks).toString());
const result = await sdk.handleRequest(body, req.headers);
if (result.event) {
console.log(result.event.type, result.event.appointmentDetails);
}
res.writeHead(result.status, { "Content-Type": "application/json" });
res.end(result.body);
return;
}
res.writeHead(404);
res.end("Not Found");
});
server.listen(3000);Persistence Layer (Schedule Storage)
When you pass a persistence config, the SDK transparently writes appointment-schedule rows for qualifying webhook events into the table eka_webhook_schedule_appointment. This is the foundation for findAppointmentsBetween() and reminder workflows.
Why this exists
You typically need to remember which appointments are scheduled in the future so you can:
- Look up all appointments in a given time window (e.g. "everyone booked between 10am and 11am tomorrow")
- Drive reminder pipelines without having to re-query Eka Care for every webhook event
- Track cancellations / status changes locally
The persistence layer handles all of that without you writing any SQL or DynamoDB code.
Schema
The table has one row per appointment_id:
| Column | Type | Notes |
|---|---|---|
| appointment_id | string | Primary key |
| start_time | epoch (seconds) | Indexed |
| end_time | epoch (seconds) | |
| status | string | E.g. BK, IN, CK, CND, CNS, AB, etc. |
| visit_type | string | E.g. FLW for followup |
| created_at | epoch (seconds) | When the SDK first stored this row. Preserved across re-upserts |
In MySQL the table is created with INDEX idx_start_time (start_time). In DynamoDB it has a Global Secondary Index named status_start_time_index (PK=status, SK=start_time, projecting visit_type).
When rows get written
| Webhook | Condition | Action |
|---|---|---|
| appointment.created | status = "BK" (normal booking) or (status = "IN" AND visit_type = "FLW") (followup booked for today) | Upsert row |
| appointment.created | otherwise | No-op |
| appointment.updated | always (status not empty) | Update existing row's status to the new value. If no row exists for that appointment_id, log and skip |
Configuration: MySQL
import { WebhookSDK } from "@eka-care/webhook-sdk";
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
persistence: {
type: "mysql",
connection: {
host: "localhost",
port: 3306, // optional
user: "your-user",
password: "your-password",
database: "your-database",
},
},
});The SDK uses mysql2/promise and creates a connection pool internally (connectionLimit: 10).
Configuration: DynamoDB
import { WebhookSDK } from "@eka-care/webhook-sdk";
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
persistence: {
type: "dynamodb",
region: "ap-south-1",
// Optional. If omitted, the standard AWS credential chain is used
// (env vars, instance role, ~/.aws/credentials, etc.).
accessKeyId: "AKIA...",
secretAccessKey: "...",
// Optional. For DynamoDB Local during development, e.g. http://localhost:8000
endpoint: "http://localhost:8000",
},
});The DynamoDB table is created with BillingMode: PAY_PER_REQUEST (no capacity to provision).
Lazy initialization
ensureTable() runs on the first webhook (or first call to findAppointmentsBetween). It is cached so subsequent webhooks pay no overhead. If it fails, the cache is reset so the next webhook can retry. Persistence errors are logged and never fail the webhook response -- the reminder layer is auxiliary; the webhook still returns 200.
Schema migration on existing tables
If the SDK starts up and the table already exists:
- MySQL: it queries
INFORMATION_SCHEMA.COLUMNSand runsALTER TABLE ... ADD COLUMNfor every non-key column that is missing (e.g.created_at, added in a later release). Each missing column is added withNOT NULL DEFAULT 0/DEFAULT ''so existing rows are backfilled safely. - DynamoDB: non-key attributes don't need DDL (they appear on items at write time), but the GSI does. If
status_start_time_indexis missing, the SDK firesUpdateTableto create it and returns immediately. GSI creation is asynchronous (can take several minutes);findAppointmentsBetweenreturns errors until it isACTIVE. Other operations (upsert, updateStatus) are unaffected.
This means upgrading the SDK on a project that already has the table is safe -- the migration is automatic.
Querying Scheduled Appointments
sdk.findAppointmentsBetween(input)
Find every booking whose start_time falls inside [start_time, end_time] (inclusive epoch range) and return the full appointment + patient + doctor + clinic details for each one.
async findAppointmentsBetween(input: {
start_time: number; // epoch seconds
end_time: number; // epoch seconds
}): Promise<{
appointments: Array<Record<string, unknown>>; // status === "BK"
followup_appointments: Array<Record<string, unknown>>; // status === "IN" AND visit_type === "FLW"
}>How it works:
- Asks the persistence layer for the matching
appointment_ids, split into normal bookings vs followups. - Calls
getAppointmentDetailsById(via the Eka Care API) in parallel for every ID. - Drops any IDs whose detail fetch fails (the failure is logged when
debug: true).
const tomorrowStart = Math.floor(Date.UTC(2026, 4, 9, 0, 0, 0) / 1000);
const tomorrowEnd = tomorrowStart + 24 * 60 * 60;
const { appointments, followup_appointments } =
await sdk.findAppointmentsBetween({
start_time: tomorrowStart,
end_time: tomorrowEnd,
});
console.log(`${appointments.length} bookings, ${followup_appointments.length} followups`);Heads up on rate limits: a window with N matching appointments fans out to N parallel API calls. For large windows, throttle in the caller (or split the range) to stay under the Eka Care rate limits.
findAppointmentsBetween throws if the SDK was constructed without a persistence config.
Reminder Scheduling
sdk.scheduleReminder(input)
Schedule a one-shot reminder for an appointment. When the cron fires, the SDK fetches fresh appointment + patient + doctor + clinic details and invokes your callback with that context. After the callback runs (success or failure), the underlying job is automatically cancelled so it cannot fire again.
Requires node-schedule to be installed.
import type { AppointmentReminderContext } from "@eka-care/webhook-sdk";
const job = await sdk.scheduleReminder({
// Cron expression OR an absolute Date (one-shot at that instant)
cronTime: new Date(appointment.start_time * 1000 - 5 * 60 * 1000),
appointment_id: appointment.appointment_id,
callback: async ({
appointmentDetails,
patientDetails,
doctorDetails,
clinicDetails,
}: AppointmentReminderContext) => {
await sendSms(
patientDetails?.mobile as string,
`Reminder: appointment with ${doctorDetails?.firstname} at ${clinicDetails?.name}`
);
},
});
// You can still cancel the job BEFORE its fire time, e.g. if the appointment
// is cancelled by the user:
job.cancel();Behavior details
- Fresh data at fire time: the callback never receives stale data. Each fire re-fetches from Eka Care so reschedules / cancellations are reflected.
- One-shot: even if
cronTimeis a recurring cron expression, the SDK auto-cancels the job after the first fire. If you actually need recurring behavior, schedule a fresh reminder from inside your callback. - In-memory only: jobs do not survive process restart. If you need durable reminders, persist
(appointment_id, cronTime)yourself and re-schedule on startup. (The schedule persistence layer described above does not automatically re-hydrate reminders.) - Errors are caught: a fetch failure or a throwing callback gets logged (when
debug: true); the scheduler keeps running.
AppointmentReminderContext shape
interface AppointmentReminderContext {
appointmentDetails: Record<string, unknown>;
patientDetails?: Record<string, unknown>;
doctorDetails?: Record<string, unknown>;
clinicDetails?: Record<string, unknown>;
}patientDetails / doctorDetails / clinicDetails are best-effort -- if a sub-fetch fails, the corresponding field is undefined and the failure is logged.
API Reference
WebhookSDK
The main class. Construct it once and reuse across your application.
Constructor
new WebhookSDK(config: WebhookSDKConfig)| Parameter | Type | Required | Description |
|---|---|---|---|
| clientId | string | Yes | Eka Care Connect client ID |
| clientSecret | string | Yes | Eka Care Connect client secret |
| apiKey | string | No | Eka Care API key (improves rate limits) |
| signingKey | string | No | Webhook signing key. If provided, every request is verified via HMAC-SHA256. If omitted, signature verification is skipped. |
| allowedEvents | string[] | No | Event types to accept. Defaults to all supported events. |
| onEvent | (event: WebhookEvent) => void \| Promise<void> | No | Callback for processing events. Required for standalone mode. Optional for handleRequest()/Express middleware -- you can use result.event instead. |
| path | string | No | Webhook path for standalone server (default: "/"). Ignored for Express middleware. |
| persistence | PersistenceConfig | No | MySQL or DynamoDB backend for schedule rows. See Persistence Layer. |
| debug | boolean | No | When true, internal SDK warnings/errors are written to the console. Default false (silent). See Debug Logging. |
Example: Initializing with everything
import { WebhookSDK } from "@eka-care/webhook-sdk";
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
apiKey: "your-api-key",
signingKey: "your-signing-key",
allowedEvents: ["appointment.created", "appointment.updated"],
onEvent: async (event) => {
console.log("Event received:", event.type);
},
persistence: {
type: "dynamodb",
region: "ap-south-1",
},
debug: true,
});sdk.handleRequest(body, headers)
Framework-agnostic core handler. All other request-handling methods delegate to this.
async handleRequest(
body: unknown,
headers: Record<string, string | string[] | undefined>
): Promise<WebhookResult>Parameters:
body-- Parsed JSON body (object) or raw JSON stringheaders-- Request headers. The SDK looks foreka-webhook-signature(lowercase).
Returns: WebhookResult
interface WebhookResult {
status: number; // HTTP status code (200, 400, 403, 500, 502)
body: string; // JSON response string
event?: WebhookEvent; // Enriched event data (present when status is 200)
error?: string; // Error message (present when status is not 200)
}When the request is processed successfully (status: 200), the event field contains the full WebhookEvent with event type, raw payload, and enriched details.
sdk.listen(port, callback?)
Starts a standalone HTTP server.
listen(port: number, callback?: () => void): http.ServerReturns the http.Server instance so you can call .close() for graceful shutdown.
sdk.expressMiddleware()
Returns an Express-compatible middleware function.
expressMiddleware(): (req: Request, res: Response, next?: NextFunction) => voidWorks with Express 4.x and 5.x. Body can be pre-parsed via express.json() or the middleware will parse it from the request stream.
sdk.findAppointmentsBetween(input)
See Querying Scheduled Appointments. Throws if persistence was not configured.
async findAppointmentsBetween(input: {
start_time: number;
end_time: number;
}): Promise<{
appointments: Array<Record<string, unknown>>;
followup_appointments: Array<Record<string, unknown>>;
}>sdk.scheduleReminder(input)
See Reminder Scheduling. Requires node-schedule to be installed.
async scheduleReminder(input: {
cronTime: string | Date;
appointment_id: string;
callback: (ctx: AppointmentReminderContext) => void | Promise<void>;
}): Promise<import("node-schedule").Job>Returns the node-schedule Job. The job is auto-cancelled after the first fire; you can also cancel it earlier yourself via job.cancel().
sdk.getAppointmentDetailsById(input)
Fetches appointment details directly from Eka Care API using the same credentials already provided when initializing WebhookSDK.
async getAppointmentDetailsById(
input: {
appointment_id: string;
partner_id?: "1";
}
): Promise<unknown>Parameters:
appointment_id-- Appointment ID to fetchpartner_id-- Optional partner context ("1")
const appointment = await sdk.getAppointmentDetailsById({
appointment_id: "your-appointment-id",
});sdk.getPatientDetailsById(input)
async getPatientDetailsById(input: { patient_id: string }): Promise<unknown>sdk.getDoctorDetailsById(input)
async getDoctorDetailsById(input: { doctor_id: string }): Promise<unknown>sdk.getClinicDetailsById(input)
async getClinicDetailsById(input: { clinic_id: string }): Promise<unknown>All four get*ById methods authenticate against the Eka Care API using the SDK's configured credentials and return the API response as unknown (cast or schema-validate at the call site).
WebhookEvent
The enriched event object. Available via result.event (from handleRequest()) or as the argument to the onEvent callback.
interface WebhookEvent {
type: string; // e.g., "appointment.created"
payload: WebhookPayload; // Raw webhook payload
appointmentDetails?: Record<string, unknown>; // Full appointment data (appointment events only)
patientDetails?: Record<string, unknown>; // Full patient data (appointment events only)
doctorDetails?: Record<string, unknown>; // Full doctor data (appointment events only)
clinicDetails?: Record<string, unknown>; // Full clinic data (appointment events only)
}PersistenceConfig
Discriminated union accepted by the persistence constructor option.
type PersistenceConfig = MySQLPersistenceConfig | DynamoDBPersistenceConfig;
interface MySQLPersistenceConfig {
type: "mysql";
connection: {
host: string;
port?: number;
user: string;
password: string;
database: string;
};
}
interface DynamoDBPersistenceConfig {
type: "dynamodb";
region: string;
accessKeyId?: string;
secretAccessKey?: string;
endpoint?: string; // for DynamoDB Local
}AppointmentSchedule
The row shape stored in the persistence layer. Exported for users who want to read the table directly.
interface AppointmentSchedule {
appointment_id: string;
start_time: number;
end_time: number;
status: string;
visit_type: string;
}The constant SCHEDULE_TABLE_NAME ("eka_webhook_schedule_appointment") is also exported.
AppointmentReminderContext
The argument passed to scheduleReminder callbacks. See Reminder Scheduling.
verifySignature(payload, signatureHeader, signingKey)
Standalone signature verification function. Exported for advanced use cases where you want to verify signatures without the full SDK pipeline.
import { verifySignature } from "@eka-care/webhook-sdk";
const result = verifySignature(requestBody, signatureHeaderValue, signingKey);
// result = { valid: true, reason: null } or { valid: false, reason: "..." }WebhookProcessingError
Custom error class thrown during webhook processing. Includes an HTTP statusCode.
import { WebhookProcessingError } from "@eka-care/webhook-sdk";
try {
const result = await sdk.handleRequest(body, headers);
} catch (err) {
if (err instanceof WebhookProcessingError) {
console.log(err.statusCode, err.message);
}
}Constants
import { SUPPORTED_EVENTS, APPOINTMENT_EVENTS, SCHEDULE_TABLE_NAME } from "@eka-care/webhook-sdk";
// SUPPORTED_EVENTS = ["appointment.created", "appointment.updated", "prescription.created", "prescription.updated"]
// APPOINTMENT_EVENTS = ["appointment.created", "appointment.updated"]
// SCHEDULE_TABLE_NAME = "eka_webhook_schedule_appointment"onEvent Callback vs result.event
The SDK provides two ways to access the processed webhook data:
| Approach | When to use |
|---|---|
| onEvent callback | Standalone mode (sdk.listen()), where the SDK owns the HTTP server and you don't control the request/response cycle directly. Also works with Express middleware if you prefer the callback pattern. |
| result.event | handleRequest() and any framework integration where you control the route handler. The enriched WebhookEvent is returned directly in the result -- no callback needed. |
Both approaches can be used together (the callback fires first, then result.event is available in the return value), but typically you'll use one or the other.
Signature Verification
When you provide a signingKey in the SDK configuration, every incoming webhook request is verified using HMAC-SHA256:
- The SDK reads the
Eka-Webhook-Signatureheader from the request - The header format is:
t=<unix_timestamp>,v1=<hex_signature> - The signed payload is constructed as:
<timestamp>.<JSON.stringify(body)> - The expected signature is computed:
HMAC-SHA256(signingKey, signedPayload) - The signatures are compared using
crypto.timingSafeEqual()(constant-time, prevents timing attacks)
If verification fails, the SDK returns a 403 response and does not invoke the onEvent callback or write to persistence.
Disabling Signature Verification
Simply omit the signingKey from the configuration:
const sdk = new WebhookSDK({
clientId: "your-client-id",
clientSecret: "your-client-secret",
// No signingKey = signature verification disabled
});Request Lifecycle
Eka Care Platform
|
v POST (with JSON body + Eka-Webhook-Signature header)
Your Server / Standalone SDK Server
|
v WebhookSDK.handleRequest(body, headers)
|
+-- 1. Parse body (if string)
+-- 2. Verify signature (if signingKey configured)
| |-- FAIL -> return 403
| |-- PASS -> continue
+-- 3. Validate event type against allowedEvents
| |-- NOT ALLOWED -> return 400
| |-- ALLOWED -> continue
+-- 4. For appointment events:
| |-- Authenticate with Eka Care API (auto-managed by SDK)
| |-- Fetch appointment details by ID
| |-- Fetch patient/doctor/clinic details (best-effort)
| |-- Attach to WebhookEvent.{appointmentDetails,patientDetails,...}
+-- 5. For prescription events:
| |-- Pass through raw payload (no API enrichment)
+-- 6. If persistence is configured (and event is appointment.*):
| |-- Lazily ensureTable() on first call (creates / migrates schema)
| |-- appointment.created with status=BK or (IN+FLW) -> upsert row
| |-- appointment.updated -> updateStatus on existing row
| |-- Errors are LOGGED, do NOT fail the webhook response
+-- 7. Invoke onEvent(webhookEvent) callback (if provided)
| |-- THROWS -> return 500
| |-- OK -> continue
+-- 8. Return 200 with success response + result.eventDebug Logging
By default the SDK is silent -- it does not write any warnings or errors to the console. This is to keep the host application's logs clean. Errors still propagate normally to your callers and to HTTP responses; only the console output is suppressed.
To turn internal logging on, pass debug: true:
const sdk = new WebhookSDK({
clientId: "...",
clientSecret: "...",
debug: true,
});When enabled, you will see messages like:
WebhookSDK: failed to fetch patient details for ...WebhookSDK: MySQL upsert failed for appointment_id=... : <error>WebhookSDK: DynamoDB table eka_webhook_schedule_appointment is missing GSI status_start_time_index; submitting UpdateTable. ...WebhookSDK: appointment.updated missing appointment_id; skipping update
The flag is process-global (the SDK is normally a singleton). If you construct multiple WebhookSDK instances in the same process with different debug values, the last one wins.
Error Handling
The SDK handles errors at each stage and returns appropriate HTTP status codes:
| Status | Cause |
|---|---|
| 200 | Webhook processed successfully (persistence errors do not affect this) |
| 400 | Invalid JSON body, missing fields, or unsupported event type |
| 403 | Signature verification failed |
| 500 | Error in onEvent callback or unhandled exception |
| 502 | Failed to fetch appointment details from Eka Care API |
Errors are returned in the response (WebhookResult.error) and -- when debug: true -- also logged to console.error / console.warn.
Environment Variables
The examples use environment variables for configuration. You can set them however fits your deployment:
| Variable | Description |
|---|---|
| EKA_CLIENT_ID | Eka Care Connect client ID |
| EKA_CLIENT_SECRET | Eka Care Connect client secret |
| EKA_API_KEY | Eka Care API key (optional) |
| EKA_SIGNING_KEY | Webhook signing key (optional) |
| PORT | Server port (default: 3000) |
Building from Source
# Install dependencies
npm install
# Build (CJS + ESM + types)
npm run build
# Type check only
npm run typecheck
# Clean build output
npm run cleanProject Structure
webhook-typescript-sdk/
├── src/
│ ├── index.ts # Public API exports
│ ├── types.ts # All TypeScript interfaces and types
│ ├── signature.ts # HMAC-SHA256 signature verification
│ ├── webhook-consumer.ts # Webhook validation + Eka Care enrichment for incoming events
│ ├── webhook-sdk.ts # Main WebhookSDK class
│ ├── enrichment.ts # Reusable appointment + patient + doctor + clinic fetcher
│ ├── logger.ts # Internal logger gated by the `debug` config
│ ├── eka-core.ts # Eka Care core SDK adapter
│ ├── appointment-details.ts # getAppointmentDetailsById helper
│ ├── patient-details.ts # getPatientDetailsById helper
│ ├── doctor-details.ts # getDoctorDetailsById helper
│ ├── clinic-details.ts # getClinicDetailsById helper
│ ├── standalone-server.ts # Built-in HTTP server (standalone mode)
│ ├── adapters/
│ │ └── express.ts # Express middleware adapter
│ └── persistence/
│ ├── types.ts # PersistenceConfig + SchedulePersistence interface
│ ├── factory.ts # Picks adapter based on config.type
│ ├── mysql.ts # MySQL adapter (mysql2/promise, dynamic import)
│ ├── dynamodb.ts # DynamoDB adapter (@aws-sdk/client-dynamodb, dynamic import)
│ └── schedule.ts # Schedule decision logic (when to insert/update)
├── examples/
│ ├── standalone.ts # Standalone server example
│ ├── express.ts # Express integration example
│ ├── fastify.ts # Fastify integration example
│ ├── tsoa-controller.ts # tsoa controller example
│ ├── nestjs.ts # NestJS integration example
│ └── node-http.ts # Plain Node.js http example
├── dist/ # Built output (CJS + ESM + type declarations)
├── package.json
├── tsconfig.json
└── tsup.config.tsRequirements
- Node.js >= 18
- An Eka Care Connect account with
client_idandclient_secret - (Optional)
mysql2or@aws-sdk/client-dynamodbif you opt into persistence - (Optional)
node-scheduleif you callscheduleReminder() - (Optional)
expressif you useexpressMiddleware()
License
MIT
