Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 4 min read

Telehealth video visits with expiring links: one API call per appointment

From the Streaming CDN team. The original, kept up to date, is at streaming-cdn.ewin888.com/guides/telehealth-video-visit-api-expiring-links. A clinic or health platform usually wants the same thing for every appointmen

From the Streaming CDN team. The original, kept up to date, is at streaming-cdn.ewin888.com/guides/telehealth-video-visit-api-expiring-links.

A clinic or health platform usually wants the same thing for every appointment: a link the patient can open in a browser without installing anything, a different link for the clinician, both dead after the visit window, and a record of when the visit really started and ended. On Streaming CDN that is one server API call per appointment. This guide walks through it with curl and a webhook handler.

1. Create the visit from your backend

Use a server API key with the temporary-meetings:write scope. Keep the key on your server; the browser only ever receives the links.

curl -X POST https://streaming-cdn.ewin888.com/v1/applications/temporary-meetings \
  -H "Authorization: Bearer scdn_your_server_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Follow-up visit",
    "guestDisplayName": "Patient",
    "staffDisplayName": "Dr. Lee",
    "mode": "face_to_face",
    "recordingEnabled": false,
    "expiresInMinutes": 60,
    "callbackUrl": "https://clinic.example.com/webhooks/visit",
    "metadata": { "appointmentId": "apt_4821" }
  }'
Field Values Notes
title 1–120 characters Shown to your staff, not to the patient
guestDisplayName / staffDisplayName 1–90 characters Defaults Guest and Staff; a second join on the same link gets a numeric suffix
mode face_to_face or observe_guest observe_guest starts the staff side with camera and microphone off
recordingEnabled true / false Records automatically when the visit begins. Default false
expiresInMinutes 5–1440 Validity of both links. Default 30
callbackUrl Public HTTPS URL Receives signed lifecycle events. Omit it to poll instead
metadata JSON object Your own ids, returned unchanged in queries and webhooks

The response contains two links and, when you passed callbackUrl, a secret that is shown only this once:

{
  "meeting": {
    "id": "temporary_meeting_...",
    "mode": "face_to_face",
    "status": "active",
    "joinCount": 0,
    "expiresAt": "2026-10-12T10:00:00.000Z"
  },
  "guestUrl": "https://.../temporary-meeting/tm_guest_...",
  "staffUrl": "https://.../temporary-meeting/tm_staff_...",
  "callbackSecret": "tmsec_store_this_once"
}

2. Send each link to the right person

  • guestUrl goes to the patient, by SMS, email or your patient portal. It opens a browser page with camera and microphone; there is no app to install.
  • staffUrl stays inside your clinician tools. It joins with the host role, so never send it to a patient.
  • The links are bearer tokens: anyone holding one can join until it expires. Store meeting.id, not the URLs, in your appointment record.

3. Expiry, rotation and revocation

Need Call Result
Patient lost the link, or it was sent to the wrong address POST /v1/applications/temporary-meetings/{id}/links/guest A new guest link; the old one stops working immediately
Replace the clinician link POST /v1/applications/temporary-meetings/{id}/links/staff Same, for the staff link
Appointment cancelled DELETE /v1/applications/temporary-meetings/{id} The room ends and both links are revoked
Check where a visit stands GET /v1/applications/temporary-meetings/{id} Status, expiry, join count, start and end times

A visit moves through active β†’ in_use β†’ completed, or ends as expired or revoked. Opening a link after the visit has expired, ended or been revoked returns 410 temporary_meeting_unavailable, and rotating a link on a finished visit returns 409; show the patient a "this visit link is no longer valid" page and, if appropriate, create a new visit.

4. Know when the visit really happened

The visit counts as started only when at least one patient and one clinician are online together, so a patient waiting alone does not start the clock. Your callbackUrl receives temporary_meeting.started and then temporary_meeting.ended with startedAt, endedAt, durationSeconds, endReason, participantPeak and your metadata. Each event has a stable id; use it to ignore duplicates.

Verify every webhook before trusting it. The signature is an HMAC-SHA256 of timestamp + "." + rawBody with the callbackSecret, base64url-encoded:

import { createHmac, timingSafeEqual } from "node:crypto";

export async function handleVisitWebhook(request: Request, callbackSecret: string) {
  const rawBody = await request.text();
  const timestamp = request.headers.get("x-streaming-cdn-timestamp") ?? "";
  const supplied = request.headers.get("x-streaming-cdn-signature") ?? "";
  const expected = "v1=" + createHmac("sha256", callbackSecret).update(`${timestamp}.${rawBody}`).digest("base64url");
  const ok = supplied.length === expected.length && timingSafeEqual(Buffer.from(supplied), Buffer.from(expected));
  if (!ok || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return new Response("invalid signature", { status: 401 });
  }
  const event = JSON.parse(rawBody);
  // event.type: "temporary_meeting.started" | "temporary_meeting.ended"
  // event.metadata.appointmentId identifies your appointment
  return new Response("ok");
}

Answer with any 2xx status. Failed deliveries are retried, so the handler should be idempotent on the event id.

5. Recording, when you need it

With recordingEnabled: true the visit is recorded from the moment it begins. Recordings are kept on the platform and can be listed with GET /v1/rtc/recordings?roomId=<meeting.roomId> and downloaded with GET /v1/rtc/recordings/{id}/download, using a server API key. Decide per appointment type whether to record; many visits should not be.

6. Optional: confirm a real person first

For visits where identity matters, create a liveness check with POST /v1/applications/liveness/sessions and send its verification link before the visit link. The patient follows on-screen actions (blink, shake the head, nod) in the browser and your backend receives a signed liveness.completed result; only then send guestUrl.

What stays with you

The platform provides the video, the links, the verification and the recording. Medical records, consent, what you record and how long you keep it remain the healthcare provider's responsibility under local regulations. Put only non-identifying ids such as an appointment id in title and metadata, not diagnoses or patient names.

Streaming CDN runs one-to-one video visits from your backend: separate patient and clinician links that expire, joined from a browser with no app. Learn more.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.