Skip to content
Get started
Integration
Viewer
Webhooks

Java

Handle Viewer webhooks in Java.

This page is the Java implementation guide: verify signatures, then handle only the events you enabled.

All webhook requests from Avara are signed using HMAC-SHA256 with Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature). Copy the webhook secret from API Config (View Webhook Secret) and set it as the AVARA_WEBHOOK_KEY environment variable. The SDK’s unwrap() method reads that variable, verifies the signature, and returns a typed event. Every Avara→partner POST expects HTTP 200 with a JSON body matching that event’s response schema. Synchronous hooks should respond within about 30 seconds.

import com.avara.client.AvaraClient;
import com.avara.client.okhttp.AvaraOkHttpClient;
import com.avara.models.webhooks.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private final AvaraClient client = AvaraOkHttpClient.fromEnv();
@PostMapping("/avara")
public ResponseEntity<?> handleWebhook(
@RequestBody String body,
HttpServletRequest request
) {
WebhookEvent event = client.webhooks().unwrap(body, getHeaders(request));
if (event instanceof StudyAccessRequestedWebhookEvent e) {
// Handle study access request
String studyId = e.getData().getStudyId();
// ...
}
if (event instanceof SecondaryCaptureAccessRequestedWebhookEvent e) {
// Handle secondary capture upload request
String studyId = e.getData().getStudyId();
// ...
}
if (event instanceof ModalityWorklistRequestedWebhookEvent e) {
// Handle modality worklist request
String clinicId = e.getData().getClinicId();
// ...
}
return ResponseEntity.ok().build();
}
private Map<String, List<String>> getHeaders(HttpServletRequest request) {
Map<String, List<String>> headers = new HashMap<>();
var headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, Collections.list(request.getHeaders(name)));
}
return headers;
}
}

The study.access_requested webhook is sent when Avara needs presigned URLs for DICOM images (and optional non-DICOM media). This is a synchronous (hard) webhook — you must respond within about 30 seconds. 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 GET URLs. The SDK’s unwrap() method returns a typed StudyAccessRequestedWebhookEvent that you can use directly.

Return authorized: false with an error when the caller must not access the study. Returning authorized: true with an empty urls array (and omitted or empty mediaUrls) is treated as no data available, not as a denial.

import com.avara.client.AvaraClient;
import com.avara.client.okhttp.AvaraOkHttpClient;
import com.avara.models.webhooks.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private final AvaraClient client = AvaraOkHttpClient.fromEnv();
@PostMapping("/avara")
public ResponseEntity<?> handleWebhook(
@RequestBody String body,
HttpServletRequest request
) {
WebhookEvent event = client.webhooks().unwrap(body, getHeaders(request));
if (event instanceof StudyAccessRequestedWebhookEvent e) {
String studyId = e.getData().getStudyId();
String studyInstanceUid = e.getData().getStudyInstanceUid();
// This is your internal business logic
List<String> presignedUrls = generatePresignedUrlsForStudy(studyInstanceUid);
if (presignedUrls.isEmpty()) {
return ResponseEntity.ok(Map.of(
"authorized", false,
"error", "Study not found in PACS"
));
}
return ResponseEntity.ok(Map.of(
"authorized", true,
"urls", presignedUrls
));
}
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"
);
}
private Map<String, List<String>> getHeaders(HttpServletRequest request) {
Map<String, List<String>> headers = new HashMap<>();
var headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, Collections.list(request.getHeaders(name)));
}
return headers;
}
}

Response Format

public class StudyAccessRequestedWebhookResponse {
private boolean authorized;
private List<String> urls; // Flat list of DICOM GET URLs (may be empty)
private List<StudyAccessRequestedMediaUrl> mediaUrls;
private String error; // Error message if authorization failed
}
public class StudyAccessRequestedMediaUrl {
private String url;
private String mimeType;
private String fileName;
}

The secondary_capture.access_requested webhook is sent when a viewer user creates a secondary capture. This is a synchronous (hard) webhook — you must return presigned PUT URLs in-request, within about 30 seconds.

Avara asks for upload URLs before writing the generated DICOM to your storage. The SDK’s unwrap() method returns a typed SecondaryCaptureAccessRequestedWebhookEvent. Respond authorized: false with an error if the study cannot be written. uploadUrls must be PUT-capable; the client uploads with Content-Type: application/dicom and writes the same object to every URL. Series and SOP UIDs are provided when available so you can pre-compute object keys. If you send contentCreatorName, Avara ignores it and derives the creator name server-side.

import com.avara.client.AvaraClient;
import com.avara.client.okhttp.AvaraOkHttpClient;
import com.avara.models.webhooks.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private final AvaraClient client = AvaraOkHttpClient.fromEnv();
@PostMapping("/avara")
public ResponseEntity<?> handleWebhook(
@RequestBody String body,
HttpServletRequest request
) {
WebhookEvent event = client.webhooks().unwrap(body, getHeaders(request));
if (event instanceof SecondaryCaptureAccessRequestedWebhookEvent e) {
String studyId = e.getData().getStudyId();
String studyInstanceUid = e.getData().getStudyInstanceUid();
String seriesInstanceUid = e.getData().getSeriesInstanceUid();
String sopInstanceUid = e.getData().getSopInstanceUid();
// This is your internal business logic
List<String> uploadUrls = generateSecondaryCaptureUploadUrls(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
if (uploadUrls.isEmpty()) {
return ResponseEntity.ok(Map.of(
"authorized", false,
"error", "Study cannot be written"
));
}
return ResponseEntity.ok(Map.of(
"authorized", true,
"uploadUrls", uploadUrls
));
}
return ResponseEntity.ok().build();
}
// This is your internal business logic
private List<String> generateSecondaryCaptureUploadUrls(
String studyInstanceUid,
String seriesInstanceUid,
String sopInstanceUid
) {
// Generate presigned PUT URLs for the secondary capture DICOM
return List.of(
"https://storage.example.com/dicom/sc/image.dcm?token=abc123"
);
}
private Map<String, List<String>> getHeaders(HttpServletRequest request) {
Map<String, List<String>> headers = new HashMap<>();
var headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, Collections.list(request.getHeaders(name)));
}
return headers;
}
}

Response Format

public class SecondaryCaptureAccessRequestedWebhookResponse {
private boolean authorized;
private List<String> uploadUrls;
private String contentCreatorName; // Ignored if provided
private String error;
}

The modality_worklist.requested webhook is sent when an on-prem modality (via an Avara PACS box) issues a C-FIND MWL. This is a synchronous (hard) webhook — the C-FIND path waits on your response, which should return within about 30 seconds.

Avara forwards the query window to your endpoint. Only one active API key per organization may have the Modality Worklist Webhook Endpoint set. The SDK’s unwrap() method returns a typed ModalityWorklistRequestedWebhookEvent. Return authorized: false with an error to surface a worklist failure to the modality. items is required and may be empty when there are no scheduled exams.

Item field names are PascalCase DICOM-style (not camelCase). StudyInstanceUID is required from the partner RIS today — do not omit it. PatientName, PatientID, and Modality must be non-empty. ScheduledProcedureStepSequence must contain at least one step.

import com.avara.client.AvaraClient;
import com.avara.client.okhttp.AvaraOkHttpClient;
import com.avara.models.webhooks.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private final AvaraClient client = AvaraOkHttpClient.fromEnv();
@PostMapping("/avara")
public ResponseEntity<?> handleWebhook(
@RequestBody String body,
HttpServletRequest request
) {
WebhookEvent event = client.webhooks().unwrap(body, getHeaders(request));
if (event instanceof ModalityWorklistRequestedWebhookEvent e) {
String clinicId = e.getData().getClinicId();
String callingAe = e.getData().getCallingAe();
String sourceIp = e.getData().getSourceIp();
String dateStart = e.getData().getDateStart();
String dateEnd = e.getData().getDateEnd();
String modality = e.getData().getModality();
// This is your internal business logic
List<Map<String, Object>> items = queryModalityWorklist(
clinicId, callingAe, sourceIp, dateStart, dateEnd, modality
);
Map<String, Object> response = new HashMap<>();
response.put("authorized", true);
response.put("items", items);
return ResponseEntity.ok(response);
}
return ResponseEntity.ok().build();
}
// This is your internal business logic
private List<Map<String, Object>> queryModalityWorklist(
String clinicId,
String callingAe,
String sourceIp,
String dateStart,
String dateEnd,
String modality
) {
Map<String, Object> step = new LinkedHashMap<>();
step.put("ScheduledProcedureStepStartDate", "20260813");
step.put("ScheduledProcedureStepStartTime", "090000");
step.put("ScheduledProcedureStepID", "SPSID-1");
step.put("Modality", "CT");
step.put("ScheduledProcedureStepDescription", "CT CHEST WO CONTRAST");
Map<String, Object> item = new LinkedHashMap<>();
item.put("PatientName", "DOE^JANE");
item.put("PatientID", "MRN12345");
item.put("PatientBirthDate", "19800115");
item.put("PatientSex", "F");
item.put("PatientSize", "1.65");
item.put("PatientWeight", "62");
item.put("Modality", "CT");
item.put("AccessionNumber", "ACC-1001");
item.put("StudyInstanceUID", "1.2.840.113619.2.55.3.604688119.868.1234567890.123");
item.put("RequestedProcedureDescription", "CT CHEST WO CONTRAST");
item.put("StudyDescription", "CT CHEST WO CONTRAST");
item.put("ProtocolName", "CHEST_WO");
item.put("ScheduledProcedureStepSequence", List.of(step));
return List.of(item);
}
private Map<String, List<String>> getHeaders(HttpServletRequest request) {
Map<String, List<String>> headers = new HashMap<>();
var headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, Collections.list(request.getHeaders(name)));
}
return headers;
}
}

Response Format

public class ModalityWorklistRequestedWebhookResponse {
private boolean authorized;
private List<ModalityWorklistItem> items;
private String error;
}
public class ModalityWorklistItem {
private String PatientName;
private String PatientID;
private String PatientBirthDate;
private String PatientSex;
private String PatientSize;
private String PatientWeight;
private String Modality;
private String AccessionNumber;
private String StudyInstanceUID;
private String RequestedProcedureDescription;
private String StudyDescription;
private String ProtocolName;
private List<ModalityWorklistScheduledStep> ScheduledProcedureStepSequence;
}
public class ModalityWorklistScheduledStep {
private String ScheduledProcedureStepStartDate;
private String ScheduledProcedureStepStartTime;
private String ScheduledProcedureStepID;
private String Modality;
private String ScheduledProcedureStepDescription;
}