Building on Lithos · Part 5 of 6

Building on Lithos, Part 5: wiring clinical events into Slack, your CRM and your support desk

Once your product reacts to clinical events, the next request comes from operations: tell support when an order stalls, tell growth when a care plan goes active, tell the team when a provider needs more information. Part 5 of the integration series is about fanning those events out without spraying patient data across your tools.

TL;DR

Send every telehealth webhook to one receiver you own, then fan out to Slack, your CRM and your help desk with a data rule per tool. Slack gets event type, internal IDs, status and an admin link, never clinical detail. A CRM without a BAA gets a neutral lifecycle stage. A help desk under a BAA can hold what an agent needs. Lithos webhooks are signed, at-least-once and unordered, so verify, dedupe on the event id, fetch the resource and act on its current status.

One receiver, then a data rule per tool

Route every Lithos webhook to one receiver you own, then send each tool only the fields it may hold. Slack and a CRM without a BAA get IDs, status and a link to your admin, never clinical detail; a help desk under a BAA gets what an agent needs to answer.

The volume makes the case. At 800 encounters a month you’ll receive 4,000 to 12,000 events, since each encounter throws off five to fifteen. Ops wants a few dozen of them.

This is Part 5 of Building on Lithos. Part 3 on event-driven integration built the receiver; this part is what you do with it.

Lithossigned webhooksYour receiververify · dedupe · fetchQueueminimize per ruleSlack channelIDs · status · admin linkCRMneutral lifecycle stageHelp deskBAA · order status, trackingData warehouseBAA · full snapshots↑ outside the PHI boundary↓ under a BAA, access-controlledone endpoint per environmentsigning secret and PHI rules live in code you review2xx as soon as the event is stored; fan out from the queuetools never subscribe to Lithos directly · adding a destination is a config change
One receiver you own verifies, dedupes and fetches each event, then a per-destination filter decides what each tool may see. Slack and most CRMs sit outside the PHI boundary and get IDs, status and a link back to your admin; tools under a BAA can hold more.

The receiver’s job is short: check the X-Lithos-Signature header, store the event id, return a 2xx and queue the work. A rule per destination on the queue side decides what to send.

Route the events where a person can still act

Lithos sends events for encounters, care plans, orders, appointments, inquiries and lab requisitions. Most are good news nobody needs to hear. Route the ones where someone can still change the outcome:

  • encounter.escalated: the provider paused for more from the patient (patient_information_required). There’s no approval event; sign-off is encounter.completed.
  • care_plan.refill_due: a refill date passed. The renewal is a follow-up encounter on the same care plan.
  • order.canceled, and orders stuck in processing. order.placed means the pharmacy accepted it; tracking appears when the pharmacy reports it.
  • appointment.no_show: a missed live visit.
  • inquiry.created: a patient or care-team message.

Keep PHI out of Slack and your CRM

The webhook payload is thin: an event id, a type and the resource id. PHI leaks later, when your receiver copies fields into tools outside your PHI boundary. Write the rules down per destination.

DestinationDefault postureMay includeKeep out unless under a BAA and configured for PHI
Slack or TeamsOutside the PHI boundaryEvent type, internal IDs, status, state code, admin linkName, email, phone, treatment, intake answers, message text
CRM or marketing automationOutside unless under a BAALifecycle stage, timestamps, internal ID, consent-safe flagsTreatment, care category, diagnosis, clinical notes
Help deskOften under a BAAOrder status, tracking, appointment time, ticket reasonIntake answers and notes beyond what the agent needs
Data warehouseUnder a BAA, access-controlledFull resource snapshotsCopies in unmanaged exports
Paging and on-callOutsideCounts, error rates, endpoint healthAnything patient-level

Two traps. Category is PHI: a CRM contact tagged “weight management: active” discloses health information, so use a neutral “member: active”. HIPAA and your marketing stack covers the CRM side. Slack is durable: messages get searched, exported and screenshotted, so post a link to your logged admin tool instead of the detail.

Write the routing table as data

Event (after fetch)ConditionDestinationAction
encounter.escalated—Help deskTicket “patient info needed,” keyed on encounter ID
encounter.escalatedStill escalated after 24hSlack #care-opsInternal ID and admin link
encounter.completedOrders presentCRMStage “treatment started” (neutral label)
encounter.abandoned—CRMStage “intake incomplete” for re-engagement
care_plan.ineligible—CRMSuppress from treatment campaigns
care_plan.refill_dueNo follow-up in 3 daysHelp desk and lifecycle messagingOutreach task
order.canceled—Slack #fulfillment and help deskInternal ID; open ticket
order.processingNot placed after 48hSlack #fulfillmentStalled-order alert
appointment.no_show—Help deskReschedule task
inquiry.created—Help deskCare-team queue, not general support

The 24-hour, 48-hour and 3-day thresholds are assumptions to tune, not Lithos service levels. The ageing rules aren’t events: they’re timers your receiver starts on one event and clears on the next.

Send us the list of tools your ops team lives in, and we’ll draft this routing table, with a PHI column for each tool, alongside you on a call. Book a call.

Act on current status, not the event type

Lithos webhooks arrive at least once and in no guaranteed order.

Duplicates: store each event id under a unique constraint and skip repeats. Then make each downstream write idempotent, because your fan-out retries too: key tickets on resource ID plus reason and CRM updates on contact ID plus stage. See idempotency key for the pattern.

Ordering is the one that bites. order.completed can arrive before order.placed. If the event type drives state, the late placed drags a shipped order backward, and support tells a patient it’s “with the pharmacy.”

Event type drives stateorder.createdorder.processingorder.completedorder.placedYour state ends at “placed”. Wrong: the order already completed.Fetch, then read statusorder.createdorder.processingorder.completedorder.placedGET the order: status is completed, so the late event changes nothing.arrives last
Webhooks carry no ordering guarantee. If the event type drives your state, a late order.placed rolls a finished order backward; if you fetch the resource and act on its current status, the late event is a no-op.

So fetch the resource and act on its status. A minimal handler (field names abbreviated; check the API reference you get with sandbox access):

export async function handle(rawBody, headers) {
  verifySignature(rawBody, headers["x-lithos-signature"]); // throws on mismatch
  const event = JSON.parse(rawBody);

  if (!(await db.insertEventIdIfNew(event.id))) return; // duplicate delivery

  const resource = await lithos.get(resourcePath(event)); // fetch on receipt
  for (const rule of routes.filter(r => r.matches(event.type, resource))) {
    await queue.enqueue({
      destination: rule.destination,
      dedupeKey: `${rule.name}:${resource.id}`,
      payload: rule.minimize(resource), // per-destination PHI filter
    });
  }
}

Each rule’s minimize is the PHI table in code. For Slack it returns an internal order ID, the status and an admin URL, nothing more. In production, acknowledge once the event is stored and do the fetch from the queue, so a slow CRM never triggers retries.

Test five cases in the sandbox

Sandbox routes drive encounters, orders and inquiries through their statuses, and a test clock moves time for the timer rules.

  1. Escalate, then complete, one encounter. One ticket opens and closes.
  2. Deliver the same event twice. One Slack post.
  3. Deliver order.completed before order.placed. The CRM ends in the right stage.
  4. Advance past a refill date with no follow-up. The outreach task appears.
  5. Search every non-BAA destination for names, emails and treatments. Find none.

Alert on exceptions, summarize the rest

A noisy channel gets muted, and then everyone assumes someone else is watching. Give every routing row an owning team and delete rows nobody owns. Post exceptions and ageing; send counts as one morning summary with links to filtered admin views. If you’re sizing the team behind the tickets, see patient support for telehealth programs.

Keep the routing table as compliance documentation. When a tool joins, ask which PHI column it sits in and whether you have a BAA. Reviewers ask for it; answering the security questionnaire shows where it fits.

Lithos sends signed webhooks for every encounter, care plan, order, appointment and inquiry change, with thin payloads your receiver fetches, so you decide what each tool sees. If you’re running a live program, bring your current monthly volume: we’ll price it against our per-visit bands and plan the migration, event mapping included. Book a call or get sandbox access.

Frequently asked questions

Can we send telehealth webhook events to Slack?

Yes, if you strip them down first. Send the event type, your internal ID, the status and a link to your own admin tool. Names, treatments, intake answers and messages don’t belong in a Slack workspace that isn’t under a BAA and configured for PHI.

What patient data can go into a CRM from a telehealth program?

Without a BAA, send lifecycle stage, timestamps and internal IDs only. A treatment or care category tied to an identifiable person is PHI. Under a BAA a CRM can hold more, but keep it to what a workflow needs.

How do we handle duplicate or out-of-order telehealth webhooks?

Store each event id under a unique constraint and skip repeats. Fetch the resource on receipt and act on its current status, not the event type. Make each downstream write idempotent too, for example keying a ticket on order ID plus reason.

Which Lithos webhook events matter most to an operations team?

encounter.escalated, order.canceled, orders stuck in processing, care_plan.refill_due, appointment.no_show and inquiry.created. Those are the moments where someone on your team can still change the outcome.

Should Slack, the CRM and the help desk each subscribe to webhooks directly?

No. Register one endpoint per environment that you control, verify and dedupe there, and fan out from your own queue. That keeps the signing secret in one place and the PHI rules in code your team reviews.

Get started

Put a medical practice behind your product

Book a call and we'll scope your program together, or get free sandbox access and start building.

Book a call Get sandbox access

Free to startNo setup or platform fees