Launch links
Generate encrypted launch URLs with stable identifiers.
Launch
Understand the end-to-end flow from encrypted browser launch URL to ChartHero's launch-context callback.
v1.0Your system
Mint the launch link
Your backend mints an encrypted launch URL carrying an opaque launch_state and optional actor evidence.
ChartHero
Validate the link
ChartHero decrypts ctx and checks issuer, audience, expiry, key id, and replay before any patient opens.
ChartHero
Call your adapter back
ChartHero calls POST /v1/launch/context with your launch_state and the resolved actor.
Your system
Answer in ChartHero’s schema
You return exactly one subject: patient, or encounter with required encounter.patient, plus optional setup_defaults.
A launch is a four-step round trip between your system and ChartHero. You mint an encrypted link, ChartHero validates it, ChartHero calls you back for the real context, and you answer in ChartHero's schema.
Everything ChartHero needs to open a patient lives in your answer to step 4, not in the link. The link carries an opaque, adapter-owned handle plus optional proof of who is launching — and the whole payload is encrypted, so nothing readable is ever exposed in the browser or in URLs. You stay in control of what context a launch resolves to.
Your backend (or a trusted link issuer) builds an encrypted launch URL. It has one
moving part: a compact JWE in the ctx query parameter.
https://app.charthero.ai/launch/{launch_alias}?ctx=<compact-jwe>
Inside the encrypted payload you put two things that matter: an opaque
launch_state, and — optionally — actor evidence for who is opening the link.
const payload = {
iss: 'acme-care-platform',
aud: 'charthero',
iat: now,
exp: now + 300, // links live at most 5 minutes
jti: '01JZ123456789ABCDEFGHJKLM', // unique per launch; comes back as launch_id
// Opaque handle you control — see "Your launch_state is a scratchpad" below
launch_state: {
appointment_id: 'apt-789',
correlation_id: 'partner-click-123',
},
// Optional — see "Actor validation works both ways" below
actor: {
email: 'clinician@example.com',
},
};
launch_state is yours — put whatever your adapter needs to rebuild the context
when ChartHero calls you back. It travels encrypted inside the launch link and
ChartHero never interprets it, so you can carry the real values your adapter needs.
Two practical limits: keep it within the size cap (~4 KB — it rides inside the
encrypted URL), and avoid long-lived secrets or credentials.
launch_state is yours. ChartHero never reads or interprets it — it stores it,
then hands it back to you verbatim in step 3.
Treat it as the note you leave yourself between when a user clicks launch and when ChartHero calls you back. Put whatever you'll need to reconstruct the right context: an appointment row id, an internal encounter key, a correlation id. When the callback arrives, you resolve that handle against your own system — applying your own authorization and workflow rules — and build the response.
ChartHero never acts on launch_state — so anything ChartHero must actually open
(the patient, demographics, setup) still has to come back in your step-4 response.
That's why a compact handle like { "appointment_id": "apt-789" } is usually all
you need: you're just leaving yourself enough to look the rest up. Carrying more is
fine when it saves you a round trip; it simply won't change what ChartHero opens.
Building a basic patient + appointment adapter workflow? You can skip this entirely. Send no actor
and run provider linking disabled. Actor evidence and provider linking are
conditional — add them only when your authorization model needs ChartHero to verify
or map the launching user. The rest of this section is for when you do.
The optional actor block is evidence about who should be opening this link.
You can identify them three ways, alone or in combination:
| You send | ChartHero matches it against |
|---|---|
actor.actor_id |
The ChartHero public user id |
actor.email |
A verified ChartHero identity (case-insensitive) |
actor.external_ids[] |
The user's mapped provider linkage |
In practice, email or provider external_ids[] is the normal partner path — you
usually know those at click time. actor.actor_id is only useful when you already
hold the ChartHero public user id, which most partners won't.
Matching is strict: if you send more than one criterion, every criterion must match the authenticated ChartHero user, or the launch is rejected before any patient context opens. This is how you assert "this link is for Dr. Smith" and have ChartHero enforce it for you.
In disabled and optional modes you don't have to send any of it. If you omit
actor, ChartHero still resolves the authenticated user and sends the enriched
actor back to you in step 3 — so you can run your own validation on your side if you
prefer to own that decision. Either way, you learn who opened the link.
If provider linking is required, this is the exception: you must include at
least one actor criterion (actor_id, email, or external_ids[]) in the payload —
ChartHero rejects the launch otherwise — and it additionally requires a durable
provider mapping before it ever calls you. If provider linking is disabled, omit
external_ids[]. See Provider Linking.
When the browser hits the launch URL, ChartHero — not your server — does the
gatekeeping. It resolves the launch alias to your installation, decrypts ctx, and
checks iss, aud, iat, exp, kid, and replays the jti. Expired or replayed
links are rejected here, before your adapter is ever called.
Once the link is valid, ChartHero calls your adapter with the opaque state and the resolved actor:
POST {base_url}/v1/launch/context
{
"launch_state": { "appointment_id": "apt-789", "correlation_id": "partner-click-123" },
"actor": {
"actor_type": "user",
"actor_id": "11111111-1111-4111-8111-111111111111",
"email": "clinician@example.com",
"display_name": "Dr. Chris Smith",
"active": true,
"external_ids": [{ "source": "provider_acme_care_prod", "type": "provider_id", "value": "prov-456" }]
},
"launch_id": "01JZ123456789ABCDEFGHJKLM"
}
launch_state is your scratchpad from step 1, handed back untouched. actor is the
enriched ChartHero user (not the raw evidence you sent) — present whenever an
authenticated user is resolved, even if you sent no actor evidence.
launch_id is the same value you minted as jti in step 1. It's named jti
inside the encrypted token (the standard JWT claim) and surfaced as launch_id in
this callback body. Use it for idempotency and replay-safe logging.
You resolve launch_state in your system and return the context ChartHero should
open. This is the authoritative context check — ChartHero opens exactly what you
return here.
This holds even in the simplest case, where you already have the patient and appointment IDs at click time: they still must come back in this response. ChartHero opens what the response returns, never what was in the link — so returning the IDs here isn't redundant, it's the only thing that opens a patient.
{
"encounter": {
"external_ids": [{ "source": "acme_care_prod", "type": "appointment_id", "value": "apt-789" }],
"patient": {
"external_ids": [{ "source": "acme_care_prod", "type": "patient_id", "value": "pat-123" }]
},
"start_at": "2026-06-01T14:00:00Z"
},
"setup_defaults": {
"mode": "phone_call",
"template_id": "0000...0013",
"phone_call": { "to_e164": "+15551234567" }
}
}
Three parts, and the relationships between them are the point:
encounter
for an encounter/appointment launch, or patient for a patient-only launch.
A patient-only response creates a new ChartHero encounter without an external
encounter reference.encounter launches include encounter.patient. When you return encounter
context, the embedded patient is required and asserts that the patient and
appointment belong together. Your adapter must verify that relationship before
returning success; if the launch state points to a different pairing, return
409 identifier_mismatch rather than open the wrong patient/appointment.setup_defaults (optional). Configures how the ChartHero workflow opens —
the mode, the template_id, the phone-call target. This is your hook to land the
user in the right UI state instead of a blank workflow. Omit it to let ChartHero
use its own defaults.This is the full appointment path: launching with an appointment means resolving it
from launch_state and returning it as the encounter here. Returning an
appointment ChartHero can query directly (appointment search) is a separate
capability that is not yet self-service enabled — see Configuration.
Repeat launches don't fragment your data. ChartHero links the encounter by the exact configured encounter identifier you return. If the same appointment is launched again later, ChartHero reuses the existing encounter rather than creating a new one — so a user clicking the same appointment's link on Monday and again on Wednesday lands in one encounter, not two.
If you can't resolve the launch, deny it with a stable error: 404 patient_not_found,
404 appointment_not_found, 403 permission_denied, or 409 identifier_mismatch.
Can launch_state contain anything?
Yes — it's an opaque JSON object you own that ChartHero never interprets, so you can
carry whatever your adapter needs to rebuild context, including real values. It
travels encrypted inside the launch link. Just keep it within the ~4 KB size cap and
skip long-lived secrets.
Do I have to tell ChartHero who's launching?
No. Sending actor lets ChartHero enforce identity for you. If you skip it,
ChartHero still returns the resolved actor in step 3 so you can validate yourself.
How long is a launch link valid?
At most five minutes (exp - iat ≤ 300s), single-use — the jti is replay-protected.
Where do patient demographics, encounter times, and setup go?
In your step-4 response — that's the only context ChartHero acts on. You can also
keep copies in launch_state for your own round-trip use, but ChartHero won't read
them.
What configures the ChartHero UI on open?
setup_defaults in the step-4 response (mode, template_id, phone_call).
Generate encrypted launch URLs with stable identifiers.
Implement the callback ChartHero calls after launch validation.
Review exact identifier matching before linkage work.