API & webhooks guide for AI Scribe
This guide helps you integrate with Healthie's webhook system to receive real-time notifications when Zoom transcripts and recordings become available.
Transcript & Recording Events
Healthie sends webhook notifications for key Zoom meeting events, allowing you to:
- Get notified immediately when transcripts are available
- Track recording lifecycle (start/stop/transcript ready)
- Monitor meeting participation
- Build automated workflows around meeting content
What's available
appointment.transcript_available
Use for: Triggering your transcript processing workflows
When you'll receive:
- Real-time transcript capture completes during a meeting
- Cloud recording transcript becomes available (usually within hours after meeting ends)
- Always sent regardless of provider's AI Scribe settings
appointment.recording_started
Use for: Tracking which appointments have recordings
When you'll receive:
- Recording begins in a Zoom meeting
appointment.recording_stopped
Use for: Knowing when recording has finished (transcript may still be processing)
When you'll receive:
- Recording ends in a Zoom meeting
appointment.participant_joined
Use for: Tracking meeting attendance
When you'll receive:
- Someone joins the Zoom meeting
- Includes participant name and email in webhook data
Quick start integration
Step 1: Set up your endpoint
Create an endpoint that accepts POST requests:
// Node.js/Express example app.post('/healthie-webhooks', express.json(), (req, res) => { const { event, resource_type, resource_id, data } = req.body; console.log(`Received webhook: ${event} for ${resource_type} ${resource_id}`); // Process the webhook if (event === 'appointment.transcript_available') { handleTranscriptAvailable(resource_id, data); } // Always return 200 OK res.status(200).json({ received: true }); }); function handleTranscriptAvailable(appointmentId, appointmentData) { console.log(`Transcript ready for appointment ${appointmentId}`); // Your logic here: // - Fetch transcript content via Healthie API // - Process transcript data // - Update your systems } # Python/Flask example from flask import Flask, request, jsonify import hmac import hashlib app = Flask(__name__) @app.route('/healthie-webhooks', methods=['POST']) def handle_webhook(): data = request.get_json() event = data.get('event') resource_id = data.get('resource_id') print(f"Received webhook: {event} for appointment {resource_id}") if event == 'appointment.transcript_available': handle_transcript_available(resource_id, data.get('data')) return jsonify({'received': True}), 200 def handle_transcript_available(appointment_id, appointment_data): print(f"Transcript ready for appointment {appointment_id}") # Your processing logic here
Step 2: Register your webhook with Healthie
Use the Healthie GraphQL API to create and manage your webhooks. You'll need API access with appropriate permissions.
Create a webhook using GraphQL
mutation CreateWebhook($input: CreateWebhookInput!) { createWebhook(input: $input) { webhook { id url isEnabled webhookEvents { eventType } } } }
Input variables
{ "input": { "url": "<https://yourapi.com/healthie-webhooks>", "webhookEventsAttributes": [ {"eventType": "appointment.transcript_available"}, {"eventType": "appointment.recording_started"}, {"eventType": "appointment.recording_stopped"}, {"eventType": "appointment.participant_joined"} ] } }
Step 3: Handle payloads
All webhooks follow this structure:
{ "resource_id": 12345, "resource_id_type": "Appointment", "event_type": "appointment.transcript_available" }
Key fields
resource_id
— The ID of the affected resource (e.g., appointment ID)resource_id_type
— The type of resource (e.g., "Appointment", "User", "FormAnswerGroup")event_type
— The specific event that occurred
Important: Healthie uses "thin payloads" - webhooks contain minimal data. After receiving a webhook, fetch the complete resource data using the Healthie GraphQL API.
Security: Webhook signature verification
⚠️ Important: Always verify webhook signatures to ensure requests are from Healthie.
Healthie uses HTTP Message Signatures (RFC 9421) with the following headers:
Content-Digest
— SHA-256 hash of the payloadSignature-Input
— Specifies signature componentsSignature
— The cryptographic signature
IP Whitelisting
Configure your firewall to only accept webhooks from Healthie's IPs:
- Production:
52.4.158.130
,3.216.152.234
,54.243.233.84
,50.19.211.21
- Staging:
18.206.70.225
,44.195.8.253
Signature verification example
// Node.js signature verification using Web Crypto API const crypto = require('crypto'); async function verifyWebhookSignature(request) { const { method, url, headers, body } = request; // Extract signature headers const contentDigest = headers['content-digest']; const signatureInput = headers['signature-input']; const signature = headers['signature']; if (!contentDigest || !signatureInput || !signature) { throw new Error('Missing required signature headers'); } // Verify content digest const bodyHash = crypto.createHash('sha256').update(body).digest('base64'); const expectedContentDigest = `sha-256=:${bodyHash}:`; if (contentDigest !== expectedContentDigest) { throw new Error('Content digest mismatch'); } // Build signature string const parsedUrl = new URL(url); const signatureString = [ `"@method": ${method}`, `"@path": ${parsedUrl.pathname}`, `"@query": ?${parsedUrl.search || ''}`, `"content-digest": ${contentDigest}`, `"content-type": ${headers['content-type']}`, `"content-length": ${headers['content-length']}` ].join('\\\\n'); // Verify signature using your webhook secret const expectedSignature = crypto .createHmac('sha256', process.env.HEALTHIE_WEBHOOK_SECRET) .update(signatureString) .digest('hex'); // Compare signatures const receivedSignature = signature.replace(/^[^=]+=:/, '').replace(/:$/, ''); return expectedSignature === receivedSignature; } // In your webhook handler: app.post('/healthie-webhooks', express.raw({type: 'application/json'}), async (req, res) => { try { const isValid = await verifyWebhookSignature({ method: req.method, url: req.originalUrl, headers: req.headers, body: req.body }); if (!isValid) { return res.status(401).send('Invalid signature'); } // Process webhook... const webhook = JSON.parse(req.body); await processWebhook(webhook); res.status(200).send('OK'); } catch (error) { console.error('Webhook verification failed:', error); res.status(401).send('Verification failed'); } });
Common integration patterns
Pattern 1: Transcript processing pipeline
// GraphQL query to fetch appointment details after webhook const GET_APPOINTMENT_QUERY = ` query GetAppointment($id: ID!) { appointment(id: $id) { id date otherParty { id firstName lastName } user { id firstName lastName } zoomMeetingId isGroup # Add other fields you need } } `; async function handleTranscriptAvailable(resourceId) { try { // 1. Fetch complete appointment data from Healthie API const appointmentData = await healthieGraphQL({ query: GET_APPOINTMENT_QUERY, variables: { id: resourceId } }); // 2. Fetch the actual transcript content const transcript = await fetchTranscriptContent(resourceId); // 3. Process the transcript (AI analysis, search indexing, etc.) const analysis = await processTranscript(transcript, appointmentData.appointment); // 4. Store results in your system await storeTranscriptAnalysis(resourceId, analysis); // 5. Notify relevant users/systems await notifyStakeholders(resourceId, analysis); console.log(`Successfully processed transcript for appointment ${resourceId}`); } catch (error) { console.error(`Failed to process transcript for appointment ${resourceId}:`, error); // Implement retry logic or alert mechanisms } }
Pattern 2: Recording lifecycle tracking
const recordingStates = new Map(); function handleWebhookEvent(event, appointmentId, data) { const state = recordingStates.get(appointmentId) || {}; switch (event) { case 'appointment.recording_started': state.recordingStarted = new Date(); state.status = 'recording'; console.log(`Recording started for appointment ${appointmentId}`); break; case 'appointment.recording_stopped': state.recordingStopped = new Date(); state.status = 'processing'; state.duration = state.recordingStopped - state.recordingStarted; console.log(`Recording stopped for appointment ${appointmentId}, duration: ${state.duration}ms`); break; case 'appointment.transcript_available': state.transcriptAvailable = new Date(); state.status = 'complete'; state.processingTime = state.transcriptAvailable - state.recordingStopped; console.log(`Transcript available for appointment ${appointmentId}, processing took: ${state.processingTime}ms`); // Trigger your transcript processing handleTranscriptAvailable(appointmentId, data); break; } recordingStates.set(appointmentId, state); }
Pattern 3: Real-time Meeting Monitoring
function handleParticipantJoined(appointmentId, extraData) { console.log(`Participant joined appointment ${appointmentId}:`); console.log(`- Name: ${extraData.user_name}`); console.log(`- Email: ${extraData.user_email}`); console.log(`- Host ID: ${extraData.host_user_id}`); // Update attendance tracking updateAttendanceRecord(appointmentId, { participantName: extraData.user_name, participantEmail: extraData.user_email, joinedAt: new Date() }); // Send real-time notifications to other systems notifyMeetingStart(appointmentId, extraData); }
Error handling & reliability
Retry logic
Healthie automatically retries failed webhook deliveries for up to 3 days using exponential backoff. Webhooks are automatically disabled after continuous failures.
Your endpoint should:
- Return
200 OK
for successful processing - Return
4xx
errors for permanent failures (won't retry) - Return
5xx
errors for temporary failures (will retry)
Idempotency
The same webhook may be delivered multiple times. Make your processing idempotent:
const processedWebhooks = new Set(); function handleWebhook(event, resourceId, data) { const webhookId = `${event}-${resourceId}-${data.updated_at || data.created_at}`; if (processedWebhooks.has(webhookId)) { console.log(`Duplicate webhook ${webhookId}, skipping`); return; } // Process webhook processWebhookEvent(event, resourceId, data); // Mark as processed processedWebhooks.add(webhookId); }
Error Monitoring
function handleWebhook(req, res) { try { const { event, resource_id, data } = req.body; // Process webhook processWebhookEvent(event, resource_id, data); // Log success console.log(`Successfully processed ${event} for resource ${resource_id}`); res.status(200).json({ received: true }); } catch (error) { // Log error with context console.error('Webhook processing failed:', { event: req.body.event, resourceId: req.body.resource_id, error: error.message, stack: error.stack }); // Return 500 to trigger retry res.status(500).json({ error: 'Processing failed' }); } }
Testing Your Integration
1. Use webhook testing tools
Tools like ngrok can expose your local development server for webhook testing:
# Install ngrok npm install -g ngrok # Expose your local server ngrok http 3000 # Use the ngrok URL for webhook registration # <https://abc123.ngrok.io/healthie-webhooks>
2. Test with sample payloads
Create test handlers with sample webhook payloads:
// Test your webhook handler with sample data const sampleWebhook = { event: 'appointment.transcript_available', resource_type: 'Appointment', resource_id: 12345, data: { id: 12345, date: '2025-01-15T10:00:00Z', other_party_id: 67890, user_id: 54321 } }; // Test your handler handleWebhook(sampleWebhook.event, sampleWebhook.resource_id, sampleWebhook.data);
3. Monitor Webhook Deliveries
Keep logs of webhook processing for debugging:
const webhookLog = []; function logWebhook(event, resourceId, status, error = null) { webhookLog.push({ timestamp: new Date(), event, resourceId, status, // 'success', 'error', 'duplicate' error: error?.message }); // Keep only recent logs if (webhookLog.length > 1000) { webhookLog.shift(); } }
Troubleshooting
Common Issues
Webhooks not arriving
- Check your endpoint URL is publicly accessible
- Verify SSL certificate is valid
- Ensure your endpoint returns
200 OK
- Contact Healthie support to verify webhook registration
Signature verification failing
- Ensure you're using the correct webhook secret
- Verify you're hashing the raw request body (not parsed JSON)
- Check for encoding issues (UTF-8)
Duplicate webhooks
- Implement idempotency using resource ID + timestamp
- Don't treat duplicates as errors
Missing transcript content
- Webhook only notifies that transcript is available
- Use Healthie API to fetch actual transcript content
- Some cloud recordings may take hours to process
Getting Help
Contact your Healthie integration team with
- Your webhook endpoint URL
- Example webhook payloads you're receiving
- Error logs or specific issues you're experiencing
- Your expected integration timeline
Include in support requests
- Appointment IDs where you expected webhooks but didn't receive them
- Timestamp ranges for webhook issues
- Your webhook processing logs
Next steps
- Implement your webhook endpoint using the examples above
- Add signature verification for security
- Test with sample payloads before going live
- Contact Healthie to register your webhook
- Monitor webhook delivery and processing
- Build your transcript processing pipeline
With webhooks in place, you'll receive real-time notifications whenever transcripts become available, enabling you to build powerful automated workflows around Healthie's meeting content.