Three steps to integrate.
Get your API key
Request access via /demo?source=api. You receive your client_id and client_secret in less than 24 business hours.
Authenticate
Exchange your credentials for an access_token via /auth/token. Renewable every hour.
First call
List doctors in your wilaya, create your first appointment, listen to your first webhooks.
curl -X POST https://api.jivox.ma/api/appointments \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"doctorId": "usr_8f3a92",
"patientId": "usr_4c7b91",
"startTime": "2026-05-12T10:30:00+01:00",
"endTime": "2026-05-12T11:00:00+01:00"
}'OAuth 2.0 client credentials
Jivox uses OAuth 2.0 with the client_credentials grant for server-to-server integrations. Tokens are valid for 1 hour and renewable via a 30-day refresh_token. Each token is limited to a set of scopes.
POST /api/auth/token
{
"grant_type": "client_credentials",
"client_id": "ck_live_…",
"client_secret": "cs_live_…",
"scope": "appointments:read appointments:write webhooks:manage"
}All subsequent calls must include the header Authorization: Bearer <access_token>. On expired token, the API returns 401 with the code token_expired.
Available scopes
users:readdoctors:readappointments:readappointments:writeprescriptions:readprescriptions:writeorders:readorders:writenotifications:writepdf:generatevideo:joinwebhooks:manageEndpoints
12 groups, 35 endpoints. All requests are JSON UTF-8; dates in ISO 8601; amounts in MAD centimes (integers). Click an endpoint to see the example.
Authentification
Sign-in, sign-out, refresh, profil — basé NextAuth + JWT.
POSTEndpoint NextAuth dispatcher (signin, signout, callback, csrf, providers, session).
Scopes :publicRequestPOST /api/auth/callback/credentials Content-Type: application/x-www-form-urlencoded email=jane%40clinic.ma&password=…&csrfToken=…Response (2xx){ "url": "https://app.jivox.ma/dashboard" }Error (4xx){ "error": "CredentialsSignin" }POSTCrée un nouveau compte patient, médecin ou pharmacien (email + password).
Scopes :publicRequest{ "email": "[email protected]", "password": "S3cret!Passw0rd", "name": "Dr. Jane", "role": "DOCTOR" }Response (2xx){ "ok": true, "user": { "id": "usr_…", "email": "[email protected]", "role": "DOCTOR" } }Error (4xx){ "error": { "code": "CONFLICT", "message": "Email already registered" } }GETRenvoie le profil de l'utilisateur authentifié (id, email, role, tenantId).
Scopes :users:readResponse (2xx){ "id": "usr_8f3a92", "email": "[email protected]", "name": "Dr. Jane", "role": "DOCTOR", "tenantId": "ten_…" }Error (4xx){ "error": { "code": "UNAUTHORIZED", "message": "Authentication required" } }
Public
Annuaire public, sans authentification, mis en cache.
GETListe paginée des médecins publics (filtres: city, specialty, languages, isTele).
Scopes :publicResponse (2xx){ "doctors": [{ "id": "usr_…", "slug": "dr-elhassani", "name": "Dr. Younes El Hassani", "specialty": "Cardiologie", "city": "Casablanca", "rating": 4.8, "isTele": true }], "pagination": { "page": 1, "pageSize": 20, "total": 142, "totalPages": 8 } }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }GETListe authentifiée des médecins du tenant courant (vue admin/staff).
Scopes :doctors:readResponse (2xx){ "doctors": [{ "id": "usr_…", "name": "Dr. Jane", "specialty": "…" }] }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }
Rendez-vous
Création, listing, mise à jour, annulation. Conflits détectés serveur.
GETListe les rendez-vous (PATIENT voit les siens, DOCTOR ses créneaux, ADMIN tout).
Scopes :appointments:readResponse (2xx){ "appointments": [{ "id": "apt_…", "startTime": "2026-05-12T10:30:00.000Z", "endTime": "2026-05-12T11:00:00.000Z", "status": "SCHEDULED", "videoRoomId": "room_…", "patient": { "id": "usr_…", "name": "Patient X" }, "doctor": { "id": "usr_…", "name": "Dr. Jane" } }], "pagination": { "page": 1, "pageSize": 20, "total": 5, "totalPages": 1 } }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }POSTCrée un rendez-vous. 409 si chevauchement avec un autre créneau du médecin.
Scopes :appointments:writeRequest{ "doctorId": "usr_…", "patientId": "usr_…", "startTime": "2026-05-12T10:30:00.000Z", "endTime": "2026-05-12T11:00:00.000Z", "notes": "Suivi diabète" }Response (2xx){ "appointment": { "id": "apt_…", "status": "SCHEDULED", "videoRoomId": "room_…" } }Error (4xx){ "error": { "code": "CONFLICT", "message": "Doctor already has an appointment in that window", "details": { "overlap": { "id": "apt_…" } } } }GETRécupère un rendez-vous par identifiant.
Scopes :appointments:readResponse (2xx){ "appointment": { "id": "apt_…", "status": "SCHEDULED", "notes": "…" } }Error (4xx){ "error": { "code": "NOT_FOUND", "message": "Appointment not found" } }PUTMet à jour le statut, l'heure, les notes ou annule le rendez-vous.
Scopes :appointments:writeRequest{ "status": "CANCELLED", "notes": "Patient indisponible" }Response (2xx){ "appointment": { "id": "apt_…", "status": "CANCELLED" } }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }DELETESupprime définitivement un rendez-vous (admin only).
Scopes :appointments:writeadminResponse (2xx){ "ok": true }Error (4xx){ "error": { "code": "FORBIDDEN", "message": "Insufficient permissions" } }
Ordonnances
Émission et listing d'ordonnances numériques signées.
GETListe les ordonnances visibles selon le rôle (patient/doctor/admin).
Scopes :prescriptions:readResponse (2xx){ "prescriptions": [{ "id": "rx_…", "medication": "Metformine 500 mg", "dosage": "1 cp x 2/j", "doctor": { "id": "usr_…", "name": "Dr. Jane" }, "patient": { "id": "usr_…", "name": "Patient X" }, "issuedAt": "2026-05-02T08:00:00.000Z" }] }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }POSTCrée une nouvelle ordonnance. Réservé aux DOCTOR/DENTIST.
Scopes :prescriptions:writeRequest{ "patientId": "usr_…", "appointmentId": "apt_…", "medication": "Amoxicilline 1g", "dosage": "1 cp x 3/j pendant 7 jours", "instructions": "À prendre au milieu du repas." }Response (2xx){ "prescription": { "id": "rx_…", "medication": "Amoxicilline 1g" } }Error (4xx){ "error": { "code": "FORBIDDEN", "message": "Only doctors can issue prescriptions" } }
Commandes pharmacie
Création, statuts, dispensation des commandes pharmaceutiques.
GETListe les commandes (patient/pharmacy/admin scope).
Scopes :orders:readResponse (2xx){ "orders": [{ "id": "ord_…", "status": "READY", "deliveryRequested": true, "prescription": { "id": "rx_…", "medication": "…" }, "pharmacy": { "id": "usr_…", "name": "Pharmacie Centrale" } }] }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }POSTCrée une commande à partir d'une ordonnance et l'envoie à une pharmacie.
Scopes :orders:writeRequest{ "prescriptionId": "rx_…", "pharmacyId": "usr_…", "deliveryRequested": false }Response (2xx){ "order": { "id": "ord_…", "status": "PENDING" } }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }GETRécupère une commande par identifiant.
Scopes :orders:readResponse (2xx){ "order": { "id": "ord_…", "status": "READY" } }Error (4xx){ "error": { "code": "NOT_FOUND", "message": "Order not found" } }PUTMet à jour le statut (PENDING → PREPARING → READY → DELIVERED).
Scopes :orders:writeRequest{ "status": "READY" }Response (2xx){ "order": { "id": "ord_…", "status": "READY" } }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }
Leads
Capture des prospects depuis le site marketing (formulaires, démo).
POSTEnregistre un lead. Source typique: /demo, /pricing, /contact.
Scopes :publicRequest{ "name": "Dr. Karim", "email": "[email protected]", "phone": "+212600000000", "source": "DEMO_FORM", "metadata": { "specialty": "Cardiologie", "city": "Rabat" } }Response (2xx){ "ok": true, "leadId": "lead_…" }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }
Notifications
Envoi de SMS/WhatsApp/Email transactionnels via les providers ChannelProvider.
POSTEnvoie un message transactionnel sur le canal préféré du destinataire.
Scopes :notifications:writeRequest{ "recipientUserId": "usr_…", "channel": "WHATSAPP", "templateCode": "appointment_reminder", "variables": { "doctorName": "Dr. Jane", "datetime": "12 mai à 10h30" } }Response (2xx){ "ok": true, "messageId": "msg_…", "providerMessageId": "wa_…", "status": "QUEUED" }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }
Génération PDF
Rendu serveur d'ordonnances, certificats, factures, attestations.
POSTGénère un PDF signé à partir d'un template. Renvoie un URL temporaire.
Scopes :pdf:generateRequest{ "templateId": "rx_default", "data": { "prescriptionId": "rx_…" } }Response (2xx){ "ok": true, "url": "https://cdn.jivox.ma/pdf/rx_…?sig=…", "expiresAt": "2026-05-02T11:30:00.000Z" }Error (4xx){ "error": { "code": "UNPROCESSABLE", "message": "Validation failed", "details": [ { "path": ["startTime"], "message": "Invalid datetime" } ] } }
Vidéo (LiveKit)
Délivre un access token JWT pour rejoindre une room LiveKit.
POSTÉmet un access token court (15 min) pour le videoRoomId d'un rendez-vous.
Scopes :video:joinRequest{ "appointmentId": "apt_…", "identity": "usr_…" }Response (2xx){ "ok": true, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…", "url": "wss://livekit.jivox.ma", "roomId": "room_…", "expiresAt": "2026-05-12T10:45:00.000Z" }Error (4xx){ "error": { "code": "FORBIDDEN", "message": "User not part of this appointment" } }
Health checks
Sondes de disponibilité — utilisées par le load balancer et le statut public.
GETSonde liveness simple — répond 200 dès que le process tourne.
Scopes :publicResponse (2xx){ "ok": true, "timestamp": "2026-05-02T10:30:00.000Z" }Error (4xx){ "ok": false, "error": "service_unavailable" }GETSonde Postgres (SELECT 1, latence < 200 ms).
Scopes :publicResponse (2xx){ "ok": true, "service": "postgres", "latencyMs": 12 }Error (4xx){ "ok": false, "service": "postgres", "error": "connection_refused" }GETSonde Redis (PING, latence < 50 ms).
Scopes :publicResponse (2xx){ "ok": true, "service": "redis", "latencyMs": 4 }Error (4xx){ "ok": false, "service": "redis", "error": "timeout" }GETSonde LiveKit (SDK admin call, latence < 500 ms).
Scopes :publicResponse (2xx){ "ok": true, "service": "livekit", "latencyMs": 87 }Error (4xx){ "ok": false, "service": "livekit", "error": "auth_failed" }
Cron jobs
Workers planifiés. Auth via en-tête X-Cron-Secret. Idempotents.
POSTEnvoie les rappels J-1 et H-2 pour les rendez-vous (cadence: toutes les 15 min).
Scopes :cron:runResponse (2xx){ "ok": true, "processed": 42, "succeeded": 41, "failed": 1 }Error (4xx){ "error": "forbidden" }POSTAnnule les RDV non confirmés au-delà de 24h. Cadence: toutes les heures.
Scopes :cron:runResponse (2xx){ "ok": true, "cancelled": 7 }Error (4xx){ "error": "forbidden" }POSTRappel des patients ALD inactifs depuis 90 jours. Cadence: quotidien 09:00 UTC.
Scopes :cron:runResponse (2xx){ "ok": true, "processed": 18, "succeeded": 16, "failed": 2 }Error (4xx){ "error": "forbidden" }POSTAvance les destinataires dans leurs séquences drip activées. Cadence: 30 min.
Scopes :cron:runResponse (2xx){ "ok": true, "advanced": 124 }Error (4xx){ "error": "forbidden" }POSTAvertit les pharmacies des médicaments arrivant en péremption (J-30, J-7).
Scopes :cron:runResponse (2xx){ "ok": true, "warnedItems": 36 }Error (4xx){ "error": "forbidden" }POSTApplique les changements tarifaires programmés dont la date est échue.
Scopes :cron:runResponse (2xx){ "ok": true, "applied": 3 }Error (4xx){ "error": "forbidden" }
Webhooks internes
Inter-app événements signés. Auth via X-Internal-Token.
POSTÉmis par doctor-portal quand un médecin confirme un rendez-vous.
Scopes :internalRequest{ "appointmentId": "apt_…" }Response (2xx){ "ok": true, "timestamp": "2026-05-02T10:30:00.000Z" }Error (4xx){ "error": "forbidden" }POSTÉmis quand une ordonnance vient d'être délivrée; pré-route vers une pharmacie si fournie.
Scopes :internalRequest{ "prescriptionId": "rx_…", "pharmacyId": "usr_…", "deliveryRequested": false }Response (2xx){ "ok": true, "prescriptionId": "rx_…", "routedOrderId": "ord_…" }Error (4xx){ "error": "rx_not_found" }POSTÉmis par pharmacy-portal à chaque transition de statut d'une commande.
Scopes :internalRequest{ "orderId": "ord_…", "status": "READY" }Response (2xx){ "ok": true, "timestamp": "2026-05-02T10:30:00.000Z" }Error (4xx){ "error": "order_not_found" }POSTÉmis par lab-portal quand un résultat est validé et publié au patient.
Scopes :internalRequest{ "labOrderId": "lab_…" }Response (2xx){ "ok": true, "timestamp": "2026-05-02T10:30:00.000Z" }Error (4xx){ "error": "lab_order_not_found" }POSTÉmis par patient-portal quand un patient accepte une consultation de suivi.
Scopes :internalRequest{ "followUpId": "fu_…" }Response (2xx){ "ok": true, "timestamp": "2026-05-02T10:30:00.000Z" }Error (4xx){ "error": "follow_up_not_found" }
Error codes
All errors return a normalized envelope with code, human-readable message, and optional details. 4xx codes are never auto-retried; 5xx are (exponential backoff).
Standard error envelope
{
"error": {
"code": "UNPROCESSABLE",
"message": "Validation failed",
"details": [
{ "path": ["startTime"], "message": "Invalid datetime" }
]
}
}For codes 429, 5xx, and network timeouts: retry with exponential backoff (250ms × 2ⁿ, max 5 attempts). Honor the Retry-After header when present.
Pagination
Lists use a page + pageSize scheme (max 100). The response includes a pagination object with total and totalPages.
GET /api/appointments?status=SCHEDULED&page=2&pageSize=50
Authorization: Bearer YOUR_ACCESS_TOKEN{
"appointments": [ /* … 50 rows … */ ],
"pagination": {
"page": 2,
"pageSize": 50,
"total": 327,
"totalPages": 7
}
}Rate limits
Each token gets quotas based on the organization's plan. Headers X-RateLimit-Limit and X-RateLimit-Remaining are returned on every call; Retry-After on 429.
15 events signed with HMAC SHA-256
Each webhook contains a X-MC-Signature header computed with your webhook_secret using HMAC SHA-256 over the raw body. Failed deliveries are retried 3 times (1 min, 10 min, 1 h).
Signature verification
Retry policy: 3 attempts with exponential backoff (1 min, 10 min, 1 h). After that, delivery is marked failed and the event remains visible in the dashboard.
import crypto from "node:crypto";
export function verify(rawBody: string, header: string, secret: string) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
// Constant-time comparison to defeat timing attacks.
const a = Buffer.from(expected, "hex");
const b = Buffer.from(header, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}appointment.createdUn nouveau rendez-vous a été créé.
{
"event": "appointment.created",
"id": "evt_…",
"createdAt": "2026-05-02T10:30:00.000Z",
"data": { "appointmentId": "apt_…", "doctorId": "usr_…", "patientId": "usr_…", "startTime": "…" }
}appointment.updatedStatut, horaire ou notes mis à jour.
{ "event": "appointment.updated", "data": { "appointmentId": "apt_…", "changes": ["status"] } }appointment.cancelledRendez-vous annulé (par le patient, le médecin ou l'auto-cancel).
{ "event": "appointment.cancelled", "data": { "appointmentId": "apt_…", "reason": "patient_request" } }appointment.completedConsultation marquée terminée.
{ "event": "appointment.completed", "data": { "appointmentId": "apt_…", "durationSec": 1620 } }prescription.createdNouvelle ordonnance émise.
{ "event": "prescription.created", "data": { "prescriptionId": "rx_…", "medication": "…" } }prescription.signedOrdonnance signée numériquement.
{ "event": "prescription.signed", "data": { "prescriptionId": "rx_…", "signature": "sha256:…" } }lab_order.createdCommande d'analyses soumise.
{ "event": "lab_order.created", "data": { "labOrderId": "lab_…", "tests": ["NFS","Glycémie"] } }lab_order.status_changedStatut LabOrder mis à jour.
{ "event": "lab_order.status_changed", "data": { "labOrderId": "lab_…", "status": "RESULTS_READY" } }lab_result.publishedRésultat publié et visible par le patient.
{ "event": "lab_result.published", "data": { "labResultId": "res_…", "url": "https://…" } }lab_result.critical_alertValeur critique détectée — alerte haute priorité.
{ "event": "lab_result.critical_alert", "data": { "labResultId": "res_…", "param": "K+", "value": 6.2 } }payment.succeededPaiement CMI ou cash comptabilisé.
{ "event": "payment.succeeded", "data": { "invoiceId": "inv_…", "amountMAD": 25000 } }payment.refundedRemboursement émis.
{ "event": "payment.refunded", "data": { "invoiceId": "inv_…", "amountMAD": 25000 } }review.createdAvis vérifié publié.
{ "event": "review.created", "data": { "reviewId": "rev_…", "rating": 5 } }patient.createdNouveau patient enregistré.
{ "event": "patient.created", "data": { "patientId": "usr_…" } }patient.consent_updatedLe patient a modifié son consentement CNDP.
{ "event": "patient.consent_updated", "data": { "patientId": "usr_…", "consents": ["MARKETING"] } }JavaScript · Python · PHP
JavaScript / TypeScript
npmnpm i @medconnect/sdkNode 20+, Next.js, Bun, Deno compatible. Built-in types, ESM + CJS.
Python
PyPIpip install medconnectPython 3.10+ compatible. Async via httpx, sync via requests.
PHP
Composercomposer require medconnect/sdkPHP 8.2+ compatible. Guzzle, PSR-7, PSR-4 autoloading.
Code samples
Five common operations in JavaScript, Python, and PHP. All operations go through an SDK client initialized with your access_token.
List appointments
import { Jivox } from "@medconnect/sdk";
const client = new Jivox({
accessToken: process.env.JIVOX_TOKEN,
});
const { appointments, pagination } = await client.appointments.list({
status: "SCHEDULED",
page: 1,
pageSize: 50,
});from medconnect import Jivox
client = Jivox(access_token=os.environ["JIVOX_TOKEN"])
resp = client.appointments.list(status="SCHEDULED", page=1, page_size=50)
for appt in resp["appointments"]:
print(appt["id"], appt["startTime"])<?php
use Jivox\Client;
$client = new Client(['access_token' => getenv('JIVOX_TOKEN')]);
$result = $client->appointments->list(['status' => 'SCHEDULED', 'pageSize' => 50]);
foreach ($result['appointments'] as $appt) {
echo $appt['id'] . PHP_EOL;
}Create appointment
const appointment = await client.appointments.create({
doctorId: "usr_8f3a92",
patientId: "usr_4c7b91",
startTime: "2026-05-12T10:30:00+01:00",
endTime: "2026-05-12T11:00:00+01:00",
});
console.log("Réservé :", appointment.id);appt = client.appointments.create(
doctor_id="usr_8f3a92",
patient_id="usr_4c7b91",
start_time="2026-05-12T10:30:00+01:00",
end_time="2026-05-12T11:00:00+01:00",
)$appt = $client->appointments->create([
'doctorId' => 'usr_8f3a92',
'patientId' => 'usr_4c7b91',
'startTime' => '2026-05-12T10:30:00+01:00',
'endTime' => '2026-05-12T11:00:00+01:00',
]);List prescriptions
const { prescriptions } = await client.prescriptions.list({
patientId: "usr_4c7b91",
});rxs = client.prescriptions.list(patient_id="usr_4c7b91")["prescriptions"]$rxs = $client->prescriptions->list(['patientId' => 'usr_4c7b91']);Send notification
await client.notifications.send({
recipientUserId: "usr_4c7b91",
channel: "WHATSAPP",
templateCode: "appointment_reminder",
variables: { doctorName: "Dr. Jane", datetime: "12 mai à 10h30" },
});client.notifications.send(
recipient_user_id="usr_4c7b91",
channel="WHATSAPP",
template_code="appointment_reminder",
variables={"doctorName": "Dr. Jane", "datetime": "12 mai à 10h30"},
)$client->notifications->send([
'recipientUserId' => 'usr_4c7b91',
'channel' => 'WHATSAPP',
'templateCode' => 'appointment_reminder',
'variables' => ['doctorName' => 'Dr. Jane', 'datetime' => '12 mai à 10h30'],
]);Health check
const health = await client.health.check();
console.log(health.ok); // truehealth = client.health.check()
assert health["ok"]$health = $client->health->check();
assert($health['ok']);Sandbox
The sandbox environment mirrors the production API with fictitious data. No real notifications are sent; payments simulate CMI.
- 1Request sandbox credentials via /demo?source=api by checking 'Sandbox only'. Delivered within 24h.
- 2Point your SDK client at https://api-sandbox.jivox.ma. Test accounts (doctors, patients, pharmacies) are preprovisioned.
- 3Sandbox data is automatically reset every Sunday at 00:00 UTC. You can force a manual reset using the endpoint below.
curl -X POST https://api-sandbox.jivox.ma/api/sandbox/reset \
-H "Authorization: Bearer SANDBOX_TOKEN"All services operational
- API Gateway uptime 100,00 % 47 msOK
- Auth (NextAuth) uptime 99,98 % 62 msOK
- Webhooks uptime 99,95 % 112 msOK
- SDK CDN uptime 100,00 % 23 msOK
- Sandbox uptime 99,90 % 78 msOK
API plans
Pour explorer l'API et bâtir un prototype.
- 60 req / min
- 5 000 req / jour
- Sandbox uniquement
- Support communauté
Pour les éditeurs SaaS et les cabinets en production.
- 600 req / min
- 100 000 req / jour
- Webhooks signés
- SLA 99,9 %
- Support email
Volumes illimités, isolation dédiée, contrats sur-mesure.
- 6 000 req / min
- Cluster dédié
- Webhooks redondants
- DPA + audit annuel
- Account manager