-
Notifications
You must be signed in to change notification settings - Fork 1
Working with Webhooks
This guide explains how to use OpenAPI 3.1 webhooks with the source generator.
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
webhooksroot element, which is only available in OpenAPI 3.1+. For OpenAPI 3.0 specs, use regular paths instead.
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 |
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- Each webhook has a name (e.g.,
newPet,orderShipped) - Use
operationIdto control the generated class names (defaults toOn{WebhookName}) -
requestBodydefines the webhook payload (optional β webhooks without a body just signal an event) -
responsesdefine the HTTP status codes your handler can return - Webhooks are typically
post, but any HTTP method is supported
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);
}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!;
}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);
}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.2query:operation, or a customadditionalOperationsverb such asLINK, is now registered on that verb viaMapMethodsrather than being silently downgraded toPOST: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.
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();
}
}
}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();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 |
| 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.
{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
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.
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 acknowledgedpublic interface IOnSystemHeartbeatWebhookHandler
{
Task<OnSystemHeartbeatWebhookResult> ExecuteAsync(
CancellationToken cancellationToken = default);
}- Working with Security β Protect webhook endpoints with authentication
- Working with Validations β Add FluentValidation to webhook parameters
- Getting Started with Basic β Set up your first project
π Home
- πΌ FAQ Business Value
- π Getting Started with Basic
- π οΈ Getting Started with CLI
- π Migration Guide
- β¬οΈ Upgrading to v2
- π Working with OpenAPI
- π³οΈ Working with Nullability
- π οΈ Working with CLI
- π How-To Guides
- π Working with Security
- π¦ Working with Rate Limiting
- π Working with Resilience
- ποΈ Working with Caching
- π’ Working with Versioning
- β Working with Validations
- π Working with Webhooks
- βοΈ Working with Aspire
- π£οΈ Working with Endpoint Definitions
- π Working with Multi-Part Specs
- π§ͺ Working with Code Coverage
- π Working with C# Client
- π§ͺ Working with C# Client Testing
- π¦ Working with TypeScript Client
- πͺ Showcase Demo
- π§ͺ Working with E2E Testing
- βοΈ Working with Configuration
- π Marker Files
- π API Reference
- π Analyzer Rules
- β FAQ and Troubleshooting
- πΊοΈ Roadmap
- π§ Development Notes
- π¦ GitHub Repository
- π₯ NuGet Package
- π Report Issues