> ## Documentation Index
> Fetch the complete documentation index at: https://developers.apps.filed.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive Webhooks

> Configure webhook endpoints with GraphQL, verify Svix signatures, and process Filed task events safely

Filed sends workspace-scoped task status events to HTTPS endpoints you
configure. Use webhooks when your integration needs task updates without
polling the [`tasks`](/apis/tasks) API.

This guide includes every argument, input field, return field, and default
value for the webhook GraphQL operations it documents because schema
introspection is not available.

```mermaid theme={null}
flowchart LR
  A["Your integration"] -->|"GraphQL: create endpoint"| B["Filed"]
  B -->|"signed HTTPS delivery"| C["Your receiver"]
  C -->|"2xx after durable acceptance"| B
  A -->|"GraphQL: reconcile task"| B
```

## Before you begin

All GraphQL requests use:

```text theme={null}
https://router.apps.filed.com/graphql
```

The event catalog is public. Listing or managing webhook endpoints requires an
administrator's read-write `workspaceToken`. Send the token as a bearer token:

```http theme={null}
Authorization: Bearer YOUR_WORKSPACE_TOKEN
Content-Type: application/json
```

See [Authentication](/guides/authentication) for token setup.

## Complete GraphQL schema

The following is the complete schema available for webhook configuration. No
other arguments or fields are required to use this API.

```graphql theme={null}
type Query {
  webhookEventCatalog: [WebhookEventType!]!
  me: Me
}

type Workspace {
  webhookEndpoints(
    limit: Int = 20
    iterator: String
  ): WebhookEndpointPage!
}

type Mutation {
  createWebhookEndpoint(
    input: CreateWebhookEndpointInput!
  ): CreateWebhookEndpointPayload!

  updateWebhookEndpoint(
    input: UpdateWebhookEndpointInput!
  ): WebhookEndpoint!

  deleteWebhookEndpoint(endpointId: ID!): Boolean!

  rotateWebhookEndpointSecret(
    input: RotateWebhookEndpointSecretInput!
  ): RotateWebhookEndpointSecretPayload!
}

type WebhookEventType {
  name: String!
  description: String!
  version: String!
  status: String!
  taskTypes: [String!]!
  payloadSchema: JSON!
}

type WebhookEndpoint {
  id: ID!
  url: String!
  description: String!
  enabled: Boolean!
  eventTypes: [String!]
  createdAt: Date!
  updatedAt: Date!
}

type WebhookEndpointPage {
  data: [WebhookEndpoint!]!
  done: Boolean!
  iterator: String
}

input CreateWebhookEndpointInput {
  url: String!
  description: String
  eventTypes: [String!]
}

type CreateWebhookEndpointPayload {
  endpoint: WebhookEndpoint!
  signingSecret: String!
}

input UpdateWebhookEndpointInput {
  endpointId: ID!
  url: String
  description: String
  enabled: Boolean
  eventTypes: [String!]
}

input RotateWebhookEndpointSecretInput {
  endpointId: ID!
  gracePeriodSeconds: Int = 86400
}

type RotateWebhookEndpointSecretPayload {
  endpoint: WebhookEndpoint!
  signingSecret: String!
}
```

## 1. Discover supported events

Query the catalog instead of hard-coding event support. This query has no
arguments or variables and does not require a workspace ID.

```graphql theme={null}
query WebhookEventCatalog {
  webhookEventCatalog {
    name
    description
    version
    status
    taskTypes
    payloadSchema
  }
}
```

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "webhookEventCatalog": [
        {
          "name": "task.running",
          "description": "A supported Filed task started running.",
          "version": "2",
          "status": "RUNNING",
          "taskTypes": [
            "BINDER",
            "TAX_PREP",
            "TAX_REVIEW",
            "TAX_ADVISOR",
            "TAX_PREP_LITE"
          ],
          "payloadSchema": {
            "$schema": "http://json-schema.org/draft-07/schema#",
            "type": "object"
          }
        }
      ]
    }
  }
  ```
</ResponseExample>

`payloadSchema` contains the complete Draft 7 JSON Schema for that event, not
only the abbreviated fields shown above.

The current event catalog is:

| Event               | Status      | Version | Supported task types                                               |
| ------------------- | ----------- | ------- | ------------------------------------------------------------------ |
| `task.running`      | `RUNNING`   | `2`     | `BINDER`, `TAX_PREP`, `TAX_REVIEW`, `TAX_ADVISOR`, `TAX_PREP_LITE` |
| `task.completed`    | `COMPLETED` | `2`     | `BINDER`, `TAX_PREP`, `TAX_REVIEW`, `TAX_ADVISOR`, `TAX_PREP_LITE` |
| `task.failed`       | `FAILED`    | `2`     | `BINDER`, `TAX_PREP`, `TAX_REVIEW`, `TAX_ADVISOR`, `TAX_PREP_LITE` |
| `subtask.completed` | `COMPLETED` | `2`     | `BINDER`, `TAX_PREP`, `TAX_REVIEW`, `TAX_ADVISOR`, `TAX_PREP_LITE` |
| `subtask.failed`    | `FAILED`    | `2`     | `BINDER`, `TAX_PREP`, `TAX_REVIEW`, `TAX_ADVISOR`, `TAX_PREP_LITE` |
| `documents.synced`  | —           | `2`     | —                                                                  |

`task.*` reports the task as a whole. `subtask.*` reports each stage inside it
— extraction, reconciliation, data entry and the rest. A stage is only reported
when it finishes, so these mark completion rather than progress.

`documents.synced` is not a task event, which is why it carries no status or
task type. Filed emits it when documents are pulled into a client from a
connected system — a document management system, or a practice management
system such as Canopy, Karbon or Truss. It arrives before any processing
begins, so it tells you documents landed, not what Filed made of them. Its
payload names the `connectionId` and `providerKey` they came from, the
`sourceExternalId` of the folder or project, and each document's
`inputDocumentId`, `fileName` and `mimeType`. `duplicateCount` reports how many
files in the same pull Filed already held and therefore left out.

Documents added by hand rather than pulled from a connection do not raise it.

**Subscribe to `subtask.*` only if you need stage detail.** A single tax prep
moves through many stages, so these are an order of magnitude more traffic than
`task.*`. `eventTypes` on an endpoint is how you choose: subscribe to `task.*`
alone and you never receive them.

An endpoint created without `eventTypes` receives every event, current and
future — so an endpoint made before `subtask.*` existed now receives it too. Set
`eventTypes` explicitly on those endpoints to keep the volume you had.

## 2. Create an endpoint

Create an endpoint once for each delivery destination. New endpoints are
enabled by default.

```graphql theme={null}
mutation CreateWebhookEndpoint($input: CreateWebhookEndpointInput!) {
  createWebhookEndpoint(input: $input) {
    endpoint {
      id
      url
      description
      enabled
      eventTypes
      createdAt
      updatedAt
    }
    signingSecret
  }
}
```

Variables using every available input field:

```json theme={null}
{
  "input": {
    "url": "https://partner.example.com/filed/webhooks",
    "description": "Production task events",
    "eventTypes": ["task.running", "task.completed", "task.failed"]
  }
}
```

| Input field   | Type        | Required | Behavior                                                                                                                                                                                                                                                                    |
| ------------- | ----------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`         | `String!`   | Yes      | HTTPS URL that receives deliveries.                                                                                                                                                                                                                                         |
| `description` | `String`    | No       | Human-readable endpoint label. Omitted or `null` becomes an empty string.                                                                                                                                                                                                   |
| `eventTypes`  | `[String!]` | No       | Events to deliver. Omit or pass `null` to subscribe to all current **and future** events — including the much chattier `subtask.*` family, so name the events explicitly unless you want everything Filed ever adds. An empty list is invalid. Duplicate names are removed. |

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "createWebhookEndpoint": {
        "endpoint": {
          "id": "ep_2xYExample",
          "url": "https://partner.example.com/filed/webhooks",
          "description": "Production task events",
          "enabled": true,
          "eventTypes": [
            "task.running",
            "task.completed",
            "task.failed"
          ],
          "createdAt": "2026-08-24T12:00:00.000Z",
          "updatedAt": "2026-08-24T12:00:00.000Z"
        },
        "signingSecret": "whsec_REPLACE_WITH_RETURNED_SECRET"
      }
    }
  }
  ```
</ResponseExample>

<Warning>
  Store `signingSecret` immediately in a secrets manager. Filed returns it only
  when the endpoint is created or its secret is rotated. Do not put it in source
  control or expose it to browser code.
</Warning>

### Complete cURL request

```bash theme={null}
curl -X POST https://router.apps.filed.com/graphql \
  -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation CreateWebhookEndpoint($input: CreateWebhookEndpointInput!) { createWebhookEndpoint(input: $input) { endpoint { id url description enabled eventTypes createdAt updatedAt } signingSecret } }",
    "variables": {
      "input": {
        "url": "https://partner.example.com/filed/webhooks",
        "description": "Production task events",
        "eventTypes": ["task.completed", "task.failed"]
      }
    }
  }'
```

## 3. List endpoints

Endpoint listing is cursor-paginated and is reached through the authenticated
workspace returned by `me`.

```graphql theme={null}
query WebhookEndpoints($limit: Int = 20, $iterator: String) {
  me {
    ... on WorkspaceUser {
      workspace {
        webhookEndpoints(limit: $limit, iterator: $iterator) {
          data {
            id
            url
            description
            enabled
            eventTypes
            createdAt
            updatedAt
          }
          done
          iterator
        }
      }
    }
  }
}
```

All query arguments:

| Argument   | Type     | Required | Default | Behavior                                                              |
| ---------- | -------- | -------- | ------- | --------------------------------------------------------------------- |
| `limit`    | `Int`    | No       | `20`    | Number of endpoints to return. Minimum `1`, maximum `100`.            |
| `iterator` | `String` | No       | `null`  | Opaque cursor returned by the previous page. Omit for the first page. |

```json theme={null}
{
  "limit": 50,
  "iterator": null
}
```

All page and endpoint return fields:

| Field                | Type                  | Meaning                                                          |
| -------------------- | --------------------- | ---------------------------------------------------------------- |
| `data`               | `[WebhookEndpoint!]!` | Endpoints in the current page.                                   |
| `done`               | `Boolean!`            | `true` when there are no more pages.                             |
| `iterator`           | `String`              | Cursor for the next page; normally `null` when `done` is `true`. |
| `data[].id`          | `ID!`                 | ID used by update, delete, and rotate operations.                |
| `data[].url`         | `String!`             | Current delivery URL.                                            |
| `data[].description` | `String!`             | Endpoint label; may be an empty string.                          |
| `data[].enabled`     | `Boolean!`            | Whether delivery is active.                                      |
| `data[].eventTypes`  | `[String!]`           | Selected events. `null` means all current and future events.     |
| `data[].createdAt`   | `Date!`               | Endpoint creation time.                                          |
| `data[].updatedAt`   | `Date!`               | Most recent endpoint update time.                                |

To read every endpoint, send the returned `iterator` in the next request until
`done` is `true`.

## 4. Update an endpoint

The update mutation is a patch: provide `endpointId` and only the fields you
want to change. At least one update field must be present.

```graphql theme={null}
mutation UpdateWebhookEndpoint($input: UpdateWebhookEndpointInput!) {
  updateWebhookEndpoint(input: $input) {
    id
    url
    description
    enabled
    eventTypes
    createdAt
    updatedAt
  }
}
```

Variables using every available input field:

```json theme={null}
{
  "input": {
    "endpointId": "ep_2xYExample",
    "url": "https://partner.example.com/filed/webhooks-v2",
    "description": "Production task events v2",
    "enabled": true,
    "eventTypes": ["task.completed", "task.failed"]
  }
}
```

| Input field   | Type        | Required | Behavior                                                                                                                                              |
| ------------- | ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `endpointId`  | `ID!`       | Yes      | Endpoint to update.                                                                                                                                   |
| `url`         | `String`    | No       | Replacement delivery URL. Omit or pass `null` to leave it unchanged.                                                                                  |
| `description` | `String`    | No       | Replacement label. Pass `null` or an empty string to clear it; omit it to leave it unchanged.                                                         |
| `enabled`     | `Boolean`   | No       | `true` enables delivery and `false` pauses it. Omit or pass `null` to leave it unchanged.                                                             |
| `eventTypes`  | `[String!]` | No       | Replacement event subscription. Pass `null` for all current and future events; omit it to leave the subscription unchanged. An empty list is invalid. |

<Note>
  GraphQL distinguishes an omitted input field from an explicitly supplied
  `null`. This matters for `description` and `eventTypes`, where `null` clears or
  resets the existing value.
</Note>

Pause an endpoint without deleting it:

```json theme={null}
{
  "input": {
    "endpointId": "ep_2xYExample",
    "enabled": false
  }
}
```

## 5. Rotate a signing secret

Rotate a secret before you suspect compromise or as part of regular credential
hygiene.

```graphql theme={null}
mutation RotateWebhookEndpointSecret(
  $input: RotateWebhookEndpointSecretInput!
) {
  rotateWebhookEndpointSecret(input: $input) {
    endpoint {
      id
      url
      description
      enabled
      eventTypes
      createdAt
      updatedAt
    }
    signingSecret
  }
}
```

```json theme={null}
{
  "input": {
    "endpointId": "ep_2xYExample",
    "gracePeriodSeconds": 86400
  }
}
```

| Input field          | Type  | Required | Default | Behavior                                                                                        |
| -------------------- | ----- | -------- | ------- | ----------------------------------------------------------------------------------------------- |
| `endpointId`         | `ID!` | Yes      | —       | Endpoint whose secret will be rotated.                                                          |
| `gracePeriodSeconds` | `Int` | No       | `86400` | Seconds during which the old and new secrets both work. Minimum `0`, maximum `604800` (7 days). |

The response contains every `WebhookEndpoint` field plus the new
`signingSecret`. Deploy the new secret before the grace period ends, verify a
delivery with it, and then remove the old secret.

## 6. Delete an endpoint

Deletion stops delivery permanently for that endpoint.

```graphql theme={null}
mutation DeleteWebhookEndpoint($endpointId: ID!) {
  deleteWebhookEndpoint(endpointId: $endpointId)
}
```

```json theme={null}
{
  "endpointId": "ep_2xYExample"
}
```

| Argument     | Type  | Required | Return value                                |
| ------------ | ----- | -------- | ------------------------------------------- |
| `endpointId` | `ID!` | Yes      | The mutation returns `true` after deletion. |

Use `enabled: false` with `updateWebhookEndpoint` instead when you may need to
resume the endpoint later.

## 7. Receive the payload

Every event uses the same version 2 envelope:

```json theme={null}
{
  "id": "33df4504-a6c5-5587-b231-0b56f29d0638",
  "event": "task.completed",
  "version": "2",
  "occurredAt": "2026-08-19T12:00:00.000Z",
  "workspaceId": "019f0fb2-42d9-72e0-b7ac-78b32ad45ef1",
  "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
  "taskId": "018f9c2b-7c4d-7e10-9a22-6b3c4d5e6f70",
  "taskType": "TAX_PREP",
  "status": "COMPLETED",
  "errorMessage": null,
  "customerError": null,
  "taxPrepResult": {
    "taxYear": 2025,
    "returnType": "F1040",
    "summary": "Prepared the 2025 return from 12 source documents.",
    "documentCount": 12,
    "extractedFormCount": 9,
    "reviewItemCount": 1,
    "reviewItems": [
      {
        "severity": "HIGH",
        "category": "INCOME",
        "description": "Box 12 code D missing on the second W-2."
      }
    ]
  }
}
```

| Payload field   | Type      | Meaning                                                                                                                                                                                                                                             |
| --------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`            | `String!` | Deterministic UUID for this task status event.                                                                                                                                                                                                      |
| `event`         | `String!` | One of the eight catalog events; the suffix always matches `status`.                                                                                                                                                                                |
| `version`       | `String!` | Payload schema version; currently `2`.                                                                                                                                                                                                              |
| `occurredAt`    | `String!` | ISO 8601 time when the task entered the status.                                                                                                                                                                                                     |
| `workspaceId`   | `ID!`     | Filed workspace that owns the task and endpoint.                                                                                                                                                                                                    |
| `clientId`      | `ID!`     | Filed client associated with the task.                                                                                                                                                                                                              |
| `taskId`        | `ID!`     | Filed task whose status changed.                                                                                                                                                                                                                    |
| `taskType`      | `String!` | Supported task type from the event catalog.                                                                                                                                                                                                         |
| `status`        | `String!` | `RUNNING`, `COMPLETED`, or `FAILED`; always matches the event suffix.                                                                                                                                                                               |
| `errorMessage`  | `String`  | Failure detail for `task.failed`; `null` for running and completed events. Raw text — branch on `customerError.code` instead.                                                                                                                       |
| `taxPrepResult` | `Object`  | Task outcome on `task.completed`, so the common path needs no follow-up query. Sent for `TAX_PREP`; `null` for other task types and other statuses.                                                                                                 |
| `customerError` | `Object`  | Stable failure classification on `task.failed`: `code`, plus an `action` naming what would resolve it (`type`, `connectionId`). `null` while a failure is inside Filed's retry grace period — the event still arrives, poll if you need the reason. |

### Sub-task payload

`subtask.*` uses the same envelope, minus `taxPrepResult` and `customerError`,
plus the stage it reports:

```json theme={null}
{
  "id": "5c1f9b2e-1f8a-4a51-9a3d-2f7c6b4d1e90",
  "event": "subtask.completed",
  "version": "2",
  "occurredAt": "2026-08-19T12:01:00.000Z",
  "workspaceId": "019f0fb2-42d9-72e0-b7ac-78b32ad45ef1",
  "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
  "taskId": "018f9c2b-7c4d-7e10-9a22-6b3c4d5e6f70",
  "taskType": "TAX_PREP",
  "subTaskId": "018f9c2c-8d5e-7f21-a333-7c4d5e6f7081",
  "subTaskType": "tax-extraction",
  "status": "COMPLETED",
  "errorMessage": null
}
```

| Payload field  | Type      | Meaning                                                                                                                                                                               |
| -------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `taskId`       | `ID!`     | The task this stage belongs to.                                                                                                                                                       |
| `subTaskId`    | `ID!`     | The stage whose status changed.                                                                                                                                                       |
| `subTaskType`  | `String!` | The stage, such as `tax-extraction` or `reconciliation`. Free text: new stages appear without a version change, so treat an unrecognised value as informational rather than an error. |
| `errorMessage` | `String`  | Why the stage failed; `null` otherwise.                                                                                                                                               |

Each request also includes these Svix signature headers:

| Header              | Purpose                                                               |
| ------------------- | --------------------------------------------------------------------- |
| `webhook-id`        | Stable delivery ID used for signature verification and deduplication. |
| `webhook-timestamp` | Signed delivery timestamp.                                            |
| `webhook-signature` | One or more Svix signatures.                                          |

## 8. Verify the signature

Verify the raw request body before parsing JSON or changing application state.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { Webhook } from "svix";

  const secret = process.env.FILED_WEBHOOK_SECRET;

  export async function receiveFiledWebhook(request: Request) {
    if (!secret) throw new Error("FILED_WEBHOOK_SECRET is not configured");

    const body = await request.text();
    const payload = new Webhook(secret).verify(body, {
      "webhook-id": request.headers.get("webhook-id") ?? "",
      "webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
      "webhook-signature": request.headers.get("webhook-signature") ?? "",
    });

    // Persist the delivery ID and enqueue payload before acknowledging it.
    await acceptWebhook(request.headers.get("webhook-id"), payload);
    return Response.json({ accepted: true }, { status: 200 });
  }
  ```

  ```python Python theme={null}
  import os

  from flask import Flask, request
  from svix.webhooks import Webhook, WebhookVerificationError

  app = Flask(__name__)


  @app.post("/filed/webhooks")
  def receive_filed_webhook():
      secret = os.environ["FILED_WEBHOOK_SECRET"]
      raw_body = request.get_data()

      try:
          payload = Webhook(secret).verify(raw_body, request.headers)
      except WebhookVerificationError:
          return {"error": "invalid signature"}, 400

      accept_webhook(request.headers["webhook-id"], payload)
      return {"accepted": True}, 200
  ```
</CodeGroup>

<Warning>
  Do not verify a parsed or re-serialized JSON object. Whitespace or key ordering
  changes invalidate the signature. Pass the exact raw request body to the Svix
  verification library.
</Warning>

## 9. Handle duplicates, ordering, and retries

Webhook delivery is at least once. Store the `webhook-id` in a table with a
uniqueness constraint before applying the event. If the ID has already been
handled, return a successful response without applying it again.

Events can be delayed or arrive out of order. Compare `occurredAt` with the
latest status transition you have stored, and do not let an older event replace
newer state.

Return a `2xx` response promptly after durable acceptance. Move slow work to a
queue. Non-`2xx` responses and network failures are retried.

<Tip>
  Treat webhook events as notifications, not as the only task record. To
  reconcile state, query the task by `taskId` through the [`tasks`](/apis/tasks)
  API.
</Tip>

## Common GraphQL errors

| Error                                                    | Cause                                                                             | Resolution                                                           |
| -------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Unauthorized                                             | The token is missing, read-only, or does not belong to a workspace administrator. | Use an administrator's read-write `workspaceToken`.                  |
| Unsupported webhook event type                           | `eventTypes` contains a value absent from the catalog.                            | Query `webhookEventCatalog` and send one or more returned names.     |
| At least one webhook event type is required              | `eventTypes` is an empty list.                                                    | Supply at least one event, or use `null` to subscribe to all events. |
| At least one endpoint field must be updated              | The update contains only `endpointId`.                                            | Add `url`, `description`, `enabled`, or `eventTypes`.                |
| Webhook endpoint limit must be between 1 and 100         | `limit` is outside the supported range.                                           | Use a value from `1` through `100`.                                  |
| Secret grace period must be between 0 and 604800 seconds | `gracePeriodSeconds` is outside the supported range.                              | Use `0` through `604800`, or omit it for `86400`.                    |
