Skip to content

Working with Webhooks

David Kallesen edited this page Sep 10, 2026 · 2 revisions

Working with Webhooks

This guide explains how to use OpenAPI 3.1 webhooks with the source generator.

Overview

Webhooks are API callbacks β€” instead of your server responding to client requests, the API sends data to your endpoint when an event occurs. The source generator creates all the infrastructure you need to receive and handle webhook callbacks.

Requires OpenAPI 3.1 β€” Webhooks use the webhooks root element, which is only available in OpenAPI 3.1+. For OpenAPI 3.0 specs, use regular paths instead.

What Gets Generated

For each webhook definition, the generator creates:

File Purpose
IOn{Name}WebhookHandler Handler interface to implement
On{Name}WebhookParameters Request body parameter class
On{Name}WebhookResult Type-safe result with factory methods
WebhookEndpointExtensions Minimal API endpoint registration
WebhookServiceCollectionExtensions DI registration template

Defining Webhooks

OpenAPI YAML

Webhooks are defined at the document root level (not inside paths):

openapi: 3.1.1
info:
  title: My API
  version: 1.0.0

paths:
  # ... your regular API paths ...

webhooks:
  newPet:
    post:
      operationId: onNewPet
      summary: A new pet was added
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
      responses:
        '200':
          description: Webhook acknowledged
        '400':
          description: Bad request

  orderShipped:
    post:
      operationId: onOrderShipped
      summary: An order was shipped
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShippingNotification'
      responses:
        '200':
          description: Notification acknowledged
        '202':
          description: Accepted for processing

components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string

    ShippingNotification:
      type: object
      required:
        - orderId
        - trackingNumber
      properties:
        orderId:
          type: string
          format: uuid
        trackingNumber:
          type: string
        carrier:
          type: string

Key Rules

  • Each webhook has a name (e.g., newPet, orderShipped)
  • Use operationId to control the generated class names (defaults to On{WebhookName})
  • requestBody defines the webhook payload (optional β€” webhooks without a body just signal an event)
  • responses define the HTTP status codes your handler can return
  • Webhooks are typically post, but any HTTP method is supported

Generated Code

Handler Interface

namespace MyApi.Generated.Webhooks.Handlers;

[GeneratedCode("Atc.Rest.Api.SourceGenerator", "1.0.0")]
public interface IOnNewPetWebhookHandler
{
    /// <summary>
    /// A new pet was added.
    /// </summary>
    Task<OnNewPetWebhookResult> ExecuteAsync(
        OnNewPetWebhookParameters parameters,
        CancellationToken cancellationToken = default);
}

Parameter Class

namespace MyApi.Generated.Webhooks.Parameters;

[GeneratedCode("Atc.Rest.Api.SourceGenerator", "1.0.0")]
public sealed class OnNewPetWebhookParameters
{
    /// <summary>
    /// The webhook payload.
    /// </summary>
    [FromBody]
    [Required]
    public Pet Payload { get; init; } = default!;
}

Result Class

Factory methods are generated from your responses definitions:

namespace MyApi.Generated.Webhooks.Results;

[GeneratedCode("Atc.Rest.Api.SourceGenerator", "1.0.0")]
public sealed class OnNewPetWebhookResult : IResult
{
    private readonly IResult innerResult;

    private OnNewPetWebhookResult(IResult innerResult)
    {
        this.innerResult = innerResult;
    }

    /// <summary>
    /// 200 - Webhook acknowledged
    /// </summary>
    public static OnNewPetWebhookResult Ok()
        => new(TypedResults.Ok());

    /// <summary>
    /// 400 - Bad request
    /// </summary>
    public static OnNewPetWebhookResult BadRequest()
        => new(TypedResults.BadRequest());

    public Task ExecuteAsync(HttpContext httpContext)
        => innerResult.ExecuteAsync(httpContext);
}

Endpoint Registration

All webhooks are registered under a route group (default: /webhooks):

namespace MyApi.Generated.Webhooks.Endpoints;

[GeneratedCode("Atc.Rest.Api.SourceGenerator", "1.0.0")]
public static class WebhookEndpointExtensions
{
    public static IEndpointRouteBuilder MapMyApiWebhooks(
        this IEndpointRouteBuilder endpoints)
    {
        var webhooksGroup = endpoints.MapGroup("/webhooks")
            .WithTags("Webhooks");

        webhooksGroup
            .MapPost(
                "/new-pet",
                async (
                    [AsParameters] OnNewPetWebhookParameters parameters,
                    IOnNewPetWebhookHandler handler,
                    CancellationToken cancellationToken
                ) =>
                {
                    return await handler.ExecuteAsync(
                        parameters,
                        cancellationToken);
                })
            .WithSummary("A new pet was added")
            .WithName("onNewPet")
            .Produces(StatusCodes.Status200OK)
            .ProducesProblem(StatusCodes.Status400BadRequest);

        // ... more webhooks ...

        return endpoints;
    }
}

πŸ’‘ Non-standard verbs are honoured. Webhooks are almost always post:, but one declared with an OpenAPI 3.2 query: operation, or a custom additionalOperations verb such as LINK, is now registered on that verb via MapMethods rather than being silently downgraded to POST:

webhooksGroup
    .MapMethods(
        "/resource-lookup",
        new[] { "QUERY" },
        async (

The standard verbs keep their dedicated MapPost / MapGet / … as shown above. See Working with OpenAPI β†’ HTTP QUERY and Custom Verbs.

Implementing Handlers

Create your handler implementation in the Domain project:

using MyApi.Generated.Webhooks.Handlers;
using MyApi.Generated.Webhooks.Parameters;
using MyApi.Generated.Webhooks.Results;

namespace MyApi.Domain.Handlers.Webhooks;

public sealed class OnNewPetWebhookHandler : IOnNewPetWebhookHandler
{
    private readonly ILogger<OnNewPetWebhookHandler> logger;
    private readonly IPetRepository repository;

    public OnNewPetWebhookHandler(
        ILogger<OnNewPetWebhookHandler> logger,
        IPetRepository repository)
    {
        this.logger = logger;
        this.repository = repository;
    }

    public async Task<OnNewPetWebhookResult> ExecuteAsync(
        OnNewPetWebhookParameters parameters,
        CancellationToken cancellationToken = default)
    {
        var pet = parameters.Payload;

        logger.LogInformation("Received new pet webhook: {PetName} (ID: {PetId})", pet.Name, pet.Id);

        try
        {
            await repository.SyncPetAsync(pet, cancellationToken);
            return OnNewPetWebhookResult.Ok();
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to process new pet webhook");
            return OnNewPetWebhookResult.BadRequest();
        }
    }
}

Wiring Up in Program.cs

var builder = WebApplication.CreateBuilder(args);

// Register webhook handlers
builder.Services.AddScoped<IOnNewPetWebhookHandler, OnNewPetWebhookHandler>();
builder.Services.AddScoped<IOnOrderShippedWebhookHandler, OnOrderShippedWebhookHandler>();

var app = builder.Build();

// Map webhook endpoints
app.MapMyApiWebhooks();

// Map regular API endpoints
app.MapApiEndpoints();

app.Run();

Configuration

Marker File Options

In your .atc-rest-api-server marker file:

{
  "generateWebhooks": true,
  "webhookBasePath": "/webhooks"
}
Property Type Default Description
generateWebhooks boolean true Enable/disable webhook generation
webhookBasePath string "/webhooks" Route prefix for webhook endpoints

Naming Convention

YAML Interface Parameters Result Route
newPet IOnNewPetWebhookHandler OnNewPetWebhookParameters OnNewPetWebhookResult /webhooks/new-pet
orderShipped IOnOrderShippedWebhookHandler OnOrderShippedWebhookParameters OnOrderShippedWebhookResult /webhooks/order-shipped
userActivity IOnUserActivityWebhookHandler OnUserActivityWebhookParameters OnUserActivityWebhookResult /webhooks/user-activity

The operationId overrides the default naming. If you specify operationId: handleNewPet, the handler becomes IHandleNewPetWebhookHandler.

Namespace Structure

{ProjectName}.Generated.Webhooks.Handlers/     β€” Handler interfaces
{ProjectName}.Generated.Webhooks.Parameters/    β€” Parameter classes
{ProjectName}.Generated.Webhooks.Results/       β€” Result classes
{ProjectName}.Generated.Webhooks.Endpoints/     β€” Endpoint registration
{ProjectName}.Generated.DependencyInjection/    β€” DI registration

Response Status Codes

The result class factory methods are generated from your responses definition:

Status Code Factory Method TypedResults Equivalent
200 Ok() TypedResults.Ok()
201 Created() TypedResults.Created()
202 Accepted() TypedResults.Accepted(null)
204 NoContent() TypedResults.NoContent()
400 BadRequest() TypedResults.BadRequest()
401 Unauthorized() TypedResults.Unauthorized()
403 Forbidden() TypedResults.Forbid()
404 NotFound() TypedResults.NotFound()
409 Conflict() TypedResults.Conflict()
Other StatusCode{N}() TypedResults.StatusCode(N)

If no responses are defined, a default Ok() method is generated.

Webhooks Without a Payload

For event-only webhooks (no request body), the handler interface omits the parameters class:

webhooks:
  systemHeartbeat:
    post:
      operationId: onSystemHeartbeat
      summary: System heartbeat signal
      responses:
        '200':
          description: Heartbeat acknowledged
public interface IOnSystemHeartbeatWebhookHandler
{
    Task<OnSystemHeartbeatWebhookResult> ExecuteAsync(
        CancellationToken cancellationToken = default);
}

Next Steps

🏠 Home

πŸ’Ό Why This Tool?

πŸ“– Getting Started

βš™οΈ Features

🌐 Frontend

πŸ“‹ Reference


πŸ”— Resources

Clone this wiki locally