Eka Care Webhook SDK for Python — process incoming webhook events with signature verification, automatic appointment data enrichment, schedule persistence, and one-shot reminder scheduling.
Project description
eka-webhook-sdk
Eka Care Webhook SDK for Python — process incoming webhook events from the Eka Care platform with signature verification, automatic appointment data enrichment, schedule persistence, and one-shot reminder scheduling.
This is the Python port of @eka-care/webhook-sdk.
It provides the same feature set, the same on-the-wire behaviour, and the
same eka_webhook_schedule_appointment table layout — so a Node service
and a Python service can share the same backing table without conflict.
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
- Flask view — drop-in view function for Flask apps
- FastAPI route — drop-in async route handler for FastAPI apps
- Framework-agnostic handler — works with Django, Starlette, aiohttp,
Sanic, Tornado, Bottle, or any framework with
sdk.handle_request()
- 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 —
find_appointments_between()returns full appointment + patient + doctor + clinic details for every booking in a given time window, split into normal vs followup - Reminder scheduling —
schedule_reminder()schedules a one-shot reminder via APScheduler and fires the callback with freshly fetched appointment context - Type-friendly — full type hints,
py.typedmarker shipped - Silent by default —
debug=Trueopt-in to surface internal warnings/errors; otherwise the SDK runs silent - Python 3.10+ — uses standard library
hmac,http.server,concurrent.futures. Persistence and reminder backends are optional extras 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
pip install eka-webhook-sdk
Optional extras
The SDK imports each of the optional backends lazily, so you only install what your code path actually uses:
| Feature you use | Install |
|---|---|
Standalone server, handle_request(), integrating with Django/Starlette/aiohttp/etc. |
nothing extra |
sdk.flask_view() |
pip install eka-webhook-sdk[flask] |
sdk.fastapi_route() |
pip install eka-webhook-sdk[fastapi] |
persistence=MySQLPersistenceConfig(...) |
pip install eka-webhook-sdk[mysql] (uses PyMySQL) |
persistence=DynamoDBPersistenceConfig(...) |
pip install eka-webhook-sdk[dynamodb] (uses boto3) |
sdk.schedule_reminder(...) |
pip install eka-webhook-sdk[schedule] (uses APScheduler) |
| Everything | pip install eka-webhook-sdk[all] |
If you forget one, the relevant SDK method raises a clear RuntimeError
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 backed by the standard-library http.server.
from eka_webhook_sdk import WebhookSDK
def on_event(event):
print("Event type:", event.type)
print("Appointment:", event.appointment_details)
# Your business logic here:
# - Save to database
# - Send notifications
# - Trigger workflows
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
api_key="your-api-key", # optional
signing_key="your-signing-key", # optional, enables signature verification
on_event=on_event,
)
server = sdk.listen(3000, on_listen=lambda: print("Webhook server listening on port 3000"))
# Block the main thread until Ctrl+C
import signal
signal.pause()
The standalone server provides:
POST /— webhook endpointGET /eka-webhook-health— health check endpoint ({"status": "ok"})
Option 2: Flask View
Drop into an existing Flask application. Use on_event for your business
logic, or read result.event directly from handle_request().
from flask import Flask
from eka_webhook_sdk import WebhookSDK
app = Flask(__name__)
def on_event(event):
print("Received:", event.type, event.appointment_details)
# Save to DB, send notifications, etc.
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
signing_key="your-signing-key",
on_event=on_event,
)
app.add_url_rule(
"/webhook/eka",
view_func=sdk.flask_view(),
methods=["POST"],
)
if __name__ == "__main__":
app.run(port=3000)
Option 3: FastAPI Route
from fastapi import FastAPI
from eka_webhook_sdk import WebhookSDK
app = FastAPI()
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
signing_key="your-signing-key",
)
app.add_api_route(
"/webhook/eka",
sdk.fastapi_route(),
methods=["POST"],
)
The SDK's core handler is synchronous (it makes blocking HTTP calls to the Eka Care API). The FastAPI adapter offloads it to a threadpool so the async event loop stays responsive.
Option 4: Generic Handler (Any Framework)
Use sdk.handle_request() directly in any Python framework. The result
includes the enriched event object — no on_event callback needed.
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
signing_key="your-signing-key",
# No on_event callback — use result.event instead
)
# In your route handler:
result = sdk.handle_request(request_body, request_headers)
if result.event:
print(result.event.type) # "appointment.created"
print(result.event.appointment_details) # full appointment object
print(result.event.payload) # raw webhook payload
# Your business logic here
save_to_database(result.event.appointment_details)
return result.body, result.status
Framework Integration Examples
Django
# views.py
import json
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from eka_webhook_sdk import WebhookSDK
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
signing_key="your-signing-key",
)
@csrf_exempt
@require_POST
def eka_webhook(request):
try:
body = json.loads(request.body.decode("utf-8"))
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON body"}, status=400)
headers = {k.lower(): v for k, v in request.headers.items()}
result = sdk.handle_request(body, headers)
if result.event:
# Your business logic here
print(result.event.type, result.event.appointment_details)
return JsonResponse(result.body, status=result.status)
# urls.py
from django.urls import path
from .views import eka_webhook
urlpatterns = [
path("webhook/eka", eka_webhook),
]
Starlette / Plain ASGI
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.concurrency import run_in_threadpool
from eka_webhook_sdk import WebhookSDK
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
signing_key="your-signing-key",
)
async def webhook(request):
try:
body = await request.json()
except Exception:
return JSONResponse({"error": "Invalid JSON body"}, status_code=400)
headers = {k.lower(): v for k, v in request.headers.items()}
result = await run_in_threadpool(sdk.handle_request, body, headers)
return JSONResponse(result.body, status_code=result.status)
app = Starlette(routes=[Route("/webhook/eka", webhook, methods=["POST"])])
aiohttp
from aiohttp import web
from eka_webhook_sdk import WebhookSDK
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
signing_key="your-signing-key",
)
async def webhook(request: web.Request) -> web.Response:
try:
body = await request.json()
except Exception:
return web.json_response({"error": "Invalid JSON body"}, status=400)
headers = {k.lower(): v for k, v in request.headers.items()}
loop = request.app.loop
result = await loop.run_in_executor(None, sdk.handle_request, body, headers)
return web.json_response(result.body, status=result.status)
app = web.Application()
app.router.add_post("/webhook/eka", webhook)
web.run_app(app, port=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
find_appointments_between() 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
from eka_webhook_sdk import (
WebhookSDK,
MySQLPersistenceConfig,
MySQLConnectionConfig,
)
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
persistence=MySQLPersistenceConfig(
connection=MySQLConnectionConfig(
host="localhost",
port=3306, # optional
user="your-user",
password="your-password",
database="your-database",
),
),
)
The SDK uses PyMySQL and maintains an internal connection pool of size
10. Connections are health-checked with ping(reconnect=True) so they
survive idle timeouts on the database side.
Configuration: DynamoDB
from eka_webhook_sdk import WebhookSDK, DynamoDBPersistenceConfig
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
persistence=DynamoDBPersistenceConfig(
region="ap-south-1",
# Optional. If omitted, the standard AWS credential chain is used
# (env vars, instance role, ~/.aws/credentials, etc.).
access_key_id="AKIA...",
secret_access_key="...",
# Optional. For DynamoDB Local during development:
endpoint="http://localhost:8000",
),
)
The DynamoDB table is created with BillingMode=PAY_PER_REQUEST (no
capacity to provision).
Lazy initialization
ensure_table() runs on the first webhook (or first call to
find_appointments_between). 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 firesupdate_tableto create it and returns immediately. GSI creation is asynchronous (can take several minutes);find_appointments_betweenreturns errors until it isACTIVE. Other operations (upsert, update_status) 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.find_appointments_between(start_time, end_time)
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.
result = sdk.find_appointments_between(
start_time=1714521600,
end_time=1714608000,
)
# result.appointments → list of dicts (status === "BK")
# result.followup_appointments → list of dicts (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
get_appointment_details_by_id(via the Eka Care API) in parallel for every ID using a bounded thread pool. - Drops any IDs whose detail fetch fails (the failure is logged when
debug=True).
from datetime import datetime, timezone
tomorrow_start = int(datetime(2026, 5, 9, tzinfo=timezone.utc).timestamp())
tomorrow_end = tomorrow_start + 24 * 60 * 60
result = sdk.find_appointments_between(
start_time=tomorrow_start,
end_time=tomorrow_end,
)
print(
f"{len(result.appointments)} bookings, "
f"{len(result.followup_appointments)} followups"
)
Heads up on rate limits: a window with N matching appointments fans out to N parallel API calls (capped at 16 concurrent). For very large windows, throttle in the caller (or split the range) to stay under the Eka Care rate limits.
find_appointments_between raises if the SDK was constructed without a
persistence config.
Reminder Scheduling
sdk.schedule_reminder(...)
Schedule a one-shot reminder for an appointment. When the trigger 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 APScheduler to be installed.
from datetime import datetime, timedelta
from eka_webhook_sdk import AppointmentReminderContext
start_time = datetime.fromtimestamp(appointment["start_time"])
def remind(ctx: AppointmentReminderContext) -> None:
patient = ctx.patient_details or {}
doctor = ctx.doctor_details or {}
clinic = ctx.clinic_details or {}
send_sms(
patient.get("mobile"),
f"Reminder: appointment with {doctor.get('firstname')} at {clinic.get('name')}",
)
reminder = sdk.schedule_reminder(
# Either a datetime (one-shot) OR a 5-/6-field cron expression
cron_time=start_time - timedelta(minutes=5),
appointment_id=appointment["appointment_id"],
callback=remind,
)
# You can still cancel the reminder BEFORE its fire time, e.g. if the
# appointment is cancelled by the user:
reminder.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
cron_timeis 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. - Sync OR async callback:
callbackmay be a regular function or a coroutine function — the SDK runs it correctly either way. - In-memory only: jobs do not survive process restart. If you need
durable reminders, persist
(appointment_id, cron_time)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.
cron_time accepted formats
| Form | Example | Meaning |
|---|---|---|
datetime |
datetime(2026, 5, 9, 14, 30) |
One-shot fire at the given absolute time |
| 5-field cron | "30 14 9 5 *" |
minute hour day month day_of_week |
| 6-field cron | "0 30 14 9 5 *" |
second minute hour day month day_of_week |
AppointmentReminderContext shape
@dataclass
class AppointmentReminderContext:
appointment_details: dict[str, Any]
patient_details: dict[str, Any] | None = None
doctor_details: dict[str, Any] | None = None
clinic_details: dict[str, Any] | None = None
patient_details / doctor_details / clinic_details are best-effort —
if a sub-fetch fails, the corresponding field is None and the failure
is logged.
API Reference
WebhookSDK
The main class. Construct it once and reuse across your application.
Constructor
WebhookSDK(
*,
client_id: str,
client_secret: str,
api_key: str | None = None,
signing_key: str | None = None,
allowed_events: list[str] | None = None,
on_event: Callable[[WebhookEvent], None | Awaitable[None]] | None = None,
path: str = "/",
persistence: PersistenceConfig | None = None,
debug: bool = False,
)
| Parameter | Type | Required | Description |
|---|---|---|---|
client_id |
str |
Yes | Eka Care Connect client ID |
client_secret |
str |
Yes | Eka Care Connect client secret |
api_key |
str |
No | Eka Care API key (improves rate limits) |
signing_key |
str |
No | Webhook signing key. If provided, every request is verified via HMAC-SHA256. If omitted, signature verification is skipped. |
allowed_events |
list[str] |
No | Event types to accept. Defaults to all supported events. |
on_event |
Callable[[WebhookEvent], None | Awaitable[None]] |
No | Callback for processing events. Required for standalone mode. Optional for handle_request() / Flask view / FastAPI route — you can use result.event instead. |
path |
str |
No | Webhook path for standalone server (default: "/"). Ignored for Flask/FastAPI adapters. |
persistence |
PersistenceConfig |
No | MySQL or DynamoDB backend for schedule rows. See Persistence Layer. |
debug |
bool |
No | When True, internal SDK warnings/errors are written to stdout. Default False (silent). |
Example: initializing with everything
from eka_webhook_sdk import WebhookSDK, DynamoDBPersistenceConfig
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
api_key="your-api-key",
signing_key="your-signing-key",
allowed_events=["appointment.created", "appointment.updated"],
on_event=lambda e: print("Event:", e.type),
persistence=DynamoDBPersistenceConfig(region="ap-south-1"),
debug=True,
)
sdk.handle_request(body, headers) -> WebhookResult
Framework-agnostic core handler. All other request-handling methods delegate to this.
Parameters:
body— Parsed JSON body (dict) or raw JSON string.headers— Request headers as a plain key-value mapping. Names are matched case-insensitively; the SDK readseka-webhook-signature.
Returns: WebhookResult.
When the request is processed successfully (status == 200), the
event field contains the full WebhookEvent.
sdk.listen(port, on_listen=None) -> ThreadingHTTPServer
Starts a standalone threaded HTTP server. Returns the server immediately
— the request loop runs on a background thread. Call
server.shutdown() for graceful shutdown.
sdk.flask_view() -> Callable
Returns a Flask view function. Mount it on any Flask route::
app.add_url_rule("/webhook/eka", view_func=sdk.flask_view(), methods=["POST"])
sdk.fastapi_route() -> Callable
Returns a FastAPI async route handler. Mount it on any FastAPI app::
app.add_api_route("/webhook/eka", sdk.fastapi_route(), methods=["POST"])
sdk.find_appointments_between(*, start_time, end_time) -> FindAppointmentsBetweenResult
See Querying Scheduled Appointments.
Raises RuntimeError if persistence was not configured.
sdk.schedule_reminder(*, cron_time, appointment_id, callback) -> Reminder
See Reminder Scheduling. Requires APScheduler
to be installed.
Returns a Reminder object. Call reminder.cancel() to unschedule
before the fire time.
sdk.get_appointment_details_by_id(appointment_id, *, partner_id=None)
Fetch appointment details directly from the Eka Care API using SDK credentials.
appointment = sdk.get_appointment_details_by_id(
appointment_id="your-appointment-id",
)
sdk.get_patient_details_by_id(patient_id)
sdk.get_doctor_details_by_id(doctor_id)
sdk.get_clinic_details_by_id(clinic_id)
All four get_*_by_id methods authenticate against the Eka Care API
using the SDK's configured credentials and return the API response as a
dict.
WebhookEvent
The enriched event object. Available via result.event (from
handle_request()) or as the argument to the on_event callback.
@dataclass
class WebhookEvent:
type: str # e.g. "appointment.created"
payload: dict[str, Any] # Raw webhook payload
appointment_details: dict[str, Any] | None = None
patient_details: dict[str, Any] | None = None
doctor_details: dict[str, Any] | None = None
clinic_details: dict[str, Any] | None = None
WebhookResult
@dataclass
class WebhookResult:
status: int # HTTP status code (200, 400, 403, 500, 502)
body: dict[str, Any] # JSON response body
event: WebhookEvent | None = None # Enriched event (present when status == 200)
error: str | None = None # Error message (present when status != 200)
PersistenceConfig
Union of MySQLPersistenceConfig and DynamoDBPersistenceConfig.
from dataclasses import dataclass
@dataclass
class MySQLConnectionConfig:
host: str
user: str
password: str
database: str
port: int | None = None
@dataclass
class MySQLPersistenceConfig:
connection: MySQLConnectionConfig
type: Literal["mysql"] = "mysql"
@dataclass
class DynamoDBPersistenceConfig:
region: str
access_key_id: str | None = None
secret_access_key: str | None = None
endpoint: str | None = None # for DynamoDB Local
type: Literal["dynamodb"] = "dynamodb"
AppointmentSchedule
The row shape stored in the persistence layer.
@dataclass
class AppointmentSchedule:
appointment_id: str
start_time: int
end_time: int
status: str
visit_type: str
The constant SCHEDULE_TABLE_NAME ("eka_webhook_schedule_appointment")
is also exported.
verify_signature(payload, signature_header, signing_key)
Standalone signature verification function. Exported for advanced use cases where you want to verify signatures without the full SDK pipeline.
from eka_webhook_sdk import verify_signature
result = verify_signature(request_body, signature_header_value, signing_key)
# result.valid → True/False
# result.reason → human-readable reason if not valid
WebhookProcessingError
Custom error class raised during webhook processing. Includes an HTTP
status_code.
from eka_webhook_sdk import WebhookProcessingError
try:
result = sdk.handle_request(body, headers)
except WebhookProcessingError as err:
print(err.status_code, err)
In normal usage you do not need to catch this — handle_request()
already turns it into a WebhookResult with the appropriate status.
Constants
from eka_webhook_sdk import (
SUPPORTED_EVENTS,
APPOINTMENT_EVENTS,
SCHEDULE_TABLE_NAME,
)
# SUPPORTED_EVENTS = (
# "appointment.created", "appointment.updated",
# "prescription.created", "prescription.updated",
# )
# APPOINTMENT_EVENTS = ("appointment.created", "appointment.updated")
# SCHEDULE_TABLE_NAME = "eka_webhook_schedule_appointment"
on_event Callback vs result.event
The SDK provides two ways to access the processed webhook data:
| Approach | When to use |
|---|---|
on_event 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 Flask/FastAPI if you prefer the callback pattern. |
result.event |
handle_request() 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 signing_key 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_serialised_body>, using JSON with no whitespace separators (matching JavaScript'sJSON.stringifydefault). - The expected signature is computed:
HMAC-SHA256(signing_key, signed_payload). - The signatures are compared using
hmac.compare_digest(constant-time, prevents timing attacks).
If verification fails, the SDK returns a 403 response and does not
invoke the on_event callback or write to persistence.
Disabling Signature Verification
Simply omit signing_key:
sdk = WebhookSDK(
client_id="your-client-id",
client_secret="your-client-secret",
# No signing_key = signature verification disabled
)
Request Lifecycle
Eka Care Platform
|
v POST (with JSON body + Eka-Webhook-Signature header)
Your Server / Standalone SDK Server
|
v WebhookSDK.handle_request(body, headers)
|
+-- 1. Parse body (if string)
+-- 2. Verify signature (if signing_key configured)
| |-- FAIL -> return 403
| |-- PASS -> continue
+-- 3. Validate event type against allowed_events
| |-- 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.{appointment_details, ...}
+-- 5. For prescription events:
| |-- Pass through raw payload (no API enrichment)
+-- 6. If persistence is configured (and event is appointment.*):
| |-- Lazily ensure_table() on first call (creates / migrates schema)
| |-- appointment.created with status=BK or (IN+FLW) -> upsert row
| |-- appointment.updated -> update_status on existing row
| |-- Errors are LOGGED, do NOT fail the webhook response
+-- 7. Invoke on_event(webhook_event) 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 stdout. 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:
sdk = WebhookSDK(
client_id="...",
client_secret="...",
debug=True,
)
When enabled, you will see messages like:
[ERROR] WebhookSDK: failed to fetch patient details for ...[ERROR] WebhookSDK: MySQL upsert failed for appointment_id=... : <error>WebhookSDK: DynamoDB table eka_webhook_schedule_appointment is missing GSI status_start_time_index; submitting UpdateTable. ...[WARN] WebhookSDK: appointment.updated missing appointment_id; skipping update
The flag is process-global. 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 on_event 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 printed to stdout.
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 in editable mode with dev tools
pip install -e ".[dev,all]"
# Run tests
pytest
# Type check
mypy eka_webhook_sdk
# Lint
ruff check eka_webhook_sdk
Project Structure
webhook-python-package/
├── eka_webhook_sdk/
│ ├── __init__.py # Public API exports
│ ├── types.py # Public dataclasses, TypedDicts, constants
│ ├── exceptions.py # WebhookProcessingError
│ ├── signature.py # HMAC-SHA256 signature verification
│ ├── eka_api.py # Thin HTTP client for Eka Care endpoints + token cache
│ ├── enrichment.py # Reusable appointment + patient + doctor + clinic fetcher
│ ├── webhook_consumer.py # Webhook validation + Eka Care enrichment for incoming events
│ ├── webhook_sdk.py # Main WebhookSDK class
│ ├── scheduler.py # APScheduler wrapper for one-shot reminders
│ ├── standalone_server.py # Built-in HTTP server (standalone mode)
│ ├── logger.py # Internal logger gated by the `debug` config
│ ├── adapters/
│ │ ├── __init__.py
│ │ ├── flask.py # Flask view adapter
│ │ └── fastapi.py # FastAPI route adapter
│ └── persistence/
│ ├── __init__.py
│ ├── base.py # PersistenceConfig + SchedulePersistence interface
│ ├── factory.py # Picks adapter based on config type
│ ├── mysql.py # MySQL adapter (PyMySQL, lazy import)
│ ├── dynamodb.py # DynamoDB adapter (boto3, lazy import)
│ └── schedule.py # Schedule decision logic (when to insert/update)
├── examples/
│ ├── standalone.py # Standalone server example
│ ├── flask_app.py # Flask integration example
│ ├── fastapi_app.py # FastAPI integration example
│ └── django_view.py # Django view example
├── pyproject.toml
├── README.md
└── LICENSE
Requirements
- Python >= 3.10
- An Eka Care Connect account with
client_idandclient_secret - (Optional)
PyMySQLorboto3if you opt into persistence - (Optional)
APSchedulerif you callschedule_reminder() - (Optional)
Flaskif you useflask_view() - (Optional)
fastapiif you usefastapi_route()
Equivalence with the Node SDK
Node (@eka-care/webhook-sdk) |
Python (eka-webhook-sdk) |
|---|---|
mysql2 |
PyMySQL |
@aws-sdk/client-dynamodb |
boto3 |
node-schedule |
APScheduler |
express |
Flask (and FastAPI) |
Built-in http standalone server |
Built-in http.server standalone server |
JSON.stringify(body) for signature canonicalisation |
json.dumps(body, separators=(",", ":"), ensure_ascii=False) |
eka_webhook_schedule_appointment table |
Same table — both SDKs are wire-compatible |
WebhookSDK.scheduleReminder() |
WebhookSDK.schedule_reminder() |
WebhookSDK.findAppointmentsBetween() |
WebhookSDK.find_appointments_between() |
appointmentDetails, patientDetails, ... |
appointment_details, patient_details, ... |
The SDKs share the same table layout, the same signature scheme, and the same Eka Care API contracts — you can mix and match Node and Python services pointing at the same persistence layer without conflict.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file eka_webhook_sdk-0.1.0.tar.gz.
File metadata
- Download URL: eka_webhook_sdk-0.1.0.tar.gz
- Upload date:
- Size: 54.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
17a94047f98739474eae283d363e398b68a652c8b4ec0f6732317ef4a7c1d97f
|
|
| MD5 |
a569c6117ba5f528ea88324f87987f65
|
|
| BLAKE2b-256 |
fc11b7285079bfef7d0c6a86cccc76d844fce1f16e64137f488f1893c1a29738
|
File details
Details for the file eka_webhook_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: eka_webhook_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 40.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
91ee3684a1735b3142f70c7989c857eb4e2f67a37360c9c76eb6a2640cc102fe
|
|
| MD5 |
d5d8f635b28dcfc28b219d025a1a5f74
|
|
| BLAKE2b-256 |
7881024a35d597dd6713e0024b9dcd719d25de1e5eb225a5966ac31b21ea3d49
|