Docs · Video visits

HIPAA-eligible video visits

Run secure, in-browser video appointments on Twilio Programmable Video. Drop in the prebuilt <VideoRoom />, or call the token endpoint and bring your own UI. The browser only ever receives a short-lived Twilio access token — never a Twilio API key.

How it works

Every video booking maps to a Twilio room. When the provider starts the call, the backend mints short-lived Twilio access tokens for each participant. The patient gets theirs from a keyless endpoint — via an emailed /j/{token} link, the keyless manage page, or the patient portal. You pass that token (with the room name) to the twilio-video SDK's connect(token, { name: room_name }).

Tokens, not secrets

The browser only receives a Twilio access token — short-lived and scoped to a single room. Your Twilio API key secret stays on the server and never reaches the page.

Option 1 — Prebuilt <VideoRoom />

The fastest path: <VideoRoom /> from @cadence/react connects, renders the remote participant(s) plus your own camera as a self-view, and gives you mute / camera / leave controls — all themable with a single accent prop.

Private beta@cadence/react is in private beta and not yet on npm. The snippets below are the usage you'll reach for once it's published. Get early access.

Install

bash
npm install @cadence/react twilio-video
# twilio-video is browser-only; @cadence/react loads it dynamically.

VideoRoom is client-only — it loads twilio-video with a dynamic import(), so it never runs during SSR/build. Render it inside a "use client" boundary:

tsx
"use client";
import { useEffect, useState } from "react";
import { CadenceClient, VideoRoom, type VideoJoin } from "@cadence/react";

export function JoinCall({ joinToken }: { joinToken: string }) {
  const [join, setJoin] = useState<VideoJoin | null>(null);

  useEffect(() => {
    // The keyless video endpoints carry org + identity in the token,
    // so `org` is irrelevant here.
    const client = new CadenceClient({ org: "" });
    void client.getVideoJoin(joinToken).then(setJoin);
  }, [joinToken]);

  if (!join?.live || !join.token || !join.room_name) {
    return <p>Waiting for your provider to start the call…</p>;
  }
  return (
    <VideoRoom
      token={join.token}
      roomName={join.room_name}
      accent="#1f5e45"
      onLeave={() => setJoin(null)}
    />
  );
}

The keyless endpoints return { live: false, token: null } until the provider starts the call. The useCadenceVideo hook polls for you and flips to ready automatically:

tsx
import { CadenceClient, VideoRoom, useCadenceVideo } from "@cadence/react";
import { useCallback, useMemo } from "react";

const client = useMemo(() => new CadenceClient({ org: "" }), []);
const { status, token, roomName } = useCadenceVideo(
  useCallback((s) => client.getVideoJoin(joinToken, s), [client, joinToken]),
);
// status flips "waiting" → "ready" the moment the provider starts the call.
if (status === "ready") return <VideoRoom token={token!} roomName={roomName!} />;

Option 2 — Fully custom UI

Prefer to build your own call screen? You don't need <VideoRoom /> at all. Call the token endpoint yourself and hand the token to your own twilio-video integration:

ts
import { connect } from "twilio-video";

// 1. Mint a short-lived token from any Cadence video endpoint.
const res = await fetch("https://cadence.abhix-ai.com/api/v1/public/video/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ token: joinToken }),
});
const { live, token, room_name } = await res.json();

// 2. If the call is live, hand the token straight to twilio-video.
if (live && token && room_name) {
  const room = await connect(token, { name: room_name, audio: true, video: true });
  // …attach tracks and render your own grid.
}

The token is a Twilio Video AccessToken JWT — pass it straight to connect(token, { name: room_name }) and render participants however you like.

Starting & ending a call (host)

Providers start, refresh, and end the call from authed endpoints (an org API key, or a dashboard/provider session via Authorization: Bearer + X-Cadence-Org):

http
# Provider/host (authed: org API key, or a dashboard/provider session).
POST /api/v1/bookings/{id}/video/start   -> { room_name, token, identity, join_url, start_at }
POST /api/v1/bookings/{id}/video/token   -> { room_name, token, identity }
GET  /api/v1/bookings/{id}/video         -> { room_name, started_at, ended_at, live }
POST /api/v1/bookings/{id}/video/end     -> status

start creates the room and returns a host token plus a join_url you can share. Once started, the patient's keyless endpoints begin returning live tokens.

The patient portal

Patients don't need an account or a per-booking link to manage care: the passwordless patient portal at /{org}/portal signs them in with an emailed magic link, then lists their upcoming and past appointments. Video visits show a prominent Join video call button the moment the provider starts the call, and patients can reschedule, cancel, or book a new appointment — all in their org's timezone.

Link to it

text
https://cadence.abhix-ai.com/your-org/portal

The portal session is a patient-scoped token (it carries the org and the customer), held in the browser — it is never an org secret. Authed portal calls need only an Authorization: Bearer header.

HIPAA & your Twilio account

You bring your own Twilio BAA

HIPAA eligibility for video requires your own signed Twilio Business Associate Agreement (BAA) and a Twilio account on the Security or Enterprise edition with HIPAA enabled. Cadence orchestrates rooms and tokens against your Twilio account; it does not provide a BAA on your behalf. Confirm your Twilio plan and BAA are in place before treating video visits as HIPAA-eligible.