> ## 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.

# Document messages

> Annotate binder documents with notes and flags, thread replies under a message, and record reviewer sign-offs with the document-message API

Document messages are the annotation, sign-off, and reply layer that lives on a
client's binder documents. Every document message is a `DocumentMessage` record
anchored to a subdocument path, with a `type` that says what kind of mark it is
and a `markType` label that further classifies it. The API is one and the same
for two distinct use cases:

1. **Annotations**, free-text notes and flags a reviewer leaves on a document
   (`type: "annotation"`), with threaded replies for back-and-forth discussion.
2. **Sign-offs**, the reviewer sign-off marks the review flow records against a
   sheet or row (`type: "activity"`, `markType: "signoff"`). See
   [Review a return and sign off](/guides/recipes/review-and-sign-off) for the
   end-to-end recipe.

All document-message operations are reached with a **`workspaceToken`** (see
[Authentication](/guides/authentication)) and go to the single GraphQL endpoint:

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

## The `DocumentMessage` type

```graphql theme={null}
type DocumentMessage {
  id: ID!
  workspaceId: ID!
  documentPath: String!
  type: DocumentMessageType!
  markType: String!
  anchorPoint: JSON!
  contentPath: String
  body: String
  hiddenAt: String
  hiddenBy: ID
  createdBy: ID!
  createdAt: String!
  updatedAt: String!
  threads: [DocumentMessageThread!]!
  taggedUsers: [DocumentMessageTaggedUser!]!
}

enum DocumentMessageType {
  annotation
  activity
  missing_document
}
```

<ResponseField name="id" type="ID!">
  The message's unique identifier. Save this to later
  [update](#update-a-document-message) or [hide](#hide-and-unhide-a-document-message)
  it.
</ResponseField>

<ResponseField name="workspaceId" type="ID!">
  The workspace that owns the message.
</ResponseField>

<ResponseField name="documentPath" type="String!">
  The subdocument path the message is anchored to. For a sign-off this is the
  subdocument being signed off; for an annotation it is the document location the
  note is attached to.
</ResponseField>

<ResponseField name="type" type="DocumentMessageType!">
  `annotation` for free-text annotations, `activity` for activity events such as
  sign-offs, `missing_document` for missing-document flags.
</ResponseField>

<ResponseField name="markType" type="String!">
  A free-form label for the kind of mark. Sign-offs use `"signoff"`. Annotations
  are surface-defined (for example `"note"`, `"flag"`).
</ResponseField>

<ResponseField name="anchorPoint" type="JSON!">
  A JSON object describing where the mark is anchored. For a sign-off the shape
  is `{ page, coordinates: { x, y }, level, user_role }`. The exact fields depend
  on the surface that created the message.
</ResponseField>

<ResponseField name="contentPath" type="String">
  Optional path into the document content that the mark references.
</ResponseField>

<ResponseField name="body" type="String">
  The message body text. Annotations carry their note text here. `null` for
  activity messages such as sign-offs that have no prose.
</ResponseField>

<ResponseField name="hiddenAt" type="String">
  ISO 8601 timestamp when the message was soft-hidden via
  [`hideDocumentMessage`](#hide-and-unhide-a-document-message). `null` while the
  message is visible.
</ResponseField>

<ResponseField name="hiddenBy" type="ID">
  The user who hid the message. `null` while the message is visible.
</ResponseField>

<ResponseField name="createdBy" type="ID!">
  The user who created the message.
</ResponseField>

<ResponseField name="createdAt" type="String!">
  ISO 8601 timestamp of creation.
</ResponseField>

<ResponseField name="updatedAt" type="String!">
  ISO 8601 timestamp of the last mutation (edit, hide, unhide).
</ResponseField>

<ResponseField name="threads" type="[DocumentMessageThread!]!">
  Replies on this message (see [Threads (replies)](#threads-replies)).
</ResponseField>

<ResponseField name="taggedUsers" type="[DocumentMessageTaggedUser!]!">
  Users tagged on this message. See the `DocumentMessageTaggedUser` type below.
</ResponseField>

### The `DocumentMessageThread` type

```graphql theme={null}
type DocumentMessageThread {
  id: ID!
  documentMessageId: ID!
  contentPath: String!
  body: String
  createdBy: ID!
  createdAt: String!
  updatedAt: String!
  taggedUsers: [DocumentMessageTaggedUser!]!
}
```

<ResponseField name="id" type="ID!">
  The thread reply's unique identifier.
</ResponseField>

<ResponseField name="documentMessageId" type="ID!">
  The parent `DocumentMessage.id`.
</ResponseField>

<ResponseField name="contentPath" type="String!">
  Path into the document content the reply is anchored to.
</ResponseField>

<ResponseField name="body" type="String">
  The reply body. `null` when empty.
</ResponseField>

<ResponseField name="createdBy" type="ID!">
  The user who posted the reply.
</ResponseField>

<ResponseField name="createdAt" type="String!">
  ISO 8601 timestamp of creation.
</ResponseField>

<ResponseField name="updatedAt" type="String!">
  ISO 8601 timestamp of the last edit.
</ResponseField>

<ResponseField name="taggedUsers" type="[DocumentMessageTaggedUser!]!">
  Users tagged on this reply.
</ResponseField>

### The `DocumentMessageTaggedUser` type

```graphql theme={null}
type DocumentMessageTaggedUser {
  id: ID!
  userId: ID!
  documentMessageId: ID
  documentMessageThreadId: ID
  createdAt: String!
  updatedAt: String!
}
```

<ResponseField name="id" type="ID!">
  The tag record's unique identifier.
</ResponseField>

<ResponseField name="userId" type="ID!">
  The workspace user who was tagged.
</ResponseField>

<ResponseField name="documentMessageId" type="ID">
  Set when the tag is on a top-level `DocumentMessage`. `null` when the tag is on
  a thread reply.
</ResponseField>

<ResponseField name="documentMessageThreadId" type="ID">
  Set when the tag is on a `DocumentMessageThread` reply. `null` when the tag is
  on a top-level message.
</ResponseField>

<ResponseField name="createdAt" type="String!">
  ISO 8601 timestamp of the tag.
</ResponseField>

<ResponseField name="updatedAt" type="String!">
  ISO 8601 timestamp of the last update to the tag.
</ResponseField>

## Read document messages

There is no top-level `documentMessages` query. Read them through the
`Client.documentMessages(filter)` field, reached via
`me { ... on WorkspaceUser { workspace { clients(...) { documentMessages(...) } } } }`.

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

### Arguments

```graphql theme={null}
input DocumentMessagesFilter {
  taskId: ID
  documentPath: String
  markTypes: [String!]
  types: [DocumentMessageType!]
  includeHidden: Boolean
}
```

<ParamField path="filter.taskId" type="ID">
  Return only messages created in the context of this [task](/apis/tasks) ID.
</ParamField>

<ParamField path="filter.documentPath" type="String">
  Return only messages anchored to this subdocument path.
</ParamField>

<ParamField path="filter.markTypes" type="[String!]">
  Return only messages whose `markType` is in this list (for example
  `["signoff"]` for sign-offs, `["note"]` for notes).
</ParamField>

<ParamField path="filter.types" type="[DocumentMessageType!]">
  Return only messages whose `type` is in this list.
</ParamField>

<ParamField path="filter.includeHidden" type="Boolean">
  When `true`, include soft-hidden messages in the results. Defaults to `false`,
  which excludes hidden messages.
</ParamField>

<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 { workspace { clients(filters: { ids: [$clientId] }) { documentMessages(filter: $filter) { id documentPath type markType anchorPoint body createdBy createdAt hiddenAt } } } } } }",
      "variables": {
        "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
        "filter": { "types": ["annotation"], "includeHidden": false }
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "me": {
        "workspace": {
          "clients": [
            {
              "documentMessages": [
                {
                  "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
                  "documentPath": "binder/jane/2025/w2.pdf#page=1",
                  "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",
                  "hiddenAt": null
                }
              ]
            }
          ]
        }
      }
    }
  }
  ```
</ResponseExample>

## Annotations

An annotation is a `DocumentMessage` with `type: "annotation"` that a reviewer
leaves on a document. Use a `markType` label your surface understands (the web
app uses `"note"` for free-text notes and `"flag"` for review flags). Annotations
carry their text in `body`, and can collect threaded replies for discussion.

### Create an annotation

`createDocumentMessage` creates a single document message anchored to a
subdocument path. For an annotation, pass `type: "annotation"`, your surface's
`markType`, and the note text in `body`.

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

#### Input: `CreateDocumentMessageInput`

```graphql theme={null}
input CreateDocumentMessageInput {
  clientId: ID!
  documentPath: String!
  type: DocumentMessageType!
  markType: String!
  anchorPoint: JSON!
  body: String
  taskId: ID
  taggedUserIds: [ID!]
}
```

<ParamField path="clientId" type="ID!" required>
  The client whose binder this message belongs to.
</ParamField>

<ParamField path="documentPath" type="String!" required>
  The subdocument path the message is anchored to.
</ParamField>

<ParamField path="type" type="DocumentMessageType!" required>
  `annotation`, `activity`, or `missing_document`. Use `annotation` for notes and
  flags, `activity` for sign-offs.
</ParamField>

<ParamField path="markType" type="String!" required>
  A label for the kind of mark. Use `"note"` or `"flag"` for annotations, and
  `"signoff"` for a sign-off.
</ParamField>

<ParamField path="anchorPoint" type="JSON!" required>
  A JSON object describing where the mark is anchored. The shape is
  surface-defined (for example `{ page, coordinates: { x, y } }` for an
  annotation, or `{ page, coordinates: { x, y }, level, user_role }` for a
  sign-off).
</ParamField>

<ParamField path="body" type="String">
  The message body text. Required for prose annotations; `null` for activity
  messages such as sign-offs that have no prose.
</ParamField>

<ParamField path="taskId" type="ID">
  Optional [task](/apis/tasks) ID to associate the message with (for example, the
  review task it was created during).
</ParamField>

<ParamField path="taggedUserIds" type="[ID!]">
  Optional list of workspace user IDs to tag on the message.
</ParamField>

#### Returns: `DocumentMessage!`

The created [`DocumentMessage`](#the-documentmessage-type). Save its `id` to
later update or hide it.

<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 } }",
      "variables": {
        "input": {
          "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
          "documentPath": "binder/jane/2025/w2.pdf#page=1",
          "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": "binder/jane/2025/w2.pdf#page=1",
        "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"
      }
    }
  }
  ```
</ResponseExample>

### Update a document message

`updateDocumentMessage` edits the body, anchor point, or tagged users on an
existing document message. `markType` and `type` are not mutable.

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

#### Input: `UpdateDocumentMessageInput`

```graphql theme={null}
input UpdateDocumentMessageInput {
  body: String
  anchorPoint: JSON
  taggedUserIds: [ID!]
}
```

<ParamField path="id" type="ID!" required>
  The document message to update.
</ParamField>

<ParamField path="input.body" type="String">
  The new body text.
</ParamField>

<ParamField path="input.anchorPoint" type="JSON">
  The new anchor point object.
</ParamField>

<ParamField path="input.taggedUserIds" type="[ID!]">
  The complete list of tagged user IDs (replaces the previous list).
</ParamField>

#### Returns: `DocumentMessage!`

The updated [`DocumentMessage`](#the-documentmessage-type).

<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>

### Hide and unhide a document message

Hiding a document message is the soft-delete the binder uses to dismiss an
annotation or to undo a sign-off (see [Sign-offs](#sign-offs)). The message is
retained with `hiddenAt` and `hiddenBy` set, and excluded from default reads
unless `filter.includeHidden: true` is passed.

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

mutation UnhideDocumentMessage($id: ID!) {
  unhideDocumentMessage(id: $id) {
    id
    hiddenAt
    hiddenBy
  }
}
```

<ParamField path="id" type="ID!" required>
  The document message to hide or unhide.
</ParamField>

#### Returns: `DocumentMessage!`

The updated [`DocumentMessage`](#the-documentmessage-type). After
`hideDocumentMessage`, `hiddenAt` and `hiddenBy` are populated. After
`unhideDocumentMessage`, both are `null` again.

<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>

### Threads (replies)

Threads are replies on a `DocumentMessage`. A thread reply is its own
`DocumentMessageThread` object, created under a parent message ID. Use threads
for the back-and-forth discussion that grows under an annotation.

#### Create a thread reply

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

##### Input: `CreateDocumentMessageThreadInput`

```graphql theme={null}
input CreateDocumentMessageThreadInput {
  documentMessageId: ID!
  body: String!
  taggedUserIds: [ID!]
}
```

<ParamField path="documentMessageId" type="ID!" required>
  The parent `DocumentMessage.id` to reply under.
</ParamField>

<ParamField path="body" type="String!" required>
  The reply body text.
</ParamField>

<ParamField path="taggedUserIds" type="[ID!]">
  Optional list of workspace user IDs to tag on the reply.
</ParamField>

##### Returns: `DocumentMessageThread!`

The created [`DocumentMessageThread`](#the-documentmessagethread-type).

<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>

#### Update a thread reply

```graphql theme={null}
mutation UpdateDocumentMessageThread($id: ID!, $input: UpdateDocumentMessageThreadInput!) {
  updateDocumentMessageThread(id: $id, input: $input) {
    id
    body
    updatedAt
  }
}
```

##### Input: `UpdateDocumentMessageThreadInput`

```graphql theme={null}
input UpdateDocumentMessageThreadInput {
  body: String!
}
```

<ParamField path="id" type="ID!" required>
  The thread reply to update.
</ParamField>

<ParamField path="input.body" type="String!" required>
  The new reply body text.
</ParamField>

##### Returns: `DocumentMessageThread!`

The updated [`DocumentMessageThread`](#the-documentmessagethread-type).

<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 UpdateDocumentMessageThread($id: ID!, $input: UpdateDocumentMessageThreadInput!) { updateDocumentMessageThread(id: $id, input: $input) { id body updatedAt } }",
      "variables": {
        "id": "019f0fb6-5b3d-7900-9c01-2b3c4d5e6f71",
        "input": { "body": "Corrected 1099-INT uploaded, box 2 now matches. Resolving." }
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "updateDocumentMessageThread": {
        "id": "019f0fb6-5b3d-7900-9c01-2b3c4d5e6f71",
        "body": "Corrected 1099-INT uploaded, box 2 now matches. Resolving.",
        "updatedAt": "2026-07-04T16:52:00.000Z"
      }
    }
  }
  ```
</ResponseExample>

#### Delete a thread reply

`deleteDocumentMessageThread` permanently removes a thread reply. It returns the
deleted reply's ID.

```graphql theme={null}
mutation DeleteDocumentMessageThread($id: ID!) {
  deleteDocumentMessageThread(id: $id)
}
```

<ParamField path="id" type="ID!" required>
  The thread reply to delete.
</ParamField>

##### Returns: `ID!`

The ID of the deleted thread reply.

<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 DeleteDocumentMessageThread($id: ID!) { deleteDocumentMessageThread(id: $id) }",
      "variables": { "id": "019f0fb6-5b3d-7900-9c01-2b3c4d5e6f71" }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "deleteDocumentMessageThread": "019f0fb6-5b3d-7900-9c01-2b3c4d5e6f71"
    }
  }
  ```
</ResponseExample>

## Sign-offs

A sign-off is a `DocumentMessage` with `type: "activity"` and
`markType: "signoff"`, anchored to the subdocument path being signed off. The
review flow records one sign-off per subdocument. Undoing a sign-off is a
soft-hide of the sign-off `DocumentMessage` via `hideDocumentMessage`.

<Warning>
  There is **no** `signOffSubDocuments` mutation. The schema defines an input
  type called `SignOffSubDocumentsInput`, but no field on `Mutation` is wired to
  it. The real sign-off write surface is `createDocumentMessage` with
  `type: "activity"` and `markType: "signoff"`, one call per subdocument you are
  signing off on. Do not look for a `signOffSubDocuments` mutation, it does not
  exist.
</Warning>

For the end-to-end review recipe that ties leadsheets, sign-offs, and refetch
together, see [Review a return and sign off](/guides/recipes/review-and-sign-off).
For the sign-off read surface on leadsheets (the `signOffs` field on
`Leadsheet` and `LeadsheetFieldRow`), see
[Leadsheets and review](/apis/leadsheets#sign-off-on-a-sheet-or-row).

### Sign off on a sheet or row

`createDocumentMessage` records a sign-off. Pass `type: "activity"`,
`markType: "signoff"`, the subdocument path being signed off as `documentPath`,
and an `anchorPoint` that carries the reviewer's `level` and `user_role` so the
UI renders the sign-off with the right label. The web app signs off one
subdocument at a time, one `createDocumentMessage` call per subdocument.

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

The input is the same `CreateDocumentMessageInput` documented under
[Create an annotation](#create-an-annotation); only the `type`, `markType`,
and `anchorPoint` values differ for a sign-off.

```json theme={null}
{
  "input": {
    "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
    "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
    "type": "activity",
    "markType": "signoff",
    "anchorPoint": {
      "page": 1,
      "coordinates": { "x": 0, "y": 0 },
      "level": 2,
      "user_role": "l2"
    }
  }
}
```

<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 hiddenAt } }",
      "variables": {
        "input": {
          "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
          "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
          "type": "activity",
          "markType": "signoff",
          "anchorPoint": {
            "page": 1,
            "coordinates": { "x": 0, "y": 0 },
            "level": 2,
            "user_role": "l2"
          }
        }
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "createDocumentMessage": {
        "id": "018f9c2c-2b3c-7f40-9b55-7e6f70829001",
        "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
        "type": "activity",
        "markType": "signoff",
        "anchorPoint": {
          "page": 1,
          "coordinates": { "x": 0, "y": 0 },
          "level": 2,
          "user_role": "l2"
        },
        "body": null,
        "createdBy": "019f0fb6-3001-7900-b7bc-0d11288504b1",
        "createdAt": "2026-07-04T18:22:01.000Z",
        "hiddenAt": null
      }
    }
  }
  ```
</ResponseExample>

Save the returned `id`. You pass it to `hideDocumentMessage` in the next step if
you ever need to undo the sign-off.

<Tip>
  `documentPath` for a subdocument sign-off is the subdocument's ID (the same
  value you read as a `LeadsheetFieldRow` parent path, or the anchor a
  `LeadsheetSheetIssue` is tied to). The web app signs off one subdocument at a
  time, one `createDocumentMessage` call per subdocument.
</Tip>

### Undo a sign-off

Undoing a sign-off is a soft-hide of the sign-off `DocumentMessage` via
`hideDocumentMessage` (see [Hide and unhide a document
message](#hide-and-unhide-a-document-message)). The sign-off row stays in
history with `hiddenAt` set, and the leadsheets query's `resolved` and
`issueCount` recomputation backs it out.

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

```json theme={null}
{
  "id": "018f9c2c-2b3c-7f40-9b55-7e6f70829001"
}
```

<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 documentPath type markType hiddenAt hiddenBy } }",
      "variables": { "id": "018f9c2c-2b3c-7f40-9b55-7e6f70829001" }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "hideDocumentMessage": {
        "id": "018f9c2c-2b3c-7f40-9b55-7e6f70829001",
        "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
        "type": "activity",
        "markType": "signoff",
        "hiddenAt": "2026-07-04T18:30:00.000Z",
        "hiddenBy": "019f0fb6-3001-7900-b7bc-0d11288504b1"
      }
    }
  }
  ```
</ResponseExample>

<Tip>
  After hiding or unhiding a sign-off, refetch any open leadsheets query so the
  server recomputes `LeadsheetSheetIssue.resolved` and `Leadsheet.issueCount`. See
  [Review a return and sign off](/guides/recipes/review-and-sign-off#refetch-the-leadsheets-query)
  for the recipe step.
</Tip>

## See also

* [Leadsheets and review](/apis/leadsheets) for the `Leadsheets`, `Leadsheet`,
  `LeadsheetSheetIssue`, `LeadsheetFieldRow`, and `LeadsheetTrace` type
  definitions, and the leadsheets sign-off read surface (`Leadsheet.signOffs`,
  `LeadsheetFieldRow.signOffs`).
* [Review a return and sign off](/guides/recipes/review-and-sign-off) for the
  end-to-end recipe that ties reading leadsheets, recording sign-offs, and
  refetching together.
* [Tasks](/apis/tasks) for the polling mechanics behind the review task whose
  `taskId` you can associate a document message with.
