Streaming CDN

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

Updated 2026-10-12

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" }
  }'
FieldValuesNotes
title1–120 charactersShown to your staff, not to the patient
guestDisplayName / staffDisplayName1–90 charactersDefaults Guest and Staff; a second join on the same link gets a numeric suffix
modeface_to_face or observe_guestobserve_guest starts the staff side with camera and microphone off
recordingEnabledtrue / falseRecords automatically when the visit begins. Default false
expiresInMinutes5–1440Validity of both links. Default 30
callbackUrlPublic HTTPS URLReceives signed lifecycle events. Omit it to poll instead
metadataJSON objectYour 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

3. Expiry, rotation and revocation

NeedCallResult
Patient lost the link, or it was sent to the wrong addressPOST /v1/applications/temporary-meetings/{id}/links/guestA new guest link; the old one stops working immediately
Replace the clinician linkPOST /v1/applications/temporary-meetings/{id}/links/staffSame, for the staff link
Appointment cancelledDELETE /v1/applications/temporary-meetings/{id}The room ends and both links are revoked
Check where a visit standsGET /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.

Telehealth

One-to-one video visits for clinics and health platforms: browser links that expire, camera liveness checks before the visit, optional recording and webhooks.

Learn more · Contact us