Skip to main content

Webhooks

Webhooks allow your application to receive real-time notifications when async jobs change status. Instead of polling the Jobs API, you can configure a webhook endpoint to automatically receive updates when jobs are created, started, completed, or failed.

Overview

TranscriMed webhooks are HTTP POST requests sent to your configured endpoint whenever specific events occur. All webhook requests are signed with HMAC-SHA256 for security and include detailed payload information about the event.

Key Features

  • Real-time notifications for async job events
  • HMAC-SHA256 signed requests for security
  • Automatic retries with exponential backoff
  • Full medical record content in completed job webhooks
  • Delivery logs and monitoring in Developer Portal
  • Test endpoint functionality

Webhook Events

Configure which events you want to receive:

EventDescription
job.createdTriggered when an async job is created and queued
job.startedTriggered when a job begins processing
job.completedTriggered when a job finishes successfully
job.failedTriggered when a job fails with errors
job.cancelledTriggered when a job is cancelled

Setup

1. Configure Webhook Endpoint

In the Developer Portal, configure your webhook based on your client type:

For Test Clients (development/testing):

  1. Test Webhook URL: Your endpoint URL (HTTP localhost allowed, e.g., http://localhost:3000/webhooks/transcrimed)
  2. Events: Select which events to receive
  3. Generate Secret: Create a test webhook secret for signature verification
  4. Enable: Activate test webhook notifications

For Production Clients (live integration):

  1. Production Webhook URL: Your HTTPS endpoint (e.g., https://your-api.com/webhooks/transcrimed)
  2. Events: Select which events to receive
  3. Generate Secret: Create a production webhook secret for signature verification
  4. Enable: Activate production webhook notifications

2. Implement Webhook Endpoint

Your webhook endpoint should:

  • Accept HTTP POST requests
  • Verify HMAC signatures (recommended)
  • Respond with HTTP 200 status for successful processing
  • Process requests idempotently (same payload may be sent multiple times)

Example Implementation (Node.js):

const express = require('express');
const crypto = require('crypto');
const app = express();

// Middleware to capture raw body for signature verification
app.use('/webhooks/transcrimed', express.raw({ type: 'application/json' }));

function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

const receivedSignature = signature.replace('sha256=', '');

return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}

app.post('/webhooks/transcrimed', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const webhookSecret = process.env.TRANSCRIMED_WEBHOOK_SECRET;

// Verify signature
if (!verifyWebhookSignature(req.body, signature, webhookSecret)) {
return res.status(401).send('Invalid signature');
}

const payload = JSON.parse(req.body);

// Process webhook event
switch (payload.event) {
case 'test.ping':
// Test webhook from Developer Portal - just respond with 200
console.log('Test webhook received:', payload.message);
break;
case 'job.completed':
handleJobCompleted(payload);
break;
case 'job.failed':
handleJobFailed(payload);
break;
// Handle other events...
}

res.status(200).send('OK');
});

function handleJobCompleted(payload) {
const { job_id, external_reference_id } = payload;
const medicalRecord = payload.data.result?.medical_record;

if (medicalRecord) {
console.log(`Job ${job_id} completed:`, {
recordId: medicalRecord.id,
title: medicalRecord.title,
externalRef: external_reference_id
});

// Process the completed medical record
// The full HTML content is available in medicalRecord.content
}
}

Webhook Payload

Common Payload Structure

All webhook payloads follow this structure:

{
"event": "job.completed",
"job_id": "01234567-89ab-cdef-0123-456789abcdef",
"external_reference_id": "your-tracking-id",
"timestamp": "2024-01-15T10:35:00Z",
"data": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"type": "medical_record_generation",
"status": "completed",
"progress": 100,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:35:00Z",
"completed_at": "2024-01-15T10:35:00Z",
"external_reference_id": "your-tracking-id",
// Event-specific data...
}
}

Job Completed Payload

When a job completes successfully, the payload includes the full medical record:

{
"event": "job.completed",
"job_id": "01234567-89ab-cdef-0123-456789abcdef",
"external_reference_id": "patient-visit-123",
"timestamp": "2024-01-15T10:35:00Z",
"data": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"type": "medical_record_generation",
"status": "completed",
"progress": 100,
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:35:00Z",
"external_reference_id": "patient-visit-123",
"result": {
"medical_record": {
"id": "fedcba98-7654-3210-fedc-ba9876543210",
"user_id": "user-uuid",
"title": "Cardiology Consultation",
"content": "<html><body><h1>Medical Record</h1>...</body></html>",
"template_id": "cardiology-template-uuid",
"patient_id": "patient-123",
"created_at": "2024-01-15T10:35:00Z",
"updated_at": "2024-01-15T10:35:00Z",
"metadata": {
"transcript": "Patient reports chest pain...",
"original_transcript": "Patient reports chest pain...",
"normalized_transcript": "Patient reports chest pain...",
"processing_duration": 45000,
"request_id": "req_abc123",
"external_reference_id": "patient-visit-123"
}
},
"processing_info": {
"mode": "async",
"duration_ms": 45000,
"template_used": "cardiology-template-uuid",
"job_id": "01234567-89ab-cdef-0123-456789abcdef"
}
}
}
}

Job Failed Payload

When a job fails, the payload includes error details:

{
"event": "job.failed",
"job_id": "01234567-89ab-cdef-0123-456789abcdef",
"external_reference_id": "patient-visit-123",
"timestamp": "2024-01-15T10:35:00Z",
"data": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"type": "medical_record_generation",
"status": "failed",
"progress": 50,
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:35:00Z",
"external_reference_id": "patient-visit-123",
"error": {
"error": "Audio processing failed: Invalid audio format",
"timestamp": "2024-01-15T10:35:00Z",
"worker_id": "worker-abc123"
}
}
}

Security

HMAC Signature Verification

All webhook requests include an HMAC-SHA256 signature in the X-Webhook-Signature header:

X-Webhook-Signature: sha256=a8b7c6d5e4f3g2h1...

Always verify signatures to ensure requests are from TranscriMed:

import hmac
import hashlib

def verify_webhook_signature(payload_body, signature_header, webhook_secret):
"""Verify webhook signature"""
expected_signature = hmac.new(
webhook_secret.encode('utf-8'),
payload_body,
hashlib.sha256
).hexdigest()

received_signature = signature_header.replace('sha256=', '')

return hmac.compare_digest(expected_signature, received_signature)

Best Practices

  1. Always verify signatures before processing payloads
  2. Use HTTPS endpoints for webhook URLs
  3. Implement idempotency - same payload may be sent multiple times
  4. Respond quickly - process webhooks asynchronously if needed
  5. Return HTTP 200 for successful processing
  6. Log webhook events for debugging and monitoring
  7. Rotate webhook secrets regularly

Delivery & Retries

Delivery Mechanism

  • Webhooks are delivered via HTTP POST to your configured URL
  • Content-Type: application/json
  • User-Agent: TranscriMed-Webhook/1.0
  • Timeout: 30 seconds per request

Retry Logic

If your webhook endpoint doesn't respond with HTTP 200:

  1. Immediate retry after 1 second
  2. Second retry after 2 seconds
  3. Third retry after 4 seconds
  4. No retries for 4xx client errors (except 408, 429)

Headers

Each webhook request includes these headers:

Content-Type: application/json
X-Webhook-Signature: sha256=signature
X-Webhook-Event: job.completed
X-Webhook-Timestamp: 2024-01-15T10:35:00Z
User-Agent: TranscriMed-Webhook/1.0

Testing

Test vs Production Webhooks

TranscriMed maintains consistency between API testing and webhook testing:

  • Test Client Credentials: Use test webhook URLs for development and testing
  • Production Client Credentials: Use production webhook URLs for live integration

Testing with the Developer Portal

The webhook test button uses server-side testing with proper HMAC signatures:

  1. For Test Clients: Tests your configured test webhook URL with valid signatures
  2. For Production Clients: Tests your configured production webhook URL with valid signatures
  3. Security: All test requests include proper HMAC-SHA256 signatures using your configured secret
  4. Comprehensive Results: Returns detailed error messages and troubleshooting guidance

Local HTTPS Testing

For production webhook testing, you need HTTPS endpoints. Here are several approaches for local HTTPS testing:

ngrok creates secure tunnels to your localhost, perfect for webhook testing:

# Install ngrok
npm install -g ngrok
# or visit https://ngrok.com/download

# Start your local webhook server
node test-webhook-server.js

# In another terminal, create HTTPS tunnel
ngrok http 3000

# Use the HTTPS URL (e.g., https://abc123.ngrok.io) in Developer Portal

Benefits:

  • Real HTTPS with valid certificates
  • Public URL accessible from TranscriMed servers
  • Request inspection with ngrok web interface
  • Free tier available

Option 2: LocalTunnel

LocalTunnel provides similar tunneling functionality:

# Install localtunnel
npm install -g localtunnel

# Start your local webhook server
node test-webhook-server.js

# Create tunnel
lt --port 3000 --subdomain your-app-name

# Use the HTTPS URL in Developer Portal

Option 3: Local SSL with mkcert

mkcert creates locally-trusted development certificates:

# Install mkcert
brew install mkcert # macOS
# or visit https://github.com/FiloSottile/mkcert

# Create local CA
mkcert -install

# Generate certificates for localhost
mkcert localhost 127.0.0.1 ::1

# Use certificates in your server (see example below)

Example HTTPS server with mkcert:

const https = require('https');
const fs = require('fs');

const options = {
key: fs.readFileSync('localhost-key.pem'),
cert: fs.readFileSync('localhost.pem')
};

https.createServer(options, (req, res) => {
// Your webhook handler code
}).listen(3000, () => {
console.log('HTTPS Server running on https://localhost:3000');
});

Note: This approach only works for testing on the same machine, as the certificates aren't publicly accessible.

Option 4: Cloudflare Tunnel

Cloudflare Tunnel provides free secure tunneling:

# Install cloudflared
brew install cloudflared # macOS
# or visit https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/

# Start tunnel
cloudflared tunnel --url http://localhost:3000

# Use the provided HTTPS URL

Testing Webhook Signatures Locally

Enhanced Test Server

Use the enhanced transcrimed-api-test-server.js with HMAC verification:

# Run with signature verification
WEBHOOK_SECRET=your_webhook_secret node transcrimed-api-test-server.js

# The server will:
# Verify HMAC-SHA256 signatures
# Display verification results
# Log all webhook details
# Provide helpful error messages

Manual Signature Verification

Example Node.js code for verifying webhook signatures:

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');

const receivedSignature = signature.replace('sha256=', '');

return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}

// In your webhook handler:
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);

if (!isValid) {
return res.status(401).send('Invalid signature');
}

// Process webhook...
res.json({success: true});
});

Signature Verification Examples

Here are complete examples for verifying webhook signatures in different programming languages:

Node.js (Express)

const express = require('express');
const crypto = require('crypto');
const app = express();

// Middleware to capture raw body for signature verification
app.use('/webhooks/transcrimed', express.raw({ type: 'application/json' }));

function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

const receivedSignature = signature.replace('sha256=', '');

return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}

app.post('/webhooks/transcrimed', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const webhookSecret = process.env.TRANSCRIMED_WEBHOOK_SECRET;

// Verify signature
if (!verifyWebhookSignature(req.body, signature, webhookSecret)) {
return res.status(401).send('Invalid signature');
}

const payload = JSON.parse(req.body);

// Process webhook event
console.log('Received webhook:', payload.event);

res.json({ success: true });
});

app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});

Python (FastAPI)

import hmac
import hashlib
import json
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse

app = FastAPI()

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()

received_signature = signature.replace('sha256=', '')

return hmac.compare_digest(expected_signature, received_signature)

@app.post("/webhooks/transcrimed")
async def webhook_handler(request: Request):
signature = request.headers.get('x-webhook-signature')
webhook_secret = os.getenv('TRANSCRIMED_WEBHOOK_SECRET')

if not signature or not webhook_secret:
raise HTTPException(status_code=401, detail="Missing signature or secret")

body = await request.body()

# Verify signature
if not verify_webhook_signature(body, signature, webhook_secret):
raise HTTPException(status_code=401, detail="Invalid signature")

payload = json.loads(body)

# Process webhook event
print(f"Received webhook: {payload.get('event')}")

return JSONResponse({"success": True})

PHP (Slim Framework)

<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

$app = AppFactory::create();

function verifyWebhookSignature($payload, $signature, $secret) {
$expectedSignature = hash_hmac('sha256', $payload, $secret);
$receivedSignature = str_replace('sha256=', '', $signature);

return hash_equals($expectedSignature, $receivedSignature);
}

$app->post('/webhooks/transcrimed', function (Request $request, Response $response) {
$signature = $request->getHeaderLine('x-webhook-signature');
$webhookSecret = $_ENV['TRANSCRIMED_WEBHOOK_SECRET'];

if (empty($signature) || empty($webhookSecret)) {
$response->getBody()->write('Missing signature or secret');
return $response->withStatus(401);
}

$body = $request->getBody()->getContents();

// Verify signature
if (!verifyWebhookSignature($body, $signature, $webhookSecret)) {
$response->getBody()->write('Invalid signature');
return $response->withStatus(401);
}

$payload = json_decode($body, true);

// Process webhook event
error_log('Received webhook: ' . $payload['event']);

$response->getBody()->write(json_encode(['success' => true]));
return $response->withHeader('Content-Type', 'application/json');
});

$app->run();
?>

Go (Gin Framework)

package main

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"os"
"strings"

"github.com/gin-gonic/gin"
)

func verifyWebhookSignature(payload []byte, signature string, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expectedSignature := hex.EncodeToString(mac.Sum(nil))

receivedSignature := strings.TrimPrefix(signature, "sha256=")

return hmac.Equal([]byte(expectedSignature), []byte(receivedSignature))
}

func webhookHandler(c *gin.Context) {
signature := c.GetHeader("x-webhook-signature")
webhookSecret := os.Getenv("TRANSCRIMED_WEBHOOK_SECRET")

if signature == "" || webhookSecret == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing signature or secret"})
return
}

body, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"})
return
}

// Verify signature
if !verifyWebhookSignature(body, signature, webhookSecret) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"})
return
}

var payload map[string]interface{}
if err := json.Unmarshal(body, &payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
return
}

// Process webhook event
log.Printf("Received webhook: %v", payload["event"])

c.JSON(http.StatusOK, gin.H{"success": true})
}

func main() {
r := gin.Default()
r.POST("/webhooks/transcrimed", webhookHandler)
r.Run(":3000")
}

Testing Signature Verification

Use our enhanced test server to validate your implementation:

  1. Start your webhook server with signature verification
  2. Run the test server with your webhook secret:
    WEBHOOK_SECRET=your_secret node scripts/test-webhook-server.js
  3. Configure webhook in Developer Portal with your server URL
  4. Test webhook - the test server will verify the signature and show results

Common Signature Issues

Signature Mismatch Issues:

  • Using wrong webhook secret
  • Modifying request body before verification
  • Incorrect encoding (use raw bytes, not string)
  • Wrong HMAC algorithm (use SHA-256)

Best Practices:

  • Verify signatures BEFORE parsing JSON
  • Use raw request body for signature calculation
  • Store webhook secrets securely (environment variables)
  • Use timing-safe comparison functions
  • Log signature verification failures for debugging

This allows you to test localhost webhooks during development when using test client credentials.

To test your webhook:

  1. Go to Webhooks
  2. Configure your webhook URL (can be localhost for test clients)
  3. Click "Test Webhook"

Troubleshooting Guide

Common Webhook Issues and Solutions

Connection Failures

Problem: NETWORK_ERROR - Connection refused or host unreachable

Solutions:

  1. Check server status: Ensure your webhook server is running
  2. Verify port: Confirm the port in your webhook URL matches your server
  3. Check firewall: Ensure no firewall is blocking connections
  4. Local development: Use host.docker.internal instead of localhost for containerized environments

Example Fix:

# Instead of: http://localhost:3000/webhook
# Use for Docker: http://host.docker.internal:3000/webhook

DNS Resolution Errors

Problem: DNS_ERROR - Cannot resolve hostname

Solutions:

  1. Verify domain: Check if the domain exists and is publicly accessible
  2. Test DNS: Use nslookup or dig to verify DNS resolution
  3. Use IP address: Try using an IP address instead of hostname for testing

Example:

# Test DNS resolution
nslookup yourwebhook.example.com
dig yourwebhook.example.com

SSL/TLS Certificate Issues

Problem: SSL_ERROR - Certificate validation failed

Solutions:

  1. Check certificate: Ensure SSL certificate is valid and not expired
  2. Use HTTPS: Production webhooks must use HTTPS URLs
  3. Certificate chain: Verify the complete certificate chain is configured
  4. Test endpoints: For development, use HTTP with test credentials

Quick Check:

# Test SSL certificate
curl -I https://yourwebhook.example.com/webhook
openssl s_client -connect yourwebhook.example.com:443

Timeout Issues

Problem: TIMEOUT_ERROR - Request timeout (>10 seconds)

Solutions:

  1. Optimize handler: Ensure webhook handler responds within 10 seconds
  2. Async processing: Move heavy operations to background jobs
  3. Quick response: Return HTTP 200 immediately, process webhook asynchronously
  4. Check load: Monitor server load and resource usage

Example Fast Handler:

app.post('/webhook', (req, res) => {
// Respond immediately
res.json({ success: true });

// Process asynchronously
setImmediate(() => {
processWebhookAsync(req.body);
});
});

HTTP Status Code Issues

Problem: HTTP_CLIENT_ERROR (4xx) or HTTP_SERVER_ERROR (5xx)

Solutions by Status Code:

StatusIssueSolution
400Bad RequestCheck request body parsing
401UnauthorizedVerify signature validation logic
404Not FoundCheck webhook URL path
405Method Not AllowedEnsure endpoint accepts POST requests
500Internal ErrorCheck server logs for errors
502Bad GatewayCheck proxy/load balancer configuration
503Service UnavailableCheck server capacity and health

Signature Verification Problems

Problem: Signature verification always fails

Debug Steps:

  1. Check webhook secret:

    # Verify you're using the correct secret
    echo "Your webhook secret: $WEBHOOK_SECRET"
  2. Log received vs expected signatures:

    console.log('Received signature:', req.headers['x-webhook-signature']);
    console.log('Expected signature:', expectedSignature);
    console.log('Request body length:', req.body.length);
  3. Verify raw body usage:

    // Wrong - body was parsed as JSON
    const signature = generateSignature(JSON.stringify(req.body));

    // Correct - use raw body buffer
    const signature = generateSignature(req.body);
  4. Check encoding:

    // Ensure consistent encoding
    const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8') // Specify encoding
    .digest('hex');

CORS Issues (Browser Testing)

Problem: CORS errors when testing from browser

Solutions:

  1. Add CORS headers:

    app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Content-Type, X-Webhook-Signature');
    next();
    });
  2. Handle preflight requests:

    app.options('/webhook', (req, res) => {
    res.sendStatus(200);
    });

Testing Best Practices

Local Development

  1. Use test credentials: Always use test client credentials for local development
  2. Start simple: Test without signature verification first, then add security
  3. Use test server: Leverage our transcrimed-api-test-server.js for initial testing
  4. Check logs: Monitor both TranscriMed logs and your server logs

Production Testing

  1. Use HTTPS: Production webhooks require secure endpoints
  2. Test thoroughly: Use staging environment before production
  3. Monitor errors: Set up error tracking and alerting
  4. Implement retries: Handle temporary failures gracefully

Security Testing

  1. Verify signatures: Always validate HMAC signatures in production
  2. Test with wrong secrets: Ensure invalid signatures are rejected
  3. Rate limiting: Implement rate limiting on webhook endpoints
  4. Log security events: Track failed authentication attempts

Getting Help

If you're still experiencing issues:

  1. Check webhook logs in the Developer Portal
  2. Test with our test server: node transcrimed-api-test-server.js
  3. Review error messages: The webhook test provides detailed error information
  4. Contact support: developers@transcrimed.com.br
  5. Check your server logs to verify the payload was received

Test Payload

The test webhook sends this simple payload:

{
"event": "test.ping",
"test_mode": true,
"timestamp": "2024-01-15T10:35:00Z",
"message": "Test webhook from TranscriMed Developer Portal",
"client_type": "test"
}

For Local Development:

  • Use test client credentials from your Developer Portal
  • Configure test webhook URL to http://localhost:3000/webhook (or your local server)
  • Click "Test Webhook" to verify your local server receives the payload
  • Your webhook server should respond with HTTP 200

For Production Testing:

  • Use production client credentials
  • Configure production webhook URL (must be HTTPS)
  • Test with staging/production servers before going live

Monitoring

Delivery Logs

The Developer Portal shows delivery logs for all webhook attempts:

  • Status: HTTP response code or error
  • Event: Which event triggered the webhook
  • Timestamp: When the webhook was sent
  • Attempts: Number of delivery attempts
  • Response: Response body from your endpoint

Troubleshooting

Test Webhook Issues

Since test webhooks are called directly from your browser:

IssueSolution
Connection refused/Network errorVerify your webhook server is running and accessible from your browser
CORS errorsConfigure your server to accept requests from the Developer Portal domain
Timeout during testEnsure your webhook endpoint responds quickly (under 30 seconds)
404 Not FoundVerify the webhook URL path and HTTP method (should accept POST)

Production Webhook Issues

For production webhooks delivered by TranscriMed servers:

IssueSolution
Signature verification failsCheck webhook secret and HMAC implementation
TimeoutsEnsure your endpoint responds within 30 seconds
4xx errorsFix endpoint URL, authentication, or request handling
Missing webhooksCheck event configuration and endpoint availability
SSL/TLS errorsVerify your HTTPS certificate is valid and properly configured

Local Development Tips

  • Use test client credentials for localhost webhook testing
  • Ensure your local server accepts POST requests
  • Check firewall settings if connection fails
  • For CORS issues, configure your server to accept requests from the browser
  • Test with a simple HTTP server first to verify connectivity

Examples

Complete Webhook Handler (Express.js)

const express = require('express');
const crypto = require('crypto');
const app = express();

// Raw body parser for signature verification
app.use('/webhooks/transcrimed', express.raw({ type: 'application/json' }));

class WebhookHandler {
constructor(secret) {
this.secret = secret;
}

verifySignature(payload, signature) {
const expectedSignature = crypto
.createHmac('sha256', this.secret)
.update(payload)
.digest('hex');

const receivedSignature = signature.replace('sha256=', '');

return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}

async handleJobCompleted(payload) {
const { job_id, external_reference_id, data } = payload;
const medicalRecord = data.result?.medical_record;

if (!medicalRecord) {
console.error('No medical record in completed job payload');
return;
}

// Save medical record to your database
await this.saveMedicalRecord({
id: medicalRecord.id,
title: medicalRecord.title,
content: medicalRecord.content,
patientId: medicalRecord.patient_id,
externalRef: external_reference_id,
metadata: medicalRecord.metadata
});

console.log(`Job ${job_id} completed - saved medical record ${medicalRecord.id}`);
}

async handleJobFailed(payload) {
const { job_id, external_reference_id, data } = payload;
const error = data.error;

// Log failure and notify relevant systems
console.error(`Job ${job_id} failed:`, error);

// Update your system to reflect the failure
await this.markJobAsFailed(external_reference_id, error);
}

async saveMedicalRecord(record) {
// Implement your database save logic
console.log('Saving medical record:', record.title);
}

async markJobAsFailed(externalRef, error) {
// Implement your error handling logic
console.log('Marking job as failed:', externalRef, error);
}
}

const webhookHandler = new WebhookHandler(process.env.TRANSCRIMED_WEBHOOK_SECRET);

app.post('/webhooks/transcrimed', async (req, res) => {
const signature = req.headers['x-webhook-signature'];

if (!signature) {
return res.status(400).send('Missing signature');
}

if (!webhookHandler.verifySignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}

try {
const payload = JSON.parse(req.body);

switch (payload.event) {
case 'test.ping':
// Test webhook from Developer Portal - just log and respond
console.log('Test webhook received from Developer Portal:', payload.message);
break;
case 'job.completed':
await webhookHandler.handleJobCompleted(payload);
break;
case 'job.failed':
await webhookHandler.handleJobFailed(payload);
break;
case 'job.started':
console.log(`Job ${payload.job_id} started processing`);
break;
case 'job.created':
console.log(`Job ${payload.job_id} created and queued`);
break;
default:
console.log(`Unknown event: ${payload.event}`);
}

res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).send('Internal server error');
}
});

app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});

Next Steps

  1. Set up your webhook endpoint with signature verification
  2. Configure webhooks in the Developer Portal
  3. Test your integration using the test webhook feature
  4. Monitor delivery logs to ensure reliable webhook processing
  5. Implement error handling for failed webhook deliveries

For more examples and SDKs, visit our Examples page.