Skip to content
Get started
Integration
AutoScribe
Webhooks

C#

Handle AutoScribe webhooks in C#.

This page is the C# 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.

using Microsoft.AspNetCore.Mvc;
using Avara;
using Avara.Models.Webhooks;
[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
private readonly AvaraClient _client = new(); // reads AVARA_WEBHOOK_KEY from env
[HttpPost("avara")]
public async Task<IActionResult> HandleWebhook()
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var webhookEvent = _client.Webhooks.Unwrap(body, GetHeaders());
switch (webhookEvent)
{
case StudyAccessRequestedWebhookEvent e:
{
// Handle study access request
var studyId = e.Data.StudyId;
break;
}
case SecondaryCaptureAccessRequestedWebhookEvent e:
{
// Handle secondary capture upload request
var studyId = e.Data.StudyId;
break;
}
case ReportDeliveredWebhookEvent e:
{
// Handle report delivery
var reportId = e.Data.ReportId;
break;
}
case ModalityWorklistRequestedWebhookEvent e:
{
// Handle modality worklist request
var clinicId = e.Data.ClinicId;
break;
}
case PatientStudyEnrichmentRequestedWebhookEvent e:
{
// Handle patient/study enrichment
var studyInstanceUid = e.Data.StudyInstanceUid;
break;
}
case ClinicalContextEnrichmentRequestedWebhookEvent e:
{
// Handle clinical context enrichment
var studyId = e.Data.StudyId;
break;
}
}
return Ok();
}
private Dictionary<string, IEnumerable<string>> GetHeaders()
{
return Request.Headers.ToDictionary(
h => h.Key,
h => (IEnumerable<string>)h.Value.ToArray()
);
}
}

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.

using Microsoft.AspNetCore.Mvc;
using Avara;
using Avara.Models.Webhooks;
[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
private readonly AvaraClient _client = new();
[HttpPost("avara")]
public async Task<IActionResult> HandleWebhook()
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var webhookEvent = _client.Webhooks.Unwrap(body, GetHeaders());
if (webhookEvent is StudyAccessRequestedWebhookEvent e)
{
var studyId = e.Data.StudyId;
var studyInstanceUid = e.Data.StudyInstanceUid;
// This is your internal business logic
var presignedUrls = GeneratePresignedUrlsForStudy(studyInstanceUid);
if (presignedUrls.Count == 0)
{
return Ok(new { authorized = false, error = "Study not found in PACS" });
}
return Ok(new { authorized = true, urls = presignedUrls });
}
return Ok();
}
// 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 new List<string>
{
"https://storage.example.com/dicom/image1.dcm?token=abc123",
"https://storage.example.com/dicom/image2.dcm?token=def456",
};
}
private Dictionary<string, IEnumerable<string>> GetHeaders()
{
return Request.Headers.ToDictionary(
h => h.Key,
h => (IEnumerable<string>)h.Value.ToArray()
);
}
}

Response Format

public class StudyAccessRequestedWebhookResponse
{
public bool Authorized { get; set; }
public List<string> Urls { get; set; } // Flat list of DICOM GET URLs (may be empty)
public List<StudyAccessRequestedMediaUrl>? MediaUrls { get; set; }
public string? Error { get; set; }
}
public class StudyAccessRequestedMediaUrl
{
public string Url { get; set; }
public string MimeType { get; set; }
public string? FileName { get; set; }
}

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.

using Microsoft.AspNetCore.Mvc;
using Avara;
using Avara.Models.Webhooks;
[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
private readonly AvaraClient _client = new();
[HttpPost("avara")]
public async Task<IActionResult> HandleWebhook()
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var webhookEvent = _client.Webhooks.Unwrap(body, GetHeaders());
if (webhookEvent is SecondaryCaptureAccessRequestedWebhookEvent e)
{
var studyId = e.Data.StudyId;
var studyInstanceUid = e.Data.StudyInstanceUid;
var seriesInstanceUid = e.Data.SeriesInstanceUid;
var sopInstanceUid = e.Data.SopInstanceUid;
// This is your internal business logic
var uploadUrls = GenerateSecondaryCaptureUploadUrls(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
if (uploadUrls.Count == 0)
{
return Ok(new { authorized = false, error = "Study cannot be written" });
}
return Ok(new { authorized = true, uploadUrls });
}
return Ok();
}
// 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 new List<string>
{
"https://storage.example.com/dicom/sc/image.dcm?token=abc123",
};
}
private Dictionary<string, IEnumerable<string>> GetHeaders()
{
return Request.Headers.ToDictionary(
h => h.Key,
h => (IEnumerable<string>)h.Value.ToArray()
);
}
}

Response Format

public class SecondaryCaptureAccessRequestedWebhookResponse
{
public bool Authorized { get; set; }
public List<string> UploadUrls { get; set; }
public string? ContentCreatorName { get; set; } // Ignored if provided
public string? Error { get; set; }
}

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 plain text content, a presigned URL for PDF download, and isCritical (whether the report was marked critical at sign-off).

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. The SDK’s unwrap() method returns a typed ReportDeliveredWebhookEvent that you can use directly.

using Microsoft.AspNetCore.Mvc;
using Avara;
using Avara.Models.Webhooks;
[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
private readonly AvaraClient _client = new();
[HttpPost("avara")]
public async Task<IActionResult> HandleWebhook()
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var webhookEvent = _client.Webhooks.Unwrap(body, GetHeaders());
if (webhookEvent is ReportDeliveredWebhookEvent e)
{
var reportId = e.Data.ReportId;
var studyId = e.Data.StudyId;
var plainText = e.Data.PlainText;
var presignedUrl = e.Data.PresignedUrl;
var isCritical = e.Data.IsCritical;
// This is your internal business logic
ProcessCompletedReport(reportId, studyId, plainText, presignedUrl, isCritical);
return Ok(new { success = true });
}
return Ok();
}
// This is your internal business logic
private void ProcessCompletedReport(
string reportId,
string studyId,
string? plainText,
string presignedUrl,
bool isCritical
)
{
// 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
}
private Dictionary<string, IEnumerable<string>> GetHeaders()
{
return Request.Headers.ToDictionary(
h => h.Key,
h => (IEnumerable<string>)h.Value.ToArray()
);
}
}

Response Format

public class ReportDeliveredWebhookResponse
{
public bool Success { get; set; }
}

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.

using Microsoft.AspNetCore.Mvc;
using Avara;
using Avara.Models.Webhooks;
[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
private readonly AvaraClient _client = new();
[HttpPost("avara")]
public async Task<IActionResult> HandleWebhook()
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var webhookEvent = _client.Webhooks.Unwrap(body, GetHeaders());
if (webhookEvent is ModalityWorklistRequestedWebhookEvent e)
{
var clinicId = e.Data.ClinicId;
var callingAe = e.Data.CallingAe;
var sourceIp = e.Data.SourceIp;
var dateStart = e.Data.DateStart;
var dateEnd = e.Data.DateEnd;
var modality = e.Data.Modality;
// This is your internal business logic
var items = QueryModalityWorklist(
clinicId, callingAe, sourceIp, dateStart, dateEnd, modality
);
return Ok(new Dictionary<string, object>
{
["authorized"] = true,
["items"] = items,
});
}
return Ok();
}
// This is your internal business logic
private List<Dictionary<string, object>> QueryModalityWorklist(
string clinicId,
string callingAe,
string sourceIp,
string dateStart,
string dateEnd,
string? modality
)
{
var step = new Dictionary<string, object>
{
["ScheduledProcedureStepStartDate"] = "20260813",
["ScheduledProcedureStepStartTime"] = "090000",
["ScheduledProcedureStepID"] = "SPSID-1",
["Modality"] = "CT",
["ScheduledProcedureStepDescription"] = "CT CHEST WO CONTRAST",
};
var item = new Dictionary<string, object>
{
["PatientName"] = "DOE^JANE",
["PatientID"] = "MRN12345",
["PatientBirthDate"] = "19800115",
["PatientSex"] = "F",
["PatientSize"] = "1.65",
["PatientWeight"] = "62",
["Modality"] = "CT",
["AccessionNumber"] = "ACC-1001",
["StudyInstanceUID"] = "1.2.840.113619.2.55.3.604688119.868.1234567890.123",
["RequestedProcedureDescription"] = "CT CHEST WO CONTRAST",
["StudyDescription"] = "CT CHEST WO CONTRAST",
["ProtocolName"] = "CHEST_WO",
["ScheduledProcedureStepSequence"] = new List<Dictionary<string, object>> { step },
};
return new List<Dictionary<string, object>> { item };
}
private Dictionary<string, IEnumerable<string>> GetHeaders()
{
return Request.Headers.ToDictionary(
h => h.Key,
h => (IEnumerable<string>)h.Value.ToArray()
);
}
}

Response Format

public class ModalityWorklistRequestedWebhookResponse
{
public bool Authorized { get; set; }
public List<ModalityWorklistItem> Items { get; set; }
public string? Error { get; set; }
}
public class ModalityWorklistItem
{
public string PatientName { get; set; }
public string PatientID { get; set; }
public string PatientBirthDate { get; set; }
public string PatientSex { get; set; }
public string PatientSize { get; set; }
public string PatientWeight { get; set; }
public string Modality { get; set; }
public string AccessionNumber { get; set; }
public string StudyInstanceUID { get; set; }
public string RequestedProcedureDescription { get; set; }
public string StudyDescription { get; set; }
public string ProtocolName { get; set; }
public List<ModalityWorklistScheduledStep> ScheduledProcedureStepSequence { get; set; }
}
public class ModalityWorklistScheduledStep
{
public string ScheduledProcedureStepStartDate { get; set; }
public string ScheduledProcedureStepStartTime { get; set; }
public string ScheduledProcedureStepID { get; set; }
public string Modality { get; set; }
public string ScheduledProcedureStepDescription { get; set; }
}

The patient_study.enrichment_requested webhook is sent after Avara PACS receives the first C-STORE and seeds the study. This is a synchronous (soft) webhook — failures, timeouts, and invalid bodies are treated as empty enrichment and do not hard-fail study creation.

Avara asks your EHR/RIS for demographic and study header fields to merge into AutoScribe study creation. The SDK’s unwrap() method returns a typed PatientStudyEnrichmentRequestedWebhookEvent. There is no authorized field; HTTP 2xx plus JSON that parses is enough. Returning {} is valid. Avara merges per-field with DICOM light metadata, then defaults. Do not rely on this webhook for hard access control.

using Microsoft.AspNetCore.Mvc;
using Avara;
using Avara.Models.Webhooks;
[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
private readonly AvaraClient _client = new();
[HttpPost("avara")]
public async Task<IActionResult> HandleWebhook()
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var webhookEvent = _client.Webhooks.Unwrap(body, GetHeaders());
if (webhookEvent is PatientStudyEnrichmentRequestedWebhookEvent e)
{
var clinicId = e.Data.ClinicId;
var studyInstanceUid = e.Data.StudyInstanceUid;
var patientId = e.Data.PatientId;
var accessionNumber = e.Data.AccessionNumber;
// This is your internal business logic
var enrichment = EnrichPatientStudy(
clinicId, studyInstanceUid, patientId, accessionNumber
);
return Ok(enrichment);
}
return Ok();
}
// This is your internal business logic
private Dictionary<string, object> EnrichPatientStudy(
string clinicId,
string studyInstanceUid,
string? patientId,
string? accessionNumber
)
{
// Look up demographics and study headers in your EHR/RIS
// Return any subset of fields — or an empty dictionary
return new Dictionary<string, object>
{
["patientName"] = "Jane Doe",
["dateOfBirth"] = "1980-01-15",
["sex"] = "female",
["height"] = new Dictionary<string, object>
{
["value"] = 165,
["unit"] = "cm",
},
["weight"] = new Dictionary<string, object>
{
["value"] = 62,
["unit"] = "kg",
},
["mrn"] = "MRN12345",
["externalPatientId"] = "EHR-PT-1001",
["procedure"] = "CT CHEST WO CONTRAST",
["studyDescription"] = "CT CHEST WO CONTRAST",
["facilityName"] = "South Tampa Imaging",
["referringPhysicianName"] = "Dr. Alan Smith",
["studyDate"] = "2026-08-13",
["studyTime"] = "09:00",
["severity"] = "normal",
};
}
private Dictionary<string, IEnumerable<string>> GetHeaders()
{
return Request.Headers.ToDictionary(
h => h.Key,
h => (IEnumerable<string>)h.Value.ToArray()
);
}
}

Response Format

public class PatientStudyEnrichmentRequestedWebhookResponse
{
public string? PatientName { get; set; }
public string? DateOfBirth { get; set; }
public string? Sex { get; set; } // "male" | "female" | "other"
public Height? Height { get; set; }
public Weight? Weight { get; set; }
public string? Mrn { get; set; }
public string? ExternalPatientId { get; set; }
public string? Procedure { get; set; }
public string? StudyDescription { get; set; }
public string? FacilityName { get; set; }
public string? ReferringPhysicianName { get; set; }
public string? StudyDate { get; set; }
public string? StudyTime { get; set; }
public string? Severity { get; set; } // "normal" | "high" | "stat"
}
public class Height
{
public double Value { get; set; }
public string Unit { get; set; } // "in" | "cm"
}
public class Weight
{
public double Value { get; set; }
public string Unit { get; set; } // "lbs" | "kg"
}

The clinical_context.enrichment_requested webhook is sent when AutoScribe needs clinical context for a study (for example, a radiologist opens the study). This is a synchronous (soft) webhook — failures are treated as empty enrichment.

Avara asks your EHR for indication, technique, priors, and supporting documents. The SDK’s unwrap() method returns a typed ClinicalContextEnrichmentRequestedWebhookEvent. There is no authorized field. Returning {} is valid.

data.studyId on this event is a raw UUID v4, not a branded stu_… id. Most other study webhooks use stu_….

documents are verbatim text chunks. documentUrls are fetched and summarized by Avara — prefer https:// URLs.

using Microsoft.AspNetCore.Mvc;
using Avara;
using Avara.Models.Webhooks;
[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
private readonly AvaraClient _client = new();
[HttpPost("avara")]
public async Task<IActionResult> HandleWebhook()
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var webhookEvent = _client.Webhooks.Unwrap(body, GetHeaders());
if (webhookEvent is ClinicalContextEnrichmentRequestedWebhookEvent e)
{
var clinicId = e.Data.ClinicId;
var studyInstanceUid = e.Data.StudyInstanceUid;
var studyId = e.Data.StudyId;
var externalPatientId = e.Data.ExternalPatientId;
var mrn = e.Data.Mrn;
// This is your internal business logic
var enrichment = EnrichClinicalContext(
clinicId, studyInstanceUid, studyId, externalPatientId, mrn
);
return Ok(enrichment);
}
return Ok();
}
// This is your internal business logic
private Dictionary<string, object> EnrichClinicalContext(
string clinicId,
string studyInstanceUid,
string studyId,
string? externalPatientId,
string? mrn
)
{
// Pull indication, technique, priors, and documents from your EHR
// Return any subset of fields — or an empty dictionary
return new Dictionary<string, object>
{
["clinicalIndication"] = "Shortness of breath, evaluate for PE",
["technologistTechnique"] = "Helical CT chest, 1.25 mm, IV contrast",
["priorReports"] = new List<Dictionary<string, object>>
{
new Dictionary<string, object>
{
["studyDescription"] = "CT CHEST WO CONTRAST",
["modality"] = "CT",
["studyDate"] = "2025-11-02",
["reportText"] = "No acute cardiopulmonary process.",
},
},
};
}
private Dictionary<string, IEnumerable<string>> GetHeaders()
{
return Request.Headers.ToDictionary(
h => h.Key,
h => (IEnumerable<string>)h.Value.ToArray()
);
}
}

Response Format

public class ClinicalContextEnrichmentRequestedWebhookResponse
{
public string? ClinicalIndication { get; set; }
public string? TechnologistTechnique { get; set; }
public List<string>? TechnologistNotes { get; set; }
public List<ClinicalContextEnrichmentPriorReport>? PriorReports { get; set; }
public List<ClinicalContextEnrichmentDocument>? Documents { get; set; }
public List<ClinicalContextEnrichmentDocumentUrl>? DocumentUrls { get; set; }
}
public class ClinicalContextEnrichmentPriorReport
{
public string? ExternalStudyId { get; set; }
public string? StudyDescription { get; set; }
public string? Modality { get; set; }
public string? StudyDate { get; set; }
public string ReportText { get; set; }
}
public class ClinicalContextEnrichmentDocument
{
public string FileName { get; set; }
public List<string> Content { get; set; }
}
public class ClinicalContextEnrichmentDocumentUrl
{
public string? FileName { get; set; }
public string Url { get; set; }
}