Skip to main content
After a client’s documents have been ingested and filed into the 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 recipe covers the sign-off use case of the same underlying API. Every operation here is documented on the Document messages reference page; this recipe sequences the calls rather than re-documenting the types. Everything uses a workspaceToken (see Authentication) and goes to the single GraphQL endpoint:
https://router.apps.filed.com/graphql
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.
This recipe does not re-document the types it touches. For the full DocumentMessage, DocumentMessageThread, DocumentMessageTaggedUser, DocumentMessageType, and DocumentMessagesFilter definitions, see Document messages. For the sign-off use case of the same API (type: "activity", markType: "signoff"), see Review a return 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) and by types: ["annotation"] to narrow to annotations only, excluding sign-offs and missing-document activity. See Read document messages for the full DocumentMessagesFilter argument.
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
            }
          }
        }
      }
    }
  }
}
{
  "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
  "filter": {
    "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
    "types": ["annotation"],
    "includeHidden": false
  }
}
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
      }
    }
  }'
{
  "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": []
              }
            ]
          }
        ]
      }
    }
  }
}
Each annotation’s id is what you pass to updateDocumentMessage (to edit the body) or hideDocumentMessage (to soft-delete it). Save it when you create the annotation in the next step.

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 for the full CreateDocumentMessageInput field list.

Create a note

mutation CreateDocumentMessage($input: CreateDocumentMessageInput!) {
  createDocumentMessage(input: $input) {
    id
    documentPath
    type
    markType
    anchorPoint
    body
    createdBy
    createdAt
    updatedAt
    threads {
      id
      documentMessageId
      body
      createdBy
      createdAt
    }
  }
}
{
  "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."
  }
}
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."
      }
    }
  }'
{
  "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": []
    }
  }
}

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).
{
  "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."
  }
}
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."
      }
    }
  }'
{
  "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"
    }
  }
}
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. The web app passes context: { scope: "workspace" } on every document-message mutation so the workspaceToken identifies the workspace.

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) for the full CreateDocumentMessageThreadInput field list.
mutation CreateDocumentMessageThread($input: CreateDocumentMessageThreadInput!) {
  createDocumentMessageThread(input: $input) {
    id
    documentMessageId
    contentPath
    body
    createdBy
    createdAt
    updatedAt
  }
}
{
  "input": {
    "documentMessageId": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
    "body": "Pulled the corrected 1099-INT from the broker portal, box 2 now matches."
  }
}
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."
      }
    }
  }'
{
  "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"
    }
  }
}

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 and Hide and unhide a document message for the full input shapes.

Edit the body

mutation UpdateDocumentMessage($id: ID!, $input: UpdateDocumentMessageInput!) {
  updateDocumentMessage(id: $id, input: $input) {
    id
    body
    anchorPoint
    updatedAt
  }
}
{
  "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
  "input": {
    "body": "Box 1 confirmed. Box 2 is high, needs a corrected 1099-INT."
  }
}
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." }
    }
  }'
{
  "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"
    }
  }
}
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.

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).
mutation HideDocumentMessage($id: ID!) {
  hideDocumentMessage(id: $id) {
    id
    hiddenAt
    hiddenBy
  }
}
{
  "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70"
}
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" }
  }'
{
  "data": {
    "hideDocumentMessage": {
      "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
      "hiddenAt": "2026-07-04T16:45:00.000Z",
      "hiddenBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1"
    }
  }
}

Unhide the annotation

To restore a hidden annotation, call unhideDocumentMessage with the same id. Both hiddenAt and hiddenBy clear back to null.
mutation UnhideDocumentMessage($id: ID!) {
  unhideDocumentMessage(id: $id) {
    id
    hiddenAt
    hiddenBy
  }
}
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" }
  }'
{
  "data": {
    "unhideDocumentMessage": {
      "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
      "hiddenAt": null,
      "hiddenBy": null
    }
  }
}
After hiding or unhiding an annotation, refetch the documentMessages query 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.

See also

  • 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 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 for the recipe that lists the subdocuments whose id you pass as documentPath here, and for Read message counts which gives you a cheap annotation-count badge via binder.messageCounts.notes.
  • Binder for the Binder.search field, whose annotations and marks buckets surface the annotations you create here.
  • Authentication for how to obtain and send the workspaceToken every call in this recipe requires.