# 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](https://learn.microsoft.com/azure/developer/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:

```bash
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:

```bash
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:

```csharp title="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

```bash
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:

```csharp
// 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:

```bash
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:

```csharp title="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`:

```csharp title="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`:

```yaml
# 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](#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:

```bash
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`:

   ```csharp
   catch (ApiException ex)
   {
       logger.LogError(ex, "API error. StatusCode: {StatusCode}", ex.ResponseStatusCode);
   }
   ```

2. **403 Forbidden** - Usually missing app role assignment:
   - **Symptom:** Token is valid but lacks required `roles` claim
   - **Solution:** Verify the downstream API's `config.yml` includes your app in `authorized_apps` with `app_roles`:
     ```yaml
     # 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
     ```
   - **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

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

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

5. **SAIF package version mismatch**
   - **Symptom:** `MissingMethodException` for `ScopeBuilder` or similar
   - **Solution:** Ensure all `SAIF.Platform.*` packages use the same version in `Directory.Packages.props`

---

## Provider-Specific Guides

- [Filevine Webhooks](filevine.md) - Legal case management webhooks with JWT validation

---

## Related Documentation

- [Aspire DevTunnels](https://learn.microsoft.com/dotnet/aspire/fundamentals/dev-tunnels)
- [APIM Webhook Security](https://learn.microsoft.com/azure/api-management/validate-jwt-policy)
- [Calling APIs](../calling-apis.md) - For calling downstream APIs from your webhook handler (needs machine-to-machine)
- [DevTunnels Webhook Foundry Example](../../../foundry/devtunnels-webhook.md) - Working example of a public webhook receiver with DevTunnels
