Read clinic data from your own app over a signed REST API, and receive webhooks when data changes. Enabled per clinic via the Developer API plugin.
developer-api plugin; the clinic owner/admin enables it for the clinic and opens Developer API (/clinic/api).403, webhooks stop) without deleting keys.Send a public key id and the secret over HTTPS:
X-Api-Key: pk_xxxxxxxxxxxx
Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxx
The key resolves to exactly one clinic — you only ever see that clinic's data.
Base URL: https://ehr.spo2x.com
curl https://ehr.spo2x.com/api/v1/patients \
-H "X-Api-Key: pk_…" -H "Authorization: Bearer sk_…"
| Method & path | Returns |
|---|---|
GET /api/v1/patients (optional ?q=) | The clinic's patients (id, full_name, email, phone, dob, gender) |
GET /api/v1/patients/{id} | One patient (404 if not in your clinic) |
POST /api/v1/patients/{id}/meal-plan-link | Push a portal "Meal plan" deep link (needs meal_plans:write) |
{ "data": [ { "id": 97, "full_name": "…", "email": "…", "phone": "…", "dob": "1971-07-20", "gender": null } ] }
Only a least-privilege patient projection is returned — never internal, marketing, or insurance fields.
| Scope | Grants |
|---|---|
patients:read | GET /api/v1/patients[/{id}] |
meal_plans:write | POST …/meal-plan-link |
A missing scope returns 403 insufficient_scope.
Default 120 requests/min per key (superadmin-configurable). Over the limit → 429 rate_limited; back off and retry.
JSON { "error": "<code>", "message": "…" } with an HTTP status:
| Status | error |
|---|---|
| 401 | invalid_api_key |
| 403 | developer_api_disabled · insufficient_scope |
| 404 | not_found |
| 429 | rate_limited |
401.Register an endpoint on /clinic/api and pick events. We POST a signed JSON payload and retry on failure (1m, 5m, 30m, 2h, 6h).
Events: patient.created, patient.updated, appointment.created. Headers: X-Webhook-Event, X-Webhook-Timestamp, X-Webhook-Signature: sha256=<hmac>.
Verify the signature — compute HMAC-SHA256(secret, timestamp + "." + rawBody) and compare constant-time:
// PHP
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $WEBHOOK_SECRET);
if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }
http_response_code(200); // ack fast
# Python
import hmac, hashlib
expected = 'sha256=' + hmac.new(SECRET.encode(), f"{ts}.{raw}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig): abort(401)
A nutrition app can push one deep link per patient (with the meal_plans:write scope) that shows on the patient portal as a "Meal plan → View" card. Only a pointer (title + url) is stored — never meal content.
curl -X POST https://ehr.spo2x.com/api/v1/patients/{id}/meal-plan-link \
-H "X-Api-Key: pk_…" -H "Authorization: Bearer sk_…" \
-H "Content-Type: application/json" \
-d '{ "title": "June meal plan", "url": "https://your-app.example.com/plan/abc" }'
The clinic controls portal display (a toggle on /clinic/api) and can remove pushed links. The EHR only forwards the URL — make it stand on its own (a login or a signed, expiring link; no PHI in the URL).
GET /api/v1/patients.patient.created / patient.updated webhooks; upsert by patient id.Thanks! Your message is on its way — we'll get back to you soon.