Outbound Webhooks
ELMED can push events to your system as they happen — no polling required. Works for any HTTPS endpoint: your PMS, pharmacy POS, medical aid admin system, or a custom integration.
Events
| Event | When it fires | Who subscribes |
|---|---|---|
| prescription.issued | Doctor writes a new prescription | Doctor, Pharmacy |
| prescription.dispensed | Pharmacy marks a script as dispensed | Pharmacy, Doctor |
| consultation.completed | Doctor marks a consultation as complete | Doctor |
| order.status_changed | Pharmacy updates an order status (dispensing, in transit, delivered, etc.) | Pharmacy |
| claim.decision | Medical aid approves, partially approves, or rejects a claim | Medical Aid |
| referral.created | A specialist referral is created | Doctor, Specialist |
Payload format
All webhooks are HTTP POST requests with a JSON body and Content-Type: application/json.
The top-level structure is always:
{
"event": "prescription.issued",
"timestamp": "2026-03-29T10:45:00.123456",
"data": { ... }
}
prescription.issued
{
"event": "prescription.issued",
"timestamp": "2026-03-29T10:45:00",
"data": {
"prescription_id": 42,
"prescription_number": "RX-2026-00042",
"patient_id": 7,
"doctor_user_id": 3,
"routed_pharmacy_id": 1,
"medication_count": 2,
"issued_at": "2026-03-29"
}
}
prescription.dispensed
{
"event": "prescription.dispensed",
"timestamp": "2026-03-29T11:00:00",
"data": {
"prescription_id": 42,
"patient_id": 7,
"pharmacy_id": 1,
"pharmacist_user_id": 5,
"collection_method": "collect"
}
}
consultation.completed
{
"event": "consultation.completed",
"timestamp": "2026-03-29T10:30:00",
"data": {
"consultation_id": 15,
"patient_id": 7,
"doctor_user_id": 3,
"type": "video",
"completed_at": "2026-03-29T10:29:55"
}
}
order.status_changed
{
"event": "order.status_changed",
"timestamp": "2026-03-29T12:00:00",
"data": {
"order_id": 88,
"order_number": "ORD-2026-00088",
"patient_id": 7,
"status": "IN_TRANSIT",
"pharmacy_id": 1,
"tracking_number": "CPT123456",
"courier": "The Courier Guy"
}
}
claim.decision
{
"event": "claim.decision",
"timestamp": "2026-03-29T14:00:00",
"data": {
"claim_id": 31,
"patient_id": 7,
"claim_type": "consultation",
"status": "approved",
"amount_claimed": 850.00,
"amount_approved": 850.00,
"rejection_reason": null,
"scheme_id": 2
}
}
Signature verification
Every request includes two extra headers:
X-ELMED-Signature: sha256=<hex>— HMAC-SHA256 of the raw request body, signed with your endpoint's secretX-ELMED-Event: <event_type>— the event name
Always verify the signature before processing the payload. Your secret is shown (truncated) in the webhooks settings page — it's generated when you add the endpoint.
Python
import hmac, hashlib
def verify_signature(secret: str, body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
# In your Flask/FastAPI handler:
body = request.get_data() # raw bytes
sig = request.headers.get("X-ELMED-Signature", "")
if not verify_signature(YOUR_SECRET, body, sig):
return 401
Node.js
const crypto = require("crypto");
function verifySignature(secret, body, header) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
// In your Express handler:
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-elmed-signature"] || "";
if (!verifySignature(YOUR_SECRET, req.body, sig)) return res.sendStatus(401);
const { event, data } = JSON.parse(req.body);
// handle event...
res.sendStatus(200);
});
PHP
$body = file_get_contents("php://input");
$header = $_SERVER["HTTP_X_ELMED_SIGNATURE"] ?? "";
$expected = "sha256=" . hash_hmac("sha256", $body, YOUR_SECRET);
if (!hash_equals($expected, $header)) {
http_response_code(401);
exit;
}
$payload = json_decode($body, true);
// handle $payload["event"] ...
Retries
If your endpoint returns a non-2xx status or times out (10 second timeout), ELMED will retry automatically:
| Attempt | Delay |
|---|---|
| 1st | Immediate |
| 2nd | 30 seconds |
| 3rd | 5 minutes |
After 3 failed attempts the delivery is marked as failed. You can view all delivery attempts and error messages in the Delivery Log inside your portal.
Your endpoint should return 200 as quickly as possible and process the event asynchronously. If you need more than a few hundred milliseconds to handle a webhook, put it in a queue and return 200 immediately.
Setting up
Webhooks are managed per portal. Log in and go to Webhooks in the sidebar:
- Doctors: elmed.healthcare/doctor/webhooks
- Pharmacies: elmed.healthcare/pharmacy/webhooks
- Medical Aids: elmed.healthcare/medical-aid/webhooks
Add your endpoint URL, choose which events to subscribe to, and save. Your unique signing secret is generated automatically and shown once — copy it and store it safely in your environment variables.
Testing your endpoint
The easiest way to test is to use webhook.site — paste the unique URL it gives you as your endpoint in ELMED, then trigger a real event (complete a consultation, dispense a script, etc.) and watch the payload arrive in real time.
Once you see the expected payload, copy the signature header and run it through your verification code before connecting your production endpoint.
localhost addresses will fail. For local development, use a tunnel tool like ngrok (ngrok http 3000) to get a public HTTPS URL that forwards to your local server.