JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 127
  • Score
    100M100P100Q117023F
  • License MIT

Eka Care Webhook SDK for Node.js - Verify webhook signatures and process appointment events with standalone server or framework integration

Package Exports

  • @eka-care/webhook-sdk

Readme

@eka-care/webhook-sdk

Eka Care Webhook SDK for Node.js -- process incoming webhook events from the Eka Care platform with signature verification and automatic appointment data enrichment.

Features

  • Signature verification -- HMAC-SHA256 verification of incoming webhook requests (optional, but recommended)
  • Automatic data enrichment -- fetches full appointment 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.)
  • TypeScript-first -- full type definitions included
  • Dual module format -- supports both CommonJS (require) and ESM (import)
  • Node.js 18+ -- uses built-in crypto module, no additional dependencies beyond @eka-care/eka-care-core

Supported Events

Event Enrichment
appointment.created Full appointment details fetched from Eka Care API
appointment.updated Full appointment details fetched from Eka Care API
prescription.created Raw payload passed through
prescription.updated Raw payload passed through

Installation

npm install @eka-care/webhook-sdk

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 /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);

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 (where the SDK owns the HTTP lifecycle). Optional for handleRequest()/Express middleware -- you can use result.event instead.
path string No Webhook path for standalone server (default: "/"). Ignored for Express middleware.

sdk.handleRequest(body, headers)

Framework-agnostic core handler. All other 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 appointment details. This allows you to write your business logic directly around the handleRequest() call without needing an onEvent callback.

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.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")

Example:

import { WebhookSDK } from "@eka-care/webhook-sdk";

const sdk = new WebhookSDK({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  apiKey: "your-api-key", // optional
});

const appointment = await sdk.getAppointmentDetailsById({
  appointment_id: "your-appointment-id",
});

console.log(appointment);

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)
}

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 } from "@eka-care/webhook-sdk";

// SUPPORTED_EVENTS = ["appointment.created", "appointment.updated", "prescription.created", "prescription.updated"]
// APPOINTMENT_EVENTS = ["appointment.created", "appointment.updated"]

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.

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
    |      |-- Attach to WebhookEvent.appointmentDetails
    +-- 5. For prescription events:
    |      |-- Pass through raw payload (no API enrichment)
    +-- 6. Invoke onEvent(webhookEvent) callback (if provided)
    |      |-- THROWS -> return 500
    |      |-- OK -> continue
    +-- 7. Return 200 with success response + result.event

Error Handling

The SDK handles errors at each stage and returns appropriate HTTP status codes:

Status Cause
200 Webhook processed successfully
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 logged to console.error and returned in the response. The WebhookResult.error field contains the error message.

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     # Core webhook processing + Eka Care API integration
│   ├── webhook-sdk.ts          # Main WebhookSDK class
│   ├── standalone-server.ts    # Built-in HTTP server (standalone mode)
│   └── adapters/
│       └── express.ts          # Express middleware adapter
├── 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

License

MIT