Telehealth video visits with expiring links: one API call per appointment
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
guestUrlgoes 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.staffUrlstays 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.