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 payload
  • Signature-Input  — Specifies signature components
  • Signature  — 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

  1. Implement your webhook endpoint using the examples above
  2. Add signature verification for security
  3. Test with sample payloads before going live
  4. Contact Healthie to register your webhook
  5. Monitor webhook delivery and processing
  6. 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.

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.