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:
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:
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¶
4. Get Your Tunnel URL¶
- Open the Aspire Dashboard (usually
https://localhost:17281) - Find the webhook-tunnel resource
- Copy the public endpoint URL (e.g.,
https://abc123.usw2.devtunnels.ms)
5. Configure Your Webhook Provider¶
Point your webhook provider to:
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:
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:
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:
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¶
- Always validate signatures in production - Webhook providers sign requests; verify them
- Implement idempotency - Webhooks may be retried; use the webhook ID to prevent duplicate processing
- Respond quickly - Return
200 OKfast, then process asynchronously if needed - Log everything - Log webhook IDs, timestamps, and event types for debugging
Troubleshooting¶
DevTunnel Not Starting¶
Solution: Ensure you're logged into Azure CLI:
Webhook Not Received¶
- Check the Aspire Dashboard for the tunnel URL
- Verify your webhook provider is configured with the correct URL
- Check the tunnel logs in the Aspire Dashboard
Signature Validation Failing¶
- Check that you're using the raw request body (not parsed JSON)
- Verify the signature algorithm matches your provider's documentation
- 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:
- Check error details - Catch
ApiExceptionand logResponseStatusCode:
catch (ApiException ex)
{
logger.LogError(ex, "API error. StatusCode: {StatusCode}", ex.ResponseStatusCode);
}
- 403 Forbidden - Usually missing app role assignment:
- Symptom: Token is valid but lacks required
rolesclaim - Solution: Verify the downstream API's
config.ymlincludes your app inauthorized_appswithapp_roles: - Verify: Run the downstream API's auth pipeline to create the app role assignment
-
Quick check: Azure Portal → Enterprise apps → Downstream API → Users and groups → Verify your app has the role
-
400 Bad Request - Check the request payload matches the API's expected schema
-
"No error factory registered" - The Kiota client is missing error type mappings. Remove
<IncludePath>from yourKiotaReferenceto include all models -
SAIF package version mismatch
- Symptom:
MissingMethodExceptionforScopeBuilderor similar - Solution: Ensure all
SAIF.Platform.*packages use the same version inDirectory.Packages.props
Provider-Specific Guides¶
- Filevine Webhooks - Legal case management webhooks with JWT validation
Related Documentation¶
- Aspire DevTunnels
- APIM Webhook Security
- Calling APIs - For calling downstream APIs from your webhook handler (needs machine-to-machine)