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:
| Event | Description |
|---|---|
job.created | Triggered when an async job is created and queued |
job.started | Triggered when a job begins processing |
job.completed | Triggered when a job finishes successfully |
job.failed | Triggered when a job fails with errors |
job.cancelled | Triggered 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):
- Test Webhook URL: Your endpoint URL (HTTP localhost allowed, e.g.,
http://localhost:3000/webhooks/transcrimed) - Events: Select which events to receive
- Generate Secret: Create a test webhook secret for signature verification
- Enable: Activate test webhook notifications
For Production Clients (live integration):
- Production Webhook URL: Your HTTPS endpoint (e.g.,
https://your-api.com/webhooks/transcrimed) - Events: Select which events to receive
- Generate Secret: Create a production webhook secret for signature verification
- 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
- Always verify signatures before processing payloads
- Use HTTPS endpoints for webhook URLs
- Implement idempotency - same payload may be sent multiple times
- Respond quickly - process webhooks asynchronously if needed
- Return HTTP 200 for successful processing
- Log webhook events for debugging and monitoring
- 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:
- Immediate retry after 1 second
- Second retry after 2 seconds
- Third retry after 4 seconds
- 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:
- For Test Clients: Tests your configured test webhook URL with valid signatures
- For Production Clients: Tests your configured production webhook URL with valid signatures
- Security: All test requests include proper HMAC-SHA256 signatures using your configured secret
- 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:
Option 1: ngrok (Recommended)
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:
- Start your webhook server with signature verification
- Run the test server with your webhook secret:
WEBHOOK_SECRET=your_secret node scripts/test-webhook-server.js - Configure webhook in Developer Portal with your server URL
- 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:
- Go to Webhooks
- Configure your webhook URL (can be localhost for test clients)
- Click "Test Webhook"
Troubleshooting Guide
Common Webhook Issues and Solutions
Connection Failures
Problem: NETWORK_ERROR - Connection refused or host unreachable
Solutions:
- Check server status: Ensure your webhook server is running
- Verify port: Confirm the port in your webhook URL matches your server
- Check firewall: Ensure no firewall is blocking connections
- Local development: Use
host.docker.internalinstead oflocalhostfor 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:
- Verify domain: Check if the domain exists and is publicly accessible
- Test DNS: Use
nslookupordigto verify DNS resolution - 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:
- Check certificate: Ensure SSL certificate is valid and not expired
- Use HTTPS: Production webhooks must use HTTPS URLs
- Certificate chain: Verify the complete certificate chain is configured
- 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:
- Optimize handler: Ensure webhook handler responds within 10 seconds
- Async processing: Move heavy operations to background jobs
- Quick response: Return HTTP 200 immediately, process webhook asynchronously
- 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:
| Status | Issue | Solution |
|---|---|---|
400 | Bad Request | Check request body parsing |
401 | Unauthorized | Verify signature validation logic |
404 | Not Found | Check webhook URL path |
405 | Method Not Allowed | Ensure endpoint accepts POST requests |
500 | Internal Error | Check server logs for errors |
502 | Bad Gateway | Check proxy/load balancer configuration |
503 | Service Unavailable | Check server capacity and health |
Signature Verification Problems
Problem: Signature verification always fails
Debug Steps:
-
Check webhook secret:
# Verify you're using the correct secret
echo "Your webhook secret: $WEBHOOK_SECRET" -
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); -
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); -
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:
-
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();
}); -
Handle preflight requests:
app.options('/webhook', (req, res) => {
res.sendStatus(200);
});
Testing Best Practices
Local Development
- Use test credentials: Always use test client credentials for local development
- Start simple: Test without signature verification first, then add security
- Use test server: Leverage our
transcrimed-api-test-server.jsfor initial testing - Check logs: Monitor both TranscriMed logs and your server logs
Production Testing
- Use HTTPS: Production webhooks require secure endpoints
- Test thoroughly: Use staging environment before production
- Monitor errors: Set up error tracking and alerting
- Implement retries: Handle temporary failures gracefully
Security Testing
- Verify signatures: Always validate HMAC signatures in production
- Test with wrong secrets: Ensure invalid signatures are rejected
- Rate limiting: Implement rate limiting on webhook endpoints
- Log security events: Track failed authentication attempts
Getting Help
If you're still experiencing issues:
- Check webhook logs in the Developer Portal
- Test with our test server:
node transcrimed-api-test-server.js - Review error messages: The webhook test provides detailed error information
- Contact support: developers@transcrimed.com.br
- 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:
| Issue | Solution |
|---|---|
| Connection refused/Network error | Verify your webhook server is running and accessible from your browser |
| CORS errors | Configure your server to accept requests from the Developer Portal domain |
| Timeout during test | Ensure your webhook endpoint responds quickly (under 30 seconds) |
| 404 Not Found | Verify the webhook URL path and HTTP method (should accept POST) |
Production Webhook Issues
For production webhooks delivered by TranscriMed servers:
| Issue | Solution |
|---|---|
| Signature verification fails | Check webhook secret and HMAC implementation |
| Timeouts | Ensure your endpoint responds within 30 seconds |
| 4xx errors | Fix endpoint URL, authentication, or request handling |
| Missing webhooks | Check event configuration and endpoint availability |
| SSL/TLS errors | Verify 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
- Set up your webhook endpoint with signature verification
- Configure webhooks in the Developer Portal
- Test your integration using the test webhook feature
- Monitor delivery logs to ensure reliable webhook processing
- Implement error handling for failed webhook deliveries
For more examples and SDKs, visit our Examples page.