Table of Contents

Create a handler

Send the drops' data to your own system. A handler is a class that inherits from WebhookHandlerBase, compiled into a DLL that the application loads at startup. You do not have to modify or rebuild the webhook.

1. Create the project

A class library for the same platform as the webhook, referencing its SESARWebHook.Core.NetCore library and the Secure Exchanges SDK, in the version the webhook uses (see SESARWebHook.Core.NetCore.csproj):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0-windows</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <!-- Provided by the webhook at run time: do not copy them with the handler -->
    <ProjectReference Include="..\SESARWebhookCore\SESARWebHook.Core.NetCore\SESARWebHook.Core.NetCore.csproj" Private="false" />
    <PackageReference Include="SecureExchanges.NetCore" Version="26.4.20.2" ExcludeAssets="runtime" />
  </ItemGroup>
</Project>

2. Write the class

using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using SecureExchangesSDK.Models.Messenging;
using SESARWebHook.Core;
using SESARWebHook.Core.Auth;
using SESARWebHook.Core.Models;

namespace Example.Handlers
{
  public class CasesHandler : WebhookHandlerBase
  {
    // A single HttpClient for the whole life of the application.
    private static readonly HttpClient Http = new HttpClient();
    private ApiKeyHelper _auth;
    private string _endpoint;

    public override string HandlerId => "my-cases";
    public override string DisplayName => "My cases";

    public override void Initialize(Dictionary<string, string> settings)
    {
      base.Initialize(settings);
      _endpoint = GetRequiredSetting("ApiUrl");                     // appsettings.json
      _auth = ApiKeyHelper.AsBearer(GetRequiredSetting("ApiKey"));  // secrets file
    }

    public override async Task<IntegrationResult> ProcessAsync(StoreManifest manifest, WebhookContext context)
    {
      var item = new
      {
        tracking = GetTrackingId(manifest),  // stable identifier: used to discard duplicates
        subject = GetSubject(manifest),
        files = GetFileCount(manifest)
      };

      using var request = new HttpRequestMessage(HttpMethod.Post, _endpoint)
      {
        Content = new StringContent(JsonSerializer.Serialize(item), Encoding.UTF8, "application/json")
      };
      _auth.ApplyTo(request);

      using var response = await Http.SendAsync(request);
      return response.IsSuccessStatusCode
        ? IntegrationResult.Ok("Case sent", HandlerId)
        : IntegrationResult.Fail("Refused by the target system", $"HTTP {(int)response.StatusCode}", HandlerId);
    }
  }
}

The webhook keeps a class if it is public, not abstract, implements IWebhookHandler and has a public parameterless constructor. Its HandlerId is part of the address: choose it stable, in lowercase, without spaces.

Members to know

Member Role
HandlerId, DisplayName Required. Description and Version are optional.
Initialize(settings) Called once, the first time the handler is used. Call base.Initialize(settings) first.
ProcessAsync(manifest, context) Called for each drop. Return IntegrationResult.Ok or IntegrationResult.Fail.
ValidateConfigurationAsync, TestConnectionAsync Called by POST /api/handlers/{id}/test. Return true by default.
GetSetting, GetRequiredSetting, GetSettingBool, GetSettingInt Read the settings and secrets. GetRequiredSetting throws an exception if the value is missing, without quoting it.
GetSubject, GetRecipientEmails, GetFileCount, GetTrackingId, IsReply, DecodeBase64 Read the manifest.

The full list is in the .NET reference.

Design rules

  • One instance for the whole life of the application. The handler is created once, then reused, including for simultaneous requests: do not keep the state of a drop in a field.
  • A light constructor. It runs when the DLL is loaded and on each GET /api/handlers. Put the setup in Initialize.
  • Less than 60 seconds per drop, and duplicates accepted: see Configure SESAR.
  • No personal information in logs or in the IntegrationResult, which is returned as is in the HTTP response. Do not use context.RawPayload outside the processing: it contains the decrypted manifest.
  • Exceptions thrown in ProcessAsync are caught and returned as a failure, with their details. Those from Initialize are not: the request receives a 500 error.

3. Configure

Settings go in appsettings.json, secrets in the secrets file. The handler receives both, merged, in its Settings.

{
  "Handler": {
    "my-cases": { "ApiUrl": "https://api.example.com/cases" }
  }
}
{
  "Handlers": {
    "my-cases": { "Secrets": { "ApiKey": "<your API key>" } }
  }
}

4. Deploy

  1. Build the project in Release.
  2. Copy the handler DLL, and its own dependencies only, to the handlers folder: Handlers, in the application folder, or the one set by HandlersPath. Subfolders are not read.
  3. Restart the application. A DLL already loaded stays in memory until the restart, and POST /api/handlers/rescan only reads the default folder again.
  4. Check that it appears in GET /api/handlers, then test it with POST /api/handlers/my-cases/test.
  5. In SESAR, set the address https://<server>/api/webhook/handler/my-cases.

If two DLLs declare the same HandlerId, the last one loaded wins, without warning.

Warning

A handler runs in the webhook's process, with all its rights, and can read all the application's secrets. Only drop in the handlers folder DLLs whose code you know, and reserve write access to this folder to administrators.

Built-in connector: for those who maintain their own version of the webhook

A built-in connector implements IIntegrationConnector. It is not loaded from a folder: you must add it to the project, then to StartupConfig.InitializeConnectors in Program.cs (ConnectorRegistry.RegisterConnector<T>()), and rebuild the webhook. The SESARWebHook.Connectors.Template.NetCore project serves as a model; its comments still mention Web.config, replaced by appsettings.json.

Prefer a handler: it survives webhook updates without merging code.