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 OKfor successful processing - Return
4xxerrors for permanent failures (won't retry) - Return
5xxerrors 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.