Canvas Medical Plugins

How YourEra extends Canvas to integrate with the rest of our backend. Now org-aware under the Canvas-first architecture.

Canvas-first architecture update (2026-04-21).

Canvas plugins are becoming organization-aware: prescription_queue now respects Patient.managingOrganization for routing (tasks go to physicians with active physician_org_memberships for that org), and portal_invite pulls the org-branded template for welcome flows. We use Canvas Plugin Assistant (CPA) — Canvas Medical's own Claude Code plugin — for plugin development (installed; run export CPA_RUNNING=1 && export CPA_WORKSPACE_DIR=$(pwd) before starting Claude to activate). CPA provides deep SDK knowledge, scaffolding, test generation, and deploy commands. See Architecture Overview for how the plugin layer fits into Canvas-first.

Why we have plugins at all

Canvas Medical is YourEra's EMR of record for DTC post-migration. Canvas exposes two integration surfaces, and we use both:

Canvas Medical EMR the EMR of record Two surfaces FHIR API SDK Plugins intake-gateway creates FHIR Patient orchestrator updates encounters integration-service FHIR client library Inbound (FHIR API) our services call Canvas integration-service Rx webhook receiver notification-service email / SMS physicians in Canvas UI custom widgets Outbound (SDK Plugins) Canvas calls our services / UI
Canvas has two surfaces. FHIR API is how our services push data in. SDK plugins are how Canvas pushes events + UI out.

We use FHIR for CRUD operations (create a Patient, update an encounter, attach a note). It's HTTP with bearer auth, and every service that needs to read or write Canvas data uses the FHIR client library directly.

We use SDK plugins for anything reactive: hooking into Canvas events (e.g., a prescription was signed, a patient was created), or rendering custom UI inside Canvas itself (physician queue, patient portal widgets). Plugins are Python, written against Canvas's SDK, and installed with canvas install ..

Plugin inventory

Five plugins across four repos. Grouped by type:

Plugin Type Trigger What it does
prescription_webhook Webhook PRESCRIPTION_CREATED
PRESCRIPTION_UPDATED
POSTs signed-Rx data to api.hisera.com/api/webhooks/canvas/prescription. The critical trigger that kicks off fulfillment.
portal_invite Lifecycle PATIENT_CREATED Sends YourEra-branded welcome email via integration service. Sets Canvas User's portal email (required for Canvas messaging to work).
message_notify Webhook Canvas message events Notifies patients outside Canvas when Canvas messages are sent. Routes through notification-service.
prescription_queue In-Canvas UI Physician UI Canvas-side workflow for reviewing pending Rx's. Custom view for physicians to triage and sign.
canvas-portal-customization In-Canvas UI Patient portal page load Custom widgets on Canvas's patient portal landing page (welcome banner, quick actions, appointments, health tips). Note: our main patient portal is at patient.yourera.com; this is for users who land on Canvas's own portal.

Event flow master diagram

Every plugin in one picture: trigger on the left, plugin in the middle, destination on the right.

CANVAS MEDICAL Event / UI hook PRESCRIPTION_ CREATED / UPDATED PATIENT_CREATED Message event Physician UI Portal page load Plugin prescription_webhook fetch patient + med + Rx, POST portal_invite message_notify prescription_queue canvas-portal-customization Destination integration-service /api/webhooks/canvas/prescription integration-service /api/send-welcome-email + UpdateUser notification-service SMS / email to patient (renders inside Canvas UI) physician-facing queue view (renders inside Canvas UI) branded patient portal widgets inside Canvas Python SDK plugin YourEra service or in-Canvas render Webhook: plugin POSTs out to a YourEra service UI: plugin renders a view inside Canvas itself Lifecycle: plugin hooks a Canvas record lifecycle event
Canvas fires an event or renders a UI hook; our plugin handles it; destination is either a YourEra service (via HTTP) or a rendered Canvas view.

Critical path: prescription_webhook

This is the single most important plugin. It's the trigger that starts the entire fulfillment pipeline. If it doesn't fire, or fires with a bad payload, nothing downstream runs.

Physician Canvas prescription_webhook Integration Service Orchestrator 1. sign Rx emit event 2. PRESCRIPTION_CREATED 3. fetch patient + med + prescriber (full data back) build payload 4. POST webhook (signed) verify secret 5. kick off pipeline 7-step pipeline source=yourera-dtc 6. accepted 7. 200 OK 8. return control
Solid arrows = request. Dashed arrows = response. Everything to the right of the plugin is asynchronous from Canvas's perspective.

Webhook payload shape

Fields the plugin assembles and POSTs (see canvas-prescription-webhook-plugin/prescription_webhook/README.md):

  • event_type - PRESCRIPTION_CREATED or PRESCRIPTION_UPDATED
  • timestamp
  • patient_id and full patient block (name, DOB, gender)
  • medication_request_id and full medication_request (med, sig, quantity, days supply, refills)
  • prescriber (name, NPI)
  • context.note_id

Authentication is a static shared secret (WEBHOOK_SECRET) configured in Canvas per environment. The integration service rejects any payload whose header doesn't match.

What breaks this

  • Canvas OAuth creds missing in the plugin - the SDK can't fetch patient data. Plugin throws, no webhook fires.
  • WEBHOOK_SECRET mismatch between Canvas config and integration service env - plugin fires, but integration service returns 401 and pipeline never starts.
  • Integration service unreachable (deploy in progress, network) - plugin hits a timeout. Canvas doesn't retry. Patient is stuck in "signed but not fulfilled" until we reconcile manually.
  • Canvas SDK version mismatch - the CANVAS_MANIFEST.json declares which SDK version the plugin expects. If Canvas upgrades without us upgrading the plugin, the event shape can shift.

Individual plugin reference

prescription_webhook
Webhook
Repo: canvas-prescription-webhook-plugin/prescription_webhook/

The fulfillment trigger. Fires whenever a prescription is created or updated in Canvas and sends the full record to the integration service, which kicks off the orchestrator pipeline.

Triggers
PRESCRIPTION_CREATED, PRESCRIPTION_UPDATED
Destination
POST api.hisera.com/api/webhooks/canvas/prescription
Auth
WEBHOOK_SECRET header (static shared secret)
Failure mode
If the plugin throws or the destination 4xx/5xx's, Canvas doesn't retry. Manual reconciliation via admin portal.
portal_invite
Lifecycle
Repo: canvas-plugins/portal_invite/

Fixes a gap in Canvas: when a patient is created via FHIR (from intake), the email goes into the FHIR Patient.telecom array, but Canvas keeps a separate email field on the CanvasUser record for portal access. Without this plugin, that second field is blank and portal invites fail.

Trigger
PATIENT_CREATED
Actions
  1. Extract email from FHIR Patient.telecom
  2. POST welcome email request to integration service (/api/send-welcome-email)
  3. Set the email on Canvas User via UpdateUserEffect
Why not Canvas's invite
Canvas's built-in invite email would point patients at Canvas's portal. We use patient.yourera.com with OTP login instead.
message_notify
Webhook
Repo: canvas-plugins/message_notify/

Pushes a notification to the patient (outside Canvas) whenever a clinical message is sent to them inside Canvas. Otherwise the patient would only see the message if they happened to open our portal.

Trigger
Canvas messaging events
Destination
notification-service (SMS / email per patient preference)
prescription_queue
In-Canvas UI
Repo: canvas-plugins/prescription_queue/

Physician-facing queue inside Canvas: pending Rx's waiting for review, sorted by urgency. Lets providers triage and sign without leaving Canvas.

Trigger
Physician opens the queue view in Canvas
Destination
Renders inside Canvas (no outbound HTTP)
canvas-portal-customization
In-Canvas UI
Repo: canvas-portal-customization/

Customizes Canvas's patient portal landing with branded widgets: welcome banner, quick actions (schedule, message, refill), appointments, health tips, contact info. Most patients use patient.yourera.com, but anyone who lands on Canvas's native portal sees YourEra branding instead of Canvas defaults.

Trigger
Patient opens Canvas's portal landing page
Destination
Renders inside Canvas (no outbound HTTP)

Plugin deployment

Every plugin has a CANVAS_MANIFEST.json that declares its protocols, permissions, and SDK version. Deployment is a two-step process:

  1. Install the plugin code to a Canvas instance:
    cd <plugin-repo>
    canvas install .
  2. Configure secrets in Canvas Settings → Plugins. Each plugin declares what secrets it needs (e.g., prescription_webhook needs WEBHOOK_URL and WEBHOOK_SECRET).

Canvas credentials for install live in ~/.canvas/credentials.ini. Dev uses a different Canvas instance from prod; keep the credentials scoped.

Viewing logs

canvas logs --host <instance-name>

Related docs