Telemedicine Calls
Real-time audio and video calls between patients and verified providers, plus server-side recording and automated reminders. The media layer is WebRTC (peer-to-peer where possible, relayed by our TURN servers on restrictive networks). You never run your own signaling or TURN infrastructure - Mediloop is the rail.
Interactive reference (try it live): api.mediloop.co/api/v1/calls/docs
How a call works
- Call
POST /api/v1/calls/initiate/. You get back the call, a set of ICE servers (STUN + TURN with short-lived credentials), and a signaling WebSocket URL. - Both parties open the signaling socket
wss://api.mediloop.co/ws/calls/{id}/?token=JWT. - Create an
RTCPeerConnectionwith the ICE servers, then exchangeoffer,answerandice-candidatemessages over the socket. - Media flows directly; on locked-down networks it is relayed by our TURN servers automatically.
Works on any network: TURN is offered over UDP, TCP and TLS/443, with ICE restart on Wi-Fi to cellular handoff. Authorization is consult-scoped - a provider can only call a patient they have an active consent (category communication) with.
Endpoints
/api/v1/calls/initiate/Start a call/api/v1/calls/ice-servers/Get STUN/TURN with ephemeral credentials/api/v1/calls/{id}/Get a call/api/v1/calls/{id}/accept/Accept (callee)/api/v1/calls/{id}/decline/Decline (callee)/api/v1/calls/{id}/end/End a call/api/v1/calls/{id}/recordings/List recordings/api/v1/calls/history/Call history/api/v1/calls/reminders/Create an automated / AI reminder/ws/calls/{id}/WebSocket signalingStart a call
Authorizes the call, notifies the callee, and returns everything the client needs to connect.
/api/v1/calls/initiate/Request Body
patient_uhidstringrequiredThe patient party
provider_uhidstringrequiredThe provider party (grantee of the consult consent)
modestringrequiredaudio or video
context_typestringoptionalappointment or conversation
context_idstringoptionalThe appointment/conversation id
recordbooleanoptionalRequest consent-gated recording
curl -X POST https://api.mediloop.co/api/v1/calls/initiate/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"patient_uhid": "UG123456789A",
"provider_uhid": "UG987654321B",
"mode": "video",
"context_type": "appointment",
"context_id": "appt-123"
}'{
"call": {
"call_id": "7b1c...e9",
"status": "ringing",
"mode": "video",
"media_mode": "p2p"
},
"ice_servers": [
{
"urls": [
"stun:98.85.3.133:3478"
]
},
{
"urls": [
"turn:98.85.3.133:3478?transport=udp"
],
"username": "1783167181:UG123456789A",
"credential": "H9Puv7oSNP0D9KXn8nyUmAbr1Tk="
}
],
"ws_url": "wss://api.mediloop.co/ws/calls/7b1c...e9/",
"turn_ttl_seconds": 3600
}Signaling protocol (WebSocket)
Connect with the JWT as a query parameter. Messages are JSON envelopes; the server relays them strictly between the two authorized parties.
/ws/calls/{id}/?token=<JWT>{ "type": "offer", "payload": { "sdp": "..." } }
{ "type": "answer", "payload": { "sdp": "..." } }
{ "type": "ice-candidate", "payload": { "candidate": "..." } }
{ "type": "accept" }
{ "type": "decline" }
{ "type": "hangup" }
{ "type": "mute", "payload": { "audio": true } }
{ "type": "camera", "payload": { "video": false } }{ "type": "peer-joined", "from_uhid": "..." }
{ "type": "offer", "payload": { "sdp": "..." } }
{ "type": "answer", "payload": { "sdp": "..." } }
{ "type": "ice-candidate", "payload": { "candidate": "..." } }
{ "type": "accepted" }
{ "type": "declined" }
{ "type": "call-state", "payload": { "status": "ended" } }
{ "type": "peer-left" }Minimal browser client
const { call, ice_servers, ws_url } = await initiateCall();
const pc = new RTCPeerConnection({ iceServers: ice_servers });
const ws = new WebSocket(ws_url + '?token=' + token);
// local media
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
stream.getTracks().forEach((t) => pc.addTrack(t, stream));
pc.onicecandidate = (e) => e.candidate &&
ws.send(JSON.stringify({ type: 'ice-candidate', payload: e.candidate }));
pc.ontrack = (e) => (remoteVideo.srcObject = e.streams[0]);
ws.onmessage = async ({ data }) => {
const m = JSON.parse(data);
if (m.type === 'offer') {
await pc.setRemoteDescription(m.payload);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({ type: 'answer', payload: answer }));
} else if (m.type === 'answer') {
await pc.setRemoteDescription(m.payload);
} else if (m.type === 'ice-candidate') {
await pc.addIceCandidate(m.payload);
}
};
// caller creates the offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: 'offer', payload: offer }));Automated & AI reminders
Schedule reminders (medication, appointment, follow-up) delivered as an in-app call, WhatsApp, SMS or push. Set use_ai to have the wording generated for the patient.
/api/v1/calls/reminders/curl -X POST https://api.mediloop.co/api/v1/calls/reminders/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"patient_uhid": "UG123456789A",
"kind": "medication",
"channel": "in_app_call",
"recurrence": "daily",
"use_ai": true,
"payload": { "drug": "Amoxicillin 500mg", "time": "08:00" }
}'{
"id": "rem-123",
"patient_uhid": "UG123456789A",
"kind": "medication",
"channel": "in_app_call",
"recurrence": "daily",
"status": "scheduled"
}Call status
| Status | Description |
|---|---|
ringing | Call created, callee notified |
ongoing | Callee accepted, media connected |
ended | Ended after being answered |
declined | Callee declined |
missed | Not answered before ring timeout |
failed | Setup failed |