SOAPNoteAPI

Streaming & Webhooks

Webhook Integration

Receive real-time push notifications when notes are generated or fail, eliminating the need for polling.

Updated March 19, 2026

Webhooks push events to your server when SOAP notes complete or fail. Instead of polling GET /v1/audio/status in a loop, your endpoint receives the result the moment processing finishes. This is the recommended pattern for production audio integrations.

Warning: Webhook payloads contain Protected Health Information (PHI), including clinical note content. It is the responsibility of the implementer to ensure their webhook endpoint is HIPAA-compliant -- receiving data over HTTPS, storing it securely, and restricting access to authorized personnel. SOAPNoteAPI's BAA covers delivery to your endpoint; security of the endpoint itself is your responsibility.

When to use webhooks

  • Audio processing -- audio-to-note takes 10-60 seconds. Webhooks deliver the result instantly without polling.
  • High volume -- polling 100 concurrent notes means 100 polling loops. Webhooks scale to any volume with a single endpoint.
  • Backend integrations -- your server receives a POST, processes it, and stores the note. No client-side code needed.
Note: Webhooks are only needed for asynchronous flows (audio upload). The synchronous POST /v1/note and streaming POST /v1/stream/note endpoints return the note directly in the response.

Step 1: Configure your webhook URL

In your SOAPNoteAPI dashboard, go to Settings > Webhooks. Enter your HTTPS endpoint URL and enable webhooks. When you save, a signing secret (whsec_...) is generated and shown once -- copy it immediately.

Warning: The webhook signing secret is only shown once when first configured or rotated. Store it securely in your environment variables. If you lose it, rotate it from the dashboard.

Your webhook URL must use HTTPS. SOAPNoteAPI will POST JSON payloads to this URL with a 10-second timeout.

Step 2: Handle webhook events

SOAPNoteAPI sends two event types:

  • note.generated -- the note was successfully created. The data field includes the full SOAP note (subjective, objective, assessment, plan, transcript, billing_codes, patient_summary, expires_at). No separate GET request needed.
  • note.failed -- processing failed (e.g., audio too short, corrupted file, transcription error). The payload includes the note_id and status but no data field.
  • test -- a synthetic event sent when you click "Send test event" in the dashboard. Use it to verify your endpoint is reachable and signature verification works.

Event payload format

JSON
// note.generated event
{
  "event": "note.generated",
  "note_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "completed",
  "created_at": "2026-03-19T15:30:00.000Z",
  "data": {
    "noteId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "subjective": "Patient presents with a 3-day history of...",
    "objective": "Vital signs: BP 120/80, HR 72, Temp 98.6F...",
    "assessment": "1. Acute upper respiratory infection...",
    "plan": "1. Supportive care with rest and fluids...",
    "transcript": "The transcribed audio content...",
    "billing_codes": {
      "icd10": [{ "code": "J06.9", "description": "Acute upper respiratory infection" }],
      "cpt": [{ "code": "99213", "description": "Office visit, established patient" }]
    },
    "patient_summary": "You visited your doctor today for cold symptoms...",
    "expires_at": "2026-03-20T15:30:00.000Z"
  }
}

// note.failed event
{
  "event": "note.failed",
  "note_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "failed",
  "created_at": "2026-03-19T15:30:00.000Z"
}

Step 3: Verify webhook signatures

Every webhook includes two headers for signature verification: X-SoapNoteAPI-Signature (hex HMAC-SHA256) and X-SoapNoteAPI-Timestamp (Unix epoch seconds). Always verify signatures before processing events to prevent spoofed requests.

The signed content is: timestamp + "." + raw JSON payload. Compute the HMAC-SHA256 of this string using your webhook secret, then compare with the signature header using a timing-safe comparison.

Node.js verification

JavaScript
const crypto = require('crypto');

function verifyWebhook(payload, signature, timestamp, secret) {
  const signedContent = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedContent)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}

// In your Express/Next.js handler:
app.post('/webhooks/soapnoteapi', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-soapnoteapi-signature'];
  const ts = req.headers['x-soapnoteapi-timestamp'];
  const body = req.body.toString();

  if (!verifyWebhook(body, sig, ts, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(body);
  console.log('Received:', event.event, event.note_id);

  // Process asynchronously, return 200 quickly
  processEvent(event).catch(console.error);
  res.status(200).json({ received: true });
});

Python verification

Python
import hmac
import hashlib

def verify_webhook(payload: str, signature: str, timestamp: str, secret: str) -> bool:
    signed_content = f"{timestamp}.{payload}"
    expected = hmac.new(
        secret.encode(),
        signed_content.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask example:
@app.route('/webhooks/soapnoteapi', methods=['POST'])
def handle_webhook():
    sig = request.headers.get('X-SoapNoteAPI-Signature')
    ts = request.headers.get('X-SoapNoteAPI-Timestamp')
    payload = request.get_data(as_text=True)

    if not verify_webhook(payload, sig, ts, WEBHOOK_SECRET):
        return jsonify(error='Invalid signature'), 401

    event = request.get_json()
    if event['event'] == 'note.generated':
        save_note(event['note_id'], event['data'])
    elif event['event'] == 'note.failed':
        handle_failure(event['note_id'])

    return jsonify(received=True), 200

Testing webhooks

The SOAPNoteAPI dashboard has a built-in "Send test event" button that delivers a synthetic test event to your configured URL. Use this to verify your endpoint is reachable and your signature verification is working.

The test event has event type "test" and a synthetic note_id. Your handler should accept it (return 200) but can skip processing.

JSON
// Test event payload
{
  "event": "test",
  "note_id": "test_1710784200000",
  "status": "test",
  "created_at": "2026-03-18T15:30:00.000Z"
}
Tip: For local development, use a tool like ngrok to expose your localhost endpoint with a public HTTPS URL. Configure the ngrok URL as your webhook in the dashboard, then run test events.

Retry policy

Webhook delivery is best-effort. If your endpoint returns a non-2xx status or times out (10-second limit), the delivery is logged as failed. SOAPNoteAPI does not currently retry failed deliveries -- your endpoint should be reliable and return 200 quickly.

Note: Return 200 immediately and process the event asynchronously. If your handler takes too long (>10 seconds), the delivery will time out and be marked as failed even though your server received the payload.

Rotating your webhook secret

You can rotate your signing secret at any time from the dashboard (Settings > Webhooks > Rotate secret). The old secret is immediately invalidated. Update your server environment variable with the new secret before rotating to avoid rejecting valid events during the transition.

Need help? Contact support@soapnoteapi.com