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.
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.
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 isencounter.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 inprocessing.order.placedmeans 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.
| Destination | Default posture | May include | Keep out unless under a BAA and configured for PHI |
|---|---|---|---|
| Slack or Teams | Outside the PHI boundary | Event type, internal IDs, status, state code, admin link | Name, email, phone, treatment, intake answers, message text |
| CRM or marketing automation | Outside unless under a BAA | Lifecycle stage, timestamps, internal ID, consent-safe flags | Treatment, care category, diagnosis, clinical notes |
| Help desk | Often under a BAA | Order status, tracking, appointment time, ticket reason | Intake answers and notes beyond what the agent needs |
| Data warehouse | Under a BAA, access-controlled | Full resource snapshots | Copies in unmanaged exports |
| Paging and on-call | Outside | Counts, error rates, endpoint health | Anything 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) | Condition | Destination | Action |
|---|---|---|---|
encounter.escalated | — | Help desk | Ticket “patient info needed,” keyed on encounter ID |
encounter.escalated | Still escalated after 24h | Slack #care-ops | Internal ID and admin link |
encounter.completed | Orders present | CRM | Stage “treatment started” (neutral label) |
encounter.abandoned | — | CRM | Stage “intake incomplete” for re-engagement |
care_plan.ineligible | — | CRM | Suppress from treatment campaigns |
care_plan.refill_due | No follow-up in 3 days | Help desk and lifecycle messaging | Outreach task |
order.canceled | — | Slack #fulfillment and help desk | Internal ID; open ticket |
order.processing | Not placed after 48h | Slack #fulfillment | Stalled-order alert |
appointment.no_show | — | Help desk | Reschedule task |
inquiry.created | — | Help desk | Care-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.”
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.
- Escalate, then complete, one encounter. One ticket opens and closes.
- Deliver the same event twice. One Slack post.
- Deliver
order.completedbeforeorder.placed. The CRM ends in the right stage. - Advance past a refill date with no follow-up. The outreach task appears.
- 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 the Journal by email
Our best guides on building compliant telehealth programs — a couple a week, unsubscribe any time.
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.