Skip to content
Get started
AutoScribe

Integration

Integrate AutoScribe into your platform using our SDKs to automate study creation and management. This is for IT personnel of PACS infrastructure.

Note: This integration guide is specifically for the AutoScribe platform. It is not intended for Viewer or the Clinical Platform integrations.

Studies can be fully created and managed using the AutoScribe dashboard, but automating and baking AutoScribe into your platform makes the entire process streamlined and more efficient. Additionally, any sort of study viewing requires integrations to provide seamless access to reports within your existing workflow.

This guide covers the essential integration patterns for developers, walking you through the complete setup process from initial configuration to production deployment. For complete API documentation and all available operations, see the API Reference.

To integrate AutoScribe into your platform, follow these steps:

  1. Create API key — Generate an API key in the AutoScribe dashboard with appropriate permissions and scopes for your integration needs.

  2. Configure webhooks — Set up webhook endpoints to handle study image access requests (if using the viewer) and report delivery notifications (optional), ensuring secure signature verification.

  3. Automate creating studies — Use our SDKs to programmatically create studies from your PACS, RIS, or other systems, enabling seamless workflow integration (manual creation in dashboard also supported).

  4. Invite your users — Set up user access and authentication to allow your team members to access AutoScribe reports and studies.

Before integrating AutoScribe into your platform, you need to create and configure an API key in the AutoScribe dashboard.

  1. After logging in, navigate to the sidebar and go to the API Config section

  2. Click the Create API Key button

  3. Configure the following values:

    • Description: A human-readable description to identify the key in the dashboard
    • Study scope: Choose between Self or All
      • Self: For teleradiology groups with multiple integrating PACS sources. This ensures each PACS can only access, edit, and manage their own studies
      • All: For customers with a unified source of studies or integrators themselves (PACS companies looking to integrate AutoScribe into their platform)
    • User access: Choose None, Read, or Write to determine if the API token can invite and manage users
    • Express customer access: Explicitly for integrators (PACS companies) looking to serve AutoScribe as a product to all of their customers. Otherwise, leave as None since it won’t be needed
    • Study data webhook endpoint: Your domain endpoint to generate study URLs for the viewer (leave blank if you are not going to use the viewer — can be edited later). Example: https://my-domain.com/api/generate-autoscribe-study-urls (see the Study Image Access webhook section below)
    • Report data webhook endpoint: Your domain endpoint for processing completed reports (if you do not intend on automating report delivery to your system, then leave blank — can be edited whenever). Example: https://my-domain.com/api/process-autoscribe-complete-report (see the Report Delivery webhook section below)
  4. Once the API key is created, save it to your environment variables — you will not be able to view it again

To edit webhook endpoints after creating an API key, click the Edit Endpoints button in the API config row for your key.

Edit Webhook Endpoints

To view the JWT secret for webhook integrations (it is auto-generated), click the View JWT Secret button in the API config row for your key. This secret can be viewed at any time.

View JWT Webhook Secret

Save this secret to your environment variables as AVARA_JWT_WEBHOOK_SECRET for use in webhook signature verification.

The simplest integration pattern is creating studies programmatically using our SDKs. This allows you to automate study creation from your PACS, RIS, or other systems. Studies can also be fully created and managed in the dashboard if desired.

import Avara from 'avara';
const avara = new Avara({
apiKey: process.env.AVARA_API_KEY
});
const study = await avara.autoScribe.studies.create({
studyUid: '1.3.6.1.4.1.543245.54356543',
studyDescription: 'Coronary CT Angiogram',
severity: 'normal',
reportMetadata: {
patientName: 'John Doe',
dateOfBirth: '1985-01-01',
facilityName: 'South Tampa Imaging',
...
},
});
import os
from avara import Avara
client = Avara(
api_key=os.environ.get("AVARA_API_KEY"),
)
study = client.auto_scribe.studies.create(
severity="normal",
study_description="Brain MRI with Contrast",
study_instance_uid="1.2.840.113619.2.55.3.604688119.868.1234567890.123",
report_metadata={
"patient_name": "John Doe",
"date_of_birth": "1985-01-01",
"facility_name": "South Tampa Imaging",
...
},
)
package com.avara.example;
import com.avara.client.AvaraClient;
import com.avara.client.okhttp.AvaraOkHttpClient;
import com.avara.models.autoscribe.StudyReportMetadata;
import com.avara.models.autoscribe.studies.StudyCreateParams;
import com.avara.models.autoscribe.studies.StudyCreateResponse;
public final class Main {
private Main() {}
public static void main(String[] args) {
AvaraClient client = AvaraOkHttpClient.fromEnv();
StudyCreateParams params = StudyCreateParams.builder()
.severity(StudyCreateParams.Severity.NORMAL)
.studyDescription("Brain MRI with Contrast")
.studyInstanceUid("1.2.840.113619.2.55.3.604688119.868.1234567890.123")
.reportMetadata(StudyReportMetadata.builder()
.patientName("John Doe")
.dateOfBirth("1985-01-01")
.facilityName("South Tampa Imaging")
.build())
.build();
StudyCreateResponse study = client.autoScribe().studies().create(params);
}
}

Temporary access views enable you to generate reroute URLs that provide secure, authenticated access to AutoScribe without requiring users to log in to the Avara AutoScribe dashboard. This feature is designed for advanced PACS company users desiring a fully embedded experience in their platform and is completely optional.

The flow for temporary access views is straightforward:

  1. Authenticated users request access: Users authenticated in your platform request access to dictate a report
  2. Server authorization: Your servers authorize the request and generate a reroute URL using our SDK. A user ID is required in order to provide auditing of who is working on the report, since only NPI validated physicians can interpret reports
  3. Client-side redirect: Serve the URL to the user and redirect them client-side to a new window where they get a secure connection to dictate a report without needing to log in to the Avara AutoScribe dashboard
import Avara from 'avara';
const client = new Avara({
apiKey: process.env['AVARA_API_KEY'],
});
const response = await client.autoScribe.studies.rerouteURL({
assignedToUserId: 'usr_1234567890abcdef1234567890abcdef',
studyId: 'stu_1234567890abcdef1234567890abcdef',
});
import os
from avara import Avara
client = Avara(
api_key=os.environ.get("AVARA_API_KEY"),
)
response = client.auto_scribe.studies.reroute_url(
assigned_to_user_id="usr_1234567890abcdef1234567890abcdef",
study_id="stu_1234567890abcdef1234567890abcdef",
)
package com.avara.example;
import com.avara.client.AvaraClient;
import com.avara.client.okhttp.AvaraOkHttpClient;
import com.avara.models.autoscribe.studies.StudyRerouteUrlParams;
import com.avara.models.autoscribe.studies.StudyRerouteUrlResponse;
public final class Main {
private Main() {}
public static void main(String[] args) {
AvaraClient client = AvaraOkHttpClient.fromEnv();
StudyRerouteUrlParams params = StudyRerouteUrlParams.builder()
.assignedToUserId("usr_1234567890abcdef1234567890abcdef")
.studyId("stu_1234567890abcdef1234567890abcdef")
.build();
StudyRerouteUrlResponse response = client.autoScribe().studies().rerouteUrl(params);
}
}

AutoScribe uses webhooks to integrate with your systems for study image access and report delivery. Webhooks allow Avara to request data from your systems and notify you when events occur.

All webhook requests from Avara include security headers that you must verify to ensure the request is authentic. Avara signs each webhook request using a JWT secret that you configure in your API key settings.

Avara sends the following headers with each webhook request:

  • X-Avara-Signature: HMAC-SHA256 signature of the request payload
  • X-Avara-Timestamp: Unix timestamp (milliseconds) when the request was signed

You must verify the webhook signature using your AVARA_JWT_WEBHOOK_SECRET environment variable before processing any webhook event. The signature is computed as:

signature = HMAC-SHA256(secret, timestamp + payload)
import crypto from 'crypto';
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
function verifyWebhookSignature(req: Request): boolean {
const secret = process.env.AVARA_JWT_WEBHOOK_SECRET;
if (!secret) {
throw new Error('AVARA_JWT_WEBHOOK_SECRET not configured');
}
const signature = req.headers['x-avara-signature'] as string;
const timestamp = req.headers['x-avara-timestamp'] as string;
if (!signature || !timestamp) {
return false;
}
const payload = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(timestamp + payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhooks/avara', async (req: Request, res: Response) => {
if (!verifyWebhookSignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
});
import os
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_webhook_signature(req) -> bool:
secret = os.environ.get('AVARA_JWT_WEBHOOK_SECRET')
if not secret:
raise ValueError('AVARA_JWT_WEBHOOK_SECRET not configured')
signature = req.headers.get('X-Avara-Signature')
timestamp = req.headers.get('X-Avara-Timestamp')
if not signature or not timestamp:
return False
payload = req.get_data(as_text=True)
expected_signature = hmac.new(
secret.encode('utf-8'),
(timestamp + payload).encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
@app.route('/webhooks/avara', methods=['POST'])
def handle_webhook():
if not verify_webhook_signature(request):
return jsonify({'error': 'Invalid signature'}), 401
# Process webhook...
return jsonify({'success': True}), 200
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private static final String HMAC_SHA256 = "HmacSHA256";
private final ObjectMapper objectMapper = new ObjectMapper();
private boolean verifyWebhookSignature(HttpServletRequest request, Object body) {
String secret = System.getenv("AVARA_JWT_WEBHOOK_SECRET");
if (secret == null) {
throw new IllegalStateException("AVARA_JWT_WEBHOOK_SECRET not configured");
}
String signature = request.getHeader("X-Avara-Signature");
String timestamp = request.getHeader("X-Avara-Timestamp");
if (signature == null || timestamp == null) {
return false;
}
try {
String payload = objectMapper.writeValueAsString(body);
Mac mac = Mac.getInstance(HMAC_SHA256);
SecretKeySpec secretKeySpec = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8),
HMAC_SHA256
);
mac.init(secretKeySpec);
String message = timestamp + payload;
byte[] hashBytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
String expectedSignature = bytesToHex(hashBytes);
return MessageDigest.isEqual(
signature.getBytes(StandardCharsets.UTF_8),
expectedSignature.getBytes(StandardCharsets.UTF_8)
);
} catch (Exception e) {
throw new RuntimeException("Failed to verify signature", e);
}
}
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
@PostMapping("/avara")
public ResponseEntity<?> handleWebhook(
@RequestBody Object body,
HttpServletRequest request
) {
if (!verifyWebhookSignature(request, body)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid signature");
}
// Process webhook...
return ResponseEntity.ok().build();
}
}

The study.access_requested webhook is sent when Avara needs presigned URLs for DICOM images. This is a synchronous webhook — you must respond with the URLs within the request timeout. This webhook is sent before a study can be viewed in the Avara interface.

When Avara needs to access study images, it sends a POST request to your webhook endpoint. Your endpoint must respond with presigned URLs for the DICOM images.

Event Structure

interface StudyAccessRequestedWebhookEvent {
id: string; // Format: whe_{32-hex-chars}
type: "study.access_requested";
data: {
studyId: string; // Format: stu_{32-hex-chars}
studyInstanceUid: string; // DICOM Study Instance UID
};
}

Implementation

import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
// See Webhook Security section for verifyWebhookSignature implementation
app.post('/webhooks/avara', async (req: Request, res: Response) => {
if (!verifyWebhookSignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body as StudyAccessRequestedWebhookEvent;
if (event.type === 'study.access_requested') {
const { studyId, studyInstanceUid } = event.data;
// This is your internal business logic
const presignedUrls = await generatePresignedUrlsForStudy(studyInstanceUid);
if (presignedUrls.length === 0) {
return res.status(200).json({
authorized: false,
error: 'Study not found in PACS',
});
}
res.status(200).json({
authorized: true,
urls: presignedUrls,
});
}
});
// This is your internal business logic
async function generatePresignedUrlsForStudy(
studyInstanceUid: string
): Promise<string[]> {
// Query your PACS/RIS system for the study
// Generate presigned URLs for each DICOM image
// Return a flat list of all image URLs
return [
'https://storage.example.com/dicom/image1.dcm?token=abc123',
'https://storage.example.com/dicom/image2.dcm?token=def456',
];
}

Response Format

interface StudyAccessRequestedWebhookResponse {
authorized: boolean;
urls?: string[]; // Presigned URLs for DICOM images
error?: string; // Error message if authorization failed
}

Event Structure

from typing import TypedDict
class StudyAccessRequestedWebhookEvent(TypedDict):
id: str # Format: whe_{32-hex-chars}
type: str # "study.access_requested"
data: dict # Contains studyId and studyInstanceUid

Implementation

from flask import Flask, request, jsonify
from typing import List
app = Flask(__name__)
# See Webhook Security section for verify_webhook_signature implementation
@app.route('/webhooks/avara', methods=['POST'])
def handle_webhook():
if not verify_webhook_signature(request):
return jsonify({'error': 'Invalid signature'}), 401
event = request.json
if event['type'] == 'study.access_requested':
study_id = event['data']['studyId']
study_instance_uid = event['data']['studyInstanceUid']
# This is your internal business logic
presigned_urls = generate_presigned_urls_for_study(study_instance_uid)
if not presigned_urls:
return jsonify({
'authorized': False,
'error': 'Study not found in PACS'
}), 200
return jsonify({
'authorized': True,
'urls': presigned_urls
}), 200
# This is your internal business logic
def generate_presigned_urls_for_study(study_instance_uid: str) -> List[str]:
# Query your PACS/RIS system for the study
# Generate presigned URLs for each DICOM image
# Return a flat list of all image URLs
return [
'https://storage.example.com/dicom/image1.dcm?token=abc123',
'https://storage.example.com/dicom/image2.dcm?token=def456',
]

Response Format

from typing import TypedDict, Optional, List
class StudyAccessRequestedWebhookResponse(TypedDict, total=False):
authorized: bool
urls: Optional[List[str]] # Presigned URLs for DICOM images
error: Optional[str] # Error message if authorization failed

Event Structure

public class StudyAccessRequestedWebhookEvent {
private String id; // Format: whe_{32-hex-chars}
private String type; // "study.access_requested"
private StudyAccessData data;
public static class StudyAccessData {
private String studyId; // Format: stu_{32-hex-chars}
private String studyInstanceUid; // DICOM Study Instance UID
}
}

Implementation

import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
// See Webhook Security section for verifyWebhookSignature implementation
@PostMapping("/avara")
public ResponseEntity<?> handleWebhook(
@RequestBody StudyAccessRequestedWebhookEvent event,
HttpServletRequest request
) {
if (!verifyWebhookSignature(request, event)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid signature");
}
if ("study.access_requested".equals(event.getType())) {
String studyId = event.getData().getStudyId();
String studyInstanceUid = event.getData().getStudyInstanceUid();
// This is your internal business logic
List<String> presignedUrls = generatePresignedUrlsForStudy(studyInstanceUid);
if (presignedUrls.isEmpty()) {
return ResponseEntity.ok(StudyAccessRequestedWebhookResponse.builder()
.authorized(false)
.error("Study not found in PACS")
.build());
}
return ResponseEntity.ok(StudyAccessRequestedWebhookResponse.builder()
.authorized(true)
.urls(presignedUrls)
.build());
}
return ResponseEntity.ok().build();
}
// This is your internal business logic
private List<String> generatePresignedUrlsForStudy(String studyInstanceUid) {
// Query your PACS/RIS system for the study
// Generate presigned URLs for each DICOM image
// Return a flat list of all image URLs
return List.of(
"https://storage.example.com/dicom/image1.dcm?token=abc123",
"https://storage.example.com/dicom/image2.dcm?token=def456"
);
}
}

Response Format

public class StudyAccessRequestedWebhookResponse {
private boolean authorized;
private List<String> urls; // Presigned URLs for DICOM images
private String error; // Error message if authorization failed
}

The report.delivered webhook is sent when a report is completed and delivered. This is an asynchronous notification — respond with a simple success acknowledgment. The webhook includes both plain text content and a presigned URL for PDF download.

When a report is completed, Avara sends a POST request to your webhook endpoint. Your endpoint should process the report and respond with a success acknowledgment.

Event Structure

interface ReportDeliveredWebhookEvent {
id: string; // Format: whe_{32-hex-chars}
type: "report.delivered";
data: {
reportId: string; // Format: rep_{32-hex-chars}
studyId: string; // Format: stu_{32-hex-chars}
plainText?: string; // Report plain text content (optional)
presignedUrl: string; // Presigned URL for PDF download (valid for ~1 hour)
};
}

Implementation

import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
// See Webhook Security section for verifyWebhookSignature implementation
app.post('/webhooks/avara', async (req: Request, res: Response) => {
if (!verifyWebhookSignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body as ReportDeliveredWebhookEvent;
if (event.type === 'report.delivered') {
const { reportId, studyId, plainText, presignedUrl } = event.data;
// This is your internal business logic
await processCompletedReport({
reportId,
studyId,
plainText,
presignedUrl,
});
res.status(200).json({
success: true,
});
}
});
// This is your internal business logic
async function processCompletedReport(data: {
reportId: string;
studyId: string;
plainText?: string;
presignedUrl: string;
}): Promise<void> {
// Download the PDF from the presigned URL
// Store the report in your system
// Update your PACS/RIS with the completed report
// Notify relevant users or systems
}

Response Format

interface ReportDeliveredWebhookResponse {
success: boolean;
}

Event Structure

from typing import TypedDict, Optional
class ReportDeliveredWebhookEvent(TypedDict):
id: str # Format: whe_{32-hex-chars}
type: str # "report.delivered"
data: dict # Contains reportId, studyId, plainText (optional), presignedUrl

Implementation

from flask import Flask, request, jsonify
app = Flask(__name__)
# See Webhook Security section for verify_webhook_signature implementation
@app.route('/webhooks/avara', methods=['POST'])
def handle_webhook():
if not verify_webhook_signature(request):
return jsonify({'error': 'Invalid signature'}), 401
event = request.json
if event['type'] == 'report.delivered':
report_id = event['data']['reportId']
study_id = event['data']['studyId']
plain_text = event['data'].get('plainText')
presigned_url = event['data']['presignedUrl']
# This is your internal business logic
process_completed_report({
'report_id': report_id,
'study_id': study_id,
'plain_text': plain_text,
'presigned_url': presigned_url,
})
return jsonify({'success': True}), 200
# This is your internal business logic
def process_completed_report(data: dict):
# Download the PDF from the presigned URL
# Store the report in your system
# Update your PACS/RIS with the completed report
# Notify relevant users or systems
pass

Response Format

from typing import TypedDict
class ReportDeliveredWebhookResponse(TypedDict):
success: bool

Event Structure

public class ReportDeliveredWebhookEvent {
private String id; // Format: whe_{32-hex-chars}
private String type; // "report.delivered"
private ReportDeliveredData data;
public static class ReportDeliveredData {
private String reportId; // Format: rep_{32-hex-chars}
private String studyId; // Format: stu_{32-hex-chars}
private String plainText; // Report plain text content (optional)
private String presignedUrl; // Presigned URL for PDF download (valid for ~1 hour)
}
}

Implementation

import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
// See Webhook Security section for verifyWebhookSignature implementation
@PostMapping("/avara")
public ResponseEntity<?> handleWebhook(
@RequestBody ReportDeliveredWebhookEvent event,
HttpServletRequest request
) {
if (!verifyWebhookSignature(request, event)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid signature");
}
if ("report.delivered".equals(event.getType())) {
String reportId = event.getData().getReportId();
String studyId = event.getData().getStudyId();
String plainText = event.getData().getPlainText();
String presignedUrl = event.getData().getPresignedUrl();
// This is your internal business logic
processCompletedReport(reportId, studyId, plainText, presignedUrl);
return ResponseEntity.ok(ReportDeliveredWebhookResponse.builder()
.success(true)
.build());
}
return ResponseEntity.ok().build();
}
// This is your internal business logic
private void processCompletedReport(
String reportId,
String studyId,
String plainText,
String presignedUrl
) {
// Download the PDF from the presigned URL
// Store the report in your system
// Update your PACS/RIS with the completed report
// Notify relevant users or systems
}
}

Response Format

public class ReportDeliveredWebhookResponse {
private boolean success;
}

For complete API documentation, including all available operations, webhooks, and advanced features, see the API Reference.