Two custom elements let you drop the full Natzar patient experience into your own pages instead of building chat/video UIs yourself:
<natzar-async> — the async-consult chat: consent prompt, message
thread, attachments, rating sheet. The composer is live as soon as the
patient consents — even while the consult is still queued waiting for
a physician, the patient can send context (symptoms, photos) and it
persists to the transcript as the physician's intake.<natzar-telehealth> — the live video consult: waiting room, camera
check, the call itself, rating.Two lines per widget. Load the embed script once, then render the tag with a session token your server minted:
<script src="https://us.app.natzar.ai/embed/v1/embed.js"></script>
<natzar-telehealth session-token="…" locale="en" theme="auto"></natzar-telehealth>
<script src="https://us.app.natzar.ai/embed/v1/embed.js"></script>
<natzar-async session-token="…" locale="en" theme="auto"></natzar-async>
The script and the widget iframe are served by the Natzar app origin for the
environment your API key belongs to. Load the script from the origin matching
the key that minted the token — a pp_test_… token will not open in the
production widget.
| Environment | Embed origin |
|---|---|
| prod | https://us.app.natzar.ai |
| stage | https://stage.us.app.natzar.ai |
| dev | https://dev.us.app.natzar.ai |
@natzar/client carries the same table, so you can build the URL instead of
hardcoding it:
import {resolveEndpoints} from '@natzar/client';
const {embedOrigin} = resolveEndpoints({environment: 'prod'});
const src = `${embedOrigin}/embed/v1/embed.js`;
Getting the token (server-side, with your API key — never in the browser):
POST /v1/telehealth-consults/{id}/embed-sessionPOST /v1/async-consults/{id}/embed-session, or take the
sessionToken returned directly by POST /v1/async-consults when you
create the consult with consent: "embed".The response carries {sessionToken, expiresAt}. Tokens are
consult-scoped (they open exactly one consult) and short-lived
(1 hour by default; ttlSeconds 60–86400 on the mint request). The
token's kind must match the tag — a telehealth token in <natzar-async>
refuses to load.
getUserMedia — browsers only grant camera and
microphone access in a secure context. A plain-HTTP host page cannot run
the video widget (localhost is exempt for development).allowedOrigins
list and refuses to load from anywhere else (origin_not_allowed,
surfaced as a natzar:error event). Register every origin you embed
from — scheme + host + port, e.g. https://portal.your-clinic.com.
The check covers the widget's ancestor origins; note that tokens being
consult-scoped and short-lived is the primary containment — treat the
allowlist as defense in depth, not as a substitute for keeping tokens
out of untrusted pages.The embed script sets allow="camera; microphone; display-capture" on the
iframe it creates, so no host-page changes are usually needed — the
browser prompts the patient inside your page and the stream stays in the
iframe.
The exception: if your embedding page is itself inside an iframe (your
page is nested in yet another product), every ancestor frame must also
delegate camera/microphone via its own allow attribute — permissions
policy delegation is per-hop. Same if your site sends a restrictive
Permissions-Policy response header: it must not deny camera /
microphone for the embed to work.
All attributes are observed — changing one re-renders the widget (changing
session-token is equivalent to calling refresh).
| Attribute | Required | Values | Meaning |
|---|---|---|---|
session-token |
yes | JWT from /embed-session |
Which consult the widget shows. Mint server-side only. |
locale |
no | en es de fr it |
UI language. Default: browser language, falling back to en. |
theme |
no | light dark auto |
Color scheme. Default auto (follows prefers-color-scheme). |
| Member | Meaning |
|---|---|
refresh(sessionToken) |
Swap in a freshly minted token without tearing down the widget — the intended reaction to natzar:expired. In-flight calls and a live video call continue uninterrupted. |
The widget dispatches DOM CustomEvents on the element, named
natzar:<event>; the payload is in event.detail (shapes:
EmbedEventDetailMap).
const el = document.querySelector('natzar-async');
el.addEventListener('natzar:message', (e) => console.log(e.detail.message));
el.addEventListener('natzar:expired', async () => el.refresh(await mintToken()));
| Event | Surface | detail |
When |
|---|---|---|---|
natzar:ready |
both | {type, consultId} |
Widget loaded, verified its token, rendered. |
natzar:consented |
async | {consultId} |
Patient accepted the consent prompt. |
natzar:queued |
both | {consultId} |
Waiting for a physician (async: no capacity yet — the chat still accepts messages, which persist for the physician; telehealth: waiting room). |
natzar:assigned |
both | {consultId, physicianName?} |
A physician took the consult. |
natzar:message |
async | {consultId, message} |
A new message appeared on the thread (any author) — useful for host-page unread badges. |
natzar:joined |
telehealth | {consultId} |
The patient entered the live call. |
natzar:ended |
telehealth | {consultId} |
The call ended (rating step may follow). |
natzar:rated |
both | {consultId, stars?} |
The patient submitted a rating. |
natzar:closed |
both | {consultId, closedReason?} |
Terminal state (a declined consent surfaces here with closedReason: 'declined'). |
natzar:expired |
both | {consultId} |
The session token expired; the widget freezes read-only until you refresh(token). |
natzar:error |
both | {code?, message} |
Something went wrong (invalid token, disallowed origin, network…). |
Plan for sessions longer than one token lifetime (a patient can sit in a chat or waiting room for a while):
el.addEventListener('natzar:expired', async ({detail}) => {
// Your endpoint calls POST /v1/…/{consultId}/embed-session with YOUR key.
const {sessionToken} = await fetch(`/api/natzar-token?consult=${detail.consultId}`)
.then((r) => r.json());
el.refresh(sessionToken);
});
Rotating your API key coarsely invalidates outstanding embed tokens (see
Authentication) — the same natzar:expired /
refresh path covers that too.
If you'd rather manage the iframe yourself, the underlying protocol is
window.postMessage: the iframe posts envelopes
{source: 'natzar-embed', event, data} ( EmbedPostMessage) to its
parent. Always check source === 'natzar-embed' AND the message origin
before trusting one. The custom elements are exactly this plumbing plus
origin checks and the natzar:-prefixed re-dispatch — prefer them.