# Binder
Source: https://docs.apps.filed.com/apis/binder
Read a client's binder: list uploaded files, missing items, message counts, and search across the binder
A client's **binder** is the container for the documents Filed has ingested for
that client. It holds the uploaded files (subdocuments), the missing-item
checklist the run produces, message counts for quick badges, and a search
surface across bookmarks, annotations, and document contents. Read it with the
`binder` field on a client.
The binder is reached through the [`me`](/apis/me) query resolved as a
`WorkspaceUser`, so every binder operation requires a **`workspaceToken`** (see
[Authentication](/guides/authentication)). All requests go to:
```
https://router.apps.filed.com/graphql
```
There is no top-level `binder` query. The binder belongs to a client, so you
read it through
`me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { binder { ... } } } } }`.
The `workspaceToken` already identifies the workspace, so you never pass a
workspace ID to read the binder.
## Query a client's binder
Use this query when an app, agent, or MCP tool needs the binder overview for a
client: filed documents, open missing items, quick counts, and signed URLs for
the original uploads and page renders.
```graphql theme={null}
query QueryClientBinder($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
id
name
binder {
id
clientId
messageCounts {
openMissing
notes
}
subdocuments {
id
parentDocumentId
fileName
type
issuer
taxYear
status
category
bucket
pageRange
canonicalPath
parentDocument {
id
fileName
contentType
url {
filePath
url
}
subdocPages {
pageNumber
imageUrl {
filePath
url
}
markdownUrl {
filePath
url
}
}
}
}
missingItemsAssessment(filter: { status: OPEN }) {
status
reason
count
items {
id
item
formType
issuer
taxYear
severity
reason
status
category
}
}
}
}
}
}
}
}
```
### Arguments
The client whose binder you want to read. Pass it via `filters.ids` on
`clients`.
### Returns
The binder for the requested client. It contains filed documents, missing
items, message counts, and search.
The logical documents Filed extracted or filed into the binder. Save each
`id`: it is the `documentPath` used by
[document messages](/apis/document-messages) and file-level
[leadsheet signoff](/apis/leadsheets#sign-off-on-a-sheet-or-row).
Signed URL for the original uploaded file. Signed URLs are time-limited.
Signed page image and markdown URLs for rendering or reading the parent
document page by page.
The status, reason, count, and filtered items for the missing-document check.
Treat an empty item list as an all-clear only when `status` is `AVAILABLE`.
Small counts for badges and summaries without fetching all messages.
```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 QueryClientBinder($clientId: ID!) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { id name binder { id clientId messageCounts { openMissing notes } subdocuments { id parentDocumentId fileName type issuer taxYear status category bucket pageRange canonicalPath parentDocument { id fileName contentType url { filePath url } subdocPages { pageNumber imageUrl { filePath url } markdownUrl { filePath url } } } } missingItemsAssessment(filter: { status: OPEN }) { status reason count items { id item formType issuer taxYear severity reason status category } } } } } } } }",
"variables": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c"
}
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"clients": [
{
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"name": "Jane Smith",
"binder": {
"id": "018f9c2a-4b6f-7a10-b2c4-9e8d7f6a5b4d",
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"messageCounts": {
"openMissing": 1,
"notes": 2
},
"subdocuments": [
{
"id": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
"parentDocumentId": "018f9c2a-6a5b-7c3d-9a4e-2f6b1c8d0e49",
"fileName": "1099-INT-Acme-Broker.pdf",
"type": "1099-INT",
"issuer": "Acme Broker",
"taxYear": 2025,
"status": "ingested",
"category": "income",
"bucket": "source_docs",
"pageRange": [1],
"canonicalPath": "clients/018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c/source_docs/018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a/canonical.json",
"parentDocument": {
"id": "018f9c2a-6a5b-7c3d-9a4e-2f6b1c8d0e49",
"fileName": "uploads-packet.pdf",
"contentType": "application/pdf",
"url": {
"filePath": "uploads/018f9c2a-6a5b-7c3d-9a4e-2f6b1c8d0e49.pdf",
"url": "https://signed.example.com/uploads-packet.pdf?sig=..."
},
"subdocPages": [
{
"pageNumber": 1,
"imageUrl": {
"filePath": "renders/page-1.png",
"url": "https://signed.example.com/page-1.png?sig=..."
},
"markdownUrl": {
"filePath": "renders/page-1.md",
"url": "https://signed.example.com/page-1.md?sig=..."
}
}
]
}
}
],
"missingItemsAssessment": {
"status": "AVAILABLE",
"reason": null,
"count": 1,
"items": [
{
"id": "018f9c2a-8c2f-7c3d-9a4e-2f6b1c8d0e5b",
"item": "W-2 from Example Employer",
"formType": "W-2",
"issuer": "Example Employer",
"taxYear": 2025,
"severity": "CRITICAL",
"reason": "Expected a W-2 from Example Employer but no matching document was found in the binder.",
"status": "OPEN",
"category": "income"
}
]
}
}
}
]
}
}
}
}
```
### MCP usage pattern
1. Start with `clients(filters: { ids: [$clientId] })` and read `binder`.
2. Use `binder.subdocuments` for the document list and preserve each
subdocument `id` for later annotation, signoff, or leadsheet operations.
3. Use `parentDocument.url` when the user needs the original upload, and
`parentDocument.subdocPages` when the user needs page images or page
markdown.
4. Use `missingItemsAssessment.count` and `messageCounts` for compact summaries
before displaying full lists.
5. Use [`binder.search`](#search-the-binder) when the user asks about specific
content, issuers, categories, annotations, marks, or values inside the
documents.
## The `Binder` type
The top-level binder container for one client.
```graphql theme={null}
type Binder {
id: ID!
clientId: ID!
subdocuments(filter: SubDocumentsFilter): [SubDocument!]!
createdAt: String!
updatedAt: String!
missingItemsAssessment(
filter: BinderMissingItemsFilter
): BinderMissingItemsAssessment!
missingItems(filter: BinderMissingItemsFilter): [BinderMissingItem!]!
@deprecated(reason: "Use missingItemsAssessment instead.")
openMissingItemsCount: Int!
@deprecated(reason: "Use missingItemsAssessment.count instead.")
messageCounts: BinderMessageCounts!
search(query: String!, limit: Int = 20): BinderSearchResults!
leadsheets(taskId: ID): Leadsheets
}
```
The binder's unique identifier.
The client this binder belongs to.
The files filed in the binder. Pass a `SubDocumentsFilter` to narrow to
unreviewed, flagged, or files under one parent document. See
[List the files in a binder](#list-the-files-in-a-binder).
When the binder was created (ISO 8601 timestamp).
When the binder was last updated (ISO 8601 timestamp).
The state and filtered result of the missing-document check. Pass a
`BinderMissingItemsFilter` to narrow by item status. See
[List missing items](#list-missing-items).
Deprecated. Use `missingItemsAssessment.items` so you can distinguish an
available empty result from a pending or unavailable assessment.
Deprecated. Use `missingItemsAssessment.count`.
Open missing-item and notes counts for badge rendering. See
[Read message counts](#read-message-counts).
Search across bookmarks, annotations, marks, and document contents. See
[Search the binder](#search-the-binder).
The leadsheets tree for a `TAX_PREP` or `TAX_REVIEW` run. Pass the run's
`taskId` to read that run's tree; omit it to read the most recent tree.
Leadsheets are documented separately on
[Leadsheets and review](/apis/leadsheets#read-a-clients-leadsheets); this page
does not re-document them.
## The `SubDocument` type
One file in a binder. A subdocument is one logical document extracted from an
uploaded parent (for example one 1099-INT inside a larger uploaded packet).
```graphql theme={null}
type SubDocument {
id: ID!
clientId: ID!
parentDocumentId: String!
fileName: String!
pageRange: [Int!]!
type: String!
issuer: String!
taxYear: Int!
status: String!
category: String
order: Int!
createdAt: String!
updatedAt: String!
parentDocument: ParentDocument
canonicalPath: String
bucket: String
}
```
The subdocument's unique identifier. This is the value you pass as
`documentPath` when [creating a document message](/apis/document-messages) or
[signing off](/apis/leadsheets#sign-off-on-a-sheet-or-row) on a file.
The client this subdocument belongs to.
The ID of the parent document this subdocument was extracted from.
The file name, for example `1099-INT-Acme-Broker.pdf`.
The pages inside the parent document this subdocument covers, 1-indexed.
The document type the extractor classified this as, for example `1099-INT` or
`W-2`.
The issuer or payer named on the document, for example `Acme Broker`.
The tax year this document covers, for example `2025`.
The ingestion or review status of this subdocument, for example `ingested` or
`reviewed`.
The binder grouping category, when one has been assigned. Nullable: some
subdocuments are uncategorized until a reviewer files them.
The sort order of this subdocument within the binder.
When the subdocument was created (ISO 8601 timestamp).
When the subdocument was last updated (ISO 8601 timestamp).
The parent document this subdocument was extracted from. See
[`ParentDocument`](#the-parentdocument-type).
The path to the `canonical.json` file in the per-client git repo. Populated
when a tax-prep run has picked up this subdocument; `null` until then.
The use-case bucket the subdocument lives in on disk, for example
`source_docs`, `prior_year_docs`, or `current_year_drafts`. `null` when the
subdocument has not yet been migrated into a bucket.
## The `ParentDocument` type
The original uploaded document a subdocument was extracted from. Carries the
file URL and per-page render URLs.
```graphql theme={null}
type ParentDocument {
id: ID!
fileName: String!
contentType: String!
url: SignedPath!
subdocPages: [SubdocPage!]!
}
type SubdocPage {
pageNumber: Int!
imageUrl: SignedPath
markdownUrl: SignedPath
}
```
The parent document's unique identifier.
The original uploaded file name.
The MIME type, for example `application/pdf`.
A signed URL for downloading the original file. `SignedPath` is
`{ filePath: String!, url: String! }`.
Per-page render URLs for the parent document: one image URL and one markdown
URL per page, both signed and time-limited.
## The `BinderMissingItemsAssessment` type
The result of the missing-document check for the requested item status.
```graphql theme={null}
type BinderMissingItemsAssessment {
status: BinderMissingItemsAssessmentStatus!
reason: String
count: Int!
items: [BinderMissingItem!]!
}
enum BinderMissingItemsAssessmentStatus {
PENDING
AVAILABLE
UNAVAILABLE
}
```
`PENDING` while the check has not completed, `AVAILABLE` when the result can be
used, or `UNAVAILABLE` when the binder lacks enough evidence to determine
missing documents.
An explanation for a pending or unavailable assessment. It is `null` when the
assessment is available.
The number of items returned for the requested filter.
The missing items matching the requested filter. For an `OPEN` filter this is
empty while the assessment is pending or unavailable.
## The `BinderMissingItem` type
One item on the missing-document checklist the binder produces: a form the run
expected to find but did not, with a severity, a reason, and a status you can
move between `OPEN`, `IGNORED`, and `RESOLVED`.
```graphql theme={null}
type BinderMissingItem {
id: ID!
binderId: ID!
item: String!
formType: String
issuer: String
taxYear: Int
severity: ChecklistItemSeverity!
reason: String!
status: ChecklistItemStatus!
category: String
createdAt: String!
updatedAt: String!
}
enum ChecklistItemSeverity {
CRITICAL
MEDIUM
LOW
}
enum ChecklistItemStatus {
OPEN
IGNORED
RESOLVED
}
```
The missing-item record ID. Pass this to
[`ignoreBinderMissingItem`](#ignore-a-missing-item) or
[`restoreBinderMissingItem`](#restore-a-missing-item).
The binder this missing item belongs to.
The missing item, for example `1099-INT` or `W-2 from Acme`.
The form type expected, when the checklist is form-specific.
The issuer the run expected to find, when relevant.
The tax year the missing item applies to.
How blocking the missing item is: `CRITICAL`, `MEDIUM`, or `LOW`.
Why the run flagged this as missing, for example
`Expected a 1099-INT from Acme Broker but no matching document was found in the binder.`
The current status: `OPEN` (still needs the document), `IGNORED` (a reviewer
dismissed it via [`ignoreBinderMissingItem`](#ignore-a-missing-item)), or
`RESOLVED` (the document was later found and filed).
A grouping category, when one has been assigned.
When the missing-item record was created (ISO 8601 timestamp).
When the missing-item record was last updated (ISO 8601 timestamp).
## The `BinderMessageCounts` type
Open missing-item and notes counts. Use `missingItemsAssessment` when you also
need to know whether the missing-document check is available.
```graphql theme={null}
type BinderMessageCounts {
openMissing: Int!
notes: Int!
}
```
The number of stored missing items in the `OPEN` status. Do not use this field
alone to infer that the missing-document check completed successfully.
The number of open annotation notes on the binder. See
[Document messages](/apis/document-messages) for the annotation API.
## The `BinderSearchResults` type
The result of `Binder.search`: four buckets of hits, one per search surface
(bookmarks, annotations, marks, and document contents).
```graphql theme={null}
type BinderSearchResults {
bookmarks: [BookmarkSearchHit!]!
annotations: [BinderMessageSearchHit!]!
marks: [BinderMessageSearchHit!]!
contents: [BinderContentSearchHit!]!
}
```
Subdocuments whose file name, issuer, type, or category matched the query. See
[`BookmarkSearchHit`](#the-bookmarksearchhit-type).
Binder messages (annotations and notes) whose body or expression matched the
query. See [`BinderMessageSearchHit`](#the-bindermessagesearchhit-type).
Binder messages that are marks, whose body or expression matched the query. See
[`BinderMessageSearchHit`](#the-bindermessagesearchhit-type).
Hits inside document contents, with the matching field names, values, and
bounding boxes. See [`BinderContentSearchHit`](#the-bindercontentsearchhit-type).
### The `BookmarkSearchHit` type
```graphql theme={null}
type BookmarkSearchHit {
subdocument: SubDocument!
matchedField: BookmarkMatchField!
snippet: String!
}
enum BookmarkMatchField {
FILE_NAME
ISSUER
TYPE
CATEGORY
}
```
The subdocument whose bookmark matched. See
[`SubDocument`](#the-subdocument-type).
Which subdocument field matched: `FILE_NAME`, `ISSUER`, `TYPE`, or `CATEGORY`.
A snippet of the matched value, for display.
### The `BinderMessageSearchHit` type
A search hit inside a binder message (an annotation or a mark). The underlying
message is a `BinderMessage`, the binder's internal message type used by
search. Annotation and sign-off writes use the `DocumentMessage` type (see
[Document messages](/apis/document-messages)); `BinderMessage` is the read
shape the search surface returns.
```graphql theme={null}
type BinderMessageSearchHit {
message: BinderMessage!
matchedField: BinderMessageMatchField!
snippet: String!
isReply: Boolean!
}
enum BinderMessageMatchField {
BODY
EXPRESSION
}
```
The binder message that matched. See [`BinderMessage`](#the-bindermessage-type).
Which message field matched: `BODY` (the message body) or `EXPRESSION` (an
expression inside the message content).
A snippet of the matched text, for display.
`true` when the hit is a reply inside a thread, `false` when it is a top-level
message.
### The `BinderMessage` type
The binder's internal message type, returned by `Binder.search`. It carries the
message's anchor (subdocument path, page number, coordinates), content, threads,
and tagged users.
```graphql theme={null}
type BinderMessage {
id: ID!
binderId: ID!
subDocumentPath: ID!
pageNumber: Int!
coordinates: JSON
type: String!
content: JSON!
createdBy: ID!
threads: [BinderMessageThread!]!
taggedUsers: [BinderTaggedUser!]!
isRead: Boolean!
deletedAt: String
deletedBy: ID
createdAt: String!
updatedAt: String!
}
```
The binder message ID.
The binder this message belongs to.
The subdocument path the message is anchored to.
The page number inside the subdocument the message is anchored to, 1-indexed.
The on-page coordinates of the message anchor, when the message is pinned to a
region. `null` for messages that are not pinned to a region.
The message type label, for example `annotation` or `mark`.
The message content as a JSON object.
The user who created the message.
Reply threads on the message.
Users tagged on the message.
Whether the message has been read by the current user.
When the message was deleted, if applicable. `null` while the message is live.
The user who deleted the message, if applicable.
When the message was created (ISO 8601 timestamp).
When the message was last updated (ISO 8601 timestamp).
### The `BinderContentSearchHit` type
A search hit inside a subdocument's contents: the matching field names, values,
and the page-level bounding boxes that pin them.
```graphql theme={null}
type BinderContentSearchHit {
subdocument: SubDocument!
snippet: String!
pages: [BinderContentPageHit!]!
}
type BinderContentPageHit {
pageNumber: Int!
fieldMatches: [BinderContentFieldMatch!]!
}
type BinderContentFieldMatch {
fieldName: String!
value: String!
bbox: BinderContentBbox
}
type BinderContentBbox {
xMin: Int!
yMin: Int!
xMax: Int!
yMax: Int!
pageNumber: Int!
}
```
The subdocument whose contents matched. See
[`SubDocument`](#the-subdocument-type).
A snippet of the matched content, for display.
The pages inside the subdocument that carried matches, with the field-level
matches on each page.
The page number, 1-indexed.
The field-level matches on this page.
The name of the field that matched.
The value of the field that matched.
The bounding box that pins this match on the page. See `BinderContentBbox`
below. `null` when the match is not pinned to a region.
Left edge of the box, in page pixels.
Top edge of the box, in page pixels.
Right edge of the box, in page pixels.
Bottom edge of the box, in page pixels.
The page this box is on, 1-indexed.
## List the files in a binder
Read `binder.subdocuments` to list the files filed in a client's binder. This
is the most common read against the binder: it backs the binder's Documents
screen.
```graphql theme={null}
query GetClientBinderSubdocuments($clientId: ID!, $filter: SubDocumentsFilter) {
me {
... on WorkspaceUser {
id
workspace {
id
clients(filters: { ids: [$clientId] }) {
id
binder {
id
subdocuments(filter: $filter) {
id
parentDocumentId
fileName
pageRange
type
issuer
taxYear
status
category
order
createdAt
bucket
canonicalPath
parentDocument {
id
fileName
contentType
url {
url
}
}
}
}
}
}
}
}
}
```
### Arguments
```graphql theme={null}
input SubDocumentsFilter {
unreviewed: Boolean
flagged: Boolean
parentDocumentId: ID
}
```
The client whose binder you want to read. Pass it via `filters.ids` on
`clients`.
When `true`, return only subdocuments that have not been reviewed yet.
When `true`, return only subdocuments that carry a flag.
Return only subdocuments extracted from this parent document.
```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 GetClientBinderSubdocuments($clientId: ID!, $filter: SubDocumentsFilter) { me { ... on WorkspaceUser { id workspace { id clients(filters: { ids: [$clientId] }) { id binder { id subdocuments(filter: $filter) { id parentDocumentId fileName pageRange type issuer taxYear status category order createdAt bucket canonicalPath parentDocument { id fileName contentType url { url } } } } } } } } }",
"variables": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"filter": null
}
}'
```
```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",
"binder": {
"id": "018f9c2a-4b6f-7a10-b2c4-9e8d7f6a5b4d",
"subdocuments": [
{
"id": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
"parentDocumentId": "018f9c2a-6a5b-7c3d-9a4e-2f6b1c8d0e49",
"fileName": "1099-INT-Acme-Broker.pdf",
"pageRange": [1],
"type": "1099-INT",
"issuer": "Acme Broker",
"taxYear": 2025,
"status": "ingested",
"category": "income",
"order": 0,
"createdAt": "2026-07-01T15:10:00.000Z",
"bucket": "source_docs",
"canonicalPath": "clients/018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c/source_docs/018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a/canonical.json",
"parentDocument": {
"id": "018f9c2a-6a5b-7c3d-9a4e-2f6b1c8d0e49",
"fileName": "uploads-packet.pdf",
"contentType": "application/pdf",
"url": {
"url": "https://signed.example.com/uploads-packet.pdf?sig=..."
}
}
}
]
}
}
]
}
}
}
}
```
The subdocument `id` is the value you pass as `documentPath` when [creating a
document message](/apis/document-messages#create-an-annotation) or
[signing off](/apis/leadsheets#sign-off-on-a-sheet-or-row) on a file. Save it
when you list the binder so you can reference it later.
## List missing items
Read `binder.missingItemsAssessment` to get the state and filtered result of
the missing-document check. Filter by item status to read only open, ignored,
or resolved history.
```graphql theme={null}
query GetClientBinderMissingItems($clientId: ID!, $filter: BinderMissingItemsFilter) {
me {
... on WorkspaceUser {
id
workspace {
clients(filters: { ids: [$clientId] }) {
id
binder {
id
missingItemsAssessment(filter: $filter) {
status
reason
count
items {
id
binderId
item
formType
issuer
taxYear
severity
reason
status
category
createdAt
updatedAt
}
}
}
}
}
}
}
}
```
### Arguments
```graphql theme={null}
input BinderMissingItemsFilter {
status: ChecklistItemStatus
}
```
The client whose binder you want to read. Pass it via `filters.ids` on
`clients`.
Return only missing items in this status: `OPEN`, `IGNORED`, or `RESOLVED`.
Omit it to use the default `OPEN` status.
### Returns
```graphql theme={null}
type BinderMissingItemsAssessment {
status: BinderMissingItemsAssessmentStatus!
reason: String
count: Int!
items: [BinderMissingItem!]!
}
```
Whether the missing-document check is `PENDING`, `AVAILABLE`, or `UNAVAILABLE`.
Why the assessment is pending or unavailable. It is `null` when available.
The number of items matching the requested filter.
The filtered missing-item rows. Open items are empty for pending or unavailable
assessments.
```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 GetClientBinderMissingItems($clientId: ID!, $filter: BinderMissingItemsFilter) { me { ... on WorkspaceUser { id workspace { clients(filters: { ids: [$clientId] }) { id binder { id missingItemsAssessment(filter: $filter) { status reason count items { id binderId item formType issuer taxYear severity reason status category createdAt updatedAt } } } } } } } }",
"variables": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"filter": { "status": "OPEN" }
}
}'
```
```json theme={null}
{
"data": {
"me": {
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"workspace": {
"clients": [
{
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"binder": {
"id": "018f9c2a-4b6f-7a10-b2c4-9e8d7f6a5b4d",
"missingItemsAssessment": {
"status": "AVAILABLE",
"reason": null,
"count": 1,
"items": [
{
"id": "018f9c2c-5d6e-7f20-9a33-7c4d5e6f7090",
"binderId": "018f9c2a-4b6f-7a10-b2c4-9e8d7f6a5b4d",
"item": "W-2 from Initech",
"formType": "W-2",
"issuer": "Initech",
"taxYear": 2025,
"severity": "CRITICAL",
"reason": "Expected a W-2 from Initech but no matching document was found in the binder.",
"status": "OPEN",
"category": "income",
"createdAt": "2026-07-01T15:30:00.000Z",
"updatedAt": "2026-07-01T15:30:00.000Z"
}
]
}
}
}
]
}
}
}
}
```
## Read message counts
Read `binder.messageCounts` for stored missing-item and open note counts. Use
`missingItemsAssessment` for a missing-document badge so the UI can distinguish
an available empty result from a pending or unavailable check.
```graphql theme={null}
query GetBinderMessageCounts($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
id
binder {
id
messageCounts {
openMissing
notes
}
}
}
}
}
}
}
```
### Arguments
The client whose binder counts you want. Pass it via `filters.ids` on
`clients`.
```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 GetBinderMessageCounts($clientId: ID!) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { id binder { id messageCounts { openMissing notes } } } } } } }",
"variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" }
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"clients": [
{
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"binder": {
"id": "018f9c2a-4b6f-7a10-b2c4-9e8d7f6a5b4d",
"messageCounts": {
"openMissing": 1,
"notes": 2
}
}
}
]
}
}
}
}
```
## Search the binder
Read `binder.search` to search across bookmarks (subdocuments by file name,
issuer, type, or category), annotations, marks, and document contents in one
call. The query is debounced in the web app: pass at least two characters.
```graphql theme={null}
query BinderSearch($clientId: ID!, $query: String!, $limit: Int = 20) {
me {
... on WorkspaceUser {
id
workspace {
clients(filters: { ids: [$clientId] }) {
id
binder {
id
search(query: $query, limit: $limit) {
bookmarks {
matchedField
snippet
subdocument {
id
fileName
type
issuer
category
pageRange
}
}
annotations {
matchedField
snippet
isReply
message {
id
type
subDocumentPath
pageNumber
content
createdBy
createdAt
}
}
marks {
matchedField
snippet
isReply
message {
id
type
subDocumentPath
pageNumber
content
createdBy
createdAt
}
}
contents {
snippet
subdocument {
id
fileName
type
issuer
category
pageRange
canonicalPath
}
pages {
pageNumber
fieldMatches {
fieldName
value
bbox {
xMin
yMin
xMax
yMax
pageNumber
}
}
}
}
}
}
}
}
}
}
}
```
### Arguments
The client whose binder you want to search. Pass it via `filters.ids` on
`clients`.
The search query. The web app debounces the input and requires at least two
characters before firing the query.
Maximum number of hits to return per bucket. Defaults to `20`.
```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 BinderSearch($clientId: ID!, $query: String!, $limit: Int = 20) { me { ... on WorkspaceUser { id workspace { clients(filters: { ids: [$clientId] }) { id binder { id search(query: $query, limit: $limit) { bookmarks { matchedField snippet subdocument { id fileName type issuer category pageRange } } annotations { matchedField snippet isReply message { id type subDocumentPath pageNumber content createdBy createdAt } } marks { matchedField snippet isReply message { id type subDocumentPath pageNumber content createdBy createdAt } } contents { snippet subdocument { id fileName type issuer category pageRange canonicalPath } pages { pageNumber fieldMatches { fieldName value bbox { xMin yMin xMax yMax pageNumber } } } } } } } } } } }",
"variables": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"query": "Acme",
"limit": 20
}
}'
```
```json theme={null}
{
"data": {
"me": {
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"workspace": {
"clients": [
{
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"binder": {
"id": "018f9c2a-4b6f-7a10-b2c4-9e8d7f6a5b4d",
"search": {
"bookmarks": [
{
"matchedField": "ISSUER",
"snippet": "Acme Broker",
"subdocument": {
"id": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
"fileName": "1099-INT-Acme-Broker.pdf",
"type": "1099-INT",
"issuer": "Acme Broker",
"category": "income",
"pageRange": [1]
}
}
],
"annotations": [],
"marks": [],
"contents": [
{
"snippet": "Acme Broker paid $210.00 in interest",
"subdocument": {
"id": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
"fileName": "1099-INT-Acme-Broker.pdf",
"type": "1099-INT",
"issuer": "Acme Broker",
"category": "income",
"pageRange": [1],
"canonicalPath": "clients/018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c/source_docs/018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a/canonical.json"
},
"pages": [
{
"pageNumber": 1,
"fieldMatches": [
{
"fieldName": "payer",
"value": "Acme Broker",
"bbox": {
"xMin": 88,
"yMin": 412,
"xMax": 220,
"yMax": 428,
"pageNumber": 1
}
}
]
}
]
}
]
}
}
}
]
}
}
}
}
```
## Ignore a missing item
`ignoreBinderMissingItem` moves a missing item from `OPEN` to `IGNORED`. Use it
when a reviewer dismisses a missing item the run flagged. The mutation takes
the missing-item `id` and the `workspaceId`, and returns the updated
`BinderMissingItem` with its new `status`.
```graphql theme={null}
mutation IgnoreBinderMissingItem($id: ID!, $workspaceId: String!) {
ignoreBinderMissingItem(id: $id, workspaceId: $workspaceId) {
id
status
}
}
```
### Input
The ID of the missing item to ignore (the `id` from
[List missing items](#list-missing-items)).
The workspace ID. Unlike the read fields, this mutation takes the workspace ID
explicitly. Use the same workspace ID the `workspaceToken` was issued for.
### Returns: `BinderMissingItem!`
The ignored [`BinderMissingItem`](#the-bindermissingitem-type), with `status`
set to `IGNORED`.
```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 IgnoreBinderMissingItem($id: ID!, $workspaceId: String!) { ignoreBinderMissingItem(id: $id, workspaceId: $workspaceId) { id status } }",
"variables": {
"id": "018f9c2c-5d6e-7f20-9a33-7c4d5e6f7090",
"workspaceId": "019f0fb6-3001-7900-b7bc-0d11288504b1"
}
}'
```
```json theme={null}
{
"data": {
"ignoreBinderMissingItem": {
"id": "018f9c2c-5d6e-7f20-9a33-7c4d5e6f7090",
"status": "IGNORED"
}
}
}
```
After ignoring a missing item, refetch the
[List missing items](#list-missing-items) query so the open count and the list
agree. The web app optimistically moves the row from the `OPEN` assessment to
the `IGNORED` assessment and updates both counts.
## Restore a missing item
`restoreBinderMissingItem` moves a missing item from `IGNORED` back to `OPEN`.
Use it when a reviewer wants to re-surface a missing item they previously
ignored.
```graphql theme={null}
mutation RestoreBinderMissingItem($id: ID!, $workspaceId: String!) {
restoreBinderMissingItem(id: $id, workspaceId: $workspaceId) {
id
status
}
}
```
### Input
The ID of the missing item to restore (the `id` from
[List missing items](#list-missing-items)).
The workspace ID. Use the same workspace ID the `workspaceToken` was issued
for.
### Returns: `BinderMissingItem!`
The restored [`BinderMissingItem`](#the-bindermissingitem-type), with `status`
set to `OPEN`.
```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 RestoreBinderMissingItem($id: ID!, $workspaceId: String!) { restoreBinderMissingItem(id: $id, workspaceId: $workspaceId) { id status } }",
"variables": {
"id": "018f9c2c-5d6e-7f20-9a33-7c4d5e6f7090",
"workspaceId": "019f0fb6-3001-7900-b7bc-0d11288504b1"
}
}'
```
```json theme={null}
{
"data": {
"restoreBinderMissingItem": {
"id": "018f9c2c-5d6e-7f20-9a33-7c4d5e6f7090",
"status": "OPEN"
}
}
}
```
After restoring a missing item, refetch the
[List missing items](#list-missing-items) query so the open count and the list
agree. The web app optimistically moves the row from the `IGNORED` assessment
to the `OPEN` assessment when the check is available.
# Clients
Source: https://docs.apps.filed.com/apis/clients
Create clients, list and fetch them, and add documents to a client's binder
A **client** is a taxpayer or return that lives inside a workspace. Every client
operation is reached through the [`me`](/apis/me) query resolved as a
`WorkspaceUser`, so you must authenticate with a **`workspaceToken`** (see
[Authentication](/guides/authentication)). All requests go to:
```
https://router.apps.filed.com/graphql
```
There is no top-level `clients` query. Clients belong to a workspace, so you read
them through `me { ... on WorkspaceUser { workspace { clients(...) } } }`. The
`workspaceToken` already identifies which workspace, so you never pass a
workspace ID.
## The `Client` type
```graphql theme={null}
type Client {
id: ID!
name: String!
externalId: String!
status: ClientStatus!
returnType: ReturnType!
taxYear: Int!
createdAt: Date!
assignees: [WorkspaceUserShortDetails!]!
tasks(type: TaskType, status: TaskStatus, triggeredBy: ID, limit: Int): [Task!]!
}
enum ClientStatus {
active
archived
}
enum ReturnType {
F1040
F1041
F1065
F1120
F1120S
F990
}
```
The client's unique identifier.
The client's display name.
Your own identifier for the client (for example the ID from your practice
management system). Unique within the workspace.
`active` or `archived`.
The tax return form: `F1040`, `F1041`, `F1065`, `F1120`, `F1120S`, or `F990`.
The tax year, for example `2025`.
When the client was created.
The workspace users assigned to this client.
```graphql theme={null}
type WorkspaceUserShortDetails {
id: ID!
userId: ID!
name: String!
email: String
role: WorkspaceRole!
kind: UserKind
createdAt: Date
invitedByName: String
invitedByEmail: String
}
enum WorkspaceRole {
admin
l1
l2
l3
}
enum UserKind {
user
backoffice_user
assistant
api
}
```
The membership ID (workspace user), not the underlying user ID.
The underlying user account ID.
The user's display name.
The user's email, when available.
Their role: `admin`, `l1`, `l2`, or `l3`.
Account kind: `user`, `backoffice_user`, `assistant`, or `api`.
When they joined the workspace.
Name of the user who invited them, if applicable.
Email of the user who invited them, if applicable.
Background tasks for this client (binder ingestion, tax prep, and so on). See the
[tasks API](/apis/tasks). Narrow with the `type`, `status`, `triggeredBy`, and
`limit` arguments.
The `Client` type exposes more fields (binder, documents, conversations, and
others) than are listed here. This page covers the fields needed to create,
list, fetch, and add documents to clients.
## Create a client
`createClient` creates a client and, optionally, kicks off binder ingestion for
any documents you have already staged (see [Uploading
documents](/guides/uploading-documents)).
```graphql theme={null}
mutation CreateClient($input: CreateClientInput!) {
createClient(input: $input) {
client {
id
name
externalId
status
returnType
taxYear
}
taskId
}
}
```
### Input: `CreateClientInput`
```graphql theme={null}
input CreateClientInput {
name: String!
externalId: String!
returnType: ReturnType!
taxYear: Int!
uploadIds: [String!]
}
```
The client's display name.
Your identifier for the client. Must be unique within the workspace.
One of `F1040`, `F1041`, `F1065`, `F1120`, `F1120S`, `F990`.
The tax year, for example `2025`.
Optional. Upload IDs from the [upload endpoint](/guides/uploading-documents). If
provided, Filed ingests these documents into the new client's binder and returns
a `taskId` you can track.
### Returns: `CreateClientResult`
```graphql theme={null}
type CreateClientResult {
client: Client!
taskId: ID
}
```
The created client.
The binder ingestion [task](/apis/tasks) ID, present only when `uploadIds` were
supplied. `null` when the client was created without documents.
```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 CreateClient($input: CreateClientInput!) { createClient(input: $input) { client { id name externalId status returnType taxYear } taskId } }",
"variables": {
"input": {
"name": "Jane Taxpayer",
"externalId": "PMS-10432",
"returnType": "F1040",
"taxYear": 2025,
"uploadIds": ["018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"]
}
}
}'
```
```json theme={null}
{
"data": {
"createClient": {
"client": {
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"name": "Jane Taxpayer",
"externalId": "PMS-10432",
"status": "active",
"returnType": "F1040",
"taxYear": 2025
},
"taskId": "018f9c2b-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}
}
}
```
## List clients
Read `workspace.clients` to list clients. Filter, page, and sort with the
arguments below.
```graphql theme={null}
query ListClients($filters: ClientFilters, $offset: Int, $limit: Int, $sortBy: SortBy) {
me {
... on WorkspaceUser {
workspace {
clients(filters: $filters, offset: $offset, limit: $limit, sortBy: $sortBy) {
id
name
externalId
status
returnType
taxYear
createdAt
}
}
}
}
}
```
### Arguments
```graphql theme={null}
input ClientFilters {
ids: [ID!]
status: [ClientStatus!]
search: String
assigneeIds: [ID!]
assignedToMe: Boolean
}
input SortBy {
field: String!
order: SortByOrder! # ASC | DESC
}
```
Return only clients with these IDs. This is how you [fetch a single
client](#fetch-a-single-client).
Return only clients in these statuses (`active`, `archived`).
Free-text search over client name and external ID.
Return only clients assigned to these workspace users.
When `true`, return only clients assigned to the authenticated user.
Number of clients to skip, for pagination.
Maximum number of clients to return.
Sort order, for example `{ "field": "createdAt", "order": "DESC" }`.
```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 ListClients($filters: ClientFilters, $limit: Int, $sortBy: SortBy) { me { ... on WorkspaceUser { workspace { clients(filters: $filters, limit: $limit, sortBy: $sortBy) { id name externalId status returnType taxYear createdAt } } } } }",
"variables": {
"filters": { "status": ["active"], "search": "jane" },
"limit": 20,
"sortBy": { "field": "createdAt", "order": "DESC" }
}
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"clients": [
{
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"name": "Jane Taxpayer",
"externalId": "PMS-10432",
"status": "active",
"returnType": "F1040",
"taxYear": 2025,
"createdAt": "2026-07-01T15:04:22.000Z"
}
]
}
}
}
}
```
## Fetch a single client
There is no `client(id:)` query. Fetch one client by passing its ID in
`filters.ids` and reading the first element.
```graphql theme={null}
query GetClient($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
id
name
externalId
status
returnType
taxYear
createdAt
assignees {
id
name
email
role
}
}
}
}
}
}
```
```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 GetClient($clientId: ID!) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { id name externalId status returnType taxYear createdAt assignees { id name email role } } } } } }",
"variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" }
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"clients": [
{
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"name": "Jane Taxpayer",
"externalId": "PMS-10432",
"status": "active",
"returnType": "F1040",
"taxYear": 2025,
"createdAt": "2026-07-01T15:04:22.000Z",
"assignees": [
{
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"name": "Jane Preparer",
"email": "jane@example-firm.com",
"role": "admin"
}
]
}
]
}
}
}
}
```
An empty `clients` array means no client with that ID exists in this workspace.
Handle it as a not-found result.
## Add documents to a client
`addClientDocuments` attaches already-staged uploads to an existing client and
ingests them into the client's binder. Stage the files first with the [upload
endpoint](/guides/uploading-documents).
```graphql theme={null}
mutation AddClientDocuments($input: AddClientDocumentsInput!) {
addClientDocuments(input: $input) {
taskId
}
}
```
### Input: `AddClientDocumentsInput`
```graphql theme={null}
input AddClientDocumentsInput {
clientId: ID!
uploadIds: [String!]!
}
```
The client to add documents to.
One or more upload IDs from the [upload
endpoint](/guides/uploading-documents).
### Returns: `AddClientDocumentsResult`
```graphql theme={null}
type AddClientDocumentsResult {
taskId: ID
}
```
The binder ingestion [task](/apis/tasks) ID. Poll it until `status` is
`COMPLETED` to know the documents are filed in the binder.
```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 AddClientDocuments($input: AddClientDocumentsInput!) { addClientDocuments(input: $input) { taskId } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"uploadIds": ["018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"]
}
}
}'
```
```json theme={null}
{
"data": {
"addClientDocuments": {
"taskId": "018f9c2b-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}
}
}
```
## Re-run binder ingestion
`retriggerIngestion` re-runs binder ingestion for a client using the documents
already attached to it, without requiring you to re-upload any files. Use it when
a prior ingestion task finished with `status: FAILED` (or otherwise did not file
the documents into the binder) and the original files have not changed. When the
files themselves have changed, re-upload them and call
[`addClientDocuments`](#add-documents-to-a-client) with the new upload IDs
instead.
```graphql theme={null}
mutation RetriggerIngestion($input: RetriggerIngestionInput!) {
retriggerIngestion(input: $input) {
taskId
}
}
```
### Input: `RetriggerIngestionInput`
```graphql theme={null}
input RetriggerIngestionInput {
clientId: ID!
}
```
The client whose binder ingestion you want to re-run.
### Returns: `RetriggerIngestionResult`
```graphql theme={null}
type RetriggerIngestionResult {
taskId: ID
}
```
The new binder ingestion [task](/apis/tasks) ID. Nullable: when the client has
no documents to ingest, or ingestion could not be started, the field is `null`.
Poll it with the same pattern as
[`addClientDocuments`](#add-documents-to-a-client): list the client's tasks
narrowed to `type: BINDER` and wait for `status` to leave `RUNNING`.
```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 RetriggerIngestion($input: RetriggerIngestionInput!) { retriggerIngestion(input: $input) { taskId } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c"
}
}
}'
```
```json theme={null}
{
"data": {
"retriggerIngestion": {
"taskId": "018f9c2b-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}
}
}
```
`retriggerIngestion` is the more direct way to retry a failed ingestion, since
you do not need to re-stage the original uploads. See the [Onboard a
client](/guides/recipes/onboard-a-client#re-run-ingestion-without-re-uploading)
recipe for where it fits in the onboarding flow.
## Manage clients
Beyond create, list, and fetch, the API exposes a set of mutations for the rest
of the client lifecycle: rename, archive, restore, permanently delete, and
assign or unassign workspace users. All of them take a single `input` argument
identified by `clientId`, return either the updated [`Client`](#the-client-type)
or a `Boolean`, and require a **`workspaceToken`** (see
[Authentication](/guides/authentication)).
### Update a client
`updateClient` renames a client. Only the `name` is mutable through this
mutation.
```graphql theme={null}
mutation UpdateClient($input: UpdateClientInput!) {
updateClient(input: $input) {
id
name
externalId
status
returnType
taxYear
}
}
```
### Input: `UpdateClientInput`
```graphql theme={null}
input UpdateClientInput {
clientId: ID!
name: String!
}
```
The ID of the client to rename.
The new display name for the client.
### Returns: `Client!`
The updated [`Client`](#the-client-type).
```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 UpdateClient($input: UpdateClientInput!) { updateClient(input: $input) { id name externalId status returnType taxYear } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"name": "Jane Q. Taxpayer"
}
}
}'
```
```json theme={null}
{
"data": {
"updateClient": {
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"name": "Jane Q. Taxpayer",
"externalId": "PMS-10432",
"status": "active",
"returnType": "F1040",
"taxYear": 2025
}
}
}
```
### Archive a client
`archiveClient` sets the client's `status` to `archived`. Archived clients are
excluded from default lists but kept on disk and can be restored.
```graphql theme={null}
mutation ArchiveClient($input: ArchiveClientInput!) {
archiveClient(input: $input) {
id
name
status
}
}
```
### Input: `ArchiveClientInput`
```graphql theme={null}
input ArchiveClientInput {
clientId: ID!
}
```
The ID of the client to archive.
### Returns: `Client!`
The archived [`Client`](#the-client-type) with `status` set to `archived`.
```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 ArchiveClient($input: ArchiveClientInput!) { archiveClient(input: $input) { id name status } }",
"variables": {
"input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" }
}
}'
```
```json theme={null}
{
"data": {
"archiveClient": {
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"name": "Jane Q. Taxpayer",
"status": "archived"
}
}
}
```
### Restore a client
`restoreClient` sets an archived client's `status` back to `active`.
```graphql theme={null}
mutation RestoreClient($input: RestoreClientInput!) {
restoreClient(input: $input) {
id
name
status
}
}
```
### Input: `RestoreClientInput`
```graphql theme={null}
input RestoreClientInput {
clientId: ID!
}
```
The ID of the archived client to restore.
### Returns: `Client!`
The restored [`Client`](#the-client-type) with `status` set to `active`.
```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 RestoreClient($input: RestoreClientInput!) { restoreClient(input: $input) { id name status } }",
"variables": {
"input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" }
}
}'
```
```json theme={null}
{
"data": {
"restoreClient": {
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"name": "Jane Q. Taxpayer",
"status": "active"
}
}
}
```
### Delete a client
`deleteClient` permanently removes a client and its binder. It cannot be undone.
`deleteClient` is irreversible. The client, its documents, and its binder are
permanently removed. Prefer [`archiveClient`](#archive-a-client) when you only
need to hide a client from active lists.
```graphql theme={null}
mutation DeleteClient($input: DeleteClientInput!) {
deleteClient(input: $input)
}
```
### Input: `DeleteClientInput`
```graphql theme={null}
input DeleteClientInput {
clientId: ID!
}
```
The ID of the client to permanently delete.
### Returns: `Boolean!`
`true` when the client was deleted.
```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 DeleteClient($input: DeleteClientInput!) { deleteClient(input: $input) }",
"variables": {
"input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" }
}
}'
```
```json theme={null}
{
"data": {
"deleteClient": true
}
}
```
### Assign a user to a client
`assignUserToClient` assigns a workspace user to a client and returns the
created `ClientAssignee`.
```graphql theme={null}
mutation AssignUserToClient($input: AssignUserToClientInput!) {
assignUserToClient(input: $input) {
id
clientId
user {
id
userId
name
email
role
}
assignedBy {
id
userId
name
email
role
}
createdAt
}
}
```
### Input: `AssignUserToClientInput`
```graphql theme={null}
input AssignUserToClientInput {
clientId: ID!
userId: ID!
}
```
The ID of the client to assign the user to.
The ID of the workspace user to assign. Use the `userId` of a
`WorkspaceUserShortDetails` from the workspace's members, or the `id` returned
by [`me`](/apis/me).
### Returns: `ClientAssignee!`
```graphql theme={null}
type ClientAssignee {
id: ID!
clientId: ID!
user: WorkspaceUserShortDetails!
assignedBy: WorkspaceUserShortDetails!
createdAt: Date!
}
```
The assignment record ID.
The client the user was assigned to.
The workspace user who was assigned. See `WorkspaceUserShortDetails` under
[`assignees`](#the-client-type) for the field shape.
The workspace user who performed the assignment (the authenticated caller).
When the assignment was created.
```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 AssignUserToClient($input: AssignUserToClientInput!) { assignUserToClient(input: $input) { id clientId user { id userId name email role } assignedBy { id userId name email role } createdAt } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"userId": "019f0fb6-37b1-7800-b7bc-0d11288504b1"
}
}
}'
```
```json theme={null}
{
"data": {
"assignUserToClient": {
"id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"user": {
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"userId": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"name": "Jane Preparer",
"email": "jane@example-firm.com",
"role": "admin"
},
"assignedBy": {
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"userId": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"name": "Jane Preparer",
"email": "jane@example-firm.com",
"role": "admin"
},
"createdAt": "2026-07-04T18:22:01.000Z"
}
}
}
```
### Unassign a user from a client
`unassignUserFromClient` removes a workspace user's assignment from a client.
```graphql theme={null}
mutation UnassignUserFromClient($input: UnassignUserFromClientInput!) {
unassignUserFromClient(input: $input)
}
```
### Input: `UnassignUserFromClientInput`
```graphql theme={null}
input UnassignUserFromClientInput {
clientId: ID!
userId: ID!
}
```
The ID of the client to remove the assignment from.
The ID of the workspace user to unassign.
### Returns: `Boolean!`
`true` when the assignment was removed.
```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 UnassignUserFromClient($input: UnassignUserFromClientInput!) { unassignUserFromClient(input: $input) }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"userId": "019f0fb6-37b1-7800-b7bc-0d11288504b1"
}
}
}'
```
```json theme={null}
{
"data": {
"unassignUserFromClient": true
}
}
```
# Conventions
Source: https://docs.apps.filed.com/apis/conventions
Cross-cutting conventions for the Filed GraphQL API: custom scalars, IDs, pagination, sorting, and filtering
The Filed API uses a small set of conventions that apply to every operation:
custom GraphQL scalars, opaque identifiers, offset-based pagination, a shared
sort input, and typed filter inputs. This page is the single reference for those
conventions so callers and AI agents do not have to infer them. For the full
type detail of a specific operation, see [Clients](/apis/clients) and
[Tasks](/apis/tasks).
All requests go to:
```
https://router.apps.filed.com/graphql
```
## Scalars
The schema builds on the standard GraphQL scalars and adds two custom ones.
Both are declared at the top of the platform schema:
```graphql theme={null}
scalar Date
scalar JSON
```
### `ID`
The `ID` scalar is the type used for every identifiable object: clients, tasks,
workspace users, uploads, and so on. Treat it as an **opaque string**. Always
send it back exactly as you received it; never parse it, slice it, or assume a
specific length. The two concrete scalar declarations in the live schema are
`ID` (the built-in) and the custom `Date` and `JSON` below.
Filed `ID` values are **UUIDv7** strings, for example
`019f0fb6-37b1-7800-b7bc-0d11288504b1`. You can confirm this from any example
response in these docs: the first three segments encode a Unix timestamp
(milliseconds) so IDs are roughly time-ordered, followed by random bits. This is
an implementation detail you can rely on for ordering and debugging, but you
should still round-trip the value as an opaque string and not construct IDs
yourself.
### `Date`
The `Date` scalar is an **ISO 8601 timestamp string** (RFC 3339), for example
`2025-07-04T17:21:43.123Z`. Every timestamp field in the schema (`createdAt`,
`startedAt`, `completedAt`, etc.) uses this scalar, even when the field name
does not contain the word `Date`. `startedAt` and `completedAt` on `Task` are
non-null `String` rather than the `Date` scalar, but they carry the same
ISO 8601 string format.
### `JSON`
The `JSON` scalar holds an arbitrary JSON value: an object, array, string,
number, boolean, or null. It is used wherever a field's value is structured
but not fixed by the schema, for example AI task results. The
`TaskTaxAdvisorResult.byDomain` field is a non-null `JSON` scalar that returns
a structured object whose shape is defined by the tax advisor task, not by the
GraphQL type system.
```graphql theme={null}
type TaskTaxAdvisorResult {
taxYear: Int!
returnType: ReturnType!
summary: String!
strategyTotal: Int!
byDomain: JSON!
bySavingsHorizon: JSON!
estimatedSavingsCentsByHorizon: JSON!
}
```
When you select a `JSON` field, you get the whole value back as-is. There is no
sub-selection, so for deeply structured results you parse the JSON on the
client. Keep your client's parser lenient: the object's keys can grow over time.
## Pagination
List fields use **offset-based pagination** with two optional integer
arguments, `offset` and `limit`. Both are plain `Int` scalars (nullable, so
you can omit either).
* `offset` is the number of items to skip before the first returned item. It is
zero-based, so `offset: 0` (or omitting it) returns from the start.
* `limit` is the maximum number of items to return in a single response.
* The list is always a non-null list of non-null items, for example
`[Client!]!` or `[Task!]!`. An empty page is an empty array, never `null`.
The Filed API does not currently expose a cursor-based `Connection` type or a
`totalCount` field. Page through a list by walking `offset` forward in steps of
`limit` until the returned list is shorter than `limit` (or empty). The
`Workspace.tasks` and `Workspace.clients` fields both follow this shape.
### Example: page through clients
```graphql theme={null}
query ListClients($offset: Int, $limit: Int) {
me {
... on WorkspaceUser {
workspace {
clients(offset: $offset, limit: $limit) {
id
name
}
}
}
}
}
```
```json theme={null}
{
"offset": 0,
"limit": 25
}
```
Send the next request with `"offset": 25` and the same `limit` to fetch the
next page. Stop when fewer than `limit` items come back. See
[Clients](/apis/clients) for the full `Client` type and the `filters`/`sortBy`
arguments.
## Sorting
List fields that accept `sortBy` use the shared `SortBy` input type. It is a
single optional argument applied to the list before pagination.
```graphql theme={null}
input SortBy {
field: String!
order: SortByOrder!
}
enum SortByOrder {
ASC
DESC
}
```
`field` is the name of the field to sort by, as a string (for example
`"createdAt"`, `"name"`, `"startedAt"`). `order` is `ASC` for ascending or
`DESC` for descending. Both `field` and `order` are non-null inside `SortBy`,
so if you pass a `sortBy` value at all you must supply both. Omit the whole
`sortBy` argument to use the API's default order.
```json theme={null}
{
"sortBy": { "field": "createdAt", "order": "DESC" }
}
```
The same `SortBy` input is reused by every list field that supports sorting:
`Workspace.clients`, `Workspace.tasks`, and `Workspace.workspaceUsers`.
## Filtering
List fields take a typed filter input named after the entity: `ClientFilters`
for clients, `TaskFilters` for tasks. Each input is a nullable argument, so
you can omit it entirely or pass only the keys you care about. Every key inside
the input is itself optional, and combining keys applies them as a logical AND.
### `ClientFilters`
```graphql theme={null}
input ClientFilters {
ids: [ID!]
status: [ClientStatus!]
search: String
assigneeIds: [ID!]
assignedToMe: Boolean
}
enum ClientStatus {
active
archived
}
```
Use `ids` to fetch a known set of clients by ID (passing a single-element list
is how you [fetch one client](/apis/clients#fetch-a-single-client)). Use
`status` to filter by lifecycle state, `search` for free-text search over
client name and external ID, and `assigneeIds` or `assignedToMe` to filter by
assignee. Full field detail is on the [Clients](/apis/clients) page.
### `TaskFilters`
```graphql theme={null}
input TaskFilters {
type: TaskType
status: TaskStatus
triggeredBy: ID
search: String
}
```
Filter tasks by `type` (for example `BINDER` to follow a binder job), by
`status` (`RUNNING`, `COMPLETED`, `FAILED`), by the user who started the task
with `triggeredBy`, or with free-text `search`. Full field detail is on the
[Tasks](/apis/tasks) page.
`Client.tasks(type:, status:, triggeredBy:, limit:)` takes the same filtering
concepts as inline scalar arguments (not a `TaskFilters` input) and does not
take `offset` or `sortBy`. Use it when you want the tasks for one client; use
`Workspace.tasks(filters:, sortBy:, limit:, offset:)` when you want tasks
across the whole workspace.
## Next steps
* [Making requests](/guides/making-requests) for the request anatomy and error
model.
* [Clients](/apis/clients) and [Tasks](/apis/tasks) for the full type detail of
each list field.
* [Authentication](/guides/authentication) to mint the `workspaceToken` these
conventions assume.
# Document messages
Source: https://docs.apps.filed.com/apis/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
```
## How notes and comments fit together
Use this page as the source of truth for creating, editing, hiding, and
replying to notes and comments. Use [Binder](/apis/binder) to discover
documents and search across existing content.
| User intent | API surface | Use |
| ------------------------------------------ | --------------------------------- | --------------------------------------------------------------- |
| List documents before adding a note | `Client.binder.subdocuments` | Get the subdocument `id` to pass as `documentPath`. |
| Find existing notes or flags on a document | `Client.documentMessages(filter)` | Filter by `documentPath`, `types`, `markTypes`, or `taskId`. |
| Search notes by text | `Binder.search(query:)` | Locate matching notes, flags, marks, and document content. |
| Add a new note or flag | `createDocumentMessage` | Create a top-level `DocumentMessage` with `type: "annotation"`. |
| Reply to an existing note | `createDocumentMessageThread` | Create a thread under the parent `DocumentMessage.id`. |
| Edit note text or anchor | `updateDocumentMessage` | Update the top-level message. |
| Edit reply text | `updateDocumentMessageThread` | Update a thread reply. |
| Hide a note, flag, or sign-off | `hideDocumentMessage` | Soft-hide the top-level message. |
| Undo hiding | `unhideDocumentMessage` | Restore the top-level message. |
In product language, a **note** or **flag** is a top-level
`DocumentMessage`. A **comment** or reply in a conversation is a
`DocumentMessageThread` under that top-level message. Search may return
`BinderMessage` objects from the binder search index, but create, update, hide,
and reply operations use the `DocumentMessage` API on this page.
## MCP usage pattern for notes and comments
1. Query the client binder first and read `binder.subdocuments.id`.
2. Use the selected subdocument `id` as `documentPath` for message reads and
writes.
3. For an overview badge, read `binder.messageCounts.notes` instead of fetching
every message.
4. To find text across notes, flags, and document contents, call
[`binder.search`](/apis/binder#search-the-binder).
5. To edit, hide, unhide, or reply, refetch
`Client.documentMessages(filter)` and use the returned `DocumentMessage.id`.
6. Prefer `filter.includeHidden: false` for normal product views. Use
`includeHidden: true` only for audit, recovery, or admin workflows.
## 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
}
```
The message's unique identifier. Save this to later
[update](#update-a-document-message) or [hide](#hide-and-unhide-a-document-message)
it.
The workspace that owns the message.
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.
`annotation` for free-text annotations, `activity` for activity events such as
sign-offs, `missing_document` for missing-document flags.
A free-form label for the kind of mark. Sign-offs use `"signoff"`. Annotations
are surface-defined (for example `"note"`, `"flag"`).
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.
Optional path into the document content that the mark references.
The message body text. Annotations carry their note text here. `null` for
activity messages such as sign-offs that have no prose.
ISO 8601 timestamp when the message was soft-hidden via
[`hideDocumentMessage`](#hide-and-unhide-a-document-message). `null` while the
message is visible.
The user who hid the message. `null` while the message is visible.
The user who created the message.
ISO 8601 timestamp of creation.
ISO 8601 timestamp of the last mutation (edit, hide, unhide).
Replies on this message (see [Threads (replies)](#threads-replies)).
Users tagged on this message. See the `DocumentMessageTaggedUser` type below.
### The `DocumentMessageThread` type
```graphql theme={null}
type DocumentMessageThread {
id: ID!
documentMessageId: ID!
contentPath: String!
body: String
createdBy: ID!
createdAt: String!
updatedAt: String!
taggedUsers: [DocumentMessageTaggedUser!]!
}
```
The thread reply's unique identifier.
The parent `DocumentMessage.id`.
Path into the document content the reply is anchored to.
The reply body. `null` when empty.
The user who posted the reply.
ISO 8601 timestamp of creation.
ISO 8601 timestamp of the last edit.
Users tagged on this reply.
### The `DocumentMessageTaggedUser` type
```graphql theme={null}
type DocumentMessageTaggedUser {
id: ID!
userId: ID!
documentMessageId: ID
documentMessageThreadId: ID
createdAt: String!
updatedAt: String!
}
```
The tag record's unique identifier.
The workspace user who was tagged.
Set when the tag is on a top-level `DocumentMessage`. `null` when the tag is on
a thread reply.
Set when the tag is on a `DocumentMessageThread` reply. `null` when the tag is
on a top-level message.
ISO 8601 timestamp of the tag.
ISO 8601 timestamp of the last update to the tag.
## 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
}
```
Return only messages created in the context of this [task](/apis/tasks) ID.
Return only messages anchored to this subdocument path.
Return only messages whose `markType` is in this list (for example
`["signoff"]` for sign-offs, `["note"]` for notes).
Return only messages whose `type` is in this list.
When `true`, include soft-hidden messages in the results. Defaults to `false`,
which excludes hidden messages.
```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 }
}
}'
```
```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
}
]
}
]
}
}
}
}
```
## 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!]
}
```
The client whose binder this message belongs to.
The subdocument path the message is anchored to.
`annotation`, `activity`, or `missing_document`. Use `annotation` for notes and
flags, `activity` for sign-offs.
A label for the kind of mark. Use `"note"` or `"flag"` for annotations, and
`"signoff"` for a sign-off.
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).
The message body text. Required for prose annotations; `null` for activity
messages such as sign-offs that have no prose.
Optional [task](/apis/tasks) ID to associate the message with (for example, the
review task it was created during).
Optional list of workspace user IDs to tag on the message.
#### Returns: `DocumentMessage!`
The created [`DocumentMessage`](#the-documentmessage-type). Save its `id` to
later update or hide it.
```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."
}
}
}'
```
```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"
}
}
}
```
### 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!]
}
```
The document message to update.
The new body text.
The new anchor point object.
The complete list of tagged user IDs (replaces the previous list).
#### Returns: `DocumentMessage!`
The updated [`DocumentMessage`](#the-documentmessage-type).
```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." }
}
}'
```
```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"
}
}
}
```
### 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
}
}
```
The document message to hide or unhide.
#### Returns: `DocumentMessage!`
The updated [`DocumentMessage`](#the-documentmessage-type). After
`hideDocumentMessage`, `hiddenAt` and `hiddenBy` are populated. After
`unhideDocumentMessage`, both are `null` again.
```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" }
}'
```
```json theme={null}
{
"data": {
"hideDocumentMessage": {
"id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70",
"hiddenAt": "2026-07-04T16:45:00.000Z",
"hiddenBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1"
}
}
}
```
### 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!]
}
```
The parent `DocumentMessage.id` to reply under.
The reply body text.
Optional list of workspace user IDs to tag on the reply.
##### Returns: `DocumentMessageThread!`
The created [`DocumentMessageThread`](#the-documentmessagethread-type).
```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."
}
}
}'
```
```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"
}
}
}
```
#### 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!
}
```
The thread reply to update.
The new reply body text.
##### Returns: `DocumentMessageThread!`
The updated [`DocumentMessageThread`](#the-documentmessagethread-type).
```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." }
}
}'
```
```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"
}
}
}
```
#### 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)
}
```
The thread reply to delete.
##### Returns: `ID!`
The ID of the deleted thread reply.
```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" }
}'
```
```json theme={null}
{
"data": {
"deleteDocumentMessageThread": "019f0fb6-5b3d-7900-9c01-2b3c4d5e6f71"
}
}
```
## 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`.
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.
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"
}
}
}
```
```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"
}
}
}
}'
```
```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
}
}
}
```
Save the returned `id`. You pass it to `hideDocumentMessage` in the next step if
you ever need to undo the sign-off.
`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.
### 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"
}
```
```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" }
}'
```
```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"
}
}
}
```
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.
## 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.
# Health
Source: https://docs.apps.filed.com/apis/health
Check that the Filed API and its services are reachable
`health` is a public liveness check. It takes no arguments and needs no token, so
it is the simplest way to confirm the GraphQL endpoint is reachable and its
backing services are up.
```
https://router.apps.filed.com/graphql
```
## Query
```graphql theme={null}
query Health {
health {
id
ai
platform
}
}
```
## Returns: `Health`
```graphql theme={null}
type Query {
health: Health!
}
type Health {
id: ID!
ai: String!
platform: String!
}
```
An identifier for the health response.
Status of the AI service.
Status of the platform service.
```bash cURL theme={null}
curl -X POST https://router.apps.filed.com/graphql \
-H "Content-Type: application/json" \
-d '{ "query": "query Health { health { id ai platform } }" }'
```
```json theme={null}
{
"data": {
"health": {
"id": "health",
"ai": "ok",
"platform": "ok"
}
}
}
```
`health` is unauthenticated. To verify that your **token** works (not just that
the endpoint is up), run the [`me`](/apis/me) query instead.
# Integration Capabilities
Source: https://docs.apps.filed.com/apis/integration-capabilities
List provider capabilities, run a capability on a connection, and poll the capability run result
Integration capabilities are the provider-specific actions Filed can run against
a connected integration. Use them when an integration needs to expose a concrete
operation such as listing files, downloading a document, syncing data, or
running a tax software action.
For direct tax software work—including reading current return data, exporting a
backup, writing reviewed basic-form updates, or downloading an artifact from an
MCP task sandbox—start with
[Read and Write Basic Tax Forms Directly With RPA](/guides/recipes/enter-tax-data-from-mcp).
The recipe includes the CCH Axcess v2 capability payload, polling flow, and
`Task.file(path:)` signed-download query.
Capabilities are reached through the [`me`](/apis/me) query as a
`WorkspaceUser`, so authenticate with a **`workspaceToken`**. All requests go to:
```http theme={null}
https://router.apps.filed.com/graphql
```
The run mutation is currently named `runConnectionCapabliltity` in the GraphQL
schema. The spelling includes `Capabliltity`. Use that exact field name until
the schema changes.
## Capability model
Capabilities are advertised on each provider. A provider belongs to a workspace,
and a connection points at one provider by `providerKey`.
```graphql theme={null}
type Workspace {
connections(providerKey: String): [Connection!]!
providers: [IntegrationProvider!]!
connectionCapabilityRun(id: ID!): ConnectionCapabilityRun
}
type Connection {
id: ID!
providerKey: String!
name: String!
provider: IntegrationProvider!
status: ConnectionStatus!
use: ConnectionUse!
settings: JSON
actions: JSON
errorReason: String
userId: ID!
workspaceId: ID!
user: UserShortDetails!
artifact(path: String): SignedPath
clientList(search: String, offset: Int, limit: Int): [TaxSoftwareClient!]!
jobs(filters: ConnectionJobFilters, limit: Int): [ConnectionJob!]!
transferRootFolder: String
authenticatorConfigured: Boolean!
botEmailAddress: String
createdAt: Date!
}
type IntegrationProvider {
id: String!
category: String!
profile: ProviderProfile!
capability(value: String!): ProviderCapability
}
type ProviderProfile {
display: ProviderDisplay!
description: String!
helpCenterUrl: String
capabilities: [ProviderCapability!]!
}
type ProviderCapability {
value: String!
label: String!
description: String
kind: String
inputSchema: JSON
outputSchema: JSON
}
type ProviderDisplay {
bgColor: String!
textColor: String!
logoUrl: String
shortName: String!
fullName: String!
}
```
The workspace's configured integration connections. Pass `providerKey` to return
only connections for one provider.
The provider catalog available to the workspace. Each provider includes its
capability definitions.
Fetches the current status and result for a previously started capability run.
Returns `null` if the run cannot be found in the workspace.
The connection ID you pass to `runConnectionCapabliltity`.
The provider key for the connection. Match this against `IntegrationProvider.id`
to understand which capabilities can run on the connection.
The current lifecycle status of the connection. Only run capabilities on a
connection that is ready for the provider action you need.
The provider key, for example a document management provider or tax software
provider key.
The provider category used for grouping in the product.
Display information, help link, description, and the provider's capabilities.
Looks up one capability by `value`. Returns `null` if the provider does not
advertise that capability.
The machine-readable capability identifier. Send this exact string as the
`capability` input when running the capability.
Human-readable capability name.
Optional human-readable detail about what the capability does.
The capability type reported by the provider workflow, commonly `query` or
`mutation`. `query` capabilities read data. `mutation` capabilities can create,
update, move, delete, sync, or otherwise change state.
The JSON schema for the `params` object expected by
`runConnectionCapabliltity`. Use this to construct valid parameters.
The JSON schema for the `result` returned after the capability run completes.
## List provider capabilities
Read `workspace.providers` to discover available providers and their capability
definitions.
```graphql theme={null}
query ListIntegrationCapabilities {
me {
... on WorkspaceUser {
workspace {
providers {
id
category
profile {
display {
fullName
shortName
}
capabilities {
value
label
description
kind
inputSchema
outputSchema
}
}
}
}
}
}
}
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"providers": [
{
"id": "example-dms",
"category": "dms",
"profile": {
"display": {
"fullName": "Example DMS",
"shortName": "DMS"
},
"capabilities": [
{
"value": "files.list",
"label": "List files",
"description": "List files from the connected document system",
"kind": "query",
"inputSchema": {
"type": "object",
"properties": {
"path": { "type": "string" }
}
},
"outputSchema": {
"type": "object",
"properties": {
"files": { "type": "array" }
}
}
}
]
}
}
]
}
}
}
}
```
## Look up one capability
Use `IntegrationProvider.capability(value:)` when you already know the provider
and capability value and only need that one definition.
```graphql theme={null}
query GetIntegrationCapability($value: String!) {
me {
... on WorkspaceUser {
workspace {
providers {
id
capability(value: $value) {
value
label
description
kind
inputSchema
outputSchema
}
}
}
}
}
}
```
```json theme={null}
{
"value": "files.list"
}
```
### Arguments
The exact `ProviderCapability.value` to look up on each provider.
## List connections for a provider
Capabilities run on a specific connection, not directly on a provider. Use
`workspace.connections(providerKey:)` to find the connection ID.
```graphql theme={null}
query ListProviderConnections($providerKey: String) {
me {
... on WorkspaceUser {
workspace {
connections(providerKey: $providerKey) {
id
providerKey
name
status
errorReason
}
}
}
}
}
```
```json theme={null}
{
"providerKey": "example-dms"
}
```
### Arguments
Optional provider key. Omit it to return connections for all providers.
## Run a capability
Use `runConnectionCapabliltity` to start a provider capability workflow. The
mutation validates `params` against the provider capability's `inputSchema`,
refreshes the connection credentials when possible, starts a background run, and
returns a run ID.
```graphql theme={null}
mutation RunConnectionCapabliltity($input: RunConnectionCapabliltityInput!) {
runConnectionCapabliltity(input: $input) {
id
status
error
result
}
}
```
```graphql theme={null}
input RunConnectionCapabliltityInput {
connectionId: ID!
capability: String!
params: JSON!
}
type ConnectionCapabilityRun {
id: ID!
status: String!
error: String
result: JSON
}
```
### Arguments
The connection to run the capability against. Get this from
`workspace.connections`.
The exact `ProviderCapability.value` to run.
The capability parameters. Shape this object from the capability's
`inputSchema`. Send `{}` when the schema accepts an empty object.
### Response
The capability run ID. Poll `workspace.connectionCapabilityRun(id:)` with this
value.
The Temporal workflow status. A newly started run usually returns `RUNNING`.
Error detail when the run fails. `null` while the run is running or when it
completes successfully.
The provider-specific result. `null` until the workflow returns a result.
### Example request
```json theme={null}
{
"input": {
"connectionId": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"capability": "files.list",
"params": {
"path": "/"
}
}
}
```
### Example response
```json theme={null}
{
"data": {
"runConnectionCapabliltity": {
"id": "capability/example-dms/files.list/019f0fba-2b44-73dd-9f22-1dd2e2bb8a4b",
"status": "RUNNING",
"error": null,
"result": null
}
}
}
```
Check `ProviderCapability.kind` before running a capability. A `mutation`
capability can change provider state, such as moving, deleting, uploading, or
syncing records.
## Poll a capability run
After the mutation returns a run ID, poll
`workspace.connectionCapabilityRun(id:)` until the run completes or fails.
```graphql theme={null}
query CapabilityRun($id: ID!) {
me {
... on WorkspaceUser {
workspace {
connectionCapabilityRun(id: $id) {
id
status
error
result
}
}
}
}
}
```
```json theme={null}
{
"id": "capability/example-dms/files.list/019f0fba-2b44-73dd-9f22-1dd2e2bb8a4b"
}
```
### Arguments
The ID returned by `runConnectionCapabliltity`.
### Response
The run status and result. Returns `null` if the run does not exist or does not
belong to the authenticated workspace.
```json theme={null}
{
"data": {
"me": {
"workspace": {
"connectionCapabilityRun": {
"id": "capability/example-dms/files.list/019f0fba-2b44-73dd-9f22-1dd2e2bb8a4b",
"status": "COMPLETED",
"error": null,
"result": {
"files": [
{
"id": "file_123",
"name": "2025 organizer.pdf"
}
]
}
}
}
}
}
}
```
## MCP usage pattern
For Filed MCP tools and AI agents, use this sequence:
1. Query `workspace.providers` and read `ProviderCapability.inputSchema`.
2. Query `workspace.connections(providerKey:)` and choose the connection ID.
3. If the capability `kind` is `mutation`, confirm the action with the user.
4. Call `runConnectionCapabliltity` with schema-valid `params`.
5. Poll `workspace.connectionCapabilityRun(id:)` until the run is no longer
`RUNNING`.
Do not invent capability parameter names. The provider's `inputSchema` is the
source of truth for `params`, and each provider can expose a different shape.
# Introduction
Source: https://docs.apps.filed.com/apis/introduction
Reference for the Filed GraphQL API: endpoint, authentication, and operations
The Filed API is a single GraphQL endpoint. You send queries and mutations to one
URL and request exactly the fields you need.
```
https://router.apps.filed.com/graphql
```
Every request except [`health`](/apis/health) and the token exchange needs a
`Bearer` token. Most operations act on a workspace and need a **`workspaceToken`**;
see [Authentication](/guides/authentication) to create an API key and exchange it
for a token.
Clients and tasks are **not** top-level queries. They belong to a workspace and
are reached through `me`, resolved as a `WorkspaceUser`:
`me { ... on WorkspaceUser { workspace { clients { ... } tasks { ... } } } }`.
The `workspaceToken` identifies the workspace, so you never pass a workspace ID.
## Operations
Start with the operation that matches the product object you are trying to
work with. Most product workflows are not top-level GraphQL queries: read
`me`, resolve the caller as a `WorkspaceUser`, then traverse through
`workspace`, `clients`, `tasks`, and each client's `binder`.
| Need | Start here | Then use |
| ------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Check auth and workspace scope | [`me`](/apis/me) | Read `workspace.id`, `workspace.name`, clients, and tasks. |
| Create or find a client | [`Clients`](/apis/clients) | Use client IDs for binder, task, and document operations. |
| Upload or attach source files | [`Clients`](/apis/clients) and [Uploading documents](/guides/uploading-documents) | Attach uploaded file IDs to a client, then read the binder. |
| Read organized client documents | [`Binder`](/apis/binder) | Query `binder.subdocuments`, `missingItemsAssessment`, `messageCounts`, and `search`. |
| Search across binder content | [`Binder.search`](/apis/binder#search-the-binder) | Search bookmarks, notes, marks, and extracted document content. |
| Add notes, flags, comments, or sign-offs | [`Document messages`](/apis/document-messages) | Use subdocument IDs from the binder as `documentPath`. |
| Read tax prep output and reviewer sign-offs | [`Leadsheets`](/apis/leadsheets) | Pass a tax prep or tax review `taskId` when you need a specific run. |
| Start data entry or tax prep work | [`Task triggers`](/apis/task-triggers) | Trigger the run, then poll [`Tasks`](/apis/tasks). |
| Monitor background work | [`Tasks`](/apis/tasks) | Poll status, read errors, and link task results back to clients. |
| Run provider-specific integration actions | [`Integration capabilities`](/apis/integration-capabilities) | List provider capabilities, run one, then poll the capability run. |
| Generate workpapers | [`Workpapers`](/apis/workpapers) | Use client and task context to request or read workpaper bundles. |
| Run planning workflows | [`Planning`](/apis/planning) | Use when the workflow is planning-specific rather than tax-prep-specific. |
| Automate repeatable work | [`Skills`](/apis/skills) | Use skill APIs for stored automation behavior. |
## MCP operation routing
When an MCP client reads these docs, prefer this routing pattern:
1. Call the docs tool first, then start at this API introduction.
2. Use [`me`](/apis/me) to verify the token resolves to a `WorkspaceUser`.
3. Use [`Clients`](/apis/clients) to find or create the client.
4. Use [`Binder`](/apis/binder) as the source of truth for client documents,
missing items, document search, and document IDs.
5. Use [`Document messages`](/apis/document-messages) for all note, flag,
comment, reply, hide, unhide, and sign-off writes.
6. Use [`Task triggers`](/apis/task-triggers) to start data entry or tax prep,
then [`Tasks`](/apis/tasks) to poll the run.
7. Use [`Leadsheets`](/apis/leadsheets) to read tax prep review output and
inspect sign-off state after a run.
8. Use [`Integration capabilities`](/apis/integration-capabilities) only after
the user has named a provider action or integration workflow.
For MCP use, prefer a small number of focused GraphQL operations over one very
large query. First discover workspace, client, binder, and task IDs. Then fetch
the specific object needed for the user's request.
Unauthenticated liveness check for the API and its services.
Identify the caller: a `User` (userToken) or `WorkspaceUser` (workspaceToken).
Create clients, list and fetch them, and add documents to a binder.
List background tasks and check a single task's status.
Scalars, IDs, pagination, sorting, and filtering.
## Guides
Create an API key and exchange it for an access token.
Request shape, variables, and the error model.
Zero to a processed client in five steps.
Stage files through the resumable upload endpoint, then attach them.
## A first request
Confirm your token works with [`me`](/apis/me):
```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 { me { __typename ... on WorkspaceUser { workspace { id name } } } }" }'
```
# Leadsheets and review
Source: https://docs.apps.filed.com/apis/leadsheets
Read a client's leadsheets, drill into field-level trace and sourcing, and sign off on review items
**Leadsheets** are the per-form workpapers a tax prep or review run produces:
each leadsheet maps a tax form (for example Schedule B) to the binder sources
that feed every line, flags the issues the run found on that form, and carries
the sign-offs a reviewer records against it. Read them with the `leadsheets`
field on the client's [binder](/apis/binder), and record sign-offs with the
`createDocumentMessage` mutation (a sign-off is an `activity` document message
with `markType: "signoff"`, see [Document messages](/apis/document-messages)).
Leadsheets are reached through the [`me`](/apis/me) query resolved as a
`WorkspaceUser`, so reading them and recording sign-offs both require a
**`workspaceToken`** (see [Authentication](/guides/authentication)). All
requests go to:
```
https://router.apps.filed.com/graphql
```
There is no top-level `leadsheets` query. Leadsheets belong to a client's
binder, so you read them through
`me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { binder { leadsheets(taskId: $taskId) { ... } } } } } }`.
The `workspaceToken` already identifies the workspace.
A leadsheets tree is the output of a `TAX_PREP` (or `TAX_REVIEW`) background
task. Pass that task's `taskId` to `leadsheets(taskId:)` to read the exact tree
that run produced. See [Tasks](/apis/tasks) for how to start and poll a run, and
[Tax prep](/apis/tax-prep) for the `TaskTaxPrepResult` shape (whose `reviewItems`
are the same review items surfaced here as `LeadsheetSheetIssue`).
## The `Leadsheets` type
The top-level leadsheets container for one client and one task run.
```graphql theme={null}
type Leadsheets {
id: ID!
task: Task!
documentPath: String!
sheets(id: ID): [Leadsheet!]!
issueCount: Int!
returnType: String!
taxYear: Int
}
```
The leadsheets container ID.
The background [task](/apis/tasks) that produced this leadsheets tree. Its
`type` is `TAX_PREP` or `TAX_REVIEW`; its `status` tells you whether the tree is
still being built (`RUNNING`) or ready to read (`COMPLETED`).
The binder path the leadsheets container lives at.
The per-form leadsheets. Pass `sheets(id: $id)` to fetch a single leadsheet by
ID; omit the argument to list them all. See [`Leadsheet`](#the-leadsheet-type).
Total number of unresolved issues across every sheet. Use this as a quick
"needs attention" count before paging into `sheets`.
The return form this run targeted, for example `"F1040"`. This is a `String`
here, not the `ReturnType` enum (see [clients](/apis/clients#the-client-type)).
The tax year this run targeted, for example `2025`. Nullable: older runs may not
record it.
## The `Leadsheet` type
One form's leadsheet: its fields, the issues the run flagged on it, and the
sign-offs reviewers have recorded against it.
```graphql theme={null}
type Leadsheet {
id: ID!
formName: String!
category: String!
issueCount: IssueCountBySeverity!
issues: [LeadsheetSheetIssue!]!
signOffs: [DocumentMessage!]!
fields: [LeadsheetField!]!
}
```
The leadsheet ID. Pass it to `Leadsheets.sheets(id:)` to fetch this sheet alone.
The ID encodes the form name and shard index (for example
`leadsheets/schedule_b/0`).
The tax form this leadsheet covers, for example `"Schedule B"` or `"Form 1040"`.
The grouping category the binder assigns this form to (for example `income` or
`deductions`).
Issue counts broken down by severity. See
[`IssueCountBySeverity`](#the-issuecountbyseverity-type).
The issues the run flagged on this sheet. See
[`LeadsheetSheetIssue`](#the-leadsheetsheetissue-type).
Sign-off messages reviewers have recorded against this sheet. Each entry is a
`DocumentMessage` with `markType: "signoff"` (see
[Sign off on a sheet or row](#sign-off-on-a-sheet-or-row)). A sheet with no
sign-offs returns an empty array.
The form's fields, each with its rows of values, prior-year values, source
anchors, and traces. See [`LeadsheetField`](#the-leadsheetfield-type).
## The `LeadsheetSheetIssue` type
One issue the run flagged on a sheet: a mismatch, a missing form, a value the
extractor was not confident about, and so on.
```graphql theme={null}
type LeadsheetSheetIssue {
id: String!
ruleId: String!
severity: Severity!
category: String!
title: String!
description: String
evidence: String
expectedValue: String
actualValue: String
fieldPath: String
lineRef: String
columnRef: String
mappingNote: String
binderMessageId: String
resolved: Boolean!
}
enum Severity {
CRITICAL
HIGH
MEDIUM
LOW
}
```
The issue's unique identifier.
The rule that fired this issue. Stable across runs of the same rule set, so you
can use it to deduplicate or track an issue across re-runs.
How blocking the issue is: `CRITICAL`, `HIGH`, `MEDIUM`, or `LOW`.
A machine-readable grouping, for example `value_mismatch` or `missing_form`.
A short, human-readable summary of the issue.
A longer explanation. Nullable: some rules only emit a `title`.
The evidence the rule used, for example the binder text the value was extracted
from.
What the rule expected to find, when relevant.
What the rule actually found, when relevant.
The leadsheet field path the issue is anchored to, when the issue is tied to a
specific field.
A reference to the line on the form, when relevant.
A reference to the column on the form, when relevant.
A note about how the issue's field was mapped to the form, when the mapping is
ambiguous.
The ID of the binder message (annotation, flag, or sign-off) linked to this
issue, when one exists.
Whether the issue has been resolved. An issue is resolved when the underlying
document message is hidden (for example a reviewer dismissed the flag, or a
sign-off covered it). Refetch the leadsheets query after a
[sign-off](#sign-off-on-a-sheet-or-row) to recompute this.
## The `LeadsheetField` type
A single field on a form, with one row per occurrence (for example one row per
1099-INT under "Interest Income").
```graphql theme={null}
type LeadsheetField {
id: ID!
rows: [LeadsheetFieldRow!]!
}
```
The field ID.
The rows for this field. See [`LeadsheetFieldRow`](#the-leadsheetfieldrow-type).
## The `LeadsheetFieldRow` type
One row of a field: its value, the prior-year value, the source anchor in the
binder, the trace that explains where the value came from, and the issues and
sign-offs tied to this row.
```graphql theme={null}
type LeadsheetFieldRow {
id: ID!
fieldPath: String!
value: String
priorYearValue: String
sourceAnchor: SubDocBBox
trace: LeadsheetTrace
issues: [DocumentMessage!]!
signOffs: [DocumentMessage!]!
}
```
The row ID.
The path of this field on the form, for example `interest_income.total`.
The value extracted for this row, for example `"428.00"`. Nullable when the row
exists for layout but carries no value.
The value the same field held in the prior year, when prior-year data is
available.
The bounding box in the binder subdocument this value was extracted from. See
[`SubDocBBox`](#the-subdocbbox-type).
The trace explaining how this row's value was sourced and reconciled. See
[`LeadsheetTrace`](#the-leadsheettrace-type).
Document messages (flags, notes) anchored to this row. Each is a
`DocumentMessage`; see [Document messages](#the-documentmessage-type).
Sign-off messages reviewers have recorded against this row. Each is a
`DocumentMessage` with `markType: "signoff"`. See
[Sign off on a sheet or row](#sign-off-on-a-sheet-or-row).
## The `LeadsheetTrace` type
The reasoning and source citations behind a row's value: why the extractor chose
this value, and which binder subdocuments (and pages, and bounding boxes) it
came from.
```graphql theme={null}
type LeadsheetTrace {
reasoning: String
sources: [LeadsheetTraceSource!]!
}
```
A human-readable explanation of how the value was sourced and reconciled.
The binder sources this value was taken from. See
[`LeadsheetTraceSource`](#the-leadsheettracesource-type).
## The `LeadsheetTraceSource` type
One source contributing to a trace: a subdocument, a label, an amount, and a
page-level bounding box.
```graphql theme={null}
type LeadsheetTraceSource {
subdocId: ID!
label: String!
amount: String
page: Int
bbox: SubDocBBox
}
```
The binder subdocument this source came from.
A human-readable label for the source, for example `"1099-INT from Acme Broker"`.
The amount this source contributed, as a string, for example `"42.00"`.
The page number inside the subdocument, when relevant.
The bounding box on the page that pins this source. See
[`SubDocBBox`](#the-subdocbbox-type).
## The `SubDocBBox` type
A bounding box that pins a value or source to a specific region on a specific
page of a binder subdocument.
```graphql theme={null}
type SubDocBBox {
yMin: Int!
xMin: Int!
yMax: Int!
xMax: Int!
pageNumber: Int!
subdocId: String!
}
```
Top edge of the box, in page pixels.
Left edge of the box, in page pixels.
Bottom edge of the box, in page pixels.
Right edge of the box, in page pixels.
The page this box is on, 1-indexed.
The subdocument this box belongs to.
## The `IssueCountBySeverity` type
Issue counts bucketed by severity, used by `Leadsheet.issueCount`.
```graphql theme={null}
type IssueCountBySeverity {
critical: Int!
high: Int!
medium: Int!
low: Int!
}
```
Number of `CRITICAL` issues.
Number of `HIGH` issues.
Number of `MEDIUM` issues.
Number of `LOW` issues.
## The `DocumentMessage` type
A document message is the underlying write surface for annotations, flags, and
sign-offs on binder documents and leadsheet rows. A sign-off is a `DocumentMessage`
with `type: "activity"` and `markType: "signoff"`.
```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
}
```
The message ID.
The workspace the message belongs to.
The binder path the message is anchored to. For a sign-off on a subdocument or
leadsheet row, this is the subdocument's path.
`annotation`, `activity`, or `missing_document`. Sign-offs use `activity`.
The kind of mark: `signoff`, `flag`, `note`, and so on. The schema types this as
a free-form `String`; the web app treats `signoff` as the sign-off mark.
A JSON object pinning the message to a location. For a sign-off it carries
`{ page, coordinates: { x, y }, level, user_role }`, where `level` is the
reviewer's sign-off level and `user_role` is their workspace role.
An optional content path.
An optional body, for notes and replies.
When the message was soft-hidden (for example when a sign-off is undone). `null`
while the message is visible.
The user who hid the message, when applicable.
The user who created the message.
When the message was created.
When the message was last updated.
Reply threads on the message.
Users tagged on the message.
A sign-off is a `DocumentMessage` with `type: "activity"` and
`markType: "signoff"`. This page documents only the two operations the review
sign-off flow uses: [`createDocumentMessage`](#sign-off-on-a-sheet-or-row) and
[`hideDocumentMessage`](#undo-a-sign-off). The wider `DocumentMessage` API
(annotations, flags, threads, hide/unhide on binder documents) is documented
separately.
## Read a client's leadsheets
Read `binder.leadsheets(taskId:)` to get the leadsheets tree a specific run
produced. Pass the `taskId` of the `TAX_PREP` or `TAX_REVIEW` task you want the
tree for; omit it to read the client's most recent tree.
```graphql theme={null}
query GetClientLeadsheets($clientId: ID!, $taskId: ID) {
me {
... on WorkspaceUser {
id
workspace {
id
clients(filters: { ids: [$clientId] }) {
id
binder {
id
leadsheets(taskId: $taskId) {
id
documentPath
issueCount
returnType
taxYear
sheets {
id
formName
category
issueCount {
critical
high
medium
low
}
signOffs {
id
markType
anchorPoint
body
createdBy
createdAt
hiddenAt
}
fields {
id
rows {
id
fieldPath
value
priorYearValue
sourceAnchor {
yMin
xMin
yMax
xMax
pageNumber
subdocId
}
trace {
reasoning
sources {
subdocId
label
amount
page
bbox {
yMin
xMin
yMax
xMax
pageNumber
subdocId
}
}
}
issues {
id
markType
anchorPoint
body
createdBy
createdAt
hiddenAt
}
signOffs {
id
markType
anchorPoint
body
createdBy
createdAt
hiddenAt
}
}
}
}
}
}
}
}
}
}
}
```
### Arguments
The client whose leadsheets you want to read. Pass it via `filters.ids` on
`clients`.
Optional. The `TAX_PREP` or `TAX_REVIEW` task whose tree you want. Omit it to
read the client's most recent tree.
Optional. Pass it on `Leadsheets.sheets(id:)` to fetch a single leadsheet by ID
instead of listing them all.
```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 GetClientLeadsheets($clientId: ID!, $taskId: ID) { me { ... on WorkspaceUser { id workspace { id clients(filters: { ids: [$clientId] }) { id binder { id leadsheets(taskId: $taskId) { id documentPath issueCount returnType taxYear sheets { id formName category issueCount { critical high medium low } signOffs { id markType anchorPoint body createdBy createdAt hiddenAt } fields { id rows { id fieldPath value priorYearValue sourceAnchor { yMin xMin yMax xMax pageNumber subdocId } trace { reasoning sources { subdocId label amount page bbox { yMin xMin yMax xMax pageNumber subdocId } } } issues { id markType anchorPoint body createdBy createdAt hiddenAt } signOffs { id markType anchorPoint body createdBy createdAt hiddenAt } } } } } } } } } } } }",
"variables": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"taskId": "018f9c2b-7c4d-7e10-9a22-6b3c4d5e6f70"
}
}'
```
```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",
"binder": {
"id": "018f9c2a-4b6f-7a10-b2c4-9e8d7f6a5b4d",
"leadsheets": {
"id": "018f9c2b-8e10-7f20-9a33-7c4d5e6f7081",
"documentPath": "leadsheets",
"issueCount": 3,
"returnType": "F1040",
"taxYear": 2025,
"sheets": [
{
"id": "leadsheets/schedule_b/0",
"formName": "Schedule B",
"category": "income",
"issueCount": {
"critical": 0,
"high": 1,
"medium": 1,
"low": 1
},
"signOffs": [],
"fields": [
{
"id": "leadsheets/schedule_b/0/interest_income",
"rows": [
{
"id": "leadsheets/schedule_b/0/interest_income/0",
"fieldPath": "interest_income.total",
"value": "428.00",
"priorYearValue": "386.00",
"sourceAnchor": {
"yMin": 412,
"xMin": 88,
"yMax": 428,
"xMax": 220,
"pageNumber": 1,
"subdocId": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"
},
"trace": {
"reasoning": "Total interest is the sum of the three 1099-INT sources in the binder.",
"sources": [
{
"subdocId": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
"label": "1099-INT from Acme Broker",
"amount": "210.00",
"page": 1,
"bbox": {
"yMin": 412,
"xMin": 88,
"yMax": 428,
"xMax": 220,
"pageNumber": 1,
"subdocId": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"
}
},
{
"subdocId": "018f9c2a-7c3d-7c3d-9a4e-2f6b1c8d2f7b",
"label": "1099-INT from Globex",
"amount": "176.00",
"page": 1,
"bbox": {
"yMin": 300,
"xMin": 88,
"yMax": 316,
"xMax": 220,
"pageNumber": 1,
"subdocId": "018f9c2a-7c3d-7c3d-9a4e-2f6b1c8d2f7b"
}
},
{
"subdocId": "018f9c2a-8e2f-7c3d-9a4e-2f6b1c8d3f8c",
"label": "1099-INT from Initech",
"amount": "42.00",
"page": 1,
"bbox": {
"yMin": 244,
"xMin": 88,
"yMax": 260,
"xMax": 220,
"pageNumber": 1,
"subdocId": "018f9c2a-8e2f-7c3d-9a4e-2f6b1c8d3f8c"
}
}
]
},
"issues": [
{
"id": "018f9c2c-1a2b-7f30-9b44-7d5e6f708190",
"markType": "flag",
"anchorPoint": { "page": 1, "coordinates": { "x": 0, "y": 0 } },
"body": "Schedule B interest total differs from 1099-INT sum by $42.",
"createdBy": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"createdAt": "2026-07-04T10:12:00.000Z",
"hiddenAt": null
}
],
"signOffs": []
}
]
}
]
}
]
}
}
}
]
}
}
}
}
```
The leadsheets query is the same one the web app's binder and review screens
run. After a sign-off, refetch it to recompute `LeadsheetSheetIssue.resolved`
and `Leadsheet.issueCount` (the server recomputes them from the document
messages you just wrote).
## Sign off on a sheet or row
A sign-off is a `createDocumentMessage` call with `type: "activity"` and
`markType: "signoff"`, anchored to the subdocument path you are signing off on.
The `anchorPoint` carries the reviewer's `level` and `user_role` so the UI can
render the sign-off with the right label.
```graphql theme={null}
mutation CreateDocumentMessage($input: CreateDocumentMessageInput!) {
createDocumentMessage(input: $input) {
id
documentPath
type
markType
anchorPoint
body
createdBy
createdAt
hiddenAt
}
}
```
### Input: `CreateDocumentMessageInput`
```graphql theme={null}
input CreateDocumentMessageInput {
clientId: ID!
documentPath: String!
type: DocumentMessageType!
markType: String!
anchorPoint: JSON!
body: String
taskId: ID
taggedUserIds: [ID!]
}
```
The client whose binder you are signing off in.
The binder path you are signing off on. For a subdocument or leadsheet row
sign-off, this is the subdocument's path (the same value you read as
`LeadsheetFieldRow.id` or `LeadsheetSheetIssue` is anchored to).
`activity` for a sign-off (the only value the sign-off flow uses).
`"signoff"` for a sign-off.
A JSON object pinning the sign-off. The web app uses
`{ "page": 1, "coordinates": { "x": 0, "y": 0 }, "level": , "user_role": "" }`,
where `level` is the reviewer's sign-off level (for example `2` for an `l2`
reviewer) and `user_role` is their workspace role.
An optional note attached to the sign-off.
The task the sign-off belongs to, when relevant.
Workspace users to tag on the sign-off.
### Returns: `DocumentMessage!`
The created [`DocumentMessage`](#the-documentmessage-type). Its `id` is what you
pass to [`hideDocumentMessage`](#undo-a-sign-off) to undo the sign-off.
```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"
}
}
}
}'
```
```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
}
}
}
```
The schema also defines an input type called `SignOffSubDocumentsInput`
(`{ binderId, subDocumentPaths }`), but no mutation field is wired to it: there
is no `signOffSubDocuments` (or similar) mutation on the live `Mutation` type.
The real sign-off write surface is `createDocumentMessage` with
`markType: "signoff"`, one call per subdocument. Do not look for a
`signOffSubDocuments` mutation, it does not exist.
## Undo a sign-off
Undoing a sign-off is a soft-hide of the sign-off `DocumentMessage`. The
sign-off row stays in history (with `hiddenAt` set), and the leadsheets query's
`resolved` / `issueCount` recomputation backs it out.
```graphql theme={null}
mutation HideDocumentMessage($id: ID!) {
hideDocumentMessage(id: $id) {
id
documentPath
type
markType
hiddenAt
hiddenBy
}
}
```
### Input
The ID of the sign-off `DocumentMessage` to undo (the `id` returned by
[`createDocumentMessage`](#sign-off-on-a-sheet-or-row)).
### Returns: `DocumentMessage!`
The hidden [`DocumentMessage`](#the-documentmessage-type), with `hiddenAt` and
`hiddenBy` now populated.
```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" }
}'
```
```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"
}
}
}
```
## Exporting leadsheets
The `LeadsheetsExportFormat` enum is used by the workpaper bundle generator
(`generateWorkpaperBundle`), not by the leadsheets query itself. When you
generate a workpaper bundle and want leadsheets included, pass
`includeLeadsheets: true` and pick a format.
```graphql theme={null}
enum LeadsheetsExportFormat {
EXCEL
CSV
}
```
The `generateWorkpaperBundle` mutation and its `GenerateWorkpaperBundleInput`
input are documented on the [Workpapers](/apis/workpapers) page. This page lists
`LeadsheetsExportFormat` only to anchor where the enum is actually consumed.
# Me
Source: https://docs.apps.filed.com/apis/me
Identify the authenticated caller: an account-wide user or a workspace member, depending on the token
`me` returns the identity behind the token you are calling with. It is the first
query to run after [authenticating](/guides/authentication): it confirms your
token works and tells you who and where you are.
```
https://router.apps.filed.com/graphql
```
## `Me` is a union
`me` returns the `Me` **union**, which resolves to a different type depending on
which token you send:
```graphql theme={null}
union Me = User | WorkspaceUser
```
| Token | `me` resolves to | Identity |
| ---------------- | ---------------- | ----------------------------------------------- |
| `userToken` | `User` | You, across your whole account (all workspaces) |
| `workspaceToken` | `WorkspaceUser` | You, scoped to one workspace, with your role |
Both tokens come from the same exchange call (see
[Authentication](/guides/authentication)); they represent the **same person** at
two different scopes. Because `me` is a union, always select fields with an
inline fragment (`... on User` / `... on WorkspaceUser`) and read `__typename`
to know which one you got.
```graphql theme={null}
type Query {
me: Me
}
```
The authenticated identity, or `null` if the token is missing or invalid.
Resolves to `User` for a `userToken` and `WorkspaceUser` for a `workspaceToken`.
## With a `userToken`: `User`
Call with the account-wide `userToken` to get your user account and the
workspaces you belong to.
```graphql theme={null}
type User {
id: ID!
name: String!
email: String!
workspaces: [WorkspaceShortDetails!]!
}
type WorkspaceShortDetails {
id: ID!
name: String!
createdAt: Date
status: WorkspaceStatus
}
```
Your user ID, stable across every workspace.
Your display name.
Your email address.
The workspaces you are a member of. Use a workspace `id` here to pick which
workspace an integration should act on.
```graphql me (userToken) theme={null}
query MeAsUser {
me {
__typename
... on User {
id
name
email
workspaces {
id
name
}
}
}
}
```
```json theme={null}
{
"data": {
"me": {
"__typename": "User",
"id": "019f0fb6-26e9-74b7-a842-cb43a2a41682",
"name": "Jane Preparer",
"email": "jane@example-firm.com",
"workspaces": [
{
"id": "019f0fb6-379a-7f72-b7ec-ebd8f41ccfa1",
"name": "Example Tax Firm"
}
]
}
}
}
```
```bash cURL theme={null}
curl -X POST https://router.apps.filed.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_USER_TOKEN" \
-d '{ "query": "query MeAsUser { me { __typename ... on User { id name email workspaces { id name } } } }" }'
```
## With a `workspaceToken`: `WorkspaceUser`
Call with the workspace-scoped `workspaceToken` to get your membership in that
workspace, including your role.
```graphql theme={null}
type WorkspaceUser {
id: ID!
role: WorkspaceRole!
createdAt: Date!
user: UserShortDetails!
workspace: Workspace!
}
type UserShortDetails {
id: ID!
name: String!
email: String!
}
enum WorkspaceRole {
admin
l1
l2
l3
}
```
The membership ID linking your user to this workspace.
Your role in the workspace: `admin`, `l1`, `l2`, or `l3`.
When you were added to the workspace.
Your underlying user account: `id`, `name`, `email`.
The workspace this token is scoped to. It is the entry point to
[clients](/apis/clients) and [tasks](/apis/tasks):
`me { ... on WorkspaceUser { workspace { clients { ... } } } }`.
```graphql me (workspaceToken) theme={null}
query MeAsWorkspaceUser {
me {
__typename
... on WorkspaceUser {
id
role
createdAt
user {
id
name
email
}
workspace {
id
name
}
}
}
}
```
```json theme={null}
{
"data": {
"me": {
"__typename": "WorkspaceUser",
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"role": "admin",
"createdAt": "2026-06-14T09:31:20.000Z",
"user": {
"id": "019f0fb6-26e9-74b7-a842-cb43a2a41682",
"name": "Jane Preparer",
"email": "jane@example-firm.com"
},
"workspace": {
"id": "019f0fb6-379a-7f72-b7ec-ebd8f41ccfa1",
"name": "Example Tax Firm"
}
}
}
}
```
```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 MeAsWorkspaceUser { me { __typename ... on WorkspaceUser { id role createdAt user { id name email } workspace { id name } } } }" }'
```
Query both members in one document so your client handles either token:
`me { __typename ... on User { ... } ... on WorkspaceUser { ... } }`, then branch
on `__typename`.
# Tax planning
Source: https://docs.apps.filed.com/apis/planning
Start a tax advisor run, poll it to completion, read the plan and its strategies, and update a strategy's status
**Tax planning** (the advisor) reads a client's binder and produces an
`AdvisorPlan`: a global summary plus a list of `AdvisorStrategy` entries, each
one a discrete tax-saving recommendation with evidence, an implementation plan,
an estimated savings range, and a `status` you can drive. It runs as a
background task: you start it with a trigger mutation and follow the resulting
task to completion with the [tasks API](/apis/tasks).
Planning operations are reached through the [`me`](/apis/me) query resolved as a
`WorkspaceUser`, so they all require a **`workspaceToken`** (see
[Authentication](/guides/authentication)). All requests go to:
```
https://router.apps.filed.com/graphql
```
The advisor run is a polled background task. This page documents how to start a
run, read the resulting plan, and update a strategy's status. For the polling
pattern itself (listing tasks, reading `status`, and the `TaskResult` union),
see [Tasks](/apis/tasks); this page does not re-explain it.
## The `AdvisorPlan` type
`AdvisorPlan` is the shape returned by the `advisorPlan` field on
[`Client`](/apis/clients#the-client-type). It carries the run identifier, a
global summary, aggregate savings and skills-applied breakdowns, and the
individual strategies.
```graphql theme={null}
type AdvisorPlan {
runId: ID!
taxYear: Int
returnType: String
strategies: [AdvisorStrategy!]!
byDomain: JSON!
bySavingsHorizon: JSON!
estimatedSavingsCentsByHorizon: JSON!
globalSummary: String!
skillsApplied: AppliedSkills!
}
type AppliedSkills {
workspace: [String!]!
user: [String!]!
}
```
The advisor run this plan belongs to. Pass it back to
`setAdvisorStrategyStatus` when updating a strategy from this plan.
The tax year the plan was prepared for, for example `2025`. May be `null` when
the run has not finished populating the plan.
The return form as a free-form string (for example `"F1040"`). Note this is a
`String`, not the `ReturnType` enum used by the trigger inputs.
The strategy recommendations. See [`AdvisorStrategy`](#the-advisorstrategy-type)
for the field shape.
Aggregate counts of strategies grouped by domain (for example `retirement`,
`income_shifting`). The exact keys depend on which strategies the run produced.
Aggregate counts of strategies grouped by savings horizon. Horizon keys match
the `SavingsHorizon` enum values (`CURRENT_YEAR`, `MULTI_YEAR`, `LIFETIME`,
`EVENT_DRIVEN`).
Estimated total savings in USD cents, keyed by savings horizon. Treat the values
as estimates, not guarantees.
A human-readable summary of the whole plan, suitable to show at the top of a
plan view.
Which workspace and user skills were applied to this run. Each field is a list
of skill names.
```graphql theme={null}
type AppliedSkills {
workspace: [String!]!
user: [String!]!
}
```
## The `AdvisorStrategy` type
Each entry in `AdvisorPlan.strategies` is an `AdvisorStrategy`: one
recommendation the advisor surfaced from the binder, with the evidence it built
on, a step-by-step implementation plan, an optional savings estimate, and a
`status` you control with
[`setAdvisorStrategyStatus`](#update-a-strategys-status).
```graphql theme={null}
type AdvisorStrategy {
id: ID!
strategyId: String!
domain: String!
title: String!
summary: String!
applicabilityEvidence: String!
sourceSubdocIds: [String!]!
implementationPlan: [String!]!
estimatedSavingsCents: Int
savingsMethod: String
savingsHorizon: SavingsHorizon!
assumptions: String
status: AdvisorStrategyStatus!
}
enum AdvisorStrategyStatus {
PROPOSED
SELECTED
DISMISSED
}
enum SavingsHorizon {
CURRENT_YEAR
MULTI_YEAR
LIFETIME
EVENT_DRIVEN
}
```
The strategy's stable row identifier for this plan.
The logical strategy key shared across runs and clients (for example
`accelerate_charitable_contributions`). Use this, together with `domain` and
`runId`, to address a strategy in
[`setAdvisorStrategyStatus`](#update-a-strategys-status).
The strategy's domain (for example `retirement`, `income_shifting`,
`entity_selection`). Used together with `strategyId` to address a strategy.
A short, human-readable strategy title.
A one-paragraph summary of the strategy and its expected effect.
The evidence from the binder that made the advisor surface this strategy. Quote
or paraphrase this when explaining a recommendation to a client.
The binder sub-document IDs the evidence was drawn from. Cross-reference these
with the [clients API](/apis/clients) to surface the source documents.
Ordered, human-readable steps to implement the strategy.
Optional estimated tax savings in USD cents. `null` when the strategy does not
produce a direct dollar estimate.
How the estimate was computed, when `estimatedSavingsCents` is present.
When the savings are expected to land: `CURRENT_YEAR`, `MULTI_YEAR`, `LIFETIME`,
or `EVENT_DRIVEN`.
Free-text assumptions behind the estimate, when relevant.
The strategy's workflow status: `PROPOSED` (the advisor surfaced it, no action
taken), `SELECTED` (the firm accepted it), or `DISMISSED` (the firm rejected
it). Drive it with
[`setAdvisorStrategyStatus`](#update-a-strategys-status).
## Start an advisor run
There are two trigger mutations for an advisor run. Both return a `taskId` you
poll as a `TAX_ADVISOR` task, both require a **`workspaceToken`**, and both
create a task whose result member is `TaskTaxAdvisorResult`. Pick the one that
matches how you stage documents:
* [`triggerTaxAdvisor`](#trigger-via-triggertaxadvisor) (the `ai` subgraph) takes
the client, return type, and tax year directly. Use it when the documents are
already in the client's binder.
* [`initiateTaxAdvisor`](#trigger-via-initiatetaxadvisor) (the `platform`
subgraph) also takes `uploadIds`, ingesting them into the binder in the same
call. This is the mutation the Filed web app's `/planning` route actually
uses.
The Filed web app's planning flow (`src/routes/.../planning/`) calls
`initiateTaxAdvisor`, not `triggerTaxAdvisor`, because the in-app flow stages
fresh uploads at the same moment it kicks off the run. Both mutations exist
live and resolve to the same `TAX_ADVISOR` task type; pick the one that matches
your ingestion path.
### Trigger via `triggerTaxAdvisor`
`triggerTaxAdvisor` starts an advisor run for a client whose binder is already
populated. It lives in the `ai` subgraph and requires a **`workspaceToken`**.
```graphql theme={null}
mutation TriggerTaxAdvisor($input: TriggerTaxAdvisorInput!) {
triggerTaxAdvisor(input: $input) {
taskId
}
}
```
#### Input: `TriggerTaxAdvisorInput`
```graphql theme={null}
input TriggerTaxAdvisorInput {
clientId: ID!
returnType: ReturnType!
taxYear: Int!
skills: RunSkillSelectionInput
}
"""
Per-run selection of tenant (firm + user) skills. Omitted = all active skills
apply; an empty list censors every skill in that scope.
"""
input RunSkillSelectionInput {
workspace: [String!]
user: [String!]
}
```
The client to plan for.
The return form: `F1040`, `F1041`, `F1065`, `F1120`, `F1120S`, or `F990` (see
[clients](/apis/clients#the-client-type)).
The tax year to plan for, for example `2025`.
Optional. Override which workspace and user skills apply to this run. Omit to
apply all active skills; pass an empty list for a scope to censor every skill
in that scope.
#### Returns: `TriggerTaskResult`
```graphql theme={null}
type TriggerTaskResult {
taskId: ID!
}
```
The ID of the started `TAX_ADVISOR` [task](/apis/tasks). Poll it until `status`
is no longer `RUNNING`, then read `advisorPlan` and, optionally, the task's
`result` as `TaskTaxAdvisorResult`.
```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 TriggerTaxAdvisor($input: TriggerTaxAdvisorInput!) { triggerTaxAdvisor(input: $input) { taskId } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"returnType": "F1040",
"taxYear": 2025
}
}
}'
```
```json theme={null}
{
"data": {
"triggerTaxAdvisor": {
"taskId": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080"
}
}
}
```
### Trigger via `initiateTaxAdvisor`
`initiateTaxAdvisor` starts an advisor run and attaches already-staged uploads
to the client's binder in the same call. It lives in the `platform` subgraph
and requires a **`workspaceToken`**. Stage the files first with the [upload
endpoint](/guides/uploading-documents); this is the mutation the Filed web app
uses for the `/planning` route.
```graphql theme={null}
mutation InitiateTaxAdvisor($input: InitiateTaxAdvisorInput!) {
initiateTaxAdvisor(input: $input) {
taskId
}
}
```
#### Input: `InitiateTaxAdvisorInput`
```graphql theme={null}
input InitiateTaxAdvisorInput {
clientId: ID!
uploadIds: [String!]!
skills: RunSkillSelectionInput
}
```
The client to plan for.
One or more upload IDs from the [upload endpoint](/guides/uploading-documents).
The advisor ingests these into the client's binder as part of starting the run.
Optional. Override which workspace and user skills apply to this run. Same
shape as [`triggerTaxAdvisor`](#trigger-via-triggertaxadvisor).
#### Returns: `InitiateTaxAdvisorResult`
```graphql theme={null}
type InitiateTaxAdvisorResult {
taskId: ID
}
```
The ID of the started `TAX_ADVISOR` [task](/apis/tasks). Poll it until `status`
is no longer `RUNNING`, then read `advisorPlan`. `null` when the ingestion
accepted the upload but did not start a task; treat that as a soft error and
retry.
```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 InitiateTaxAdvisor($input: InitiateTaxAdvisorInput!) { initiateTaxAdvisor(input: $input) { taskId } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"uploadIds": ["018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"]
}
}
}'
```
```json theme={null}
{
"data": {
"initiateTaxAdvisor": {
"taskId": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080"
}
}
}
```
## Poll the task to completion
There is no `task(id:)` query. Poll the task you just started by listing the
client's `TAX_ADVISOR` tasks and reading the entry whose `id` matches the
`taskId` returned above. The polling mechanics are documented on
[Tasks](/apis/tasks#check-a-single-tasks-status); the short version:
```graphql theme={null}
query PollTaxAdvisor($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
tasks(type: TAX_ADVISOR, limit: 1) {
id
status
startedAt
completedAt
errorMessage
subTasks {
type
status
}
}
}
}
}
}
}
```
```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 PollTaxAdvisor($clientId: ID!) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { tasks(type: TAX_ADVISOR, limit: 1) { id status startedAt completedAt errorMessage subTasks { type status } } } } } } }",
"variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" }
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"clients": [
{
"tasks": [
{
"id": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080",
"status": "RUNNING",
"startedAt": "2026-07-04T11:02:00.000Z",
"completedAt": null,
"errorMessage": null,
"subTasks": [
{ "type": "BUILD_ADVISOR_MANIFEST", "status": "COMPLETED" },
{ "type": "RUN_ADVISOR_AGENT", "status": "RUNNING" }
]
}
]
}
]
}
}
}
}
```
Poll on an interval (for example every few seconds) until `status` is no longer
`RUNNING`. `COMPLETED` means the run succeeded and `advisorPlan` is now
readable; `FAILED` means it did not, and `errorMessage` (plus
`subTasks[].errorMessage`) explains which stage failed. Typical advisor
sub-task types are `BUILD_ADVISOR_MANIFEST`, `RUN_ADVISOR_AGENT`,
`LOCATE_ADVISOR_REFERENCES`, and `EXPORT_ADVISOR`.
## Read the plan
Read the plan through `Client.advisorPlan`. There is no top-level `advisorPlan`
query; reach it through `me { ... on WorkspaceUser { workspace { clients(...) {
advisorPlan } } } }` (see [clients](/apis/clients)). Call it without a `runId`
to read the client's current plan, or pass the `runId` from a specific task to
read that run's plan.
```graphql theme={null}
query ClientAdvisorPlan($clientId: ID!, $runId: ID) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
id
advisorPlan(runId: $runId) {
runId
taxYear
returnType
globalSummary
estimatedSavingsCentsByHorizon
skillsApplied {
workspace
user
}
strategies {
id
strategyId
domain
title
summary
applicabilityEvidence
sourceSubdocIds
implementationPlan
estimatedSavingsCents
savingsMethod
savingsHorizon
assumptions
status
}
}
}
}
}
}
}
```
```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 ClientAdvisorPlan($clientId: ID!) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { id advisorPlan { runId taxYear returnType globalSummary estimatedSavingsCentsByHorizon skillsApplied { workspace user } strategies { id strategyId domain title summary applicabilityEvidence sourceSubdocIds implementationPlan estimatedSavingsCents savingsMethod savingsHorizon assumptions status } } } } } } }",
"variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" }
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"clients": [
{
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"advisorPlan": {
"runId": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080",
"taxYear": 2025,
"returnType": "F1040",
"globalSummary": "5 strategies surfaced across retirement, income shifting, and entity selection. Estimated 3-year savings of $18,400.",
"estimatedSavingsCentsByHorizon": {
"CURRENT_YEAR": 420000,
"MULTI_YEAR": 1840000,
"LIFETIME": 0,
"EVENT_DRIVEN": 0
},
"skillsApplied": {
"workspace": ["advisor_evidence_qa"],
"user": []
},
"strategies": [
{
"id": "019a1b2c-3d4e-7f10-aa12-1c2d3e4f5060",
"strategyId": "accelerate_charitable_contributions",
"domain": "charitable",
"title": "Bunch charitable contributions into 2025",
"summary": "Combine two years of charitable giving into 2025 to exceed the standard deduction and itemize this year.",
"applicabilityEvidence": "Client has donated $4,200 and $4,800 in each of the last two years per binder sub-doc sd_8842 and sd_8843.",
"sourceSubdocIds": ["sd_8842", "sd_8843"],
"implementationPlan": [
"Confirm intended 2025 and 2026 giving totals with the client.",
"Move 2026 contributions into December 2025.",
"Rebuild the itemized deduction worksheet."
],
"estimatedSavingsCents": 82000,
"savingsMethod": "marginal_rate_x_deduction_delta",
"savingsHorizon": "CURRENT_YEAR",
"assumptions": "Marginal rate stays at 24%; no further AGI-limit changes.",
"status": "PROPOSED"
},
{
"id": "019a1b2c-3d4e-7f10-aa12-1c2d3e4f5061",
"strategyId": "roth_conversion_window",
"domain": "retirement",
"title": "Roth convert up to the 24% bracket cap",
"summary": "Convert traditional IRA funds to Roth up to the top of the 24% bracket this year.",
"applicabilityEvidence": "Client has $120,000 in traditional IRA assets and taxable income is temporarily lower this year per sd_7101.",
"sourceSubdocIds": ["sd_7101"],
"implementationPlan": [
"Model the bracket headroom for the current year.",
"Convert up to the cap.",
"Withhold or pay estimated tax on the conversion."
],
"estimatedSavingsCents": null,
"savingsMethod": null,
"savingsHorizon": "MULTI_YEAR",
"assumptions": "Future marginal rate is 32% or higher.",
"status": "PROPOSED"
}
]
}
}
]
}
}
}
}
```
`advisorPlan` returns `null` while the run is still `RUNNING`, or when the
client has no advisor run yet. Treat `null` as "no plan to show", and keep
polling the task until `status` is `COMPLETED` before re-reading.
## Update a strategy's status
`setAdvisorStrategyStatus` moves a strategy between `PROPOSED`, `SELECTED`, and
`DISMISSED`. It lives in the `ai` subgraph and requires a
**`workspaceToken`**. Identify the strategy with `clientId` plus the strategy's
`domain` and `strategyId` (both from `AdvisorStrategy`), and pass the plan's
`runId` so the status change is recorded against the right run.
```graphql theme={null}
mutation SetAdvisorStrategyStatus($input: SetAdvisorStrategyStatusInput!) {
setAdvisorStrategyStatus(input: $input) {
id
status
}
}
```
### Input: `SetAdvisorStrategyStatusInput`
```graphql theme={null}
input SetAdvisorStrategyStatusInput {
clientId: ID!
domain: String!
strategyId: String!
status: AdvisorStrategyStatus!
runId: ID
}
enum AdvisorStrategyStatus {
PROPOSED
SELECTED
DISMISSED
}
```
The client the plan belongs to.
The strategy's `domain` (from `AdvisorStrategy.domain`).
The strategy's logical key (from `AdvisorStrategy.strategyId`), not the row
`id`.
The new status: `PROPOSED`, `SELECTED`, or `DISMISSED`. Use `SELECTED` for
strategies the firm accepts, `DISMISSED` for those it rejects, and `PROPOSED`
to revert either back to the advisor's original state.
The plan's `runId` (from `AdvisorPlan.runId`). Optional in the schema but
recommended: it pins the status change to a specific run, which matters when a
client has more than one advisor run on file.
### Returns: `AdvisorStrategy`
The mutation returns the updated `AdvisorStrategy`, typically just `id` and
`status`. The full type is documented
[above](#the-advisorstrategy-type).
The strategy row identifier that was updated.
The strategy's new status.
```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 SetAdvisorStrategyStatus($input: SetAdvisorStrategyStatusInput!) { setAdvisorStrategyStatus(input: $input) { id status } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"domain": "charitable",
"strategyId": "accelerate_charitable_contributions",
"runId": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080",
"status": "SELECTED"
}
}
}'
```
```json theme={null}
{
"data": {
"setAdvisorStrategyStatus": {
"id": "019a1b2c-3d4e-7f10-aa12-1c2d3e4f5060",
"status": "SELECTED"
}
}
}
```
After a successful `setAdvisorStrategyStatus`, re-read
[`advisorPlan`](#read-the-plan) to get the refreshed `strategies[].status`
values. The Filed web app does this by including `ClientAdvisorPlan` in the
mutation's `refetchQueries`.
## Task result member: `TaskTaxAdvisorResult`
When a `TAX_ADVISOR` task reaches `status: COMPLETED`, its `result` field
resolves to `TaskTaxAdvisorResult`. This is the same data the
[`advisorPlan`](#read-the-plan) field exposes as `AdvisorPlan`, just delivered
through the task poll. Most callers prefer `advisorPlan` for its richer
`strategies` list; `TaskTaxAdvisorResult` is useful when you are already
polling the task and want the high-level summary in the same response.
```graphql theme={null}
type TaskTaxAdvisorResult {
taxYear: Int!
returnType: ReturnType!
summary: String!
strategyTotal: Int!
byDomain: JSON!
bySavingsHorizon: JSON!
estimatedSavingsCentsByHorizon: JSON!
}
```
Select it with an inline fragment on the task's `result`, alongside the
`TaskUnknownResult` fallback:
```graphql theme={null}
query TaxAdvisorResult($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
tasks(type: TAX_ADVISOR, limit: 1) {
id
status
completedAt
result {
__typename
... on TaskTaxAdvisorResult {
taxYear
returnType
summary
strategyTotal
byDomain
bySavingsHorizon
estimatedSavingsCentsByHorizon
}
... on TaskUnknownResult {
message
}
}
}
}
}
}
}
}
```
`TaskTaxAdvisorResult` is one member of the `TaskResult` union. The other tax
members are `TaskTaxPrepResult` (returned for `TAX_PREP` tasks, see
[tax prep](/apis/tax-prep#the-tasktaxprepresult-type)) and `TaskTaxReviewResult`
(returned for `TAX_REVIEW` tasks). See [Tasks, task result](/apis/tasks#task-result)
for the full union and the inline-fragment pattern used to select it.
# Skills
Source: https://docs.apps.filed.com/apis/skills
List, inspect, promote, approve, deny, activate, and delete the skills that power Playbook workflows
A **skill** is a reusable, versioned rule set that the Filed analyst applies
during a run. Skills are scoped to a workspace (`WORKSPACE` skills, shared firm
protocols) or to a single user (`USER` skills, personal protocols). The
Playbook screen in the Filed web app is the human UI over this API.
Skill operations are reached through the [`me`](/apis/me) query resolved as a
`WorkspaceUser`, so every read and mutation on this page requires a
**`workspaceToken`** (see [Authentication](/guides/authentication)). All
requests go to:
```
https://router.apps.filed.com/graphql
```
There is no top-level `skills` or `skill` query. Both are fields on `Workspace`,
reached through `me { ... on WorkspaceUser { workspace { skills(...) } } }`.
The `workspaceToken` already identifies which workspace, so you never pass a
workspace ID.
## The `Skill` type
```graphql theme={null}
type Skill {
kind: SkillKind!
taskType: String!
name: String!
description: String!
body: String
updatedAt: String!
createdAt: String!
returnType: ReturnType
applicableWhen: String
rules: [SkillRule!]
strategies: [SkillStrategy!]
status: SkillStatus
owner: UserShortDetails
activity: [SkillActivityEvent!]
}
enum SkillKind {
WORKSPACE
USER
}
enum SkillStatus {
NONE
PENDING
APPROVED
DENIED
DISABLED
}
enum ReturnType {
F1040
F1041
F1065
F1120
F1120S
F990
}
```
The skill's scope: `WORKSPACE` (shared firm protocol) or `USER` (personal
protocol). Determines who can edit, promote, and delete it.
The task family this skill applies to, for example `tax-prep` or
`tax-advisor`. Skills are grouped and listed by `taskType`.
The skill's unique name within its `taskType` and `kind`. Together
`kind` + `taskType` + `name` (+ optional `returnType`) identifies a single
skill.
A short human-readable summary of what the skill does.
The full rule body (the prompt / instruction text the analyst applies). May be
empty for curated skills.
ISO 8601 timestamp of the last edit.
ISO 8601 timestamp of creation.
When set, the skill only applies to clients of this return type
(`F1040`, `F1041`, `F1065`, `F1120`, `F1120S`, `F990`). When `null`, the skill
applies to all return types.
A free-text condition describing when the skill should fire. Display only.
The structured rules attached to the skill. See the [`SkillRule`](#the-skillrule-type) type.
The strategies attached to the skill. See the [`SkillStrategy`](#the-skillstrategy-type) type.
The skill's lifecycle state: `NONE`, `PENDING`, `APPROVED`, `DENIED`, or
`DISABLED`. `PENDING` means a `USER` skill has been shared with the firm and is
awaiting approval; `APPROVED`/`DENIED` are the resolved promotion states;
`DISABLED` means an admin has turned it off without deleting it.
The user who owns the skill. See [`UserShortDetails`](#the-usershortdetails-type).
The skill's activity timeline (promotions, approvals, activations, edits). See
[`SkillActivityEvent`](#the-skillactivityevent-type).
## The `SkillRule` type
```graphql theme={null}
type SkillRule {
id: String!
severity: String!
category: String!
domain: String!
tolerance: Int
titleTemplate: String
}
```
The rule's unique identifier within the skill.
The severity the rule raises when it fires (for example `critical`, `high`,
`medium`, `low`).
The review category the rule maps to (for example `data-entry`,
`reconciliation`).
The knowledge domain the rule belongs to.
An optional numeric tolerance the rule allows before flagging.
A template string used to render the rule's title in review output.
## The `SkillStrategy` type
```graphql theme={null}
type SkillStrategy {
id: String!
titleTemplate: String
}
```
The strategy's unique identifier within the skill.
A template string used to render the strategy's title in planning output.
## The `SkillActivityEvent` type
```graphql theme={null}
type SkillActivityEvent {
action: String!
timestamp: String!
triggeredBy: UserShortDetails
note: String
}
```
What happened, for example `created`, `updated`, `shared`, `approved`, `denied`,
`enabled`, `disabled`. The web app humanizes this by replacing hyphens and
underscores with spaces and title-casing the result.
ISO 8601 timestamp of the event.
The user who triggered the event. See
[`UserShortDetails`](#the-usershortdetails-type).
An optional human-readable note attached to the event (for example the denial
reason from `denySkillPromotion`).
## The `UserShortDetails` type
`UserShortDetails` is a federated entity. The `ai` subgraph declares it with
only `id`; the `platform` subgraph resolves the human-readable fields.
```graphql theme={null}
type UserShortDetails {
id: ID!
name: String!
email: String!
emailHash: String
}
```
The user's account ID.
The user's display name.
The user's email.
An optional hash of the email (used for avatars).
## List skills
Read `workspace.skills` to list skills for a task family. The web app's
Playbook screen calls this with `showCuratedSkills: true` to include Filed's
built-in curated skills alongside the workspace's own.
```graphql theme={null}
query GetTaskSkills($taskType: String!) {
me {
... on WorkspaceUser {
id
workspace {
id
skills(taskType: $taskType, showCuratedSkills: true) {
kind
taskType
name
description
status
returnType
updatedAt
owner {
id
name
email
}
}
}
}
}
}
```
### Arguments
```graphql theme={null}
skills(
showCuratedSkills: Boolean = false
taskType: String
returnType: ReturnType
): [Skill!]!
```
When `true`, include Filed's curated (built-in) skills in the result alongside
the workspace's own. Defaults to `false`.
Filter to one task family, for example `tax-prep` or `tax-advisor`. Omit to
list skills across all task families.
Filter to skills that apply to a specific return type. Omit to list skills
that apply to all return types.
```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 GetTaskSkills($taskType: String!) { me { ... on WorkspaceUser { id workspace { id skills(taskType: $taskType, showCuratedSkills: true) { kind taskType name description status returnType updatedAt owner { id name email } } } } } }",
"variables": { "taskType": "tax-prep" }
}'
```
```json theme={null}
{
"data": {
"me": {
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"workspace": {
"id": "018f9c20-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"skills": [
{
"kind": "WORKSPACE",
"taskType": "tax-prep",
"name": "check-w2-totals",
"description": "Verify W-2 wage totals against binder extractions.",
"status": "APPROVED",
"returnType": "F1040",
"updatedAt": "2026-06-22T10:14:00.000Z",
"owner": {
"id": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"name": "Jane Preparer",
"email": "jane@example-firm.com"
}
},
{
"kind": "USER",
"taskType": "tax-prep",
"name": "my-firm-reconciliation",
"description": "Personal reconciliation protocol.",
"status": "PENDING",
"returnType": null,
"updatedAt": "2026-07-04T18:22:01.000Z",
"owner": {
"id": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"name": "Jane Preparer",
"email": "jane@example-firm.com"
}
}
]
}
}
}
}
```
## View a single skill
Read `workspace.skill` to fetch one skill's full detail, including its `body`,
`rules`, `strategies`, and `activity` timeline. Identify the skill with
`kind` + `taskType` + `name` (and optional `returnType`).
```graphql theme={null}
query GetSkill(
$kind: SkillKind!
$taskType: String!
$name: String!
$returnType: ReturnType
) {
me {
... on WorkspaceUser {
id
workspace {
id
skill(
kind: $kind
taskType: $taskType
name: $name
returnType: $returnType
) {
kind
taskType
name
description
body
status
returnType
updatedAt
owner {
id
name
email
}
activity {
action
timestamp
note
triggeredBy {
id
name
email
}
}
}
}
}
}
}
```
### Arguments
```graphql theme={null}
skill(
kind: SkillKind!
taskType: String!
name: String!
returnType: ReturnType
): Skill
```
`WORKSPACE` or `USER`.
The task family, for example `tax-prep`.
The skill's name within its `taskType` and `kind`.
When the skill is scoped to a return type, pass it to disambiguate. Omit for
skills that apply to all return types.
```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 GetSkill($kind: SkillKind!, $taskType: String!, $name: String!, $returnType: ReturnType) { me { ... on WorkspaceUser { id workspace { id skill(kind: $kind, taskType: $taskType, name: $name, returnType: $returnType) { kind taskType name description body status returnType updatedAt owner { id name email } activity { action timestamp note triggeredBy { id name email } } } } } } }",
"variables": {
"kind": "WORKSPACE",
"taskType": "tax-prep",
"name": "check-w2-totals",
"returnType": "F1040"
}
}'
```
```json theme={null}
{
"data": {
"me": {
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"workspace": {
"id": "018f9c20-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"skill": {
"kind": "WORKSPACE",
"taskType": "tax-prep",
"name": "check-w2-totals",
"description": "Verify W-2 wage totals against binder extractions.",
"body": "Flag any W-2 where the extracted wage total differs from the source document by more than $1.",
"status": "APPROVED",
"returnType": "F1040",
"updatedAt": "2026-06-22T10:14:00.000Z",
"owner": {
"id": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"name": "Jane Preparer",
"email": "jane@example-firm.com"
},
"activity": [
{
"action": "created",
"timestamp": "2026-06-10T09:00:00.000Z",
"note": null,
"triggeredBy": {
"id": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"name": "Jane Preparer",
"email": "jane@example-firm.com"
}
},
{
"action": "approved",
"timestamp": "2026-06-22T10:14:00.000Z",
"note": null,
"triggeredBy": {
"id": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"name": "Jane Preparer",
"email": "jane@example-firm.com"
}
}
]
}
}
}
}
}
```
`workspace.skill` returns `null` when no skill matches the supplied
`kind` + `taskType` + `name` (+ `returnType`). Handle `null` as a not-found
result.
## The promotion workflow
A `USER` skill starts as a personal protocol visible only to its owner. To share
it with the whole firm, the owner requests a promotion; a workspace admin then
approves or denies it. On approval the skill becomes a `WORKSPACE` skill (or
its `WORKSPACE` counterpart is activated) and applies for every user in the
workspace. The three mutations below drive that flow. All require a
**`workspaceToken`**.
```mermaid theme={null}
flowchart LR
A[USER skill
status: NONE] -->|requestSkillPromotion| B[status: PENDING]
B -->|approveSkillPromotion| C[status: APPROVED]
B -->|denySkillPromotion| D[status: DENIED]
C -->|setSkillActive active: false| E[status: DISABLED]
E -->|setSkillActive active: true| C
```
### Request a promotion
`requestSkillPromotion` submits a `USER` skill for firm-wide review. Its
`status` becomes `PENDING`.
```graphql theme={null}
mutation RequestSkillPromotion($taskType: String!, $name: String!) {
requestSkillPromotion(taskType: $taskType, name: $name) {
kind
taskType
name
status
}
}
```
The skill's task family.
The skill's name.
### Returns: `Skill!`
The promoted [`Skill`](#the-skill-type) with its updated `status`.
```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 RequestSkillPromotion($taskType: String!, $name: String!) { requestSkillPromotion(taskType: $taskType, name: $name) { kind taskType name status } }",
"variables": { "taskType": "tax-prep", "name": "my-firm-reconciliation" }
}'
```
```json theme={null}
{
"data": {
"requestSkillPromotion": {
"kind": "USER",
"taskType": "tax-prep",
"name": "my-firm-reconciliation",
"status": "PENDING"
}
}
}
```
### Approve a promotion
`approveSkillPromotion` approves a `PENDING` skill. Its `status` becomes
`APPROVED` and it applies firm-wide.
```graphql theme={null}
mutation ApproveSkillPromotion($taskType: String!, $name: String!) {
approveSkillPromotion(taskType: $taskType, name: $name) {
kind
taskType
name
status
}
}
```
The skill's task family.
The skill's name.
### Returns: `Skill!`
The approved [`Skill`](#the-skill-type) with `status: APPROVED`.
```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 ApproveSkillPromotion($taskType: String!, $name: String!) { approveSkillPromotion(taskType: $taskType, name: $name) { kind taskType name status } }",
"variables": { "taskType": "tax-prep", "name": "my-firm-reconciliation" }
}'
```
```json theme={null}
{
"data": {
"approveSkillPromotion": {
"kind": "USER",
"taskType": "tax-prep",
"name": "my-firm-reconciliation",
"status": "APPROVED"
}
}
}
```
### Deny a promotion
`denySkillPromotion` denies a `PENDING` skill. Its `status` becomes `DENIED`
and the optional `reason` is recorded on the activity timeline.
```graphql theme={null}
mutation DenySkillPromotion(
$taskType: String!
$name: String!
$reason: String
) {
denySkillPromotion(taskType: $taskType, name: $name, reason: $reason) {
kind
taskType
name
status
}
}
```
The skill's task family.
The skill's name.
An optional denial note. Stored on the `SkillActivityEvent` so the owner can see
why the promotion was rejected.
### Returns: `Skill!`
The denied [`Skill`](#the-skill-type) with `status: DENIED`.
```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 DenySkillPromotion($taskType: String!, $name: String!, $reason: String) { denySkillPromotion(taskType: $taskType, name: $name, reason: $reason) { kind taskType name status } }",
"variables": {
"taskType": "tax-prep",
"name": "my-firm-reconciliation",
"reason": "Overlaps with existing firm protocol check-w2-totals."
}
}'
```
```json theme={null}
{
"data": {
"denySkillPromotion": {
"kind": "USER",
"taskType": "tax-prep",
"name": "my-firm-reconciliation",
"status": "DENIED"
}
}
}
```
## Toggle a skill active or inactive
`setSkillActive` enables or disables a skill without deleting it. Disabling sets
`status: DISABLED` so the skill stops applying but is still listed and can be
re-enabled. Pass `returnType` when the skill is scoped to a return type.
```graphql theme={null}
mutation SetSkillActive(
$taskType: String!
$name: String!
$active: Boolean!
$returnType: ReturnType
) {
setSkillActive(
taskType: $taskType
name: $name
active: $active
returnType: $returnType
) {
kind
taskType
name
status
}
}
```
The skill's task family.
The skill's name.
`true` to enable, `false` to disable.
When the skill is scoped to a return type, pass it to disambiguate. Omit for
skills that apply to all return types.
### Returns: `Skill!`
The updated [`Skill`](#the-skill-type). Its `status` reflects the new active
state (`APPROVED` when enabled, `DISABLED` when disabled).
```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 SetSkillActive($taskType: String!, $name: String!, $active: Boolean!, $returnType: ReturnType) { setSkillActive(taskType: $taskType, name: $name, active: $active, returnType: $returnType) { kind taskType name status } }",
"variables": {
"taskType": "tax-prep",
"name": "check-w2-totals",
"active": false,
"returnType": "F1040"
}
}'
```
```json theme={null}
{
"data": {
"setSkillActive": {
"kind": "WORKSPACE",
"taskType": "tax-prep",
"name": "check-w2-totals",
"status": "DISABLED"
}
}
}
```
## Delete a skill
`deleteSkill` permanently removes a skill. For `WORKSPACE` skills this deletes
the skill for the whole firm (curated skills revert to their default). For
`USER` skills this deletes the personal protocol. The mutation returns `true`
on success.
`deleteSkill` is irreversible. For a `WORKSPACE` skill it removes the protocol
for every user in the firm. Prefer [`setSkillActive`](#toggle-a-skill-active-or-inactive)
with `active: false` when you only need to turn a skill off.
```graphql theme={null}
mutation DeleteSkill(
$kind: SkillKind!
$taskType: String!
$name: String!
$returnType: ReturnType
) {
deleteSkill(
kind: $kind
taskType: $taskType
name: $name
returnType: $returnType
)
}
```
`WORKSPACE` or `USER`.
The skill's task family.
The skill's name.
When the skill is scoped to a return type, pass it to disambiguate. Omit for
skills that apply to all return types.
### Returns: `Boolean!`
`true` when the skill was deleted.
```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 DeleteSkill($kind: SkillKind!, $taskType: String!, $name: String!, $returnType: ReturnType) { deleteSkill(kind: $kind, taskType: $taskType, name: $name, returnType: $returnType) }",
"variables": {
"kind": "USER",
"taskType": "tax-prep",
"name": "my-firm-reconciliation"
}
}'
```
```json theme={null}
{
"data": {
"deleteSkill": true
}
}
```
After any skill mutation, refetch `GetTaskSkills` and `GetSkill` so the
Playbook UI reflects the new `status`. The web app calls
`client.refetchQueries({ include: ["GetTaskSkills", "GetSkill"] })` after every
bulk action.
# Task Triggers
Source: https://docs.apps.filed.com/apis/task-triggers
Start tax prep and data entry tasks from the Filed GraphQL API
Task trigger mutations start background work and return a `taskId`. Use the
[Tasks API](/apis/tasks) to poll that task until it is no longer `RUNNING`.
Task triggers are reached through the [`me`](/apis/me) flow as a
`WorkspaceUser`, so authenticate with a **`workspaceToken`**. All requests go to:
```http theme={null}
https://router.apps.filed.com/graphql
```
This page is optimized for Filed MCP tools and AI agents. It spells out the
minimal mutation sequence for tax prep and data entry so agents do not need to
infer trigger arguments from product UI code.
## Shared return type
Both `triggerTaxPrep` and `triggerDataEntry` return `TriggerTaskResult`.
```graphql theme={null}
type TriggerTaskResult {
taskId: ID!
}
```
The background task ID. Poll it through [Tasks](/apis/tasks) or through the
client's task list until the task reaches `COMPLETED` or `FAILED`.
## Trigger tax prep
`triggerTaxPrep` starts a `TAX_PREP` task for a client. The task extracts forms,
reconciles data, and can optionally continue into data entry if the tax software
fields are provided.
```graphql theme={null}
mutation TriggerTaxPrep($input: TriggerTaxPrepInput!) {
triggerTaxPrep(input: $input) {
taskId
}
}
```
```graphql theme={null}
input TriggerTaxPrepInput {
taskId: ID
clientId: ID!
returnType: ReturnType!
software: String
softwareConnectionId: ID
softwareClientId: String
softwareClientVersion: String
runDataEntry: Boolean
forceReconcile: Boolean
force: Boolean
skills: RunSkillSelectionInput
}
enum ReturnType {
F1040
F1041
F1065
F1120
F1120S
F990
}
input RunSkillSelectionInput {
workspace: [String!]
user: [String!]
}
```
### Arguments
The client to run tax prep for.
The return form for the client: `F1040`, `F1041`, `F1065`, `F1120`, `F1120S`,
or `F990`.
Optional existing tax prep task ID. Pass this when reusing or re-running a
specific tax prep task. Omit it to start a fresh task.
The tax software provider key. Use this with `softwareConnectionId` and
`softwareClientId` when the run should prepare for or continue into data entry.
The Filed connection ID for the tax software integration.
The client identifier inside the tax software.
Optional software-specific client version string.
When `true`, tax prep continues into data entry during the same run. Provide
`software`, `softwareConnectionId`, and `softwareClientId` when setting this.
When `true`, reconciliation runs again even if an earlier reconciliation result
exists.
When `true`, start a run even when an existing tax prep task is already running
for the client.
Optional per-run skill selection. Omit it to apply all active skills. Pass
explicit workspace or user skill name lists to constrain the run.
### Example: tax prep only
```json theme={null}
{
"input": {
"clientId": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"returnType": "F1040",
"runDataEntry": false
}
}
```
```json theme={null}
{
"data": {
"triggerTaxPrep": {
"taskId": "019f0fc4-9298-7bdd-91f6-7f6e77cdbd9b"
}
}
}
```
### Example: tax prep with data entry
```json theme={null}
{
"input": {
"clientId": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"returnType": "F1040",
"software": "proconnect",
"softwareConnectionId": "019f0fb8-f8c9-7f6a-b504-7d8e989c3c34",
"softwareClientId": "CLIENT-123",
"runDataEntry": true
}
}
```
Only set `runDataEntry: true` when the user has selected the target tax software
connection and client. Data entry can write data back to the tax software.
## Trigger data entry
`triggerDataEntry` starts data entry from an existing tax prep task. Use this
when tax prep has already completed or prepared fields and the user now wants to
send those fields to tax software.
```graphql theme={null}
mutation TriggerDataEntry($input: TriggerDataEntryInput!) {
triggerDataEntry(input: $input) {
taskId
}
}
```
```graphql theme={null}
input TriggerDataEntryInput {
taskId: ID!
clientId: ID!
software: String
softwareConnectionId: ID!
softwareClientId: String!
}
```
### Arguments
The source tax prep task ID. In the web app, this is the latest `TAX_PREP` task
for the client.
The client whose prepared fields should be entered.
Optional tax software provider key.
The Filed connection ID for the target tax software integration.
The client identifier inside the tax software.
### Example request
```json theme={null}
{
"input": {
"taskId": "019f0fc4-9298-7bdd-91f6-7f6e77cdbd9b",
"clientId": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"software": "proconnect",
"softwareConnectionId": "019f0fb8-f8c9-7f6a-b504-7d8e989c3c34",
"softwareClientId": "CLIENT-123"
}
}
```
### Example response
```json theme={null}
{
"data": {
"triggerDataEntry": {
"taskId": "019f0fd1-15ef-74e8-adff-42c89edbf0fd"
}
}
}
```
Data entry writes to the selected tax software connection. Confirm the
connection and `softwareClientId` with the user before calling this mutation.
## Find the latest tax prep task
If an agent needs to trigger data entry and does not already have a tax prep
task ID, read the latest client `TAX_PREP` task first.
```graphql theme={null}
query LatestTaxPrepTask($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
id
tasks(type: TAX_PREP, limit: 1) {
id
status
errorMessage
}
}
}
}
}
}
```
The client whose latest tax prep task should be used as the data entry source.
Use this value as `TriggerDataEntryInput.taskId`.
The task state: `RUNNING`, `COMPLETED`, or `FAILED`. Prefer a completed tax prep
task before triggering data entry.
## Poll trigger results
Poll the task returned by either mutation through the client's task list. Use
`TAX_PREP` for tax prep runs. Data entry is represented as stages within the tax
prep pipeline and by the task returned from `triggerDataEntry`.
```graphql theme={null}
query PollClientTaxPrepTasks($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
id
tasks(type: TAX_PREP, limit: 5) {
id
status
startedAt
completedAt
errorMessage
subTasks {
id
type
status
errorMessage
errorCode
}
}
}
}
}
}
}
```
```graphql theme={null}
enum TaskStatus {
RUNNING
COMPLETED
FAILED
}
enum DataEntryErrorCode {
INVALID_CREDENTIALS
CAPTCHA_FAILED
CLIENT_NOT_FOUND
CLIENT_ALREADY_OPEN
NOT_REGISTERED
TIMEOUT
UNKNOWN
}
```
Use `RUNNING` to keep polling, `COMPLETED` to read results, and `FAILED` to show
or report `errorMessage`.
Machine-readable data entry failure code when a data entry stage fails. Values
include `INVALID_CREDENTIALS`, `CLIENT_NOT_FOUND`, `TIMEOUT`, and `UNKNOWN`.
## MCP usage pattern
For Filed MCP tools and AI agents:
1. Query the client and confirm `returnType`.
2. Query workspace connections and select the tax software connection.
3. For tax prep only, call `triggerTaxPrep` with `runDataEntry: false`.
4. For tax prep plus data entry, call `triggerTaxPrep` with `runDataEntry: true`
and include the software connection fields.
5. For data entry after tax prep, query the latest `TAX_PREP` task and call
`triggerDataEntry`.
6. Poll the returned `taskId` and surface `errorMessage` or `subTasks.errorCode`
if the task fails.
When triggering data entry, never guess the tax software client ID. Ask the user
or read it from the selected integration connection's client list before calling
the mutation.
# Tasks
Source: https://docs.apps.filed.com/apis/tasks
List background tasks and check the status of a single task
A **task** is a background job Filed runs for a workspace: binder ingestion, tax
prep, tax review, tax advisor, or chat. Mutations like
[`createClient`](/apis/clients#create-a-client) and
[`addClientDocuments`](/apis/clients#add-documents-to-a-client) return a
`taskId`; you use the tasks API to follow that work to completion.
Tasks are reached through the [`me`](/apis/me) query as a `WorkspaceUser`, so
authenticate with a **`workspaceToken`** (see
[Authentication](/guides/authentication)). All requests go to:
```
https://router.apps.filed.com/graphql
```
## The `Task` type
```graphql theme={null}
type Task {
id: ID!
type: TaskType!
status: TaskStatus!
startedAt: String!
completedAt: String
errorMessage: String
attributes: [TaskAttribute!]!
subTasks: [SubTask!]!
result: TaskResult!
client: ClientShortDetails
triggeredBy: UserShortDetails
}
enum TaskType {
BINDER
TAX_PREP
TAX_REVIEW
TAX_ADVISOR
CHAT
}
enum TaskStatus {
RUNNING
COMPLETED
FAILED
}
```
The task's unique identifier. This is the value returned as `taskId` by the
mutations that start work.
What kind of work this is: `BINDER`, `TAX_PREP`, `TAX_REVIEW`, `TAX_ADVISOR`, or
`CHAT`.
`RUNNING`, `COMPLETED`, or `FAILED`. Poll this to know when work finishes.
ISO 8601 timestamp of when the task started.
ISO 8601 timestamp of when the task finished. `null` while `RUNNING`.
A human-readable error message when `status` is `FAILED`. `null` otherwise.
Arbitrary `name`/`value` metadata pairs describing the task.
The individual stages of the task, each with its own status. Use these for
granular progress while a task is `RUNNING`.
The typed result of the task, resolved by `type`. See [Task
result](#task-result).
The client this task belongs to (`id`, `name`), when applicable.
The user who started the task (`id`, `name`, `email`).
### Supporting types
```graphql theme={null}
type TaskAttribute {
name: String!
value: String!
}
type SubTask {
id: ID!
type: SubTaskType!
status: TaskStatus!
startedAt: String!
completedAt: String
errorMessage: String
errorCode: DataEntryErrorCode
}
type ClientShortDetails {
id: ID!
name: String!
}
type UserShortDetails {
id: ID!
name: String!
email: String!
emailHash: String
}
```
The stage, for example `CONVERT_DOCUMENTS`, `CLASSIFY_SUBDOCS`,
`EXTRACT_SUBDOCS`, or `EXPORT_AND_INDEX`. The full set of stages depends on the
parent task's `type`.
A machine-readable code when a data-entry stage fails, for example
`INVALID_CREDENTIALS`, `CLIENT_NOT_FOUND`, or `TIMEOUT`. `null` otherwise.
## List tasks
Read `workspace.tasks` to list tasks across the whole workspace. Filter, page,
and sort with the arguments below.
```graphql theme={null}
query ListTasks($filters: TaskFilters, $sortBy: SortBy, $limit: Int, $offset: Int) {
me {
... on WorkspaceUser {
workspace {
tasks(filters: $filters, sortBy: $sortBy, limit: $limit, offset: $offset) {
id
type
status
startedAt
completedAt
client {
id
name
}
triggeredBy {
id
name
}
}
}
}
}
}
```
### Arguments
```graphql theme={null}
input TaskFilters {
type: TaskType
status: TaskStatus
triggeredBy: ID
search: String
}
input SortBy {
field: String!
order: SortByOrder! # ASC | DESC
}
```
Return only tasks of this type.
Return only tasks in this status (`RUNNING`, `COMPLETED`, `FAILED`).
Return only tasks started by this user.
Free-text search over task metadata.
Sort order, for example `{ "field": "startedAt", "order": "DESC" }`.
Maximum number of tasks to return.
Number of tasks to skip, for pagination.
```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 ListTasks($filters: TaskFilters, $sortBy: SortBy, $limit: Int) { me { ... on WorkspaceUser { workspace { tasks(filters: $filters, sortBy: $sortBy, limit: $limit) { id type status startedAt completedAt client { id name } triggeredBy { id name } } } } } }",
"variables": {
"filters": { "type": "BINDER", "status": "RUNNING" },
"sortBy": { "field": "startedAt", "order": "DESC" },
"limit": 20
}
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"tasks": [
{
"id": "018f9c2b-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"type": "BINDER",
"status": "RUNNING",
"startedAt": "2026-07-04T09:15:00.000Z",
"completedAt": null,
"client": {
"id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"name": "Jane Taxpayer"
},
"triggeredBy": {
"id": "019f0fb6-26e9-74b7-a842-cb43a2a41682",
"name": "Jane Preparer"
}
}
]
}
}
}
}
```
## Check a single task's status
There is no `task(id:)` query. To follow one task (for example the `taskId`
returned by `createClient` or `addClientDocuments`), list the tasks for its
client with `client.tasks` and read the entry whose `id` matches. Because a
client's task list is small and typed, this is the reliable way to poll a
specific task.
```graphql theme={null}
query ClientTaskStatus($clientId: ID!, $type: TaskType) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
tasks(type: $type) {
id
status
startedAt
completedAt
errorMessage
subTasks {
type
status
}
}
}
}
}
}
}
```
`Client.tasks` accepts these arguments:
```graphql theme={null}
tasks(type: TaskType, status: TaskStatus, triggeredBy: ID, limit: Int): [Task!]!
```
Narrow to one task type, for example `BINDER` to watch document ingestion.
Cap the number of tasks returned (for example `1` for the most recent).
```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 ClientTaskStatus($clientId: ID!, $type: TaskType) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { tasks(type: $type) { id status startedAt completedAt errorMessage subTasks { type status } } } } } } }",
"variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "type": "BINDER" }
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"clients": [
{
"tasks": [
{
"id": "018f9c2b-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"status": "COMPLETED",
"startedAt": "2026-07-04T09:15:00.000Z",
"completedAt": "2026-07-04T09:17:42.000Z",
"errorMessage": null,
"subTasks": [
{ "type": "CONVERT_DOCUMENTS", "status": "COMPLETED" },
{ "type": "CLASSIFY_SUBDOCS", "status": "COMPLETED" },
{ "type": "EXTRACT_SUBDOCS", "status": "COMPLETED" },
{ "type": "EXPORT_AND_INDEX", "status": "COMPLETED" }
]
}
]
}
]
}
}
}
}
```
Poll on an interval (for example every few seconds) until `status` is no longer
`RUNNING`. `COMPLETED` means the work succeeded; `FAILED` means it did not, and
`errorMessage` explains why. `subTasks` show which stage is currently running.
## Task result
Every task carries a typed `result`. `TaskResult` is a union whose concrete type
is determined by the task's `type`. Select fields with an inline fragment on the
member you expect, and read `__typename` to know which one you got.
```graphql theme={null}
union TaskResult =
TaskUnknownResult
| TaskTaxPrepResult
| TaskTaxReviewResult
| TaskTaxAdvisorResult
```
The fallback result, including for `BINDER` and `CHAT` tasks.
```graphql theme={null}
type TaskUnknownResult {
message: String!
}
```
A human-readable description of the outcome.
Returned for `TAX_PREP` tasks: a summary plus document/form counts and the
individual review items.
```graphql theme={null}
type TaskTaxPrepResult {
taxYear: Int!
returnType: ReturnType!
summary: String!
documentCount: Int!
extractedFormCount: Int!
reviewItemCount: Int!
reviewItems: [TaxPrepReviewItem!]!
}
type TaxPrepReviewItem {
severity: String!
category: String!
description: String!
}
```
The tax year.
The return form (see [ReturnType](/apis/clients#the-client-type)).
A summary of the tax prep result.
Number of documents processed.
Number of forms extracted.
Number of review items produced.
The review items. Each has `severity`, `category`, and `description` (all `String!`).
Returned for `TAX_REVIEW` tasks: a summary plus issue breakdowns by severity and
by form.
```graphql theme={null}
type TaskTaxReviewResult {
taxYear: Int!
returnType: ReturnType!
summary: String!
issueCountBySeverity: IssueCountBySeverity!
issueCountByForm: [FormIssueCount!]!
}
type IssueCountBySeverity {
critical: Int!
high: Int!
medium: Int!
low: Int!
}
type FormIssueCount {
form: String!
count: Int!
}
```
The tax year.
The return form.
A summary of the review result.
Issue counts by severity: `critical`, `high`, `medium`, `low` (all `Int!`).
Issue counts per form, each `{ form: String!, count: Int! }`.
Returned for `TAX_ADVISOR` tasks: a summary plus strategy counts. The `by*`
fields are `JSON` maps.
```graphql theme={null}
type TaskTaxAdvisorResult {
taxYear: Int!
returnType: ReturnType!
summary: String!
strategyTotal: Int!
byDomain: JSON!
bySavingsHorizon: JSON!
estimatedSavingsCentsByHorizon: JSON!
}
```
The tax year.
The return form.
A summary of the advisor result.
Total number of strategies.
Strategy counts keyed by domain.
Strategy counts keyed by savings horizon.
Estimated savings (in cents) keyed by horizon.
Because the members share `taxYear`, `returnType`, and `summary`, you can select
those on each fragment and branch on `__typename`:
```graphql theme={null}
query TaskResult($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
tasks(type: TAX_PREP, limit: 1) {
id
status
result {
__typename
... on TaskTaxPrepResult {
summary
documentCount
extractedFormCount
reviewItems {
severity
category
description
}
}
... on TaskTaxReviewResult {
summary
issueCountBySeverity {
critical
high
medium
low
}
issueCountByForm {
form
count
}
}
... on TaskUnknownResult {
message
}
}
}
}
}
}
}
}
```
# Tax prep
Source: https://docs.apps.filed.com/apis/tax-prep
Start a tax prep run, poll it to completion, and read the review items it produces
**Tax prep** is the pipeline that extracts forms from a client's binder,
reconciles them, optionally enters them into tax software, and produces a set of
review items. It is a background task: you start it with the `triggerTaxPrep`
mutation and follow the resulting task to completion with the
[tasks API](/apis/tasks).
Tax prep operations are reached through the [`me`](/apis/me) query resolved as
a `WorkspaceUser`, so the trigger mutation and the task poll both require a
**`workspaceToken`** (see [Authentication](/guides/authentication)). The two
backoffice operations on this page (`retriggerTaxPrepStep` and
`setTaxPrepTaskStatus`) require a **user** token instead, called out below. All
requests go to:
```
https://router.apps.filed.com/graphql
```
Tax prep runs as a polled background task. This page documents how to start a
run and read its result. For the polling pattern itself (listing tasks, reading
`status`, `subTasks`, and the `TaskResult` union), see
[Tasks](/apis/tasks); this page does not re-explain it.
## The `TaskTaxPrepResult` type
When a `TAX_PREP` task reaches `status: COMPLETED`, its `result` field resolves
to `TaskTaxPrepResult`. It carries a summary plus document and form counts, and
the individual review items the run produced.
```graphql theme={null}
type TaskTaxPrepResult {
taxYear: Int!
returnType: ReturnType!
summary: String!
documentCount: Int!
extractedFormCount: Int!
reviewItemCount: Int!
reviewItems: [TaxPrepReviewItem!]!
}
type TaxPrepReviewItem {
severity: String!
category: String!
description: String!
}
enum ReturnType {
F1040
F1041
F1065
F1120
F1120S
F990
}
```
The tax year the run was prepared for, for example `2025`.
The return form: `F1040`, `F1041`, `F1065`, `F1120`, `F1120S`, or `F990` (see
[ReturnType](/apis/clients#the-client-type)).
A human-readable summary of the prepared return.
Number of documents processed from the client's binder.
Number of forms extracted from those documents.
Number of review items produced. Use this as a quick "needs attention" count
before paging through `reviewItems`.
The review items. Each has `severity`, `category`, and `description` (all
`String!`).
`TaskTaxPrepResult` is one member of the `TaskResult` union. The other tax
members are `TaskTaxReviewResult` (returned for `TAX_REVIEW` tasks, with issue
counts by severity and form) and `TaskTaxAdvisorResult` (returned for
`TAX_ADVISOR` tasks). See [Tasks, task result](/apis/tasks#task-result) for the
full union and the inline-fragment pattern used to select it.
## Start a tax prep run
`triggerTaxPrep` starts a tax prep run for a client and returns the
`taskId` you poll. It requires a **`workspaceToken`**.
```graphql theme={null}
mutation TriggerTaxPrep($input: TriggerTaxPrepInput!) {
triggerTaxPrep(input: $input) {
taskId
}
}
```
### Input: `TriggerTaxPrepInput`
```graphql theme={null}
input TriggerTaxPrepInput {
taskId: ID
clientId: ID!
returnType: ReturnType!
software: String
softwareClientId: String
softwareClientVersion: String
runDataEntry: Boolean
forceReconcile: Boolean
force: Boolean
skills: RunSkillSelectionInput
}
"""
Per-run selection of tenant (firm + user) skills. Omitted = all active skills
apply; an empty list censors every skill in that scope.
"""
input RunSkillSelectionInput {
workspace: [String!]
user: [String!]
}
```
The client to prepare the return for.
The return form to prepare. Must match the client's `returnType` (see
[clients](/apis/clients#the-client-type)).
Optional. Pass an existing tax prep task ID to target an in-flight or prior run
rather than starting a brand-new one. Omit for a fresh run.
The tax software provider key, as exposed by the workspace's connected tax
software integrations. Required only when `runDataEntry` is `true`.
The client identifier inside the tax software. Required only when
`runDataEntry` is `true`.
The tax software client version string, when relevant to the integration.
When `true`, the pipeline continues past extraction and reconciliation into
data entry, writing the prepared return back to the tax software. Requires
`software` and `softwareClientId`.
When `true`, re-run reconciliation even if a prior reconcile already
succeeded.
When `true`, start a fresh run even if another tax prep task for this client
is already `RUNNING`.
Optional. Override which workspace and user skills apply to this run. Omit to
apply all active skills; pass an empty list for a scope to censor every skill
in that scope.
### Returns: `TriggerTaskResult`
```graphql theme={null}
type TriggerTaskResult {
taskId: ID!
}
```
The ID of the started `TAX_PREP` [task](/apis/tasks). Poll it until `status` is
no longer `RUNNING`, then read `result` as `TaskTaxPrepResult`.
```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 TriggerTaxPrep($input: TriggerTaxPrepInput!) { triggerTaxPrep(input: $input) { taskId } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"returnType": "F1040",
"runDataEntry": false
}
}
}'
```
```json theme={null}
{
"data": {
"triggerTaxPrep": {
"taskId": "018f9c2b-7c4d-7e10-9a22-6b3c4d5e6f70"
}
}
}
```
## Poll the task to completion
There is no `task(id:)` query. Poll the task you just started by listing the
client's `TAX_PREP` tasks and reading the entry whose `id` matches the
`taskId` returned above. The polling mechanics are documented on
[Tasks](/apis/tasks#check-a-single-tasks-status); the short version:
```graphql theme={null}
query PollTaxPrep($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
tasks(type: TAX_PREP, limit: 1) {
id
status
startedAt
completedAt
errorMessage
subTasks {
type
status
}
}
}
}
}
}
}
```
```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 PollTaxPrep($clientId: ID!) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { tasks(type: TAX_PREP, limit: 1) { id status startedAt completedAt errorMessage subTasks { type status } } } } } } }",
"variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" }
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"clients": [
{
"tasks": [
{
"id": "018f9c2b-7c4d-7e10-9a22-6b3c4d5e6f70",
"status": "RUNNING",
"startedAt": "2026-07-04T10:02:00.000Z",
"completedAt": null,
"errorMessage": null,
"subTasks": [
{ "type": "EXTRACT", "status": "COMPLETED" },
{ "type": "RECONCILE", "status": "RUNNING" }
]
}
]
}
]
}
}
}
}
```
Poll on an interval (for example every few seconds) until `status` is no longer
`RUNNING`. `COMPLETED` means the run succeeded and `result` is now
selectable as `TaskTaxPrepResult`; `FAILED` means it did not, and
`errorMessage` (plus `subTasks[].errorMessage`) explains which stage failed.
## Read the completed result
When the task is `COMPLETED`, select `result` with an inline fragment on
`TaskTaxPrepResult` to read the summary, counts, and review items.
```graphql theme={null}
query TaxPrepResult($clientId: ID!) {
me {
... on WorkspaceUser {
workspace {
clients(filters: { ids: [$clientId] }) {
tasks(type: TAX_PREP, limit: 1) {
id
status
completedAt
result {
__typename
... on TaskTaxPrepResult {
taxYear
returnType
summary
documentCount
extractedFormCount
reviewItemCount
reviewItems {
severity
category
description
}
}
... on TaskUnknownResult {
message
}
}
}
}
}
}
}
}
```
```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 TaxPrepResult($clientId: ID!) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { tasks(type: TAX_PREP, limit: 1) { id status completedAt result { __typename ... on TaskTaxPrepResult { taxYear returnType summary documentCount extractedFormCount reviewItemCount reviewItems { severity category description } } ... on TaskUnknownResult { message } } } } } } } } }",
"variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" }
}'
```
```json theme={null}
{
"data": {
"me": {
"workspace": {
"clients": [
{
"tasks": [
{
"id": "018f9c2b-7c4d-7e10-9a22-6b3c4d5e6f70",
"status": "COMPLETED",
"completedAt": "2026-07-04T10:11:48.000Z",
"result": {
"__typename": "TaskTaxPrepResult",
"taxYear": 2025,
"returnType": "F1040",
"summary": "Return prepared from 14 documents with 9 extracted forms. 3 items need review before sign-off.",
"documentCount": 14,
"extractedFormCount": 9,
"reviewItemCount": 3,
"reviewItems": [
{
"severity": "high",
"category": "missing_form",
"description": "W-2 from Acme Corp referenced in prior year but not present in this year's binder."
},
{
"severity": "medium",
"category": "value_mismatch",
"description": "Schedule B interest total differs from 1099-INT sum by $42."
},
{
"severity": "low",
"category": "data_entry",
"description": "Filing status set to Married Filing Jointly; confirm against intake form."
}
]
}
}
]
}
]
}
}
}
}
```
Always read `__typename` on `result` and include a `... on TaskUnknownResult`
fallback. A `TAX_PREP` task that fails after the run starts can resolve to
`TaskUnknownResult` instead of `TaskTaxPrepResult`; branching on `__typename`
keeps your client from throwing on the unexpected member.
## Re-trigger a tax prep step
`retriggerTaxPrepStep` re-runs a single stage of an existing tax prep task. It
is a backoffice operation and requires a **user** token (not a
`workspaceToken`); the schema marks it `@requiresScopes(scopes: [["user"]])`.
```graphql theme={null}
mutation RetriggerTaxPrepStep($input: RetriggerTaxPrepStepInput!) {
retriggerTaxPrepStep(input: $input) {
taskId
}
}
```
### Input: `RetriggerTaxPrepStepInput`
```graphql theme={null}
input RetriggerTaxPrepStepInput {
workspaceId: ID!
clientId: ID!
step: TaxPrepStep!
software: String
softwareClientId: String
}
enum TaxPrepStep {
IMPORT_PRIOR_YEAR
EXTRACT
RECONCILE
PRE_ENTRY_EXPORT
DATA_ENTRY
POST_ENTRY_EXPORT
VALIDATE
}
```
The workspace the client belongs to.
The client whose tax prep run you want to re-run a step for.
The stage to re-run: `IMPORT_PRIOR_YEAR`, `EXTRACT`, `RECONCILE`,
`PRE_ENTRY_EXPORT`, `DATA_ENTRY`, `POST_ENTRY_EXPORT`, or `VALIDATE`.
The tax software provider key. Pass it when the re-triggered step writes to or
reads from the tax software (the data-entry and export stages).
The client identifier inside the tax software. Pass it alongside `software`
for the data-entry and export stages.
### Returns: `BackofficeTriggerResult`
```graphql theme={null}
type BackofficeTriggerResult {
taskId: ID!
}
```
The ID of the task the re-triggered step belongs to. Poll it with the
[tasks API](/apis/tasks#check-a-single-tasks-status) for the new stage's
outcome.
`retriggerTaxPrepStep` requires a **user** token (a personal backoffice
session), not the `workspaceToken` used by the rest of the tax prep flow. The
`workspaceToken` issued from an API key is rejected by this operation.
## Set a tax prep task's status
`setTaxPrepTaskStatus` forces a tax prep task into a given `TaskStatus`. Like
`retriggerTaxPrepStep`, it is a backoffice operation and requires a **user**
token (`@requiresScopes(scopes: [["user"]])`).
```graphql theme={null}
mutation SetTaxPrepTaskStatus($input: SetTaxPrepTaskStatusInput!) {
setTaxPrepTaskStatus(input: $input) {
taskId
}
}
```
### Input: `SetTaxPrepTaskStatusInput`
```graphql theme={null}
input SetTaxPrepTaskStatusInput {
workspaceId: ID!
taskId: ID!
status: TaskStatus!
}
enum TaskStatus {
RUNNING
COMPLETED
FAILED
}
```
The workspace the task belongs to.
The tax prep task whose status you want to set.
The status to force the task into: `RUNNING`, `COMPLETED`, or `FAILED`.
### Returns: `BackofficeTriggerResult`
The same `BackofficeTriggerResult { taskId: ID! }` shape as
`retriggerTaxPrepStep`. See [above](#returns-backofficetriggerresult) for the
field.
This mutation overwrites the task's `status` directly, bypassing the normal
pipeline. Use it for backoffice recovery (for example marking a task
`COMPLETED` after a manual fix, or `FAILED` to release a stuck run). It
requires a **user** token, not a `workspaceToken`.
# Workpapers
Source: https://docs.apps.filed.com/apis/workpapers
Save a tax workpaper workbook, generate a bundled workpaper download, and trigger workpaper translation
A **workpaper** is the workbook or bundled packet that captures a client's tax
work: the editable `tax_workpaper.xlsx` for a business return, or a generated
bundle (PDF, leadsheets, forms, checklist, source documents) assembled from the
binder. The workpaper API covers three operations:
1. **Save** an edited xlsx workbook back to the client's git-backed file store
(`saveTaxWorkpaperXlsx`).
2. **Generate** a bundled workpaper download rendered server-side from the
binder (`generateWorkpaperBundle` plus `checkWorkpaperGenerationStatus`).
3. **Translate** a return into a workpaper via a background task
(`triggerWorkpaperTranslate`), and list the templates available for a given
return type (`workpaperTemplates`).
All workpaper operations are reached through the [`me`](/apis/me) query resolved
as a `WorkspaceUser` (for the `Workspace.workpaperTemplates` and
`Workspace.checkWorkpaperGenerationStatus` reads) or are top-level mutations.
They require a **`workspaceToken`** (see [Authentication](/guides/authentication)).
All requests go to:
```
https://router.apps.filed.com/graphql
```
There is no top-level `workpapers` query. The two read fields
(`workpaperTemplates` and `checkWorkpaperGenerationStatus`) live on the
`Workspace` type, so you read them through
`me { ... on WorkspaceUser { workspace { ... } } }`. The `workspaceToken`
already identifies the workspace.
## Save a tax workpaper xlsx
`saveTaxWorkpaperXlsx` commits an edited xlsx workbook back to the client's
git-backed file store as `tax_workpaper.xlsx`. The workbook is sent as a
base64-encoded `.xlsx` payload. The mutation returns a `ClientCommit` describing
the new git commit.
The web app uses this as an autosave for the in-browser workpaper editor (an
`exceljs` workbook). The `summary` argument names the cell that was just edited
(for example `Trial Balance!B12`) and becomes part of the git commit message.
```graphql theme={null}
mutation SaveTaxWorkpaperXlsx($input: SaveTaxWorkpaperXlsxInput!) {
saveTaxWorkpaperXlsx(input: $input) {
sha
shortSha
message
committedAt
author {
kind
name
email
userId
agentName
}
parents
files {
path
status
additions
deletions
renamedFrom
}
taskId
runId
}
}
```
### Input: `SaveTaxWorkpaperXlsxInput`
```graphql theme={null}
input SaveTaxWorkpaperXlsxInput {
clientId: ID!
xlsxBase64: String!
summary: String
}
```
The client whose `tax_workpaper.xlsx` you are saving.
The full xlsx workbook, base64-encoded. The server writes this verbatim to
`tax_workpaper.xlsx` in the client's file store.
Optional. A short summary of the edit (for example the cell coordinate that
changed). Becomes part of the git commit message.
### Returns: `ClientCommit!`
A `ClientCommit` is the git commit the AI service recorded for the write. The
same type is returned by other git-backed writes (`updateSubDocumentField`,
`revertClientCommit`).
```graphql theme={null}
type ClientCommit {
sha: String!
shortSha: String!
author: GitAuthor!
committedAt: String!
message: String!
parents: [String!]!
files: [GitFileChange!]!
taskId: String
runId: String
}
type GitAuthor {
kind: GitAuthorKind!
name: String!
email: String!
userId: ID
agentName: String
}
type GitFileChange {
path: String!
status: GitChangeStatus!
additions: Int!
deletions: Int!
renamedFrom: String
}
enum GitAuthorKind {
USER
AGENT
SYSTEM
}
enum GitChangeStatus {
ADDED
MODIFIED
DELETED
RENAMED
COPIED
}
```
The full commit SHA.
The abbreviated commit SHA.
Who made the commit. `kind` is `USER` for a workspace user, `AGENT` for an
automated agent, or `SYSTEM` for a system-level write.
ISO timestamp when the commit was recorded.
The commit message (includes the `summary` you passed in, when supplied).
Parent commit SHAs. Empty for the initial commit.
Files changed by this commit. For `saveTaxWorkpaperXlsx` this is the
`tax_workpaper.xlsx` row.
The task ID associated with the write, when the commit was produced by a
background task. `null` for direct user edits.
The run ID associated with the write, when relevant. `null` for direct user
edits.
```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 SaveTaxWorkpaperXlsx($input: SaveTaxWorkpaperXlsxInput!) { saveTaxWorkpaperXlsx(input: $input) { sha shortSha message committedAt author { kind name email userId agentName } parents files { path status additions deletions renamedFrom } taskId runId } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"xlsxBase64": "UEsDBBQACAgIAAAAAAAAAAAAAAAAAAAAAAA=",
"summary": "Trial Balance!B12"
}
}
}'
```
```json theme={null}
{
"data": {
"saveTaxWorkpaperXlsx": {
"sha": "7c4d5e6f7081901a2b3c4d5e6f7081901a2b3c4d",
"shortSha": "7c4d5e6",
"message": "Edit Trial Balance!B12",
"committedAt": "2026-07-05T14:08:22.000Z",
"author": {
"kind": "USER",
"name": "Jane Preparer",
"email": "jane@example-firm.com",
"userId": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"agentName": null
},
"parents": [
"3d5e6f7081901a2b3c4d5e6f7081901a2b3c4d5e"
],
"files": [
{
"path": "tax_workpaper.xlsx",
"status": "MODIFIED",
"additions": 0,
"deletions": 0,
"renamedFrom": null
}
],
"taskId": null,
"runId": null
}
}
}
```
## Generate a workpaper bundle
`generateWorkpaperBundle` queues a server-side render that assembles a bundled
workpaper download (PDF, leadsheets, forms, checklist, source documents) from
the binder. It returns a `WorkpaperRenderJob` carrying the
`workflowExecutionId` you poll with
[`checkWorkpaperGenerationStatus`](#check-a-bundle-render-status).
The bundle's table of contents and section order is driven by `orderedGroups`:
a list of buckets, each containing categories, each containing the subdocument
IDs to include. This mirrors the binder sidebar outline.
```graphql theme={null}
mutation GenerateWorkpaperBundle($input: GenerateWorkpaperBundleInput!) {
generateWorkpaperBundle(input: $input) {
workflowExecutionId
status
}
}
```
### Input: `GenerateWorkpaperBundleInput`
```graphql theme={null}
input GenerateWorkpaperBundleInput {
binderId: ID!
orderedGroups: [WorkpaperBucketGroupInput!]!
clientName: String
includePdf: Boolean
includeLeadsheets: Boolean
leadsheetsFormat: LeadsheetsExportFormat
includeForms: Boolean
includeChecklist: Boolean
includeSourceDocs: Boolean
}
input WorkpaperBucketGroupInput {
bucketLabel: String
categories: [WorkpaperCategoryGroupInput!]!
}
input WorkpaperCategoryGroupInput {
category: String!
subdocumentIds: [ID!]!
}
```
The client's binder ID (read it from
`me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { binder { id } } } } }`,
see [Binder](/apis/binder)).
The bucket-to-category-to-subdocument outline that drives the bundle's section
order and PDF TOC. Each entry has a `bucketLabel` and a list of `categories`;
each category has a `category` label and the `subdocumentIds` to include.
Optional. The client's display name, used to set the download filename (for
example `Filed workpaper - Jane Taxpayer.pdf`).
When `true`, render a combined PDF in the bundle.
When `true`, include the client's leadsheets in the bundle, exported in the
format set by `leadsheetsFormat`.
The leadsheets export format: `EXCEL` or `CSV` (see
[Leadsheets and review](/apis/leadsheets#exporting-leadsheets)). Only meaningful
when `includeLeadsheets` is `true`.
When `true`, include the client's forms in the bundle.
When `true`, include the binder checklist in the bundle.
When `true`, include the binder's source documents in the bundle.
### Returns: `WorkpaperRenderJob!`
```graphql theme={null}
type WorkpaperRenderJob {
workflowExecutionId: ID!
status: WorkpaperRenderStatus!
}
enum WorkpaperRenderStatus {
RUNNING
COMPLETED
FAILED
}
```
The render job's workflow execution ID. Pass it to
[`checkWorkpaperGenerationStatus`](#check-a-bundle-render-status) to poll for
completion and the download URL.
The render status: `RUNNING` while the bundle is being rendered, `COMPLETED`
when the download is ready, or `FAILED` if the render errored. Immediately after
`generateWorkpaperBundle` returns, this is `RUNNING`.
```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 GenerateWorkpaperBundle($input: GenerateWorkpaperBundleInput!) { generateWorkpaperBundle(input: $input) { workflowExecutionId status } }",
"variables": {
"input": {
"binderId": "018f9c2a-4b6f-7a10-b2c4-9e8d7f6a5b4d",
"orderedGroups": [
{
"bucketLabel": "Income",
"categories": [
{
"category": "1099s",
"subdocumentIds": [
"018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a",
"018f9c2a-7c3d-7c3d-9a4e-2f6b1c8d2f7b"
]
}
]
}
],
"clientName": "Jane Taxpayer",
"includePdf": true,
"includeLeadsheets": true,
"leadsheetsFormat": "EXCEL",
"includeForms": true,
"includeChecklist": true,
"includeSourceDocs": false
}
}
}'
```
```json theme={null}
{
"data": {
"generateWorkpaperBundle": {
"workflowExecutionId": "018f9c2c-3d4e-7f50-9b66-7f7082901234",
"status": "RUNNING"
}
}
}
```
## Check a bundle render status
`checkWorkpaperGenerationStatus` is a `Workspace` field that returns the current
state of a workpaper render job. Poll it with the `workflowExecutionId` returned
by [`generateWorkpaperBundle`](#generate-a-workpaper-bundle) until `status` is
`COMPLETED` (then read `downloadUrl.url`) or `FAILED` (then read `errorMessage`).
```graphql theme={null}
query CheckWorkpaperGenerationStatus($workflowExecutionId: ID!) {
me {
... on WorkspaceUser {
id
workspace {
id
checkWorkpaperGenerationStatus(workflowExecutionId: $workflowExecutionId) {
workflowExecutionId
status
downloadUrl {
filePath
url
}
generatedAt
errorMessage
}
}
}
}
}
```
### Arguments
The render job's workflow execution ID (from
`generateWorkpaperBundle.workflowExecutionId`).
### Returns: `WorkpaperRender!`
```graphql theme={null}
type WorkpaperRender {
workflowExecutionId: ID!
status: WorkpaperRenderStatus!
downloadUrl: SignedPath
generatedAt: String
errorMessage: String
}
type SignedPath {
filePath: String!
url: String!
}
```
The render job's workflow execution ID.
`RUNNING`, `COMPLETED`, or `FAILED` (see
[`WorkpaperRenderStatus`](#returns-workpaperrenderjob)).
The signed download URL for the rendered bundle. Present only when `status` is
`COMPLETED`. `SignedPath` is `{ filePath, url }`; the `url` is the one you fetch
(or hand to the browser) to download the bundle. `null` while the render is
running or if it failed.
ISO timestamp when the render completed. `null` while `RUNNING` or `FAILED`.
The error message when `status` is `FAILED`. `null` otherwise.
```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 CheckWorkpaperGenerationStatus($workflowExecutionId: ID!) { me { ... on WorkspaceUser { id workspace { id checkWorkpaperGenerationStatus(workflowExecutionId: $workflowExecutionId) { workflowExecutionId status downloadUrl { filePath url } generatedAt errorMessage } } } } }",
"variables": {
"workflowExecutionId": "018f9c2c-3d4e-7f50-9b66-7f7082901234"
}
}'
```
```json theme={null}
{
"data": {
"me": {
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"workspace": {
"id": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"checkWorkpaperGenerationStatus": {
"workflowExecutionId": "018f9c2c-3d4e-7f50-9b66-7f7082901234",
"status": "COMPLETED",
"downloadUrl": {
"filePath": "workpaper-bundles/018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c/2026-07-05.zip",
"url": "https://signed.example.com/workpaper-bundles/018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c/2026-07-05.zip?token=..."
},
"generatedAt": "2026-07-05T14:10:11.000Z",
"errorMessage": null
}
}
}
}
}
```
The web app polls `checkWorkpaperGenerationStatus` every 1500ms with a hard
timeout of 5 minutes, and on `COMPLETED` opens `downloadUrl.url` in a new tab.
Mirror that pattern: poll on a short interval, stop on `COMPLETED` or `FAILED`,
and treat a missing `downloadUrl` on `COMPLETED` as a failure.
This poll pattern is **distinct from the [Task](/apis/tasks) polling pattern**.
Workpaper renders use a `workflowExecutionId` and the `WorkpaperRenderStatus`
enum (`RUNNING`, `COMPLETED`, `FAILED`) on a `WorkpaperRender` shape returned by
`Workspace.checkWorkpaperGenerationStatus`. They do **not** use the `Task` /
`TaskStatus` type, the `clients.tasks` list, or `TaskResult` unions. Do not
conflate the two: a workpaper render is not a `Task` and cannot be read through
`tasks(type:, status:)`.
## Trigger a workpaper translation
`triggerWorkpaperTranslate` kicks off a background translation task that
produces a workpaper for a client. It returns a `TriggerTaskResult` carrying the
`taskId` you poll through the standard [Task](/apis/tasks) polling pattern
(unlike the [bundle render](#check-a-bundle-render-status) above, the translate
flow is a real `Task`).
As of this writing, `triggerWorkpaperTranslate` and
[`workpaperTemplates`](#list-workpaper-templates) are not yet wired into the web
app's surface code (they exist only in the generated GraphQL types). They are
documented here from the live schema for completeness. If you build against
them, verify the behavior end to end against your own workspace before relying
on a specific shape.
```graphql theme={null}
mutation TriggerWorkpaperTranslate($input: TriggerWorkpaperTranslateInput!) {
triggerWorkpaperTranslate(input: $input) {
taskId
}
}
```
### Input: `TriggerWorkpaperTranslateInput`
```graphql theme={null}
input TriggerWorkpaperTranslateInput {
clientId: ID!
returnType: ReturnType!
sourceTaskType: String
sourceRunId: ID
templateName: String
}
enum ReturnType {
F1040
F1041
F1065
F1120
F1120S
F990
}
```
The client to translate a workpaper for.
The return form to target: `F1040`, `F1041`, `F1065`, `F1120`, `F1120S`, or
`F990` (see [clients](/apis/clients#the-client-type)).
Optional. The task type that produced the source data, when you are translating
from the output of a prior run.
Optional. The run ID of the source run, when relevant.
Optional. A specific template to apply. List the available templates for a
return type with [`workpaperTemplates`](#list-workpaper-templates).
### Returns: `TriggerTaskResult!`
```graphql theme={null}
type TriggerTaskResult {
taskId: ID!
}
```
The background [task](/apis/tasks) ID. Poll
`me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { tasks(type: ..., limit: 1) { id status } } } } }`
until `status` is `COMPLETED` or `FAILED`. See [Tasks](/apis/tasks) for the
polling pattern and the `TaskResult` union.
```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 TriggerWorkpaperTranslate($input: TriggerWorkpaperTranslateInput!) { triggerWorkpaperTranslate(input: $input) { taskId } }",
"variables": {
"input": {
"clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
"returnType": "F1120",
"templateName": "default"
}
}
}'
```
```json theme={null}
{
"data": {
"triggerWorkpaperTranslate": {
"taskId": "018f9c2c-4e5f-7f60-9c77-8082901234ab"
}
}
}
```
## List workpaper templates
`workpaperTemplates` is a `Workspace` field that returns the list of workpaper
template names available for a given return type. Pass a template name as
`templateName` to [`triggerWorkpaperTranslate`](#trigger-a-workpaper-translation).
```graphql theme={null}
query WorkpaperTemplates($returnType: ReturnType!) {
me {
... on WorkspaceUser {
id
workspace {
id
workpaperTemplates(returnType: $returnType)
}
}
}
}
```
### Arguments
The return form to list templates for: `F1040`, `F1041`, `F1065`, `F1120`,
`F1120S`, or `F990`.
### Returns: `[String!]!`
A list of template name strings. Pass any one of them as `templateName` to
`triggerWorkpaperTranslate`.
```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 WorkpaperTemplates($returnType: ReturnType!) { me { ... on WorkspaceUser { id workspace { id workpaperTemplates(returnType: $returnType) } } } }",
"variables": { "returnType": "F1120" }
}'
```
```json theme={null}
{
"data": {
"me": {
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"workspace": {
"id": "019f0fb6-3001-7900-b7bc-0d11288504b1",
"workpaperTemplates": ["default", "detailed"]
}
}
}
}
```
# Authentication
Source: https://docs.apps.filed.com/guides/authentication
Create an API key, understand its scope, and exchange it for an access token
The Filed API uses a **two-step token model**. You create a long-lived **API key**
in the Filed web app, then exchange it at request time for a short-lived
**access token** that you send as a `Bearer` token on every GraphQL call.
```mermaid theme={null}
flowchart LR
A["API key
(long-lived, workspace-scoped)"] -->|exchange| B["Access token
(short-lived, ~30 min)"]
B -->|"Bearer"| C["GraphQL API
(router.apps.filed.com)"]
```
All requests go to a single endpoint:
```
https://router.apps.filed.com/graphql
```
## 1. Create an API key
API keys are created from the Filed web app, per workspace:
1. Open the workspace you want to grant access to.
2. Go to **Plugins → Filed API**.
3. Click **Create an API Key**.
4. Choose an **Expiry** and an **Access** level (see below).
5. Copy the key. **It is shown only once**, so store it in a secret manager. If you
lose it, revoke it and create a new one.
An API key is **personal**: it authenticates as **you**, the user who created
it. Every call made with a token minted from the key acts on your behalf and is
limited to what your account is allowed to do in that workspace (the `userToken`
from the exchange identifies you). If you leave the workspace or your access
changes, the key's access changes with you. For a shared or service integration,
create the key from an account you intend to own that integration.
An API key is a credential. Treat it like a password: never commit it to source
control, never expose it in a browser or mobile client, and rotate it if it may
have leaked.
### Expiry
The key is valid for the window you pick at creation time. After it expires, the
exchange step (below) stops working and you must create a new key.
| Option | Key lifetime |
| ------ | ----------------- |
| `30` | 30 days |
| `60` | 60 days |
| `90` | 90 days (default) |
| `365` | 1 year |
## 2. Scope: one key, one workspace
An API key is **scoped to the single workspace it was created in**. The access
token you get from it can only read and write data in that workspace. To
integrate with several workspaces, create one key per workspace.
This is different from legacy partner API keys, whose token could reach every
workspace the partner created. New Filed API keys are deliberately
workspace-scoped for tighter, per-workspace access control.
### Access levels: read vs read-write
The **Access** level you pick at creation time is baked into every access token
minted from that key:
| Access | Value | What the token can do |
| ------------------ | ------------ | ----------------------------------------------------------------------------------------------------------- |
| **Read only** | `read_only` | Run **queries** to look up clients, tasks, documents, and other workspace data. All mutations are rejected. |
| **Read and write** | `read_write` | Everything read-only can do, **plus mutations**: upload documents, trigger runs, and modify workspace data. |
Pick the narrowest level that fits your integration. If you only pull data, use
**Read only** so a leaked key can never mutate your workspace.
## 3. Exchange the API key for an access token
Send your API key as the `refreshToken` argument to
`exchangeSurfaceRefreshTokenForAccessTokens`. This is a **public** mutation: it
is the only call you make *without* a `Bearer` token.
```graphql theme={null}
mutation ExchangeApiKey($apiKey: String!) {
exchangeSurfaceRefreshTokenForAccessTokens(refreshToken: $apiKey) {
userToken
workspaceToken
}
}
```
### Arguments
Your API key, exactly as copied from the Filed web app.
### Returns: `AccessTokens`
A short-lived token identifying **you** (the user the key belongs to) across
your account, not tied to any one workspace.
A short-lived token that is **also you**, scoped to **the key's workspace**. This
is the token you use for API calls. It carries the key's access level: a
`read_only` key mints a read-only `workspaceToken`.
Both tokens authenticate as the user who created the key; neither is a separate
service identity. The `userToken` is you account-wide, the `workspaceToken` is you
within the key's workspace at the key's access level.
Both tokens are short-lived (about 30 minutes). When they expire, call the
exchange again with the same API key to mint fresh ones. The API key itself
lasts until its expiry.
### Example
```bash cURL theme={null}
curl -X POST https://router.apps.filed.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation ExchangeApiKey($apiKey: String!) { exchangeSurfaceRefreshTokenForAccessTokens(refreshToken: $apiKey) { userToken workspaceToken } }",
"variables": { "apiKey": "YOUR_API_KEY" }
}'
```
```json theme={null}
{
"data": {
"exchangeSurfaceRefreshTokenForAccessTokens": {
"userToken": "eyJhbGciOiJFUzI1NiI...",
"workspaceToken": "eyJhbGciOiJFUzI1NiI..."
}
}
}
```
## 4. Call the API with the access token
Send the `workspaceToken` in the `Authorization` header on every subsequent
request:
```http theme={null}
Authorization: Bearer YOUR_WORKSPACE_TOKEN
```
Verify it works with a `me` query. `me` returns the `Me` union, which resolves to
`WorkspaceUser` when you authenticate with a `workspaceToken` (and to `User` with
an account-wide `userToken`). Because it is a union, select fields with an inline
fragment on the type you expect:
```graphql theme={null}
query Me {
me {
__typename
... on WorkspaceUser {
id
role
createdAt
user {
id
name
email
}
workspace {
id
name
}
}
}
}
```
```json theme={null}
{
"data": {
"me": {
"__typename": "WorkspaceUser",
"id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
"role": "admin",
"createdAt": "2026-06-14T09:31:20.000Z",
"user": {
"id": "019f0fb6-26e9-74b7-a842-cb43a2a41682",
"name": "Jane Preparer",
"email": "jane@example-firm.com"
},
"workspace": {
"id": "019f0fb6-379a-7f72-b7ec-ebd8f41ccfa1",
"name": "Example Tax Firm"
}
}
}
}
```
Run it over HTTP the same way as the exchange, adding your token:
```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 Me { me { __typename ... on WorkspaceUser { id role createdAt user { id name email } workspace { id name } } } }" }'
```
### Types
```graphql theme={null}
union Me = User | WorkspaceUser
type WorkspaceUser {
id: ID!
role: WorkspaceRole!
createdAt: Date!
user: UserShortDetails!
workspace: Workspace!
}
type UserShortDetails {
id: ID!
name: String!
email: String!
}
```
Union of `User` (returned with an account-wide `userToken`) and `WorkspaceUser`
(returned with a workspace-scoped `workspaceToken`). Query it with
`... on WorkspaceUser { ... }` to read workspace fields.
The membership id that links this user to the workspace.
The user's role in the workspace, for example `admin` or `member`.
When the user was added to the workspace.
The underlying user account: `id`, `name`, and `email`.
The workspace this `workspaceToken` is scoped to.
## Putting it together
A typical integration:
1. **Once**, in the web app: create a workspace-scoped API key with the access
level you need, and store it as a secret.
2. **On startup / on 401**: exchange the API key for a fresh `workspaceToken`.
3. **Per request**: send `Authorization: Bearer `.
4. **When the token expires (\~30 min)**: repeat step 2 with the same API key.
Cache the `workspaceToken` and only re-exchange when it expires (or when a call
returns an auth error) rather than exchanging on every request.
## Troubleshooting
**`exchangeSurfaceRefreshTokenForAccessTokens` returns an error**
* The API key is wrong, revoked, or past its expiry. Create a new one.
* Confirm you are posting to `https://router.apps.filed.com/graphql`.
**Queries work but mutations are rejected**
* The key was created as **Read only** (`read_only`). Create a **Read and write**
key to allow mutations.
**Requests fail after \~30 minutes**
* The `workspaceToken` expired. Re-exchange the API key for a new one.
# Connect via MCP
Source: https://docs.apps.filed.com/guides/connect-mcp
Connect Claude, Codex, and other MCP clients to Filed with OAuth, no API key required
The Filed MCP server is not live in production yet. The connection details
below are accurate for the built server, but the URLs will not resolve until
it ships. Check with Filed before sharing this page with an integrator who
needs it working today.
Filed runs a remote [MCP](https://modelcontextprotocol.io) server that lets AI
assistants like Claude and Codex act directly on a Filed workspace: run
GraphQL queries and mutations, upload documents, and read the API docs, all
without you writing any integration code.
Unlike the [API key model](/guides/authentication) used for custom
integrations, connecting an MCP client to Filed is a one-time OAuth consent
flow in your browser. There is no key to copy or paste.
```mermaid theme={null}
flowchart LR
A["MCP client
(Claude, Codex, ...)"] -->|"OAuth: register, authorize, consent"| B["Filed web app
(sign in + approve)"]
B -->|"surface refresh token"| C["Filed MCP server
mcp.apps.filed.com"]
C -->|"GraphQL"| D["Filed router
router.apps.filed.com"]
```
## Connect a client
## Point your client at the MCP endpoint
```
https://mcp.apps.filed.com/mcp
```
| Client | How to add it |
| ------------------------------- | ---------------------------------------------------------------------- |
| **Claude.ai** | Settings → Connectors → Add custom connector → paste the URL above |
| **Claude Code** | `claude mcp add --transport http filed https://mcp.apps.filed.com/mcp` |
| **ChatGPT** (developer mode) | Paste the URL above as a custom connector |
| **Codex** and other MCP clients | Add a remote server with the URL above, streamable HTTP transport |
## Approve the connection
Your client performs Dynamic Client Registration and redirects you to sign in
to the Filed web app (if you aren't already) and approve the connection for a
specific workspace. There is no key to generate or copy: approving the
consent screen is the entire setup.
## Confirm it works
Ask your assistant to call the `docs` tool, or run a simple query like `me`
through `run_batch_queries`, to confirm the connection is live and scoped to
the workspace you approved.
Approving the connection creates an MCP-type entry in the workspace's
Integrations page, the same place API keys live. Revoke access at any time by
deleting it there: the next request from that client gets a `401`.
## What the server exposes
The MCP server does not expose the whole GraphQL schema as individual MCP
tools. Instead it gives your assistant a small number of general-purpose
tools and expects it to read the docs and write GraphQL itself.
| Tool | What it does |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `docs` | Points the assistant at the Filed API docs (this site), including the LLM-readable `/llms.txt` variant. Clients are instructed to call this first. |
| `run_batch_queries` | Runs one or more read-only GraphQL queries. Queries run in parallel (chunked, five at a time) and results come back in the same order as the input. |
| `run_batch_mutations` | Runs one or more GraphQL mutations, in order. If one fails, the rest still run; check each result individually. |
| `get_file_upload_info` | Returns a short-lived upload token and the platform's upload URL, for attaching documents to a client. |
Your assistant can discover the full schema with a standard GraphQL
introspection query through `run_batch_queries`, the same schema documented in
the [API reference](/apis/introduction).
## How agents should choose APIs
Agents should use the API docs as a routing map, not only as schema reference.
The best default flow is:
1. Call `docs`, then read the [API introduction](/apis/introduction).
2. Call `run_batch_queries` with a small [`me`](/apis/me) query to confirm
workspace scope.
3. Use [`Clients`](/apis/clients) to find the client by ID, name, or external
identifier.
4. Use [`Binder`](/apis/binder) for organized files, source documents, missing
items, counts, search, and signed document URLs.
5. Use [`Document messages`](/apis/document-messages) for notes, flags,
comments, replies, hides, unhides, and sign-off writes.
6. Use [`Task triggers`](/apis/task-triggers) to start data entry or tax prep,
then [`Tasks`](/apis/tasks) to poll status and errors.
7. Use [`Leadsheets`](/apis/leadsheets) to read tax prep output, issue state,
trace, and existing sign-offs.
8. Use [`Integration capabilities`](/apis/integration-capabilities) when the
user asks what an integration can do or asks to run a provider-specific
action. For requests to read current tax software data, export a backup, or
write reviewed basic-form updates directly, follow
[Read and Write Basic Tax Forms Directly With RPA](/guides/recipes/enter-tax-data-from-mcp).
That recipe covers CCH Axcess v2 dispatch, MCP task sandboxes, and signed
file access.
For reads, prefer `run_batch_queries` and batch independent lookups together.
For writes, prefer `run_batch_mutations` and check every result because a
failed mutation does not stop the rest of the batch.
The binder is usually the central object for client document work. It gives
agents the subdocument IDs they need for notes, comments, sign-offs, leadsheet
drilldown, and document search.
## Uploading documents
MCP tool calls carry a single JSON message, so file bytes don't travel through
a tool call directly. To upload a document, the assistant:
1. Calls `get_file_upload_info` to get a short-lived (10-minute) upload token
and the platform's [tus](https://tus.io) upload URL.
2. Drives the resumable tus upload directly against that URL, exactly as
described in [Uploading documents](/guides/uploading-documents).
3. Passes the resulting `uploadId` into a mutation such as `createClient` or
`addClientDocuments` through `run_batch_mutations`.
## Access and revocation
A connection is scoped to **one workspace**, chosen when you approve it. To
connect a second workspace, add another connection and approve it separately.
To revoke access, delete the connection from the workspace's Integrations
page. The next token exchange (within about two minutes, due to caching)
starts failing, and the next MCP request from that client gets a `401`.
## Troubleshooting
**The client won't complete the connection**
* Confirm you're signed in to the Filed web app in the same browser that
completes the OAuth redirect.
* Confirm your MCP client supports streamable HTTP (not only SSE).
**Tool calls return `401`**
* The connection was revoked, or the underlying session cache expired and the
next exchange failed. Reconnect the client.
**A mutation in `run_batch_mutations` failed but others succeeded**
* This is expected: mutations run in order and a failure doesn't stop the
batch. Check each item's result individually rather than assuming
all-or-nothing.
# Embed the binder in your product
Source: https://docs.apps.filed.com/guides/embedding-the-binder
Show a client's binder inside your own site as a chromeless iframe, authenticated with a workspace token
The binder (Documents, Forms, and Leadsheets) can be shown inside your own
product as an `