Skillshub abridge-webhooks-events
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/abridge-webhooks-events" ~/.claude/skills/comeonoliver-skillshub-abridge-webhooks-events && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/abridge-webhooks-events/SKILL.mdsource content
Abridge Webhooks & Events
Overview
Handle Abridge webhook events for clinical documentation lifecycle: note completion, encounter status changes, quality alerts, and provider enrollment notifications. All webhook payloads are HIPAA-scoped (contain session IDs but no PHI).
Abridge Event Types
| Event | Trigger | Use Case |
|---|---|---|
| Note generation finished | Push note to EHR |
| Note generation failed | Alert clinical team |
| Clinician reviewed/edited note | Update EHR with final version |
| Clinician signed the note | Lock note in EHR |
| Patient summary generated | Push to patient portal |
| Provider onboarded | Update provider roster |
| Low confidence or missing content | Flag for clinical review |
Instructions
Step 1: Webhook Endpoint with Signature Verification
// src/webhooks/abridge-webhook-handler.ts import express from 'express'; import crypto from 'crypto'; const router = express.Router(); // CRITICAL: Use raw body for signature verification router.post('/webhooks/abridge', express.raw({ type: 'application/json' }), async (req, res) => { const signature = req.headers['x-abridge-signature'] as string; const timestamp = req.headers['x-abridge-timestamp'] as string; if (!verifySignature(req.body, signature, timestamp)) { return res.status(401).json({ error: 'Invalid signature' }); } const event = JSON.parse(req.body.toString()); // Idempotency check if (await isProcessed(event.event_id)) { return res.status(200).json({ status: 'already_processed' }); } // Process asynchronously — respond immediately processEvent(event).catch(err => console.error(`Event processing failed: ${event.event_id}`, err) ); res.status(200).json({ received: true }); } ); function verifySignature(payload: Buffer, signature: string, timestamp: string): boolean { const secret = process.env.ABRIDGE_WEBHOOK_SECRET!; const maxAge = 300000; // 5 minutes if (Date.now() - parseInt(timestamp) * 1000 > maxAge) { console.error('Webhook timestamp expired'); return false; } const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${payload.toString()}`) .digest('hex'); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); }
Step 2: Event Router
// src/webhooks/event-router.ts interface AbridgeEvent { event_id: string; type: string; timestamp: string; data: { session_id?: string; note_id?: string; provider_id?: string; status?: string; quality_score?: number; }; } type EventHandler = (data: AbridgeEvent['data']) => Promise<void>; const handlers: Record<string, EventHandler> = { 'encounter.session.completed': async (data) => { console.log(`Note ready for session ${data.session_id}`); // Fetch note and push to EHR await fetchAndPushNote(data.session_id!); }, 'encounter.session.failed': async (data) => { console.error(`Note generation failed: ${data.session_id} — ${data.status}`); // Alert clinical operations team await sendClinicalAlert(data.session_id!, 'Note generation failed'); }, 'encounter.note.signed': async (data) => { console.log(`Note signed: ${data.note_id}`); // Lock note in EHR — no further edits await lockNoteInEhr(data.note_id!); }, 'patient.summary.ready': async (data) => { console.log(`Patient summary ready: ${data.session_id}`); // Push to patient portal await pushSummaryToPortal(data.session_id!); }, 'quality.alert': async (data) => { console.warn(`Quality alert: session ${data.session_id}, score ${data.quality_score}`); // Flag for clinical review if score < 0.7 if ((data.quality_score || 0) < 0.7) { await flagForReview(data.session_id!); } }, }; async function processEvent(event: AbridgeEvent): Promise<void> { const handler = handlers[event.type]; if (!handler) { console.log(`Unhandled event: ${event.type}`); return; } await handler(event.data); await markProcessed(event.event_id); }
Step 3: Idempotency Store
// src/webhooks/idempotency.ts // Use Redis or database for production — in-memory for dev const processedEvents = new Map<string, number>(); async function isProcessed(eventId: string): Promise<boolean> { return processedEvents.has(eventId); } async function markProcessed(eventId: string): Promise<void> { processedEvents.set(eventId, Date.now()); // Clean up events older than 7 days const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; for (const [id, ts] of processedEvents) { if (ts < cutoff) processedEvents.delete(id); } }
Step 4: Webhook Registration
# Register webhook endpoint with Abridge curl -X POST "${ABRIDGE_BASE_URL}/webhooks" \ -H "Authorization: Bearer ${ABRIDGE_CLIENT_SECRET}" \ -H "X-Org-Id: ${ABRIDGE_ORG_ID}" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-service.run.app/webhooks/abridge", "events": [ "encounter.session.completed", "encounter.session.failed", "encounter.note.signed", "patient.summary.ready", "quality.alert" ], "secret": "your-webhook-secret" }'
Output
- Secure webhook endpoint with HMAC signature verification
- Event router with handlers for all clinical documentation events
- Idempotency store preventing duplicate processing
- Webhook registration via Abridge API
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Invalid signature | Wrong webhook secret | Verify secret matches Abridge config |
| Timestamp expired | Clock drift > 5 min | Sync server clock via NTP |
| Duplicate processing | Missing idempotency | Implement event ID tracking |
| Handler timeout | Slow EHR push | Process async; respond 200 immediately |
Resources
Next Steps
For performance optimization, see
abridge-performance-tuning.