npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 via node-schedule and 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: true opt-in to surface internal warnings/errors; otherwise the SDK runs silent
  • Node.js 18+ -- uses built-in crypto module; 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-sdk

Optional 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 endpoint
  • GET /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.COLUMNS and runs ALTER TABLE ... ADD COLUMN for every non-key column that is missing (e.g. created_at, added in a later release). Each missing column is added with NOT 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_index is missing, the SDK fires UpdateTable to create it and returns immediately. GSI creation is asynchronous (can take several minutes); findAppointmentsBetween returns errors until it is ACTIVE. 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:

  1. Asks the persistence layer for the matching appointment_ids, split into normal bookings vs followups.
  2. Calls getAppointmentDetailsById (via the Eka Care API) in parallel for every ID.
  3. 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 cronTime is 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 string
  • headers -- Request headers. The SDK looks for eka-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.Server

Returns 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) => void

Works 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 fetch
  • partner_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:

  1. The SDK reads the Eka-Webhook-Signature header from the request
  2. The header format is: t=<unix_timestamp>,v1=<hex_signature>
  3. The signed payload is constructed as: <timestamp>.<JSON.stringify(body)>
  4. The expected signature is computed: HMAC-SHA256(signingKey, signedPayload)
  5. 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.event

Debug 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 clean

Project 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.ts

Requirements

  • Node.js >= 18
  • An Eka Care Connect account with client_id and client_secret
  • (Optional) mysql2 or @aws-sdk/client-dynamodb if you opt into persistence
  • (Optional) node-schedule if you call scheduleReminder()
  • (Optional) express if you use expressMiddleware()

License

MIT