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

# Annotate a document

> Leave a note or flag on a binder document, read existing annotations, reply in a thread, and edit or hide a message

After a client's documents have been [ingested](/guides/recipes/onboard-a-client)
and filed into the [binder](/apis/binder), reviewers leave notes and flags on
individual pages of a document. These annotations are `DocumentMessage` records
with `type: "annotation"`, threaded replies underneath for back-and-forth
discussion. This recipe is the minimal call sequence to read existing
annotations, leave a new note or flag, reply in a thread, and edit or hide a
message. It is the annotation use case of the document-message API; the sibling
[Review a return and sign off](/guides/recipes/review-and-sign-off) recipe
covers the sign-off use case of the same underlying API.

Every operation here is documented on the
[Document messages](/apis/document-messages) reference page; this recipe
sequences the calls rather than re-documenting the types. Everything uses a
**`workspaceToken`** (see [Authentication](/guides/authentication)) and goes to
the single GraphQL endpoint:

```
https://router.apps.filed.com/graphql
```

<Note>
  Document messages belong to a client, so there is no top-level
  `documentMessages` query. Reach them through
  `me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { documentMessages(filter: ...) { ... } } } } }`.
  The `workspaceToken` already identifies the workspace.
</Note>

```mermaid theme={null}
flowchart LR
  A["Client ID +<br/>subdocument id"] --> B["1. Read annotations<br/>(filter: types=[annotation])"]
  B --> C["2. Create a note / flag<br/>createDocumentMessage<br/>type=annotation"]
  C --> D["3. Reply in a thread<br/>createDocumentMessageThread"]
  D --> E["4. Edit or hide<br/>updateDocumentMessage /<br/>hideDocumentMessage"]
  E --> F["Refetch<br/>documentMessages"]
```

This recipe does not re-document the types it touches. For the full
`DocumentMessage`, `DocumentMessageThread`, `DocumentMessageTaggedUser`,
`DocumentMessageType`, and `DocumentMessagesFilter` definitions, see
[Document messages](/apis/document-messages#the-documentmessage-type). For the
sign-off use case of the same API (`type: "activity"`, `markType: "signoff"`),
see [Review a return and sign off](/guides/recipes/review-and-sign-off).

## 1. Read existing annotations

Read `Client.documentMessages(filter)` to list the annotations already on a
document. Filter by `documentPath` (the subdocument `id` you read from
[binder.subdocuments](/apis/binder#list-the-files-in-a-binder)) and by
`types: ["annotation"]` to narrow to annotations only, excluding sign-offs and
missing-document activity. See
[Read document messages](/apis/document-messages#read-document-messages) for the
full `DocumentMessagesFilter` argument.

```graphql theme={null}
query GetClientDocumentMessages($clientId: ID!, $filter: DocumentMessagesFilter) {
  me {
    ... on WorkspaceUser {
      id
      workspace {
        id
        clients(filters: { ids: [$clientId] }) {
          id
          documentMessages(filter: $filter) {
            id
            documentPath
            type
            markType
            anchorPoint
            body
            createdBy
            createdAt
            updatedAt
            hiddenAt
            hiddenBy
            threads {
              id
              documentMessageId
              contentPath
              body
              createdBy
              createdAt
              updatedAt
            }
            taggedUsers {
              id
              userId
              documentMessageId
              documentMessageThreadId
            }
          }
        }
      }
    }
  }
}
```

```json theme={null}
{
  "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
  "filter": {
    "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
    "types": ["annotation"],
    "includeHidden": false
  }
}
```

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://router.apps.filed.com/graphql \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
    -d '{
      "query": "query GetClientDocumentMessages($clientId: ID!, $filter: DocumentMessagesFilter) { me { ... on WorkspaceUser { id workspace { id clients(filters: { ids: [$clientId] }) { id documentMessages(filter: $filter) { id documentPath type markType anchorPoint body createdBy createdAt updatedAt hiddenAt hiddenBy threads { id documentMessageId contentPath body createdBy createdAt updatedAt } taggedUsers { id userId documentMessageId documentMessageThreadId } } } } } } }",
      "variables": {
        "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
        "filter": {
          "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
          "types": ["annotation"],
          "includeHidden": false
        }
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "me": {
        "id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
        "workspace": {
          "id": "019f0fb6-3001-7900-b7bc-0d11288504b1",
          "clients": [
            {
              "id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
              "documentMessages": [
                {
                  "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
                  "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
                  "type": "annotation",
                  "markType": "note",
                  "anchorPoint": {
                    "page": 1,
                    "coordinates": { "x": 120, "y": 340 }
                  },
                  "body": "Box 1 total matches the 1099-INT sum, but box 2 looks high. Recheck.",
                  "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
                  "createdAt": "2026-07-04T16:10:00.000Z",
                  "updatedAt": "2026-07-04T16:10:00.000Z",
                  "hiddenAt": null,
                  "hiddenBy": null,
                  "threads": [
                    {
                      "id": "019f0fb6-5b3d-7900-9c01-2b3c4d5e6f71",
                      "documentMessageId": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
                      "contentPath": "",
                      "body": "Pulled the corrected 1099-INT from the broker portal, box 2 now matches.",
                      "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
                      "createdAt": "2026-07-04T16:50:00.000Z",
                      "updatedAt": "2026-07-04T16:50:00.000Z"
                    }
                  ],
                  "taggedUsers": []
                }
              ]
            }
          ]
        }
      }
    }
  }
  ```
</ResponseExample>

<Tip>
  Each annotation's `id` is what you pass to
  [`updateDocumentMessage`](#3-edit-or-hide-an-annotation) (to edit the body) or
  [`hideDocumentMessage`](#3-edit-or-hide-an-annotation) (to soft-delete it). Save
  it when you create the annotation in the next step.
</Tip>

## 2. Create a note or flag

Annotations are `createDocumentMessage` calls with `type: "annotation"`. The
`markType` field is a free-form label your surface understands; the web app uses
`"note"` for free-text notes and `"flag"` for review flags. The annotation's
text goes in `body`, and the `anchorPoint` carries the `page` and
`coordinates: { x, y }` that position the mark on the page. See
[Create an annotation](/apis/document-messages#create-an-annotation) for the
full `CreateDocumentMessageInput` field list.

### Create a note

```graphql theme={null}
mutation CreateDocumentMessage($input: CreateDocumentMessageInput!) {
  createDocumentMessage(input: $input) {
    id
    documentPath
    type
    markType
    anchorPoint
    body
    createdBy
    createdAt
    updatedAt
    threads {
      id
      documentMessageId
      body
      createdBy
      createdAt
    }
  }
}
```

```json theme={null}
{
  "input": {
    "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
    "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
    "type": "annotation",
    "markType": "note",
    "anchorPoint": {
      "page": 1,
      "coordinates": { "x": 120, "y": 340 }
    },
    "body": "Box 1 total matches the 1099-INT sum, but box 2 looks high. Recheck."
  }
}
```

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://router.apps.filed.com/graphql \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
    -d '{
      "query": "mutation CreateDocumentMessage($input: CreateDocumentMessageInput!) { createDocumentMessage(input: $input) { id documentPath type markType anchorPoint body createdBy createdAt updatedAt threads { id documentMessageId body createdBy createdAt } } }",
      "variables": {
        "input": {
          "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
          "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
          "type": "annotation",
          "markType": "note",
          "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } },
          "body": "Box 1 total matches the 1099-INT sum, but box 2 looks high. Recheck."
        }
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "createDocumentMessage": {
        "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
        "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
        "type": "annotation",
        "markType": "note",
        "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } },
        "body": "Box 1 total matches the 1099-INT sum, but box 2 looks high. Recheck.",
        "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
        "createdAt": "2026-07-04T16:10:00.000Z",
        "updatedAt": "2026-07-04T16:10:00.000Z",
        "threads": []
      }
    }
  }
  ```
</ResponseExample>

### Create a flag

A flag uses the same `createDocumentMessage` mutation with `markType: "flag"`.
The body is optional for a flag (a flag may carry no note text).

```json theme={null}
{
  "input": {
    "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
    "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
    "type": "annotation",
    "markType": "flag",
    "anchorPoint": {
      "page": 1,
      "coordinates": { "x": 120, "y": 340 }
    },
    "body": "Schedule B interest total differs from 1099-INT sum by $42."
  }
}
```

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://router.apps.filed.com/graphql \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
    -d '{
      "query": "mutation CreateDocumentMessage($input: CreateDocumentMessageInput!) { createDocumentMessage(input: $input) { id documentPath type markType anchorPoint body createdBy createdAt updatedAt } }",
      "variables": {
        "input": {
          "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
          "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
          "type": "annotation",
          "markType": "flag",
          "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } },
          "body": "Schedule B interest total differs from 1099-INT sum by $42."
        }
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "createDocumentMessage": {
        "id": "019f0fb6-4c1d-7900-9c01-2b3c4d5e6f72",
        "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
        "type": "annotation",
        "markType": "flag",
        "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } },
        "body": "Schedule B interest total differs from 1099-INT sum by $42.",
        "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
        "createdAt": "2026-07-04T16:15:00.000Z",
        "updatedAt": "2026-07-04T16:15:00.000Z"
      }
    }
  }
  ```
</ResponseExample>

<Tip>
  `documentPath` for a subdocument annotation is the subdocument's `id`, the same
  value you read as `SubDocument.id` from
  [List the files in a binder](/apis/binder#list-the-files-in-a-binder). The web
  app passes `context: { scope: "workspace" }` on every document-message mutation
  so the `workspaceToken` identifies the workspace.
</Tip>

## 3. Reply in a thread

Threaded replies on an annotation are `DocumentMessageThread` records, created
under the parent annotation's `id` via `createDocumentMessageThread`. Use threads
for the back-and-forth discussion that grows under a note or flag. See
[Threads (replies)](/apis/document-messages#threads-replies) for the full
`CreateDocumentMessageThreadInput` field list.

```graphql theme={null}
mutation CreateDocumentMessageThread($input: CreateDocumentMessageThreadInput!) {
  createDocumentMessageThread(input: $input) {
    id
    documentMessageId
    contentPath
    body
    createdBy
    createdAt
    updatedAt
  }
}
```

```json theme={null}
{
  "input": {
    "documentMessageId": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
    "body": "Pulled the corrected 1099-INT from the broker portal, box 2 now matches."
  }
}
```

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://router.apps.filed.com/graphql \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
    -d '{
      "query": "mutation CreateDocumentMessageThread($input: CreateDocumentMessageThreadInput!) { createDocumentMessageThread(input: $input) { id documentMessageId contentPath body createdBy createdAt updatedAt } }",
      "variables": {
        "input": {
          "documentMessageId": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
          "body": "Pulled the corrected 1099-INT from the broker portal, box 2 now matches."
        }
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "createDocumentMessageThread": {
        "id": "019f0fb6-5b3d-7900-9c01-2b3c4d5e6f71",
        "documentMessageId": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
        "contentPath": "",
        "body": "Pulled the corrected 1099-INT from the broker portal, box 2 now matches.",
        "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
        "createdAt": "2026-07-04T16:50:00.000Z",
        "updatedAt": "2026-07-04T16:50:00.000Z"
      }
    }
  }
  ```
</ResponseExample>

## 4. Edit or hide an annotation

To edit the body or anchor point of an annotation, use
`updateDocumentMessage`. To soft-delete it (so it disappears from default reads
but stays in history), use `hideDocumentMessage`. To restore a hidden
annotation, use `unhideDocumentMessage`. See
[Update a document message](/apis/document-messages#update-a-document-message)
and
[Hide and unhide a document message](/apis/document-messages#hide-and-unhide-a-document-message)
for the full input shapes.

### Edit the body

```graphql theme={null}
mutation UpdateDocumentMessage($id: ID!, $input: UpdateDocumentMessageInput!) {
  updateDocumentMessage(id: $id, input: $input) {
    id
    body
    anchorPoint
    updatedAt
  }
}
```

```json theme={null}
{
  "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
  "input": {
    "body": "Box 1 confirmed. Box 2 is high, needs a corrected 1099-INT."
  }
}
```

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://router.apps.filed.com/graphql \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
    -d '{
      "query": "mutation UpdateDocumentMessage($id: ID!, $input: UpdateDocumentMessageInput!) { updateDocumentMessage(id: $id, input: $input) { id body anchorPoint updatedAt } }",
      "variables": {
        "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
        "input": { "body": "Box 1 confirmed. Box 2 is high, needs a corrected 1099-INT." }
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "updateDocumentMessage": {
        "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
        "body": "Box 1 confirmed. Box 2 is high, needs a corrected 1099-INT.",
        "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } },
        "updatedAt": "2026-07-04T16:30:00.000Z"
      }
    }
  }
  ```
</ResponseExample>

<Note>
  `updateDocumentMessage` can edit `body`, `anchorPoint`, and `taggedUserIds`.
  The `type` and `markType` of a document message are not mutable; to convert a
  note into a flag (or vice versa), hide the old one and create a new one.
</Note>

### Hide the annotation

Hiding is the soft-delete the binder uses to dismiss an annotation. The message
stays in history with `hiddenAt` and `hiddenBy` set, and is excluded from
default reads unless you pass `filter.includeHidden: true` (see step 1).

```graphql theme={null}
mutation HideDocumentMessage($id: ID!) {
  hideDocumentMessage(id: $id) {
    id
    hiddenAt
    hiddenBy
  }
}
```

```json theme={null}
{
  "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70"
}
```

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://router.apps.filed.com/graphql \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
    -d '{
      "query": "mutation HideDocumentMessage($id: ID!) { hideDocumentMessage(id: $id) { id hiddenAt hiddenBy } }",
      "variables": { "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70" }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "hideDocumentMessage": {
        "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
        "hiddenAt": "2026-07-04T16:45:00.000Z",
        "hiddenBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1"
      }
    }
  }
  ```
</ResponseExample>

### Unhide the annotation

To restore a hidden annotation, call `unhideDocumentMessage` with the same `id`.
Both `hiddenAt` and `hiddenBy` clear back to `null`.

```graphql theme={null}
mutation UnhideDocumentMessage($id: ID!) {
  unhideDocumentMessage(id: $id) {
    id
    hiddenAt
    hiddenBy
  }
}
```

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://router.apps.filed.com/graphql \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
    -d '{
      "query": "mutation UnhideDocumentMessage($id: ID!) { unhideDocumentMessage(id: $id) { id hiddenAt hiddenBy } }",
      "variables": { "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70" }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "unhideDocumentMessage": {
        "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
        "hiddenAt": null,
        "hiddenBy": null
      }
    }
  }
  ```
</ResponseExample>

<Tip>
  After hiding or unhiding an annotation, refetch the
  [documentMessages query](#1-read-existing-annotations) so the list reflects the
  new state. The web app optimistically flips `hiddenAt` and `hiddenBy` in its
  Apollo cache, then lets the server response confirm it.
</Tip>

## See also

* [Document messages](/apis/document-messages) for the full `DocumentMessage`,
  `DocumentMessageThread`, `DocumentMessageTaggedUser`, `DocumentMessageType`,
  and `DocumentMessagesFilter` type definitions, plus the
  `createDocumentMessage`, `updateDocumentMessage`, `hideDocumentMessage`,
  `unhideDocumentMessage`, `createDocumentMessageThread`,
  `updateDocumentMessageThread`, and `deleteDocumentMessageThread` mutation
  signatures.
* [Review a return and sign off](/guides/recipes/review-and-sign-off) for the
  sibling recipe that uses the same `createDocumentMessage` mutation with
  `type: "activity"` and `markType: "signoff"` to record reviewer sign-offs on a
  leadsheet sheet or row.
* [Browse a client's binder](/guides/recipes/browse-the-binder) for the recipe
  that lists the subdocuments whose `id` you pass as `documentPath` here, and
  for [Read message counts](/apis/binder#read-message-counts) which gives you a
  cheap annotation-count badge via `binder.messageCounts.notes`.
* [Binder](/apis/binder) for the `Binder.search` field, whose `annotations` and
  `marks` buckets surface the annotations you create here.
* [Authentication](/guides/authentication) for how to obtain and send the
  `workspaceToken` every call in this recipe requires.
