Sari la conținut
plusmedical logo for light backgrounds

Webhooks

Webhook endpoints are managed by an organization admin in the PlusMedical app (Settings → API). Endpoints must be https:// URLs resolving to public addresses, and each gets a signing secret (whsec_…, shown once, rotatable).

Events

EventFires when
appointment.createdAn appointment is booked on ANY channel (reception, widget, portal, API)
appointment.updatedTime/doctor/status changes (other than the two below)
appointment.cancelledAn appointment is cancelled
appointment.no_showAn appointment is marked no-show
patient.createdA patient is registered with (or first linked to) the organization
patient.updatedA patient's contact data changes
invoice.issuedAn invoice is issued (gets its number)
invoice.paidAn issued invoice becomes fully paid
invoice.stornoedA credit note is issued against an invoice
lab_order.validatedA lab order is fully validated — reference only: order id, patient ref and analysis CODES, never values
pingThe "send test event" button

Envelope

data uses the same public DTOs as the REST API — a field that is not in the API is not in a webhook either.

{
  "id": "0197a3c2-7f1e-7a31-bb1c-2f6a9d2e4c10",
  "type": "appointment.created",
  "created_at": "2026-06-14T09:30:00+00:00",
  "data": {
    "id": "0197a3c2-6d2b-7c44-9e1f-8b3d5a7e9f21",
    "status": "confirmed",
    "starts_at": "2026-06-15T08:00:00+00:00",
    "ends_at": "2026-06-15T08:30:00+00:00",
    "source": "api",
    "patient": { "id": "…" },
    "doctor": { "id": "…" },
    "location": { "id": "…" },
    "service": { "id": "…" }
  }
}

Idempotency

id is the event id and your idempotency key: automatic retries and manual redeliveries reuse it, and when one event fans out to several of your endpoints they all share it. Process each event id at most once.

Signature verification

Every delivery is signed Stripe-style. The X-PlusMedical-Signature header carries t={unix},v1={hmac} where the HMAC is HMAC-SHA256(secret, "{t}.{raw_body}"). Verify the timestamp within a 5-minute tolerance (replay protection) and compare digests in constant time. Always sign-check the raw body, before any JSON parsing.

PHP

<?php
// $secret  = the endpoint secret (whsec_...)
// $payload = the RAW request body (do not re-encode it)
// $header  = $_SERVER['HTTP_X_PLUSMEDICAL_SIGNATURE']

function verifyPlusMedicalSignature(string $secret, string $payload, string $header): bool
{
    if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m)) {
        return false;
    }
    [, $timestamp, $signature] = $m;

    if (abs(time() - (int) $timestamp) > 300) { // 5-minute tolerance
        return false;                            // replay guard
    }

    $expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);

    return hash_equals($expected, $signature);
}

Node.js

const crypto = require('node:crypto')

// secret  = the endpoint secret (whsec_...)
// payload = the RAW request body as a string (use express.raw / rawBody)
// header  = req.headers['x-plusmedical-signature']
function verifyPlusMedicalSignature(secret, payload, header) {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '')
  if (!match) return false
  const [, timestamp, signature] = match

  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + payload)
    .digest('hex')

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}

Python

import hashlib, hmac, re, time

# secret  = the endpoint secret (whsec_...)
# payload = the RAW request body as bytes
# header  = request.headers["X-PlusMedical-Signature"]
def verify_plusmedical_signature(secret: str, payload: bytes, header: str) -> bool:
    match = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
    if not match:
        return False
    timestamp, signature = match.groups()

    if abs(time.time() - int(timestamp)) > 300:  # 5-minute tolerance
        return False

    signed = f"{timestamp}.".encode() + payload
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()

    return hmac.compare_digest(expected, signature)

Delivery & retries

Respond fast: acknowledge with 200 immediately and process asynchronously. Slow handlers hit the 10s timeout and count as failures.