---
title: C# | Avara
description: Handle Viewer webhooks in C#.
---

Viewer only implements three webhooks. Read [What webhooks should I configure](/integration/viewer/what-webhooks-should-i-configure/index.md) first — it maps each integration profile to the endpoints you should set. Leave unused endpoints blank. Report delivery, patient/study enrichment, and clinical context enrichment are AutoScribe, not Viewer.

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

## Webhook security

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](/integration/viewer/api-config/index.md) (**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**.

You can use `unsafeUnwrap()` instead of `unwrap()` to skip signature verification, but this is not recommended for production as it bypasses security checks.

```
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 ModalityWorklistRequestedWebhookEvent e:
            {
                // Handle modality worklist request
                var clinicId = e.Data.ClinicId;
                break;
            }
        }


        return Ok();
    }


    private Dictionary<string, IEnumerable<string>> GetHeaders()
    {
        return Request.Headers.ToDictionary(
            h => h.Key,
            h => (IEnumerable<string>)h.Value.ToArray()
        );
    }
}
```

## Study Image Access

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; }
}
```

## Secondary Capture Upload

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; }
}
```

## Modality Worklist

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; }
}
```
