Skip to content

Webhook Development

Learn how to receive and process webhook events from external providers during local development and in production.

Property Value
Goal Receive webhooks from external providers
Prerequisites SAIF CLI installed, Azure CLI logged in
Time Estimate 15-30 minutes
Difficulty Beginner

Overview

Webhooks allow external services to send real-time notifications to your application when events occur. Forge provides built-in support for webhook development using Azure Dev Tunnels to expose your local endpoints to the internet.

Key Capabilities:

  • 🔗 DevTunnel Integration - Expose local endpoints publicly for webhook testing
  • 🛡️ APIM Protection - Validate webhook signatures/JWTs in production
  • 🔄 Dev/Prod Parity - Same endpoint paths work locally and in Azure

Quick Start

1. Create an Experience API with Webhook Support

Run the SAIF CLI interactively and select Yes for webhook support when prompted:

saif new saif-api-exp

The CLI will prompt you for project details including Webhook Support - select Yes to enable webhook features.

Alternatively, use non-interactive mode with all required parameters:

saif new saif-api-exp \
  --name MyWebhookApp \
  --business_domain it \
  --owner YourTeam \
  --has_webhook true

This generates:

Component Purpose
WebhookEndpoints.cs Generic webhook receiver at /api/webhooks
WebhookResourceBuilder.g.cs AppHost DevTunnel configuration
appsettings.webhook.json Webhook-specific settings

2. Enable External HTTP Endpoints

In your ApiResourceBuilder.g.cs, add .WithExternalHttpEndpoints() to allow DevTunnel access:

src/YourApp.AppHost/ResourceBuilders/ApiResourceBuilder.g.cs
var backend = builder
    .AddProject<Projects.YourApp>("your-project-id")
    .WithExternalHttpEndpoints();  // Required for DevTunnel to access the default https endpoint

The WebhookResourceBuilder uses the default https endpoint from your launchSettings.json.

3. Run Your Application

aspire run

4. Get Your Tunnel URL

  1. Open the Aspire Dashboard (usually https://localhost:17281)
  2. Find the webhook-tunnel resource
  3. Copy the public endpoint URL (e.g., https://abc123.usw2.devtunnels.ms)

5. Configure Your Webhook Provider

Point your webhook provider to:

https://{tunnel-url}/api/webhooks

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│ Local Development                                                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Webhook Provider ──► DevTunnel ──► Your API (/api/webhooks)               │
│                        (public)      (localhost)                            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ Production                                                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Webhook Provider ──► Front Door ──► APIM ──► App Service                  │
│                                        │       (/api/webhooks)              │
│                                        │                                    │
│                                   JWT/Signature                             │
│                                   Validation                                │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Implementing Webhook Handlers

The generated WebhookEndpoints.cs provides a generic handler. Customize it for your webhook provider:

// src/YourApp/Endpoints/WebhookEndpoints.cs

private static async Task<Results<Ok<WebhookResponse>, BadRequest<ProblemDetails>>> HandleWebhook(
    [FromHeader(Name = "X-Webhook-Event")] string? eventType,
    [FromHeader(Name = "X-Webhook-Signature")] string? signature,
    [FromHeader(Name = "X-Webhook-Timestamp")] string? timestamp,
    [FromHeader(Name = "X-Webhook-Id")] string? webhookId,
    HttpRequest request,
    ILogger<WebhookResponse> logger,
    CancellationToken cancellationToken)
{
    // 1. Read the raw body
    using var reader = new StreamReader(request.Body);
    var rawBody = await reader.ReadToEndAsync(cancellationToken);

    // 2. Validate signature (provider-specific)
    if (!ValidateSignature(rawBody, signature, timestamp))
    {
        return TypedResults.BadRequest(new ProblemDetails
        {
            Title = "Invalid signature"
        });
    }

    // 3. Check idempotency
    if (await _webhookStore.HasProcessed(webhookId))
    {
        return TypedResults.Ok(new WebhookResponse { Received = true, Duplicate = true });
    }

    // 4. Process the event
    await ProcessEvent(eventType, rawBody);

    // 5. Mark as processed
    await _webhookStore.MarkProcessed(webhookId);

    return TypedResults.Ok(new WebhookResponse { Received = true });
}

Adding Webhook Support to Existing Projects

If you have an existing Experience API and want to add webhook support, run:

saif new saif-feature-webhook --name YourAppName

This generates:

File Purpose
src/YourApp/Endpoints/WebhookEndpoints.cs Webhook receiver endpoint
src/YourApp.AppHost/ResourceBuilders/WebhookResourceBuilder.g.cs DevTunnel extension method
src/YourApp.AppHost/appsettings.webhook.json Webhook-specific settings

After running the template, you must manually update your AppHost to enable the DevTunnel:

1. Update AppHost.cs

Add the AddWebhookTunnel call to your AppHost:

AppHost.cs
var api = builder.AddApi();  // Your existing API resource builder

// Add webhook tunnel for local development
builder.AddWebhookTunnel(api);

await builder.Build().RunAsync();

2. Register the Endpoint

Add the webhook endpoints to your API's Program.cs:

Program.cs
var app = builder.Build();

// ... other middleware

app.MapWebhookEndpoints();  // Add this line

app.Run();

Production Deployment

APIM Policy Configuration

For production, APIM validates webhook requests before they reach your API. Configure your vars.yml with the appropriate api_policy_type:

# infra/web/vars.yml

application_name: your-app-name
application_type: Api
api_type: Experience
owner: YourTeam
project_id: your-project-id

# Enable webhook-specific APIM policy
api_policy_type: standard
Policy Type Description
standard Default Okta/Entra routing (no webhook support)
filevine Filevine JWT validation with corp header override
subscription_key APIM subscription key auth for legacy server-to-server access

See Provider-Specific Guides for detailed configuration.


Security Considerations

Aspect Local Development Production
Authentication None (DevTunnel is public) APIM validates JWT/signature
Signature Validation Optional (for testing) Required
Idempotency Recommended Required
Rate Limiting None APIM policy

Best Practices

  1. Always validate signatures in production - Webhook providers sign requests; verify them
  2. Implement idempotency - Webhooks may be retried; use the webhook ID to prevent duplicate processing
  3. Respond quickly - Return 200 OK fast, then process asynchronously if needed
  4. Log everything - Log webhook IDs, timestamps, and event types for debugging

Troubleshooting

DevTunnel Not Starting

Error: Failed to create DevTunnel

Solution: Ensure you're logged into Azure CLI:

az login
devtunnel user login

Webhook Not Received

  1. Check the Aspire Dashboard for the tunnel URL
  2. Verify your webhook provider is configured with the correct URL
  3. Check the tunnel logs in the Aspire Dashboard

Signature Validation Failing

  1. Check that you're using the raw request body (not parsed JSON)
  2. Verify the signature algorithm matches your provider's documentation
  3. Check for clock skew between your system and the webhook provider

Downstream API Returning 400/403/500

When your webhook handler calls downstream APIs and receives errors:

  1. Check error details - Catch ApiException and log ResponseStatusCode:
catch (ApiException ex)
{
    logger.LogError(ex, "API error. StatusCode: {StatusCode}", ex.ResponseStatusCode);
}
  1. 403 Forbidden - Usually missing app role assignment:
  2. Symptom: Token is valid but lacks required roles claim
  3. Solution: Verify the downstream API's config.yml includes your app in authorized_apps with app_roles:
    # infra/auth/corp/config.yml (downstream API)
    authorized_apps:
      - project_id: it-api-exp-yourapp  # Your calling app
        app_roles:
          - App.Read                     # Required role(s)
        scopes: user_impersonation       # Required for pre-authorization
    
  4. Verify: Run the downstream API's auth pipeline to create the app role assignment
  5. Quick check: Azure Portal → Enterprise apps → Downstream API → Users and groups → Verify your app has the role

  6. 400 Bad Request - Check the request payload matches the API's expected schema

  7. "No error factory registered" - The Kiota client is missing error type mappings. Remove <IncludePath> from your KiotaReference to include all models

  8. SAIF package version mismatch

  9. Symptom: MissingMethodException for ScopeBuilder or similar
  10. Solution: Ensure all SAIF.Platform.* packages use the same version in Directory.Packages.props

Provider-Specific Guides