# Filevine Webhook Integration

Configure your Experience API to receive webhooks from Filevine's legal case management platform.

| Property          | Value                                                             |
| ----------------- | ----------------------------------------------------------------- |
| **Goal**          | Receive and process Filevine webhook events                       |
| **Prerequisites** | Filevine account, completed [Webhook Development](index.md) setup |
| **Time Estimate** | 15-30 minutes (after base setup)                                  |
| **Difficulty**    | Intermediate                                                      |

---

## Overview

[Filevine](https://www.filevine.com/) is a legal case management SaaS platform that sends webhook notifications when events occur (e.g., project created, document added, task completed).

**Filevine Webhook Characteristics:**

| Aspect                   | Details                                        |
| ------------------------ | ---------------------------------------------- |
| **Authentication**       | JWT Bearer token from Filevine Identity Server |
| **Identity Server (US)** | `https://identity.filevine.com`                |
| **JWT Audience**         | `filevine-v2-webhooks`                         |
| **Required Scope**       | `filevine-v2-webhooks-access`                  |
| **Signature**            | JWT signature validated via OIDC discovery     |

---

## Prerequisites

!!! warning "Complete Base Setup First"
**STOP** - Before continuing, you MUST complete the [Webhook Development Quick Start](index.md#quick-start):

    - [ ] Run `saif new saif-api-exp` with webhook support enabled
    - [ ] Add `builder.AddWebhookTunnel(api)` to your AppHost
    - [ ] Verify the webhook-tunnel appears in Aspire Dashboard

    **Do not proceed** until your DevTunnel URL is working.

Once you have your DevTunnel URL, continue with the Filevine-specific configuration below.

---

## Step 1: Configure Filevine Webhook

In Filevine's webhook settings, configure:

- **URL:** `https://{tunnel-url}/api/webhooks`
- **Events:** Select the events you want to receive

> ⚠️ **Local Development Only:** The DevTunnel URL changes when sessions expire. For production, use your APIM endpoint.

---

## Step 2: Customize Webhook Handler

The template generates a generic `WebhookEndpoints.cs`. Customize it to handle Filevine-specific headers and payload structure:

```csharp
// src/YourApp/Endpoints/WebhookEndpoints.cs

public static class WebhookEndpoints
{
    public static IEndpointRouteBuilder MapWebhookEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/webhooks")
            .WithTags("Webhooks");

        group.MapPost("/", HandleFilevineWebhook)
            .WithName("ReceiveFilevineWebhook")
            .WithSummary("Receive Filevine webhook events");

        return app;
    }

    private static async Task<IResult> HandleFilevineWebhook(
        [FromHeader(Name = "x-fv-messageid")] string? messageId,
        [FromHeader(Name = "x-fv-timestamp")] string? timestamp,
        HttpRequest request,
        ILogger<FilevineWebhookResponse> logger,
        CancellationToken cancellationToken)
    {
        using var reader = new StreamReader(request.Body);
        var rawBody = await reader.ReadToEndAsync(cancellationToken);

        var payload = JsonSerializer.Deserialize<FilevineWebhookPayload>(rawBody,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

        logger.LogInformation(
            "Filevine {Object}.{Event} received. NoteId: {NoteId}, ProjectId: {ProjectId}",
            payload?.Object, payload?.Event, payload?.ObjectId?.NoteId, payload?.ProjectId);

        // TODO: Implement idempotency check using messageId
        // TODO: Route to handlers based on Object and Event type

        return Results.Ok(new { received = true, messageId });
    }
}
```

### Register the Endpoint

Add the endpoint mapping in `Program.cs`:

```csharp
// src/YourApp/Program.cs

var app = builder.Build();

app.MapGet("/", () => new { Message = "Hello World!", ServerTime = DateTime.UtcNow });

// Register Filevine webhook endpoints
app.MapWebhookEndpoints();

app
    .UseServiceDefaults()
    .UseJwtBearerServices();

app.Run();
```

If `MapWebhookEndpoints()` isn't recognized, add the namespace to `GlobalUsings.cs`:

```csharp
// src/YourApp/GlobalUsings.cs

global using YourApp.Endpoints;
```

!!! warning "Required Step"
Without `app.MapWebhookEndpoints()`, the `/api/webhooks` route won't exist at runtime and requests will return 404.

### Filevine Headers

| Header           | Description                                      |
| ---------------- | ------------------------------------------------ |
| `Authorization`  | `Bearer {JWT}` - Validated by APIM in production |
| `x-fv-messageid` | Unique message ID for idempotency                |
| `x-fv-timestamp` | ISO timestamp (e.g., `2024-01-15T10:30:00.000Z`) |

---

## Step 3: Define Webhook Endpoint in TypeSpec

Define the webhook endpoint contract in your TypeSpec file to generate the OpenAPI specification:

```typespec
// src/YourApp.TypeSpec/main.tsp

import "@typespec/http";
import "@typespec/rest";
import "@typespec/openapi";
import "@typespec/openapi3";
import "@typespec/versioning";
import "@saif/platform-typespec";

using TypeSpec.Http;
using TypeSpec.Rest;
using TypeSpec.OpenAPI;
using TypeSpec.Versioning;
using SAIF.Platform;

@service(#{ title: "YourApp Experience API" })
@versioned(Versions)
namespace YourApp;

enum Versions {
  v1,
}

// Filevine webhook endpoint
@route("/api/webhooks")
@added(Versions.v1)
interface Webhooks {
  @post
  @summary("Receive Filevine webhook events")
  @extension("x-webhook-provider", "filevine")
  receiveWebhook(
    @header("x-fv-messageid") messageId?: string,
    @header("x-fv-timestamp") timestamp?: string,
    @body payload: FilevineWebhookPayload,
  ): FilevineWebhookResponse;
}

// Filevine webhook payload models
// See: https://developer.filevine.io/docs/v2-us/62724ee7c344d-note-created
@added(Versions.v1)
model FilevineWebhookPayload {
  @doc("Object type (e.g., 'Note')")
  Object?: string;

  @doc("Event type (e.g., 'Created', 'Updated')")
  Event?: string;

  @doc("Unix timestamp of the event")
  Timestamp?: int64;

  @doc("The org (firm) ID")
  OrgId: int32;

  @doc("The project (case) ID")
  ProjectId?: int32;

  @doc("The user ID who triggered the event")
  UserId?: int32;

  @doc("The user type (e.g., 'Employee', 'Customer')")
  UserType?: string;

  @doc("Object-specific ID container")
  ObjectId?: FilevineObjectId;
}

@added(Versions.v1)
model FilevineObjectId {
  @doc("Note ID (present for Note events)")
  NoteId?: int32;
}

@added(Versions.v1)
model FilevineWebhookResponse {
  @doc("Whether the webhook was received successfully")
  received: boolean;

  @doc("The message ID echoed back")
  messageId?: string;

  @doc("The object type processed")
  objectType?: string;

  @doc("The event type processed")
  eventType?: string;

  @doc("The timestamp echoed back")
  timestamp?: string;

  @doc("The note ID if applicable")
  noteId?: int32;

  @doc("The project ID if applicable")
  projectId?: int32;

  @doc("The org ID")
  orgId?: int32;
}
```

### Build the OpenAPI Specification

After defining your TypeSpec, build the OpenAPI file:

```bash
cd src/YourApp.TypeSpec
npm install
npm run build
```

This generates the OpenAPI specification at `infra/api/openapi/openapi.v1.yaml`, which is used by:

- **APIM** - For API definition and policy enforcement
- **Kiota** - For generating typed API clients
- **Documentation** - For API reference docs

---

## Step 4: Typed Payload Models (Optional)

Create typed models that match the [Filevine webhook payload structure](https://developer.filevine.io/docs/v2-us/62724ee7c344d-note-created):

```csharp
// src/YourApp/Models/FilevineModels.cs

/// <summary>
/// Filevine webhook payload structure.
/// All Filevine webhooks follow this format with Object/Event identifying the type.
/// </summary>
public record FilevineWebhookPayload
{
    /// <summary>Object type (e.g., "Note", "Project", "Document")</summary>
    public string? Object { get; init; }

    /// <summary>Event type (e.g., "Created", "Updated")</summary>
    public string? Event { get; init; }

    /// <summary>Unix timestamp of the event</summary>
    public long? Timestamp { get; init; }

    /// <summary>The org (firm) ID</summary>
    public int OrgId { get; init; }

    /// <summary>The project (case) ID</summary>
    public int? ProjectId { get; init; }

    /// <summary>The user ID who triggered the event</summary>
    public int? UserId { get; init; }

    /// <summary>The user type (e.g., "Employee", "Customer")</summary>
    public string? UserType { get; init; }

    /// <summary>Object-specific ID container</summary>
    public FilevineObjectId? ObjectId { get; init; }
}

/// <summary>
/// Container for object-specific IDs. Only relevant fields are populated per event type.
/// </summary>
public record FilevineObjectId
{
    public int? NoteId { get; init; }
    public int? DocumentId { get; init; }
    public int? ContactId { get; init; }
    // Add other ID fields as needed for different event types
}

/// <summary>Known Filevine object types for pattern matching.</summary>
public static class FilevineObjectTypes
{
    public const string Note = "Note";
    public const string Project = "Project";
    public const string Document = "Document";
    public const string Contact = "Contact";
}

/// <summary>Known Filevine event types for pattern matching.</summary>
public static class FilevineEventTypes
{
    public const string Created = "Created";
    public const string Updated = "Updated";
    public const string Deleted = "Deleted";
}
```

!!! tip "Pattern Matching"
Use C# pattern matching with the constants for clean event handling:
`csharp
    var action = (payload.Object, payload.Event) switch
    {
        (FilevineObjectTypes.Note, FilevineEventTypes.Created) => "Note Created",
        (FilevineObjectTypes.Note, FilevineEventTypes.Updated) => "Note Updated",
        _ => "Unknown event"
    };
    `

---

## Step 5: Production Terraform Configuration

### Enable Filevine Webhook Policy

In your Experience API's `vars.yml`, add/change the `api_policy_type` setting:

```yaml
# infra/web/vars.yml

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

# Enable Filevine webhook JWT validation
api_policy_type: filevine
```

**What This Does:**

When `api_policy_type = "filevine"`:

1. **APIM Policy** - Uses `api_policies_filevine` instead of standard Okta/Entra routing
2. **JWT Validation** - Validates Filevine JWTs using OIDC discovery from `identity.filevine.com`
3. **Header Override** - Sets `x-forward-tenant-origin: corp` for downstream API calls

### Deploy

Commit your changes and push to trigger the Azure DevOps pipeline (main automatically).

---

## Step 6: Configure Filevine Production Webhook

After deployment, configure Filevine with your APIM endpoint:

```

https://{your-apim}.azure-api.net/exp-{app-name}/api/webhooks

```

---

## Calling Downstream APIs

After receiving a Filevine webhook, your Experience API typically calls Process/System APIs. Since webhooks have no user context, use **client credentials** (app-to-app) authentication.

### Step 1: Add KiotaReference

Add a reference to the downstream API's OpenAPI spec in your `.csproj`:

```xml
<ItemGroup>
  <KiotaReference Include="ProcessApiClient" OpenApi="https://openapi.saif.com/it-api-prc-cases/test/openapi.v1.yaml">
    <NamespaceName>YourApp.Clients.ProcessApi</NamespaceName>
  </KiotaReference>
</ItemGroup>
```

### Step 2: Configure the Process API Authorization

In the **Process API's** `infra/auth/corp/config.yml`, authorize the Experience API to call it:

```yaml
# infra/auth/corp/config.yml (Process API)
authorized_apps:
  - project_id: it-api-exp-yourapp  # The calling Experience API's project ID
    app_roles:
      - App.Read                     # App role(s) to grant
    scopes: user_impersonation       # Required
```

!!! info "Understanding scopes vs app_roles" - **`scopes: user_impersonation`** - Required for the pre-authorization to work - **`app_roles: [App.Read]`** - The actual permission granted to the calling app's service principal

    For client credentials flow, the token will contain `"roles": ["App.Read"]` (not scopes). The Process API validates this role in its `@useAuth(Roles<["App.Read"]>)` TypeSpec decorator.

### Step 3: Configure the Client

In the **Experience API's** `Program.cs`, configure the client with `ClientCredentialsTokenProvider`:

```csharp
// Configure Kiota client for downstream Process API (no user context)
builder.ConfigureHttpClient<ProcessApiClient, ClientCredentialsTokenProvider>(
    "it-api-proc-cases",
    options => options.Scopes = [".default"]);
```

### Step 4: Use the Client

Inject and use the client in your webhook handler:

```csharp
public class FilevineWebhookHandler
{
    private readonly ProcessApiClient _processApi;

    public FilevineWebhookHandler(ProcessApiClient processApi)
    {
        _processApi = processApi;
    }

    public async Task HandleProjectCreated(FilevineProject project)
    {
        // This call goes through APIM with Entra client credentials
        await _processApi.Cases.PostAsync(new CreateCaseRequest
        {
            ExternalId = project.ProjectId.ToString(),
            Name = project.ProjectName
        });
    }
}
```

> 💡 The APIM policy sets `x-forward-tenant-origin: corp`, so downstream calls automatically use Entra authentication.

See [Calling APIs](../calling-apis.md#clientcredentialstokenprovider) for more details on `ClientCredentialsTokenProvider`.

---

## Three-Tier Architecture

```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Filevine Webhook → Experience API → Process API → System API                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Filevine ──► APIM ──► Experience API                                      │
│                 │            │                                              │
│            Filevine JWT      │                                              │
│            Validation        │                                              │
│                              ▼                                              │
│                         APIM ──► Process API                                │
│                          │                                                  │
│                     Entra JWT                                               │
│                     (corp header)                                           │
│                              ▼                                              │
│                         APIM ──► System API                                 │
│                          │                                                  │
│                     Entra JWT                                               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

---

## Troubleshooting

### 401 Unauthorized in APIM

**Cause:** Invalid or missing Filevine JWT

**Debug:**

1. Check APIM returns error headers (`ErrorSource`, `ErrorReason`, `ErrorMessage`)
2. Verify the JWT in [jwt.io](https://jwt.io)
3. Check issuer, audience, and scope claims

### Webhook Not Reaching Your API

1. Check APIM logs in Application Insights
2. Verify the webhook URL in Filevine settings
3. Ensure Front Door is routing correctly

### DevTunnel URL Changed

**Cause:** The DevTunnel URL includes the port number. If you change the API port in `launchSettings.json` or modify endpoint configuration in `ApiResourceBuilder.g.cs`, a new tunnel URL is generated.

**Solution:**

1. Check the new tunnel URL in the Aspire Dashboard under the "webhook-tunnel" resource
2. Or run `devtunnel list` and `devtunnel show <tunnel-id>` to find the new URL
3. Update the webhook URL in Filevine settings to match

**Example:** Port change from `22443` → `5001` changes URL from:

```
https://abc123-22443.usw2.devtunnels.ms/api/webhooks
```

to:

```
https://abc123-5001.usw2.devtunnels.ms/api/webhooks
```

### Downstream API Calls Failing (403)

**Filevine-specific cause:**

- **Missing `x-forward-tenant-origin: corp` header** - Ensure `api_policy_type = "filevine"` in your Experience API's `vars.yml`

**General causes:** See [Webhook Troubleshooting](index.md#downstream-api-returning-400403500) for common 403 causes including missing app role assignments and package version mismatches.

---

## Common Pitfalls

### TypeSpec Error Response Types

When defining API operations that return error responses, use the SAIF Platform models, not the bare TypeSpec.Http responses:

```typespec
// ✅ Correct - Uses SAIF.Platform models with ProblemDetails body
createNote(@body request: CreateNoteRequest): CreateNoteResponse | BadRequest;
getNote(@path noteId: int32): Note | NotFound;

// ❌ Wrong - Uses TypeSpec.Http responses without body schema
createNote(@body request: CreateNoteRequest): CreateNoteResponse | BadRequestResponse;
getNote(@path noteId: int32): Note | NotFoundResponse;
```

**Why this matters:** `BadRequestResponse` from `@typespec/http` generates a 400 response without a body schema. Kiota won't generate error mapping code, causing "no error factory is registered for this code" errors at runtime.

### ProblemDetails Namespace Conflict

When Kiota generates a `ProblemDetails` model, it can conflict with `Microsoft.AspNetCore.Mvc.ProblemDetails`:

```csharp
// Add alias to resolve ambiguity
using MvcProblemDetails = Microsoft.AspNetCore.Mvc.ProblemDetails;

// Then use the alias in your endpoint signatures
private static async Task<Results<Ok<Response>, BadRequest<MvcProblemDetails>>> HandleWebhook(...)
```

---

## References

- [Filevine Webhook Security](https://developer.filevine.io/docs/v2-us/15de82e461c3e-webhook-security)
- [Filevine Webhook Payloads](https://developer.filevine.io/docs/v2-us/b16689da4bcd4-webhook-payloads)
- [Filevine Note Created Event](https://developer.filevine.io/docs/v2-us/62724ee7c344d-note-created)
- [Filevine API Examples (GitHub)](https://github.com/Filevine/filevine-api-examples)
- [APIM validate-jwt Policy](https://learn.microsoft.com/azure/api-management/validate-jwt-policy)
