Login Start free
Developer API

Build on the Spo2x Healthcare API

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.

This API returns patient data (PHI). Use it only under a signed Business Associate / data-processing agreement with the clinic, over HTTPS, storing the minimum you need. Every request is audited.

1. Enable it

  • The platform superadmin enables the developer-api plugin; the clinic owner/admin enables it for the clinic and opens Developer API (/clinic/api).
  • The clinic creates an API key there — the secret is shown once, so copy it.
  • Disabling the plugin is an instant kill-switch (API returns 403, webhooks stop) without deleting keys.

2. Authentication

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.

3. Endpoints

Base URL: https://ehr.spo2x.com

curl https://ehr.spo2x.com/api/v1/patients \
  -H "X-Api-Key: pk_…" -H "Authorization: Bearer sk_…"
Method & pathReturns
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-linkPush 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.

4. Scopes

ScopeGrants
patients:readGET /api/v1/patients[/{id}]
meal_plans:writePOST …/meal-plan-link

A missing scope returns 403 insufficient_scope.

5. Rate limiting

Default 120 requests/min per key (superadmin-configurable). Over the limit → 429 rate_limited; back off and retry.

6. Errors

JSON { "error": "<code>", "message": "…" } with an HTTP status:

Statuserror
401invalid_api_key
403developer_api_disabled · insufficient_scope
404not_found
429rate_limited

7. Key rotation & expiry

  • Keys can expire; expired keys fail with 401.
  • Rotate issues a new secret while the old keeps working for a 48h grace window — swap with zero downtime.
  • Optional auto-rotate cadence; owners/admins get a header-bell reminder before expiry/rotation.

8. Webhooks

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)

9. Meal-plan deep link → patient portal

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).

Recommended pattern

  • Backfill once by paging GET /api/v1/patients.
  • Stay in sync via patient.created / patient.updated webhooks; upsert by patient id.
  • Reconcile nightly by re-listing, in case a delivery was missed.