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

  1. 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.
  2. Both parties open the signaling socket wss://api.mediloop.co/ws/calls/{id}/?token=JWT.
  3. Create an RTCPeerConnection with the ICE servers, then exchange offer, answer and ice-candidate messages over the socket.
  4. 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

POST/api/v1/calls/initiate/Start a call
GET/api/v1/calls/ice-servers/Get STUN/TURN with ephemeral credentials
GET/api/v1/calls/{id}/Get a call
POST/api/v1/calls/{id}/accept/Accept (callee)
POST/api/v1/calls/{id}/decline/Decline (callee)
POST/api/v1/calls/{id}/end/End a call
GET/api/v1/calls/{id}/recordings/List recordings
GET/api/v1/calls/history/Call history
POST/api/v1/calls/reminders/Create an automated / AI reminder
GET/ws/calls/{id}/WebSocket signaling

Start a call

Authorizes the call, notifies the callee, and returns everything the client needs to connect.

POST/api/v1/calls/initiate/

Request Body

patient_uhidstringrequired

The patient party

provider_uhidstringrequired

The provider party (grantee of the consult consent)

modestringrequired

audio or video

context_typestringoptional

appointment or conversation

context_idstringoptional

The appointment/conversation id

recordbooleanoptional

Request consent-gated recording

Request
bash
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"
  }'
RESPONSE201
{
  "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.

GET/ws/calls/{id}/?token=<JWT>
Client to server
json
{ "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 } }
Server to client
json
{ "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

JavaScript (browser WebRTC)
javascript
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.

POST/api/v1/calls/reminders/
Request
bash
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" }
  }'
RESPONSE201
{
  "id": "rem-123",
  "patient_uhid": "UG123456789A",
  "kind": "medication",
  "channel": "in_app_call",
  "recurrence": "daily",
  "status": "scheduled"
}

Call status

StatusDescription
ringingCall created, callee notified
ongoingCallee accepted, media connected
endedEnded after being answered
declinedCallee declined
missedNot answered before ring timeout
failedSetup failed