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
cryptomodule, 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-sdkQuick 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);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. |
Example: Initializing the SDK
import { WebhookSDK } from "@eka-care/webhook-sdk";
const sdk = new WebhookSDK({
clientId: "your-client-id", // Required: Eka Care Connect client ID
clientSecret: "your-client-secret", // Required: Eka Care Connect client secret
apiKey: "your-api-key", // Optional: improves rate limits
signingKey: "your-signing-key", // Optional: enables HMAC-SHA256 signature verification
allowedEvents: [ // Optional: filter which events to accept
"appointment.created",
"appointment.updated",
],
onEvent: async (event) => { // Optional: callback for processing events
console.log("Event received:", event.type);
if (event.appointmentDetails) {
console.log("Appointment:", event.appointmentDetails);
}
// Your business logic here
},
path: "/webhook", // Optional: webhook path for standalone server (default: "/")
});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 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 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.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.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")
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:
- 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.
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.eventError 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 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 # 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.tsRequirements
- Node.js >= 18
- An Eka Care Connect account with
client_idandclient_secret
License
MIT