# 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 ` ``` ## Token expiry Like every `workspaceToken`, the one used here expires after about 30 minutes. When it does, the binder shows its own "session expired" state inside the iframe rather than redirecting out (there is nowhere sensible to redirect an iframe to on another site). Mint a fresh `workspaceToken`, build a new `/sign-in/token` URL, and reload the iframe's `src` to recover. Rebuild and reset the iframe `src` on an interval shorter than 30 minutes (for example every 20) so visitors rarely see the expired state. ## Next steps * **Authentication**: review how API keys, `workspaceToken`s, and access levels work in [Authentication](/guides/authentication). * **Browse the binder over the API**: to read binder data yourself (files, missing items, search) instead of embedding the UI, see [Browse a client's binder](/guides/recipes/browse-the-binder). # Introduction Source: https://docs.apps.filed.com/guides/introduction Introduction to Filed APIs # GraphQL Introduction > Powerful GraphQL API for flexible server management and real-time data querying The Filed GraphQL API provides a flexible, efficient way to query and manage your taxpreps. Unlike REST APIs, GraphQL allows you to request exactly the data you need in a single request, reducing over-fetching and improving performance. ## Key Features * **Flexible Queries**: Request only the data you need * **Type Safety**: Strongly typed schema with comprehensive documentation * **Single Endpoint**: All operations through one GraphQL endpoint ## Getting Started ### Endpoint ``` https://router.apps.filed.com/graphql ``` ### Authentication All GraphQL requests require authentication using a Bearer token in the Authorization header. ```http theme={null} theme={null} Authorization: Bearer YOUR_JWT_ACCESS_TOKEN ``` ### Basic Query Example ```graphql theme={null} theme={null} query Health { health { id ai platform } } ``` ## GraphQL desktop client Explore the GraphQL API interactively using a desktop client: Test queries with this desktop client (Httpie) ## Benefits Over REST Fetch multiple related resources in a single request instead of multiple REST calls. Request only the fields you need, reducing payload size and improving performance. Strongly typed schema prevents runtime errors and improves developer experience. ## Next steps * **Authentication**: Create an API key and exchange it for an access token in the [Authentication guide](/guides/authentication). * **Quickstart**: Go from zero to a processed client in five steps in the [Quickstart](/guides/quickstart). * **Making requests**: Learn the request shape, variables, and the error model in [Making requests](/guides/making-requests). * **Uploading documents**: Stage files through the resumable upload endpoint and attach them to a client in the [Uploading documents guide](/guides/uploading-documents). * **API reference**: Browse the operations, [`health`](/apis/health), [`me`](/apis/me), [Clients](/apis/clients), [Tasks](/apis/tasks), and [Conventions](/apis/conventions), in the [API reference](/apis/introduction). Looking for the previous REST API? It is preserved under [Legacy](/legacy/apis/introduction). # Making requests Source: https://docs.apps.filed.com/guides/making-requests Shape a GraphQL request, pass variables, and read error responses Every Filed API call is a single HTTP `POST` to the same endpoint, with a JSON body and a `Bearer` token. This page covers the request shape, a first request, variables, and the error model, so you can call any operation in the rest of these docs once you know how the wire looks. ``` https://router.apps.filed.com/graphql ``` ## Request anatomy A request is one HTTP `POST` with a JSON body of two keys, `query` and `variables`, and two headers, `Content-Type: application/json` and `Authorization: Bearer YOUR_WORKSPACE_TOKEN`. ```http theme={null} POST /graphql HTTP/1.1 Host: router.apps.filed.com Content-Type: application/json Authorization: Bearer YOUR_WORKSPACE_TOKEN { "query": "", "variables": { } } ``` A GraphQL document: one (or more) `query` / `mutation` / `fragment` definitions. For a single operation you usually send one operation and omit the operation name. When you send several operations, give each a name and pass `"operationName"` alongside `query` and `variables`. A JSON object of values for the operation's `$variables`. Pass complex or user-supplied values here rather than string-interpolating them into `query`, so the GraphQL server validates their types and you avoid injection mistakes. Omit it (or send `{}`) for operations that take no arguments. `Bearer ` followed by a `workspaceToken` from [Authentication](/guides/authentication). Omit only for `@public` operations such as [`health`](/apis/health) and the token exchange. `application/json`. The body is always JSON, never form-encoded. A successful response is JSON with a `data` object (and, on partial failure, an `errors` array, see [Error responses](#error-responses)): ```json theme={null} { "data": { }, "errors": [ ] } ``` ## A first request Start with [`health`](/apis/health). It is `@public`, so it needs no token, and it confirms the endpoint is reachable. ```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" } } } ``` Now add a token and run [`me`](/apis/me) to confirm the token works and see who you are in the workspace: ```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 workspace { id name } } } }" }' ``` `me` returns the `Me` union; with a `workspaceToken` it resolves to `WorkspaceUser`, which carries your `role` and the `workspace` you are scoped to (see [`me`](/apis/me) for the full type and `User` member). ## Variables Pass `$variables` for any operation that takes arguments, so values are type-checked by the server instead of string-interpolated. The [`workspace.clients`](/apis/clients#list-clients) field takes a `ClientFilters` input, an `offset`, a `limit`, and a `SortBy`. Send the document once with `$variables` placeholders, then send the values in the `variables` JSON object. ```graphql theme={null} query ListClients($filters: ClientFilters, $limit: Int, $sortBy: SortBy) { me { ... on WorkspaceUser { workspace { clients(filters: $filters, limit: $limit, sortBy: $sortBy) { id name externalId 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 ListClients($filters: ClientFilters, $limit: Int, $sortBy: SortBy) { me { ... on WorkspaceUser { workspace { clients(filters: $filters, limit: $limit, sortBy: $sortBy) { id name externalId status } } } } }", "variables": { "filters": { "status": ["active"], "search": "jane" }, "limit": 20, "sortBy": { "field": "createdAt", "order": "DESC" } } }' ``` Always send user-supplied or dynamic values through `variables`, never by splicing them into the `query` string. The server coerces and validates the types, and you avoid quoting bugs and injection risk. See [`clients`](/apis/clients) for the full `ClientFilters` input and [`tasks`](/apis/tasks) for `TaskFilters`. The same `$variable` pattern applies to every mutation in the API, for example `createClient(input: $input)`. ## Error responses On failure the response is JSON with an `errors` array. Each entry has at least a `message`, and most also carry an `extensions` object with a `code`. When the operation failed before any data could be produced, `data` is `null` (or omitted); when a field resolver fails mid-operation, that field is `null` in `data` and the matching entry is in `errors` with a `path` pointing at it. The two error shapes you will see most often: ### Validation error (malformed query) A query that references a field the type does not have fails GraphQL validation before any resolver runs. `data` is omitted and `errors` carries one entry per invalid field, with `extensions.code` of `GRAPHQL_VALIDATION_FAILED`. ```bash cURL theme={null} curl -X POST http://localhost:7020/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "query { health { id ai platform thisFieldDoesNotExist } }" }' ``` ```json theme={null} { "errors": [ { "message": "Cannot query field \"thisFieldDoesNotExist\" on type \"Health\".", "locations": [ { "line": 1, "column": 33 } ], "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" } } ] } ``` ### Authentication error (no token) A resolver that requires a token (everything except `@public` operations like `health` and the token exchange) returns `UNAUTHENTICATED` for that field. The field is `null` in `data` and `errors` carries the entry with a `path` naming the field. ```bash cURL theme={null} curl -X POST http://localhost:7020/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "query { me { __typename } }" }' ``` ```json theme={null} { "errors": [ { "message": "UNAUTHENTICATED", "path": ["me"], "extensions": { "code": "UNAUTHENTICATED", "serviceName": "platform" } } ], "data": { "me": null } } ``` The live router may also include a `stacktrace` in `extensions` for server-side errors. Treat it as debugging detail, not a stable contract: log it for troubleshooting, but key your retry / surface-error logic off `extensions.code` and the `path`. ### Reading errors in a client Handle errors defensively: 1. Check `errors` first. If present, at least part of the operation failed. 2. When `data` is `null`, the whole operation failed; surface the first `errors[0].message`. 3. When `data` is present but a field is `null`, look up the matching `path` in `errors` to know which field failed and why. 4. Branch on `extensions.code` for the common cases: * `GRAPHQL_VALIDATION_FAILED` - your query is wrong; do not retry, fix the document. * `UNAUTHENTICATED` - re-exchange your API key for a fresh `workspaceToken` (see [Authentication](/guides/authentication)) and retry once. * Other codes (for example `INTERNAL_SERVER_ERROR`) - safe to retry with backoff. Always inspect `errors` before `data`. A non-empty `errors` array means the operation did not fully succeed, even when `data` looks populated, because individual fields can fail while the rest resolve. ## Next steps * [Authentication](/guides/authentication) - get the `workspaceToken` you send as `Bearer`. * [`health`](/apis/health) and [`me`](/apis/me) - the two queries from the first-request example above, documented in full. * [`clients`](/apis/clients) and [`tasks`](/apis/tasks) - the workspace-scoped resources you reach through `me { ... on WorkspaceUser { workspace { ... } } }`. * [Uploading documents](/guides/uploading-documents) - the other wire (tus) you use to stage files before attaching them to a client. # Quickstart Source: https://docs.apps.filed.com/guides/quickstart Go from zero to a processed client in five steps: authenticate, upload, create, and poll This quickstart walks a new integrator through the golden path: from an API key to a client whose documents have been ingested into its binder. Each step links to the detailed reference page for full type detail rather than duplicating it. All requests go to: ``` https://router.apps.filed.com/graphql ``` Uploads (step 3) go to a separate host: ``` https://web.apps.filed.com/api/uploads ``` ## Get a token Create a workspace-scoped API key in the Filed web app and exchange it for a short-lived `workspaceToken`. This is the only call you make without a Bearer token. ```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" } }' ``` Save the returned `workspaceToken`; you send it as `Authorization: Bearer YOUR_WORKSPACE_TOKEN` on every later request. Full details, including access levels and key expiry, are in [Authentication](/guides/authentication). ## Confirm it works Run [`me`](/apis/me) with the `workspaceToken` to confirm the token works and to see which workspace it is scoped to. `me` returns the `Me` union, which resolves to `WorkspaceUser` for a `workspaceToken`, so select fields with an inline fragment: ```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 user { id name email } workspace { id name } } } }" }' ``` A successful response shows your workspace id and name. If you get an `UNAUTHENTICATED` error, re-exchange your API key for a fresh token (they last about 30 minutes). ## Upload a document Filed does not accept file bytes over GraphQL. Stage each file with the resumable [tus](https://tus.io) upload endpoint first, then pass the returned upload ID to `createClient` (next step). A minimal upload is two requests: a `POST` that creates the upload and returns its location, then a `PATCH` that sends the bytes. ```bash cURL theme={null} # 1. Create the upload. Metadata values are base64-encoded. # filename=w2_1040.pdf filetype=application/pdf intent=client-document LOCATION=$(curl -sS -D - -o /dev/null -X POST https://web.apps.filed.com/api/uploads \ -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \ -H "Tus-Resumable: 1.0.0" \ -H "Upload-Length: $(wc -c < w2_1040.pdf)" \ -H "Upload-Metadata: filename dzJfMTA0MC5wZGY=,filetype YXBwbGljYXRpb24vcGRm,intent Y2xpZW50LWRvY3VtZW50" \ | tr -d '\r' | awk '/^Location:/ {print $2}') UPLOAD_ID="${LOCATION##*/}" # 2. Send the bytes. curl -sS -X PATCH "$LOCATION" \ -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \ -H "Tus-Resumable: 1.0.0" \ -H "Upload-Offset: 0" \ -H "Content-Type: application/offset+octet-stream" \ --data-binary @w2_1040.pdf ``` The **upload ID is the last path segment** of the upload's `Location` URL. Keep it for the next step. Full tus details, including chunking and retries, are in [Uploading documents](/guides/uploading-documents). ## Create a client with the document Pass the upload ID to [`createClient`](/apis/clients#create-a-client) to create a client and kick off binder ingestion in one call. The mutation returns `{ client { id }, taskId }`: the `client.id` identifies the new client, and the `taskId` is the binder ingestion task you poll next. ```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": ["YOUR_UPLOAD_ID"] } } }' ``` ```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" } } } ``` Save both `client.id` and `taskId`. If you created the client without documents, `taskId` is `null` and there is nothing to poll. Full input and return type details are in [Clients](/apis/clients). ## Poll the binder task Filed ingests the staged documents into the client's binder as a background task of type `BINDER`. Read the task through the client's `tasks(type: BINDER)` field and poll until `status` is `COMPLETED` (or `FAILED`). ```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": "YOUR_CLIENT_ID", "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 documents are filed in the binder; `FAILED` means ingestion did not succeed, and `errorMessage` explains why. There is no `task(id:)` query; you read a task through its client. Full task, filter, and result type details are in [Tasks](/apis/tasks). ## Next steps * Learn the [request anatomy and error model](/guides/making-requests). * Add more documents to an existing client with [`addClientDocuments`](/apis/clients#add-documents-to-a-client). * Manage clients (rename, archive, delete, assign) in the [Clients](/apis/clients#manage-clients) reference. * Trigger and follow other task types (`TAX_PREP`, `TAX_REVIEW`, `TAX_ADVISOR`) in the [Tasks](/apis/tasks) reference. # Annotate a document Source: https://docs.apps.filed.com/guides/recipes/annotate-a-document Leave a note or flag on a binder document, read existing annotations, reply in a thread, and edit or hide a message After a client's documents have been [ingested](/guides/recipes/onboard-a-client) and filed into the [binder](/apis/binder), reviewers leave notes and flags on individual pages of a document. These annotations are `DocumentMessage` records with `type: "annotation"`, threaded replies underneath for back-and-forth discussion. This recipe is the minimal call sequence to read existing annotations, leave a new note or flag, reply in a thread, and edit or hide a message. It is the annotation use case of the document-message API; the sibling [Review a return and sign off](/guides/recipes/review-and-sign-off) recipe covers the sign-off use case of the same underlying API. Every operation here is documented on the [Document messages](/apis/document-messages) reference page; this recipe sequences the calls rather than re-documenting the types. Everything uses a **`workspaceToken`** (see [Authentication](/guides/authentication)) and goes to the single GraphQL endpoint: ``` https://router.apps.filed.com/graphql ``` Document messages belong to a client, so there is no top-level `documentMessages` query. Reach them through `me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { documentMessages(filter: ...) { ... } } } } }`. The `workspaceToken` already identifies the workspace. ```mermaid theme={null} flowchart LR A["Client ID +
subdocument id"] --> B["1. Read annotations
(filter: types=[annotation])"] B --> C["2. Create a note / flag
createDocumentMessage
type=annotation"] C --> D["3. Reply in a thread
createDocumentMessageThread"] D --> E["4. Edit or hide
updateDocumentMessage /
hideDocumentMessage"] E --> F["Refetch
documentMessages"] ``` This recipe does not re-document the types it touches. For the full `DocumentMessage`, `DocumentMessageThread`, `DocumentMessageTaggedUser`, `DocumentMessageType`, and `DocumentMessagesFilter` definitions, see [Document messages](/apis/document-messages#the-documentmessage-type). For the sign-off use case of the same API (`type: "activity"`, `markType: "signoff"`), see [Review a return and sign off](/guides/recipes/review-and-sign-off). ## 1. Read existing annotations Read `Client.documentMessages(filter)` to list the annotations already on a document. Filter by `documentPath` (the subdocument `id` you read from [binder.subdocuments](/apis/binder#list-the-files-in-a-binder)) and by `types: ["annotation"]` to narrow to annotations only, excluding sign-offs and missing-document activity. See [Read document messages](/apis/document-messages#read-document-messages) for the full `DocumentMessagesFilter` argument. ```graphql theme={null} query GetClientDocumentMessages($clientId: ID!, $filter: DocumentMessagesFilter) { me { ... on WorkspaceUser { id workspace { id clients(filters: { ids: [$clientId] }) { id documentMessages(filter: $filter) { id documentPath type markType anchorPoint body createdBy createdAt updatedAt hiddenAt hiddenBy threads { id documentMessageId contentPath body createdBy createdAt updatedAt } taggedUsers { id userId documentMessageId documentMessageThreadId } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "filter": { "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "types": ["annotation"], "includeHidden": false } } ``` ```bash cURL theme={null} curl -X POST https://router.apps.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \ -d '{ "query": "query GetClientDocumentMessages($clientId: ID!, $filter: DocumentMessagesFilter) { me { ... on WorkspaceUser { id workspace { id clients(filters: { ids: [$clientId] }) { id documentMessages(filter: $filter) { id documentPath type markType anchorPoint body createdBy createdAt updatedAt hiddenAt hiddenBy threads { id documentMessageId contentPath body createdBy createdAt updatedAt } taggedUsers { id userId documentMessageId documentMessageThreadId } } } } } } }", "variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "filter": { "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "types": ["annotation"], "includeHidden": false } } }' ``` ```json theme={null} { "data": { "me": { "id": "019f0fb6-37b1-7800-b7bc-0d11288504b1", "workspace": { "id": "019f0fb6-3001-7900-b7bc-0d11288504b1", "clients": [ { "id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "documentMessages": [ { "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70", "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "type": "annotation", "markType": "note", "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } }, "body": "Box 1 total matches the 1099-INT sum, but box 2 looks high. Recheck.", "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1", "createdAt": "2026-07-04T16:10:00.000Z", "updatedAt": "2026-07-04T16:10:00.000Z", "hiddenAt": null, "hiddenBy": null, "threads": [ { "id": "019f0fb6-5b3d-7900-9c01-2b3c4d5e6f71", "documentMessageId": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70", "contentPath": "", "body": "Pulled the corrected 1099-INT from the broker portal, box 2 now matches.", "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1", "createdAt": "2026-07-04T16:50:00.000Z", "updatedAt": "2026-07-04T16:50:00.000Z" } ], "taggedUsers": [] } ] } ] } } } } ``` Each annotation's `id` is what you pass to [`updateDocumentMessage`](#3-edit-or-hide-an-annotation) (to edit the body) or [`hideDocumentMessage`](#3-edit-or-hide-an-annotation) (to soft-delete it). Save it when you create the annotation in the next step. ## 2. Create a note or flag Annotations are `createDocumentMessage` calls with `type: "annotation"`. The `markType` field is a free-form label your surface understands; the web app uses `"note"` for free-text notes and `"flag"` for review flags. The annotation's text goes in `body`, and the `anchorPoint` carries the `page` and `coordinates: { x, y }` that position the mark on the page. See [Create an annotation](/apis/document-messages#create-an-annotation) for the full `CreateDocumentMessageInput` field list. ### Create a note ```graphql theme={null} mutation CreateDocumentMessage($input: CreateDocumentMessageInput!) { createDocumentMessage(input: $input) { id documentPath type markType anchorPoint body createdBy createdAt updatedAt threads { id documentMessageId body createdBy createdAt } } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "type": "annotation", "markType": "note", "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } }, "body": "Box 1 total matches the 1099-INT sum, but box 2 looks high. Recheck." } } ``` ```bash cURL theme={null} curl -X POST https://router.apps.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \ -d '{ "query": "mutation CreateDocumentMessage($input: CreateDocumentMessageInput!) { createDocumentMessage(input: $input) { id documentPath type markType anchorPoint body createdBy createdAt updatedAt threads { id documentMessageId body createdBy createdAt } } }", "variables": { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "type": "annotation", "markType": "note", "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } }, "body": "Box 1 total matches the 1099-INT sum, but box 2 looks high. Recheck." } } }' ``` ```json theme={null} { "data": { "createDocumentMessage": { "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70", "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "type": "annotation", "markType": "note", "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } }, "body": "Box 1 total matches the 1099-INT sum, but box 2 looks high. Recheck.", "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1", "createdAt": "2026-07-04T16:10:00.000Z", "updatedAt": "2026-07-04T16:10:00.000Z", "threads": [] } } } ``` ### Create a flag A flag uses the same `createDocumentMessage` mutation with `markType: "flag"`. The body is optional for a flag (a flag may carry no note text). ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "type": "annotation", "markType": "flag", "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } }, "body": "Schedule B interest total differs from 1099-INT sum by $42." } } ``` ```bash cURL theme={null} curl -X POST https://router.apps.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \ -d '{ "query": "mutation CreateDocumentMessage($input: CreateDocumentMessageInput!) { createDocumentMessage(input: $input) { id documentPath type markType anchorPoint body createdBy createdAt updatedAt } }", "variables": { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "type": "annotation", "markType": "flag", "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } }, "body": "Schedule B interest total differs from 1099-INT sum by $42." } } }' ``` ```json theme={null} { "data": { "createDocumentMessage": { "id": "019f0fb6-4c1d-7900-9c01-2b3c4d5e6f72", "documentPath": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "type": "annotation", "markType": "flag", "anchorPoint": { "page": 1, "coordinates": { "x": 120, "y": 340 } }, "body": "Schedule B interest total differs from 1099-INT sum by $42.", "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1", "createdAt": "2026-07-04T16:15:00.000Z", "updatedAt": "2026-07-04T16:15:00.000Z" } } } ``` `documentPath` for a subdocument annotation is the subdocument's `id`, the same value you read as `SubDocument.id` from [List the files in a binder](/apis/binder#list-the-files-in-a-binder). The web app passes `context: { scope: "workspace" }` on every document-message mutation so the `workspaceToken` identifies the workspace. ## 3. Reply in a thread Threaded replies on an annotation are `DocumentMessageThread` records, created under the parent annotation's `id` via `createDocumentMessageThread`. Use threads for the back-and-forth discussion that grows under a note or flag. See [Threads (replies)](/apis/document-messages#threads-replies) for the full `CreateDocumentMessageThreadInput` field list. ```graphql theme={null} mutation CreateDocumentMessageThread($input: CreateDocumentMessageThreadInput!) { createDocumentMessageThread(input: $input) { id documentMessageId contentPath body createdBy createdAt updatedAt } } ``` ```json theme={null} { "input": { "documentMessageId": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70", "body": "Pulled the corrected 1099-INT from the broker portal, box 2 now matches." } } ``` ```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" } } } ``` ## 4. Edit or hide an annotation To edit the body or anchor point of an annotation, use `updateDocumentMessage`. To soft-delete it (so it disappears from default reads but stays in history), use `hideDocumentMessage`. To restore a hidden annotation, use `unhideDocumentMessage`. See [Update a document message](/apis/document-messages#update-a-document-message) and [Hide and unhide a document message](/apis/document-messages#hide-and-unhide-a-document-message) for the full input shapes. ### Edit the body ```graphql theme={null} mutation UpdateDocumentMessage($id: ID!, $input: UpdateDocumentMessageInput!) { updateDocumentMessage(id: $id, input: $input) { id body anchorPoint updatedAt } } ``` ```json theme={null} { "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70", "input": { "body": "Box 1 confirmed. Box 2 is high, needs a corrected 1099-INT." } } ``` ```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" } } } ``` `updateDocumentMessage` can edit `body`, `anchorPoint`, and `taggedUserIds`. The `type` and `markType` of a document message are not mutable; to convert a note into a flag (or vice versa), hide the old one and create a new one. ### Hide the annotation Hiding is the soft-delete the binder uses to dismiss an annotation. The message stays in history with `hiddenAt` and `hiddenBy` set, and is excluded from default reads unless you pass `filter.includeHidden: true` (see step 1). ```graphql theme={null} mutation HideDocumentMessage($id: ID!) { hideDocumentMessage(id: $id) { id hiddenAt hiddenBy } } ``` ```json theme={null} { "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70" } ``` ```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" } } } ``` ### Unhide the annotation To restore a hidden annotation, call `unhideDocumentMessage` with the same `id`. Both `hiddenAt` and `hiddenBy` clear back to `null`. ```graphql theme={null} mutation UnhideDocumentMessage($id: ID!) { unhideDocumentMessage(id: $id) { id hiddenAt hiddenBy } } ``` ```bash cURL theme={null} curl -X POST https://router.apps.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \ -d '{ "query": "mutation UnhideDocumentMessage($id: ID!) { unhideDocumentMessage(id: $id) { id hiddenAt hiddenBy } }", "variables": { "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70" } }' ``` ```json theme={null} { "data": { "unhideDocumentMessage": { "id": "019f0fb6-4a2c-7900-9c01-2b3c4d5e6f70", "hiddenAt": null, "hiddenBy": null } } } ``` After hiding or unhiding an annotation, refetch the [documentMessages query](#1-read-existing-annotations) so the list reflects the new state. The web app optimistically flips `hiddenAt` and `hiddenBy` in its Apollo cache, then lets the server response confirm it. ## See also * [Document messages](/apis/document-messages) for the full `DocumentMessage`, `DocumentMessageThread`, `DocumentMessageTaggedUser`, `DocumentMessageType`, and `DocumentMessagesFilter` type definitions, plus the `createDocumentMessage`, `updateDocumentMessage`, `hideDocumentMessage`, `unhideDocumentMessage`, `createDocumentMessageThread`, `updateDocumentMessageThread`, and `deleteDocumentMessageThread` mutation signatures. * [Review a return and sign off](/guides/recipes/review-and-sign-off) for the sibling recipe that uses the same `createDocumentMessage` mutation with `type: "activity"` and `markType: "signoff"` to record reviewer sign-offs on a leadsheet sheet or row. * [Browse a client's binder](/guides/recipes/browse-the-binder) for the recipe that lists the subdocuments whose `id` you pass as `documentPath` here, and for [Read message counts](/apis/binder#read-message-counts) which gives you a cheap annotation-count badge via `binder.messageCounts.notes`. * [Binder](/apis/binder) for the `Binder.search` field, whose `annotations` and `marks` buckets surface the annotations you create here. * [Authentication](/guides/authentication) for how to obtain and send the `workspaceToken` every call in this recipe requires. # Automate a workflow with Skills (Playbook) Source: https://docs.apps.filed.com/guides/recipes/automate-with-skills List a workspace's skills, promote a personal skill to firm-wide, toggle it on or off, and scope which skills apply to a single run A **skill** is a reusable, versioned rule set the Filed analyst applies automatically during a tax prep or tax advisor 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. This recipe is the minimal call sequence to list a workspace's skills for a task family, share a personal skill with the firm, approve or deny that promotion, toggle a skill on or off, and scope which skills apply to a single run. Every call uses a **`workspaceToken`** (see [Authentication](/guides/authentication)) and goes to the single GraphQL endpoint: ``` https://router.apps.filed.com/graphql ``` ```mermaid theme={null} flowchart LR A["USER skill
status: NONE"] -->|"requestSkillPromotion"| B["status: PENDING"] B -->|"approveSkillPromotion"| C["WORKSPACE skill
status: APPROVED"] B -->|"denySkillPromotion"| D["status: DENIED"] C -->|"setSkillActive active: false"| E["status: DISABLED"] E -->|"setSkillActive active: true"| C C -.->|"applied automatically
by triggerTaxPrep / triggerTaxAdvisor"| F["A run with
the skill active"] ``` This recipe does not re-document the types it touches. For the full `Skill`, `SkillRule`, `SkillStrategy`, `SkillActivityEvent`, and `UserShortDetails` field lists, the `SkillKind` and `SkillStatus` enums, and the `deleteSkill` mutation, see [Skills](/apis/skills). ## 1. List a workspace's skills for a task type There is no top-level `skills` query. Read `workspace.skills` through `me { ... on WorkspaceUser { workspace { skills(...) } } }` (see [Skills](/apis/skills#list-skills)). Pass `taskType` to scope the list to one task family, and `showCuratedSkills: true` to include Filed's curated built-in 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 } } } } } } ``` ```json theme={null} { "taskType": "tax-prep" } ``` ```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": "NONE", "returnType": null, "updatedAt": "2026-07-04T18:22:01.000Z", "owner": { "id": "019f0fb6-3001-7900-b7bc-0d11288504b1", "name": "Jane Preparer", "email": "jane@example-firm.com" } } ] } } } } ``` Pick the `USER` skill you want to share with the firm. Save its `taskType` and `name`; the promotion mutations in steps 3 and 4 identify a skill by those two values (no `kind` argument). ## 2. Fetch one skill's detail and activity Before promoting a skill, read its full detail and activity timeline with [`workspace.skill`](/apis/skills#view-a-single-skill). Identify the skill with `kind` + `taskType` + `name` (and optional `returnType` when the skill is scoped to a return type). ```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 } } } } } } } ``` ```json theme={null} { "kind": "USER", "taskType": "tax-prep", "name": "my-firm-reconciliation" } ``` ```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": "USER", "taskType": "tax-prep", "name": "my-firm-reconciliation" } }' ``` ```json theme={null} { "data": { "me": { "id": "019f0fb6-37b1-7800-b7bc-0d11288504b1", "workspace": { "id": "018f9c20-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "skill": { "kind": "USER", "taskType": "tax-prep", "name": "my-firm-reconciliation", "description": "Personal reconciliation protocol.", "body": "Flag any 1099-B where proceeds differ from the broker statement by more than $1, and surface the discrepancy as a review item.", "status": "NONE", "returnType": null, "updatedAt": "2026-07-04T18:22:01.000Z", "owner": { "id": "019f0fb6-3001-7900-b7bc-0d11288504b1", "name": "Jane Preparer", "email": "jane@example-firm.com" }, "activity": [ { "action": "created", "timestamp": "2026-07-04T18:22:01.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 before you try to promote. See [Skills, view a single skill](/apis/skills#view-a-single-skill). ## 3. Request promoting a personal skill to the workspace [`requestSkillPromotion`](/apis/skills#request-a-promotion) submits a `USER` skill for firm-wide review. The skill's `status` moves from `NONE` to `PENDING`. A workspace admin then approves or denies it in step 4. The mutation identifies the skill by `taskType` + `name` only; there is no `kind` argument because only `USER` skills can be promoted. ```graphql theme={null} mutation RequestSkillPromotion($taskType: String!, $name: String!) { requestSkillPromotion(taskType: $taskType, name: $name) { kind taskType name status } } ``` ```json theme={null} { "taskType": "tax-prep", "name": "my-firm-reconciliation" } ``` ```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" } } } ``` ## 4. Approve or deny the promotion A workspace admin resolves the `PENDING` promotion with one of two mutations. Both identify the skill by `taskType` + `name` and return the updated `Skill`. To approve, call [`approveSkillPromotion`](/apis/skills#approve-a-promotion). The skill's `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 } } ``` ```json theme={null} { "taskType": "tax-prep", "name": "my-firm-reconciliation" } ``` ```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" } } } ``` To deny, call [`denySkillPromotion`](/apis/skills#deny-a-promotion). The skill's `status` becomes `DENIED` and the optional `reason` is recorded on the activity timeline so the owner can see why the promotion was rejected. ```graphql theme={null} mutation DenySkillPromotion( $taskType: String! $name: String! $reason: String ) { denySkillPromotion(taskType: $taskType, name: $name, reason: $reason) { kind taskType name status } } ``` ```json theme={null} { "taskType": "tax-prep", "name": "my-firm-reconciliation", "reason": "Overlaps with existing firm protocol check-w2-totals." } ``` ```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" } } } ``` After any of the promotion mutations, 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. See [Skills, toggle a skill active or inactive](/apis/skills#toggle-a-skill-active-or-inactive). ## 5. Toggle a skill active or inactive [`setSkillActive`](/apis/skills#toggle-a-skill-active-or-inactive) enables or disables a skill without deleting it. Disabling sets `status: DISABLED` so the skill stops applying during runs 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 } } ``` ```json theme={null} { "taskType": "tax-prep", "name": "check-w2-totals", "active": false, "returnType": "F1040" } ``` ```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" } } } ``` Re-enable the same skill by calling `setSkillActive` again with `active: true`; its `status` returns to `APPROVED`. There is no `runSkill` mutation. Skills are never invoked directly. The analyst applies every `APPROVED` (active) skill automatically during a `triggerTaxPrep` or `triggerTaxAdvisor` run. The next section shows how to scope which active skills apply to one specific run. ## 6. Scope which skills apply to a single run Because skills apply automatically based on their `status`, the way to control which skills a particular run uses is the optional `skills` argument (`RunSkillSelectionInput`) on the trigger mutations: * [`triggerTaxPrep(input: { ..., skills: { workspace: [...], user: [...] } })`](/apis/tax-prep#start-a-tax-prep-run) for a tax prep run. * [`triggerTaxAdvisor(input: { ..., skills: { workspace: [...], user: [...] } })`](/apis/planning#trigger-via-triggertaxadvisor) or [`initiateTaxAdvisor(input: { ..., skills: { workspace: [...], user: [...] } })`](/apis/planning#trigger-via-initiatetaxadvisor) for a tax advisor run. `RunSkillSelectionInput` takes two optional lists of skill names: ```graphql theme={null} input RunSkillSelectionInput { workspace: [String!] user: [String!] } ``` Omit `skills` entirely to apply all active skills in both scopes. Pass an empty list in one scope to censor every skill in that scope. Pass a non-empty list to apply only those named skills. For example, to start a tax prep run that applies only the workspace skill `check-w2-totals` and the user skill `my-firm-reconciliation`, and no others: ```graphql theme={null} mutation TriggerTaxPrep($input: TriggerTaxPrepInput!) { triggerTaxPrep(input: $input) { taskId } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "returnType": "F1040", "skills": { "workspace": ["check-w2-totals"], "user": ["my-firm-reconciliation"] } } } ``` ```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", "skills": { "workspace": ["check-w2-totals"], "user": ["my-firm-reconciliation"] } } } }' ``` ```json theme={null} { "data": { "triggerTaxPrep": { "taskId": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080" } } } ``` Poll the returned `taskId` per [Run tax prep end to end](/guides/recipes/run-tax-prep) and [Tasks](/apis/tasks#check-a-single-tasks-status). The selected skills shape the review items the analyst raises during the run. ## Next steps * For the full `Skill`, `SkillRule`, `SkillStrategy`, `SkillActivityEvent`, and `UserShortDetails` field lists, the `SkillKind` and `SkillStatus` enums, and the `deleteSkill` mutation, see [Skills](/apis/skills). * To run tax prep with the active skills and read the resulting review items, see [Run tax prep end to end](/guides/recipes/run-tax-prep) and [Tax prep](/apis/tax-prep). * To run tax planning with the active skills and read the resulting strategies, see [Run tax planning / advisory](/guides/recipes/run-tax-planning) and [Tax planning](/apis/planning). # Browse a client's binder Source: https://docs.apps.filed.com/guides/recipes/browse-the-binder List the files in a client's binder, check the missing-item checklist, ignore or restore an item, and search across the binder After a client's documents have been [ingested](/guides/recipes/onboard-a-client), the binder is the place to browse what Filed filed: the uploaded files (subdocuments), the missing-item checklist the run produced, and a search surface across bookmarks, annotations, and document contents. This recipe walks through the real call sequence a reviewer-facing integration uses to browse a client's binder. Every operation here is documented on the [Binder](/apis/binder) reference page; this recipe sequences them rather than re-documenting the types. Everything uses a **`workspaceToken`** (see [Authentication](/guides/authentication)) and goes to the single GraphQL endpoint: ``` https://router.apps.filed.com/graphql ``` The binder belongs to a client, so there is no top-level `binder` query. Reach 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. ```mermaid theme={null} flowchart LR A["Client ID"] --> B["1. List files
(subdocuments)"] A --> C["2. Check missing items
(missingItemsAssessment)"] C --> D["3. Ignore / restore
an item"] A --> E["4. Search
(binder.search)"] B --> F["subdocument id =
documentPath for
annotations & sign-offs"] E --> F ``` ## 1. List the files in the binder Read `binder.subdocuments` to list the files Filed filed for the client. This is the most common read against the binder and backs the binder's Documents screen. Pass a `SubDocumentsFilter` to narrow to unreviewed, flagged, or files under one parent document; pass `null` to list everything. See [List the files in a binder](/apis/binder#list-the-files-in-a-binder) for the full `SubDocumentsFilter` arguments. ```graphql theme={null} query GetClientBinderSubdocuments($clientId: ID!, $filter: SubDocumentsFilter) { me { ... on WorkspaceUser { id workspace { clients(filters: { ids: [$clientId] }) { id binder { id subdocuments(filter: $filter) { id fileName type issuer taxYear status category } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "filter": { "unreviewed": true } } ``` ```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 { clients(filters: { ids: [$clientId] }) { id binder { id subdocuments(filter: $filter) { id fileName type issuer taxYear status category } } } } } } }", "variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "filter": { "unreviewed": true } } }' ``` ```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", "subdocuments": [ { "id": "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "fileName": "1099-INT-Acme-Broker.pdf", "type": "1099-INT", "issuer": "Acme Broker", "taxYear": 2025, "status": "ingested", "category": "income" } ] } } ] } } } } ``` Save each subdocument's `id`. It is the value you pass as `documentPath` when [creating a document message](/apis/document-messages#annotations) or [signing off](/apis/leadsheets#sign-off-on-a-sheet-or-row) on a file later. ## 2. Check the missing-item checklist Read `binder.missingItemsAssessment` to learn whether the missing-document check is ready and list the checklist the run produced. Each item carries a `severity` (`CRITICAL`, `MEDIUM`, `LOW`), a `status` (`OPEN`, `IGNORED`, `RESOLVED`), and a `reason`. Filter by status to read only the open items, which is what a reviewer-facing UI shows first. See [List missing items](/apis/binder#list-missing-items) for the full `BinderMissingItemsFilter` argument. The assessment returns `PENDING`, `AVAILABLE`, or `UNAVAILABLE`, an optional reason, a count, and the filtered items. Treat an empty list as an all-clear only when the status is `AVAILABLE`. ```graphql theme={null} type BinderMissingItemsAssessment { status: BinderMissingItemsAssessmentStatus! reason: String count: Int! items: [BinderMissingItem!]! } enum BinderMissingItemsAssessmentStatus { PENDING AVAILABLE UNAVAILABLE } ``` 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 status filter. The filtered missing-document checklist rows. ```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 item formType issuer taxYear severity reason status category } } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "filter": { "status": "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": "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 item formType issuer taxYear severity reason status category } } } } } } } }", "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", "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" } ] } } } ] } } } } ``` ## 3. Ignore or restore a missing item When a reviewer dismisses a missing item, move it from `OPEN` to `IGNORED` with [`ignoreBinderMissingItem`](/apis/binder#ignore-a-missing-item). When they change their mind, move it back to `OPEN` with [`restoreBinderMissingItem`](/apis/binder#restore-a-missing-item). Both mutations take the missing-item `id` (from step 2) and a `workspaceId: String!`, and return the updated `BinderMissingItem` with its new `status`. Unlike the binder read fields, these two mutations take the `workspaceId` explicitly. Use the same workspace ID the `workspaceToken` was issued for. ```graphql theme={null} mutation IgnoreBinderMissingItem($id: ID!, $workspaceId: String!) { ignoreBinderMissingItem(id: $id, workspaceId: $workspaceId) { id status } } ``` ```json theme={null} { "id": "018f9c2c-5d6e-7f20-9a33-7c4d5e6f7090", "workspaceId": "019f0fb6-3001-7900-b7bc-0d11288504b1" } ``` ```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" } } } ``` To undo, call `restoreBinderMissingItem` with the same `id` and `workspaceId`; the item's `status` returns to `OPEN`. ```graphql theme={null} mutation RestoreBinderMissingItem($id: ID!, $workspaceId: String!) { restoreBinderMissingItem(id: $id, workspaceId: $workspaceId) { id 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 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 either mutation, update or refetch the [List missing items](/apis/binder#list-missing-items) assessment so its count and items stay aligned. The web app updates both values in one optimistic cache patch. ## 4. 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. Pass the `clientId`, the `query` string, and an optional `limit` (defaults to `20`). See [Search the binder](/apis/binder#search-the-binder) for the full `BinderSearchResults` shape. The web app debounces the input and requires at least two characters before firing the query; mirror that to avoid noisy partial queries. ```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 } } } } } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "query": "W-2", "limit": 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": "W-2", "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": "TYPE", "snippet": "W-2", "subdocument": { "id": "018f9c2a-9c1e-7c3d-9a4e-2f6b1c8d0e6a", "fileName": "W-2-Initech.pdf", "type": "W-2", "issuer": "Initech", "category": "income", "pageRange": [1] } } ], "annotations": [ { "matchedField": "BODY", "snippet": "W-2 from Initech looks correct", "isReply": false, "message": { "id": "018f9c2d-1a2b-7c3d-9a4e-2f6b1c8d0e7a", "type": "annotation", "subDocumentPath": "018f9c2a-9c1e-7c3d-9a4e-2f6b1c8d0e6a", "pageNumber": 1, "content": { "body": "W-2 from Initech looks correct" }, "createdBy": "019f0fb6-37b1-7800-b7bc-0d11288504b1", "createdAt": "2026-07-04T16:20:00.000Z" } } ], "marks": [], "contents": [ { "snippet": "Wages: 52,000.00", "subdocument": { "id": "018f9c2a-9c1e-7c3d-9a4e-2f6b1c8d0e6a", "fileName": "W-2-Initech.pdf", "type": "W-2", "issuer": "Initech", "category": "income", "pageRange": [1], "canonicalPath": "clients/018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c/source_docs/018f9c2a-9c1e-7c3d-9a4e-2f6b1c8d0e6a/canonical.json" }, "pages": [ { "pageNumber": 1, "fieldMatches": [ { "fieldName": "wages", "value": "52000.00", "bbox": { "xMin": 88, "yMin": 412, "xMax": 220, "yMax": 428, "pageNumber": 1 } } ] } ] } ] } } } ] } } } } ``` ## Next steps From here you can go deeper on the binder's related surfaces: * **Annotations and sign-offs**: when search surfaces an annotation you want to reply to, or you want to leave a new note on a file, use the [Document messages API](/apis/document-messages#annotations). Sign-offs on a leadsheet sheet or row use the same `createDocumentMessage` mutation with `type: "activity"`, `markType: "signoff"`; see [Sign off on a sheet or row](/apis/leadsheets#sign-off-on-a-sheet-or-row). * **Leadsheets and review**: the binder also carries a `leadsheets` field that returns the leadsheets tree for a `TAX_PREP` or `TAX_REVIEW` run. See [Read a client's leadsheets](/apis/leadsheets#read-a-clients-leadsheets) for that flow. * **Badge counts**: for a quick badge without fetching the full missing-items list, read `binder.messageCounts` via [Read message counts](/apis/binder#read-message-counts). * **Full reference**: every binder field, type, and mutation is documented on [Binder](/apis/binder). # Generate a workpaper bundle Source: https://docs.apps.filed.com/guides/recipes/generate-a-workpaper-bundle Queue a server-side workpaper render, poll it to completion, download the bundle, and optionally save an edited workbook or list templates Once a client's binder has been [browsed](/guides/recipes/browse-the-binder) and the team is ready to package the work, the next step is to assemble a **workpaper bundle**: a single downloadable archive that combines the binder's PDF, leadsheets, forms, checklist, and source documents in the order the firm wants. The bundle is rendered server-side from the binder, so you queue a render job and poll it until the download URL is ready. This recipe walks through the real call sequence an integration uses to generate, poll, and download a workpaper bundle, and the two optional operations around it. Every operation here is documented on the [Workpapers](/apis/workpapers) reference page; this recipe sequences them rather than re-documenting the types. Everything uses a **`workspaceToken`** (see [Authentication](/guides/authentication)) and goes to the single GraphQL endpoint: ``` https://router.apps.filed.com/graphql ``` The bundle render does **not** use the [Tasks](/apis/tasks) polling pattern. It has its own `workflowExecutionId` and its own `WorkpaperRenderStatus` enum (`RUNNING`, `COMPLETED`, `FAILED`), reached through `me { ... on WorkspaceUser { workspace { checkWorkpaperGenerationStatus(...) } } }`. Do not conflate it with `TaskStatus`. The optional [`triggerWorkpaperTranslate`](#optional-trigger-a-translation-task) flow at the end of this recipe is the one that does use the `Task` pattern. ```mermaid theme={null} flowchart LR A["Binder
(subdocument IDs)"] --> B["1. Start a render
(generateWorkpaperBundle)"] B --> C["2. Poll until done
(checkWorkpaperGenerationStatus)"] C -->|"COMPLETED"| D["3. Download
downloadUrl.url"] C -->|"FAILED"| E["Read errorMessage"] D --> F["Optional:
saveTaxWorkpaperXlsx /
workpaperTemplates"] ``` ## 1. Start a render Call [`generateWorkpaperBundle`](/apis/workpapers#generate-a-workpaper-bundle) with the client's `binderId`, the `orderedGroups` outline that drives the bundle's section order, and the flags for which sections to include. The mutation returns a `WorkpaperRenderJob` carrying the `workflowExecutionId` you poll in step 2. ```graphql theme={null} mutation GenerateWorkpaperBundle($input: GenerateWorkpaperBundleInput!) { generateWorkpaperBundle(input: $input) { workflowExecutionId status } } ``` ```json theme={null} { "input": { "binderId": "018f9c2a-4b6f-7a10-b2c4-9e8d7f6a5b4d", "orderedGroups": [ { "bucketLabel": "Income", "categories": [ { "category": "1099s", "subdocumentIds": [ "018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a", "018f9c2a-7b3d-7c3d-9a4e-2f6b1c8d2f7b" ] } ] }, { "bucketLabel": "Deductions", "categories": [ { "category": "Receipts", "subdocumentIds": [ "018f9c2a-7c4d-7c3d-9a4e-2f6b1c8d3f8c" ] } ] } ], "clientName": "Jane Taxpayer", "includePdf": true, "includeLeadsheets": true, "leadsheetsFormat": "EXCEL", "includeForms": true, "includeChecklist": true, "includeSourceDocs": false } } ``` The `subdocumentIds` in `orderedGroups` are the `id` values from [`binder.subdocuments`](/apis/binder#list-the-files-in-a-binder). Run the [browse the binder](/guides/recipes/browse-the-binder#1-list-the-files-in-the-binder) recipe first to collect them, then group them into the buckets and categories that match the firm's preferred workpaper outline. ```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-7b3d-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" } } } ``` ## 2. Poll until the render completes Poll [`checkWorkpaperGenerationStatus`](/apis/workpapers#check-a-bundle-render-status) with the `workflowExecutionId` from step 1 until `status` is `COMPLETED` (then read `downloadUrl.url`) or `FAILED` (then read `errorMessage`). The web app polls every 1500ms with a hard timeout of 5 minutes; mirror that pattern and treat a missing `downloadUrl` on `COMPLETED` as a failure. ```graphql theme={null} query CheckWorkpaperGenerationStatus($workflowExecutionId: ID!) { me { ... on WorkspaceUser { id workspace { id checkWorkpaperGenerationStatus(workflowExecutionId: $workflowExecutionId) { workflowExecutionId status downloadUrl { filePath url } generatedAt errorMessage } } } } } ``` ```json theme={null} { "workflowExecutionId": "018f9c2c-3d4e-7f50-9b66-7f7082901234" } ``` 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. A workpaper render is not a `Task` and cannot be read through `tasks(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 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 } } } } } ``` A `RUNNING` response (poll again) looks like: ```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": "RUNNING", "downloadUrl": null, "generatedAt": null, "errorMessage": null } } } } } ``` And a `FAILED` response (stop polling and surface the error) looks like: ```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": "FAILED", "downloadUrl": null, "generatedAt": null, "errorMessage": "Subdocument 018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a is no longer in the binder." } } } } } ``` ## 3. Download the bundle Once `status` is `COMPLETED`, fetch `downloadUrl.url` (or hand it to the browser to download). The `url` is a signed link with a limited lifetime, so download it promptly. `downloadUrl.filePath` shows where the bundle is stored server-side and is useful for support tickets but is not a fetch target itself. ```bash theme={null} curl -L -o jane-taxpayer-workpapers.zip \ "https://signed.example.com/workpaper-bundles/018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c/2026-07-05.zip?token=..." ``` The web app opens `downloadUrl.url` in a new tab to trigger the browser's download prompt; either approach works. ## Optional: Save an edited workpaper xlsx If your integration edits the client's `tax_workpaper.xlsx` (for example an in-browser workbook editor), commit each edit back with [`saveTaxWorkpaperXlsx`](/apis/workpapers#save-a-tax-workpaper-xlsx). It takes the full xlsx workbook base64-encoded plus a short `summary` (typically the cell coordinate that changed) and returns a `ClientCommit` describing the new git commit in the client's file store. ```graphql theme={null} mutation SaveTaxWorkpaperXlsx($input: SaveTaxWorkpaperXlsxInput!) { saveTaxWorkpaperXlsx(input: $input) { sha shortSha message committedAt } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "xlsxBase64": "UEsDBBQACAgIAAAAAAAAAAAAAAAAAAAAAAA=", "summary": "Trial Balance!B12" } } ``` ```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 } }", "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" } } } ``` ## Optional: Trigger a translation task When you want a fresh workpaper produced for a return type from a prior run's output, call [`triggerWorkpaperTranslate`](/apis/workpapers#trigger-a-workpaper-translation). Unlike the bundle render, this is a real background [Task](/apis/tasks): it returns a `TriggerTaskResult { taskId }` and you poll `me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { tasks(type: ..., limit: 1) { id status } } } } }` until `status` is `COMPLETED` or `FAILED`. ```graphql theme={null} mutation TriggerWorkpaperTranslate($input: TriggerWorkpaperTranslateInput!) { triggerWorkpaperTranslate(input: $input) { taskId } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "returnType": "F1120", "templateName": "default" } } ``` ```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 the available template names for a return type with [`workpaperTemplates`](/apis/workpapers#list-workpaper-templates): ```graphql theme={null} query WorkpaperTemplates($returnType: ReturnType!) { me { ... on WorkspaceUser { id workspace { id workpaperTemplates(returnType: $returnType) } } } } ``` ```json theme={null} { "returnType": "F1120" } ``` ```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"] } } } } ``` As of this writing, `triggerWorkpaperTranslate` and `workpaperTemplates` are not yet wired into the web app's surface code. They exist in the live schema and are documented here for completeness, but verify the behavior end to end against your own workspace before relying on a specific shape. ## Next steps From here you can go deeper on the related surfaces: * **Browse the binder first**: the `subdocumentIds` that drive `orderedGroups` come from `binder.subdocuments`. See [Browse a client's binder](/guides/recipes/browse-the-binder#1-list-the-files-in-the-binder). * **Leadsheets export format**: when `includeLeadsheets` is `true`, set `leadsheetsFormat` to `EXCEL` or `CSV` (see [Exporting leadsheets](/apis/leadsheets#exporting-leadsheets)). * **Task polling (for the translate flow)**: the optional `triggerWorkpaperTranslate` flow returns a `taskId` you poll through the standard `clients.tasks` pattern. See [Tasks](/apis/tasks). * **Full reference**: every workpaper mutation, type, and `Workspace` field is documented on [Workpapers](/apis/workpapers). # Onboard a client and ingest documents Source: https://docs.apps.filed.com/guides/recipes/onboard-a-client Create a new client with initial documents, or add documents to an existing client, and track binder ingestion Most workflows in Filed start the same way: a client exists, documents are attached to that client, and Filed ingests them into the client's binder as a background task. This recipe covers the two most common onboarding paths using operations already documented on the [Clients](/apis/clients) reference page: * **New client with initial documents**: upload files, then create the client with those upload IDs in one call. * **Add documents to an existing client**: upload files, then attach them to a client that already exists. Both paths end with the same step: poll the binder ingestion [task](/apis/tasks) until it completes. Everything here uses a **`workspaceToken`** (see [Authentication](/guides/authentication)) and goes to the single GraphQL endpoint: ``` https://router.apps.filed.com/graphql ``` ```mermaid theme={null} flowchart LR A["Your files"] -->|"upload"| B["Upload IDs"] B -->|new client| C["createClient
(input.uploadIds)"] B -->|existing client| D["addClientDocuments
(input.uploadIds)"] C -->|"returns taskId"| E["Binder ingestion task"] D -->|"returns taskId"| E E -->|poll| F["COMPLETED"] ``` ## 1. Upload the files Filed does not accept file bytes over GraphQL. Upload each file to the upload endpoint first and collect the **upload IDs** it returns. Both the resumable (tus) and direct upload paths produce the same kind of upload ID, and both attach to a client the same way. See [Uploading documents](/guides/uploading-documents) for the full upload contract. At the end of this step you have one upload ID per file, for example `018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a`. ## 2. New client with initial documents Create the client and pass the upload IDs in the same call. [`createClient`](/apis/clients#create-a-client) takes a `CreateClientInput` with the client's `name`, `externalId`, `returnType`, `taxYear`, and an optional `uploadIds` list. When `uploadIds` is provided, Filed creates the client and immediately starts a binder ingestion task for those documents. ```graphql theme={null} mutation CreateClient($input: CreateClientInput!) { createClient(input: $input) { client { id name externalId status returnType taxYear } taskId } } ``` ```json theme={null} { "input": { "name": "Jane Taxpayer", "externalId": "PMS-10432", "returnType": "F1040", "taxYear": 2025, "uploadIds": ["018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"] } } ``` `CreateClientResult` returns the created `client` (read its `id` to use in later steps) and a `taskId` for the binder ingestion task. `taskId` is `null` when no `uploadIds` were supplied, so guard for that before polling. ```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" } } } ``` Save the `client.id` (you need it for later workflows such as [tax prep](/apis/tax-prep)) and the `taskId` (for the polling step below). ## 3. Add documents to an existing client When the client already exists, attach new uploads with [`addClientDocuments`](/apis/clients#add-documents-to-a-client). It takes an `AddClientDocumentsInput` with the `clientId` and one or more `uploadIds`, and returns a `taskId` for the binder ingestion task. ```graphql theme={null} mutation AddClientDocuments($input: AddClientDocumentsInput!) { addClientDocuments(input: $input) { taskId } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "uploadIds": ["018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"] } } ``` ```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" } } } ``` `addClientDocuments` always starts a fresh ingestion task for the supplied uploads. To retry a prior failed ingestion without re-uploading the files, use [`retriggerIngestion`](#re-run-ingestion-without-re-uploading) instead. ### Re-run ingestion without re-uploading When a client's binder ingestion task finished with `status: FAILED` and the original files have not changed, you do not need to upload them again. Call [`retriggerIngestion`](/apis/clients#re-run-binder-ingestion) with just the `clientId` and Filed re-runs ingestion over the documents already attached to the client. ```graphql theme={null} mutation RetriggerIngestion($input: RetriggerIngestionInput!) { retriggerIngestion(input: $input) { taskId } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" } } ``` ```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" } } } ``` It returns a new `taskId` for the binder ingestion task, which you poll with the same pattern as `addClientDocuments` (see [step 4](#4-poll-the-binder-ingestion-task)). Use `retriggerIngestion` when the files have not changed and you only need to retry the failed run. Use `addClientDocuments` with fresh upload IDs when the files themselves have changed, since `retriggerIngestion` only re-processes what is already attached to the client. ## 4. Poll the binder ingestion task Both paths return a `taskId` for a `BINDER` task. Follow it to completion with the [tasks API](/apis/tasks#check-a-single-tasks-status): list the client's tasks narrowed to `type: BINDER`, read the entry whose `id` matches, and poll on an interval until `status` is no longer `RUNNING`. ```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 } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "type": "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": "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" } ] } ] } ] } } } } ``` `COMPLETED` means the documents are filed in the binder. `FAILED` means it did not, and `errorMessage` explains why. `subTasks` show which stage is currently running, which is useful for progress UI while you poll. ## Next steps Once ingestion is `COMPLETED`, the client's binder is ready. From here you can: * Add more documents any time with [`addClientDocuments`](/apis/clients#add-documents-to-a-client). * List and fetch the client through the [Clients API](/apis/clients). * Follow any other background work with the [tasks API](/apis/tasks). # Review a return and sign off Source: https://docs.apps.filed.com/guides/recipes/review-and-sign-off Read a client's leadsheets and review items, then record reviewer sign-offs with the real document-message write surface After a tax prep run completes, the work moves to review: read the leadsheets the run produced, walk the issues it flagged, and record sign-offs on the sheets and rows you have reviewed. This recipe is the minimal call sequence for that flow against one client and one completed tax prep task. Every call uses a **`workspaceToken`** (see [Authentication](/guides/authentication)) and goes to the single GraphQL endpoint: ``` https://router.apps.filed.com/graphql ``` ```mermaid theme={null} flowchart LR A["Completed
TAX_PREP task"] -->|"leadsheets(taskId)"| B["Leadsheets tree"] B --> C["Sheets + issues
+ rows + traces"] C --> D{"Reviewer
signs off?"} D -->|yes| E["createDocumentMessage
type=activity, markType=signoff"] D -->|undo| F["hideDocumentMessage
(id of sign-off)"] E --> G["Refetch leadsheets
resolved/issueCount recompute"] F --> G G --> H["Done"] ``` This recipe does not re-document the types it touches. For the full `Leadsheets` / `Leadsheet` / `LeadsheetSheetIssue` / `LeadsheetFieldRow` / `LeadsheetTrace` field list, and for the `DocumentMessage` annotation and thread APIs, see [Leadsheets and review](/apis/leadsheets). For how to start and poll the `TAX_PREP` task whose `taskId` you feed into this flow, see [Run tax prep end to end](/guides/recipes/run-tax-prep) and [Tasks](/apis/tasks). ## 1. Read the leadsheets for a completed run A leadsheets tree is the output of a `TAX_PREP` (or `TAX_REVIEW`) background task. Pass that task's `taskId` to `binder.leadsheets(taskId:)` to read the exact tree that run produced. The query below mirrors the web app's `GetBinderLeadsheets` document: it walks sheets, their issues and per-severity counts, and the field rows with their traces and sources. ```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 } issues { id markType anchorPoint body createdBy createdAt hiddenAt resolved } fields { id rows { id fieldPath value trace { reasoning sources { subdocId label amount page } } } } } } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "taskId": "018f9c2b-7c4d-7e10-9a22-6b3c4d5e6f70" } ``` ```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 } issues { id markType anchorPoint body createdBy createdAt hiddenAt resolved } fields { id rows { id fieldPath value trace { reasoning sources { subdocId label amount page } } } } } } } } } } } } }", "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 }, "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, "resolved": false } ], "fields": [ { "id": "leadsheets/schedule_b/0/interest_income", "rows": [ { "id": "leadsheets/schedule_b/0/interest_income/0", "fieldPath": "interest_income.total", "value": "428.00", "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 }, { "subdocId": "018f9c2a-7c3d-7c3d-9a4e-2f6b1c8d2f7b", "label": "1099-INT from Globex", "amount": "176.00", "page": 1 }, { "subdocId": "018f9c2a-8e2f-7c3d-9a4e-2f6b1c8d3f8c", "label": "1099-INT from Initech", "amount": "42.00", "page": 1 } ] } } ] } ] } ] } } } ] } } } } ``` The shape above matches what the web app's binder and review screens read. Use `Leadsheets.issueCount` for a quick "needs attention" number, drill into `Leadsheet.issueCount` for per-severity counts, then page through `LeadsheetSheetIssue` for per-issue detail. `resolved` is server-computed from the document messages on the sheet, so refresh the query after every sign-off or undo (see step 4). 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. See [Leadsheets and review](/apis/leadsheets#read-a-clients-leadsheets) for the full field list. ## 2. Record a sign-off 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`](/apis/leadsheets#sign-off-on-a-sheet-or-row) 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. A sign-off is a `createDocumentMessage` call anchored to the subdocument path you are signing off on. The `anchorPoint` carries the reviewer's `level` and `user_role` so the UI renders 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 } } ``` ```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 step 3 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 `LeadsheetFieldRow.id` 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. ## 3. 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` 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" } } } ``` ## 4. Refetch the leadsheets query `LeadsheetSheetIssue.resolved` and `Leadsheet.issueCount` (and the per-severity `Leadsheet.issueCount { critical high medium low }` breakdown) are server-computed from the document messages on the sheet. After any sign-off or undo, refetch the `GetClientLeadsheets` query from step 1 so the server recomputes them. The web app does exactly this: it refetches the leadsheets query (and the missing-items query) whenever the review task ends or a sign-off is toggled. ```graphql theme={null} # Same query as step 1. Re-run it with the same { clientId, taskId }. query GetClientLeadsheets($clientId: ID!, $taskId: ID) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { binder { leadsheets(taskId: $taskId) { id issueCount sheets { id formName issueCount { critical high medium low } issues { id resolved } signOffs { id markType hiddenAt } } } } } } } } } ``` After the refetch, the issue you signed off on shows `resolved: true`, the sheet's `issueCount` drops by one, and the `Leadsheets.issueCount` total reflects the new state. If you undo a sign-off, the same refetch backs the issue out again. ## See also * [Leadsheets and review](/apis/leadsheets) for the full `Leadsheets`, `Leadsheet`, `LeadsheetSheetIssue`, `LeadsheetFieldRow`, and `LeadsheetTrace` type definitions, plus the `DocumentMessage` annotation and thread APIs. * [Document messages](/apis/document-messages#the-documentmessage-type) for the broader `DocumentMessage` API (annotations, flags, threads, update/unhide) that sign-offs are one use of. * [Tasks](/apis/tasks) for the polling mechanics behind the `TAX_PREP` task whose `taskId` you feed into `leadsheets(taskId:)`. * [Run tax prep end to end](/guides/recipes/run-tax-prep) for the recipe that produces the review items this flow consumes. # Run tax planning / advisory Source: https://docs.apps.filed.com/guides/recipes/run-tax-planning Start a tax advisor run for a client, poll it to completion, read the plan, 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` recommendations, each with evidence, an implementation plan, an optional savings estimate, and a `status` you can drive. It runs as a background task, so the end-to-end sequence is: start the run, poll the task until it finishes, read the plan, then accept or dismiss each strategy. This recipe is the minimal call sequence to do that for one client. Every call uses a **`workspaceToken`** (see [Authentication](/guides/authentication)) and goes to the single GraphQL endpoint: ``` https://router.apps.filed.com/graphql ``` ```mermaid theme={null} flowchart LR A["Client with
ingested binder"] -->|"initiateTaxAdvisor"| B["TAX_ADVISOR task"] A2["Documents
already ingested"] -->|"triggerTaxAdvisor"| B B -->|"poll tasks(type: TAX_ADVISOR)"| C{"status"} C -->|RUNNING| B C -->|COMPLETED| D["advisorPlan"] D -->|strategies| E["Each AdvisorStrategy"] E -->|"setAdvisorStrategyStatus"| F["PROPOSED / SELECTED / DISMISSED"] ``` This recipe does not re-document the types it touches. For the full `AdvisorPlan` and `AdvisorStrategy` field lists, the second trigger path, the task result member, and the `SavingsHorizon` enum, see [Tax planning](/apis/planning). For the polling mechanics, see [Tasks](/apis/tasks). ## 1. Start the run There are two trigger mutations for an advisor run, both requiring a **`workspaceToken`** and both returning a `taskId` you poll as a `TAX_ADVISOR` task. Use the one that matches how you stage documents. The Filed web app's `/planning` route uses [`initiateTaxAdvisor`](/apis/planning#trigger-via-initiatetaxadvisor) because the in-app flow stages fresh uploads at the same moment it kicks off the run. Recommend it as the primary path: upload files first (see [Uploading documents](/guides/uploading-documents)) to get `uploadIds`, then call the mutation. ```graphql theme={null} mutation InitiateTaxAdvisor($input: InitiateTaxAdvisorInput!) { initiateTaxAdvisor(input: $input) { taskId } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "uploadIds": ["018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"] } } ``` ```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" } } } ``` `InitiateTaxAdvisorResult.taskId` is nullable. A `null` value means the ingestion accepted the upload but did not start a task; treat it as a soft error and retry. See [Tax planning, trigger via initiateTaxAdvisor](/apis/planning#trigger-via-initiatetaxadvisor). If the client's binder is already populated and you do not need to stage fresh uploads, use [`triggerTaxAdvisor(input: { clientId, returnType, taxYear })`](/apis/planning#trigger-via-triggertaxadvisor) instead. It returns a non-null `TriggerTaskResult.taskId`. To scope which workspace and user skills apply to this run, pass `skills: { workspace: [...], user: [...] }` (`RunSkillSelectionInput`) on either mutation; omit it to apply all active skills. Save the `taskId`. You will use it to find the task in the poll step. ## 2. 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 } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" } ``` ```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. ## 3. 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#the-client-type)). Call it without a `runId` to read the client's current plan, or pass the `runId` from the completed task to read that run's plan. This recipe passes `runId` to pin the read to the run you just polled. ```graphql theme={null} query ClientAdvisorPlan($clientId: ID!, $runId: ID) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { id advisorPlan(runId: $runId) { runId globalSummary strategies { id domain title summary estimatedSavingsCents savingsHorizon status } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "runId": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080" } ``` ```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!, $runId: ID) { me { ... on WorkspaceUser { workspace { clients(filters: { ids: [$clientId] }) { id advisorPlan(runId: $runId) { runId globalSummary strategies { id domain title summary estimatedSavingsCents savingsHorizon status } } } } } } }", "variables": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "runId": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080" } }' ``` ```json theme={null} { "data": { "me": { "workspace": { "clients": [ { "id": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "advisorPlan": { "runId": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080", "globalSummary": "5 strategies surfaced across retirement, income shifting, and entity selection. Estimated 3-year savings of $18,400.", "strategies": [ { "id": "019a1b2c-3d4e-7f10-aa12-1c2d3e4f5060", "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.", "estimatedSavingsCents": 82000, "savingsHorizon": "CURRENT_YEAR", "status": "PROPOSED" }, { "id": "019a1b2c-3d4e-7f10-aa12-1c2d3e4f5061", "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.", "estimatedSavingsCents": null, "savingsHorizon": "MULTI_YEAR", "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. For the full `AdvisorPlan` field list (including `taxYear`, `returnType`, `byDomain`, `bySavingsHorizon`, `estimatedSavingsCentsByHorizon`, and `skillsApplied`), see [Tax planning, read the plan](/apis/planning#read-the-plan). ## 4. Update a strategy's status Drive each strategy's workflow status with [`setAdvisorStrategyStatus`](/apis/planning#update-a-strategys-status). It requires a **`workspaceToken`** and identifies the strategy with `clientId` plus the strategy's `domain` and `strategyId` (both from `AdvisorStrategy`), plus the plan's `runId` to pin the change to the right run. The real enum values are `PROPOSED`, `SELECTED`, and `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. There is no `ACCEPTED` value; "accept" maps to `SELECTED`. ```graphql theme={null} mutation SetAdvisorStrategyStatus($input: SetAdvisorStrategyStatusInput!) { setAdvisorStrategyStatus(input: $input) { id status } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "domain": "charitable", "strategyId": "accelerate_charitable_contributions", "runId": "018f9c2c-4a1b-7e20-8b33-7c4d5e6f7080", "status": "SELECTED" } } ``` ```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` (step 3) to get the refreshed `strategies[].status` values. The Filed web app does this by including `ClientAdvisorPlan` in the mutation's `refetchQueries`. See [Tax planning, update a strategy's status](/apis/planning#update-a-strategys-status). ## Next steps Once you have accepted (`SELECTED`) the strategies you want to act on, the natural next steps are to work through each strategy's `implementationPlan` and to re-run tax prep so the accepted planning changes flow into the prepared return. See [Run tax prep end to end](/guides/recipes/run-tax-prep) for that recipe, and [Tax planning](/apis/planning) for the full reference. # Run tax prep end to end Source: https://docs.apps.filed.com/guides/recipes/run-tax-prep Start a tax prep run for a client, poll it to completion, and read the review items it produces Tax prep extracts forms from a client's binder, reconciles them, optionally enters them into tax software, and produces a list of review items. It runs as a background task, so the end-to-end sequence is: start the run, poll the task until it finishes, then read the result. This recipe is the minimal call sequence to do that for one client. Every call uses a **`workspaceToken`** (see [Authentication](/guides/authentication)) and goes to the single GraphQL endpoint: ``` https://router.apps.filed.com/graphql ``` ```mermaid theme={null} flowchart LR A["Client with
ingested binder"] -->|"triggerTaxPrep"| B["TAX_PREP task"] B -->|"poll tasks(type: TAX_PREP)"| C{"status"} C -->|RUNNING| B C -->|COMPLETED| D["TaskTaxPrepResult"] D -->|reviewItemCount| E["Quick check"] D -->|reviewItems| F["Per-item detail"] F -->|"next"| G["Review and sign off"] ``` This recipe does not re-document the types it touches. For the full input shape, the backoffice re-trigger/status operations, and the complete `TaskTaxPrepResult` field list, see [Tax prep](/apis/tax-prep). For the polling mechanics, see [Tasks](/apis/tasks). ## 1. Start the run Start a tax prep run with [`triggerTaxPrep`](/apis/tax-prep#start-a-tax-prep-run). The only required fields are the client's `clientId` and the `returnType` you want to prepare (it must match the client's `returnType`, see [Clients](/apis/clients#the-client-type)). The mutation returns a `taskId` you poll in the next step. ```graphql theme={null} mutation TriggerTaxPrep($input: TriggerTaxPrepInput!) { triggerTaxPrep(input: $input) { taskId } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "returnType": "F1040" } } ``` ```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" } } }' ``` ```json theme={null} { "data": { "triggerTaxPrep": { "taskId": "018f9c2b-7c4d-7e10-9a22-6b3c4d5e6f70" } } } ``` Save the `taskId`. You will use it to find the task in the poll step. To scope which workspace and user skills apply to this run, pass `skills: { workspace: [...], user: [...] }` (`RunSkillSelectionInput`). Omit it to apply all active skills. See [Tax prep, start a run](/apis/tax-prep#start-a-tax-prep-run) for the full input. ## 2. 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 } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" } ``` ```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. ## 3. 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. 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`, and branching on `__typename` keeps your client from throwing on the unexpected member. ```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 } } } } } } } } ``` ```json theme={null} { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c" } ``` ```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." } ] } } ] } ] } } } } ``` ## 4. Use the review items Two reads cover most needs: * **Quick check**: read `reviewItemCount` for a single "how much needs attention" number before paging through the items. * **Per-item detail**: read `reviewItems`, each with `severity`, `category`, and `description`. `severity` is a free-form `String!` (not an enum), so sort and group it in your client as you see fit. `TaskTaxPrepResult` is one member of the `TaskResult` union. The other tax members are `TaskTaxReviewResult` (returned for `TAX_REVIEW` tasks) 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. ## Next steps Once review items exist, the natural next step is to work through them and record sign-offs on the sheets and rows that produced each item. See [Review and sign off](/guides/recipes/review-and-sign-off) for that recipe. # Uploading documents Source: https://docs.apps.filed.com/guides/uploading-documents Upload a file (resumable or direct), get an upload ID, and attach it to a client's binder Filed does not accept file bytes over GraphQL. Instead, you upload each file to an **upload endpoint**, which returns an **upload ID**. You then pass those upload IDs to a GraphQL mutation: [`createClient`](/apis/clients#create-a-client) to create a client with initial documents, or [`addClientDocuments`](/apis/clients#add-documents-to-a-client) to add documents to an existing client. Filed ingests the staged files into the client's binder as a background [task](/apis/tasks). There are two ways to upload. Both return an upload ID that you attach the same way: * **Resumable upload (recommended)** uses the [tus](https://tus.io) protocol. It survives network drops and handles large files by chunking, so it is the right default for production integrations. * **Direct upload** is a single `POST` for small files, when you do not need resumability. ```mermaid theme={null} flowchart LR A["Your file"] -->|"resumable (tus) or direct POST"| B["Upload ID"] B -->|"uploadIds"| C["createClient /
addClientDocuments"] C -->|"returns taskId"| D["Binder ingestion task"] ``` ## Endpoints | Purpose | Endpoint | | ---------------------- | ---------------------------------------------- | | Resumable upload (tus) | `https://web.apps.filed.com/api/uploads` | | Direct upload | `https://web.apps.filed.com/api/upload/direct` | | GraphQL | `https://router.apps.filed.com/graphql` | Uploads and GraphQL are on different hosts. Send file bytes to an upload endpoint, then send the returned upload IDs to the GraphQL endpoint. Both use the same `Authorization: Bearer YOUR_WORKSPACE_TOKEN` (see [Authentication](/guides/authentication)). ## Resumable upload (recommended) The resumable endpoint speaks tus 1.0.0. A minimal upload is two requests: a `POST` that creates the upload and returns its location, then a `PATCH` that sends the bytes. Any tus client library works; the raw requests are shown so you can see exactly what is on the wire. ### Create the upload ```http theme={null} POST /api/uploads HTTP/1.1 Host: web.apps.filed.com Authorization: Bearer YOUR_WORKSPACE_TOKEN Tus-Resumable: 1.0.0 Upload-Length: 20480 Upload-Metadata: filename dzJfMTA0MC5wZGY=,filetype YXBwbGljYXRpb24vcGRm,intent Y2xpZW50LWRvY3VtZW50 ``` The exact size of the file in bytes. Comma-separated `key base64(value)` pairs, per the tus protocol. Set `filename` and `filetype` (the MIME type). Optionally set `intent` to select the server's validation policy: use `client-document` for client files. If `intent` is omitted, a default policy applies. A successful create returns `201 Created` with a `Location` header. The **upload ID is the last path segment** of that location. ```http theme={null} HTTP/1.1 201 Created Location: https://web.apps.filed.com/api/uploads/018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a ``` ### Send the bytes `PATCH` the file to the location from the previous step. ```http theme={null} PATCH /api/uploads/018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a HTTP/1.1 Host: web.apps.filed.com Authorization: Bearer YOUR_WORKSPACE_TOKEN Tus-Resumable: 1.0.0 Upload-Offset: 0 Content-Type: application/offset+octet-stream ``` A successful `PATCH` returns `204 No Content`. Large files can be sent in multiple `PATCH` chunks, advancing `Upload-Offset` each time; the Filed web app uses 5 MB chunks. A per-file size cap is enforced by the server. ### Upload with a tus client In practice, use a tus client library rather than hand-writing the `POST` and `PATCH`. It handles chunking, retries, and resuming after a network drop, and exposes the finished upload's URL, whose last path segment is the **upload ID**. ```js tus-js-client (Node) theme={null} import { Upload } from "tus-js-client"; import fs from "node:fs"; const path = "w2_1040.pdf"; const size = fs.statSync(path).size; const upload = new Upload(fs.createReadStream(path), { endpoint: "https://web.apps.filed.com/api/uploads", uploadSize: size, chunkSize: 5 * 1024 * 1024, retryDelays: [0, 1000, 3000, 5000], headers: { Authorization: "Bearer YOUR_WORKSPACE_TOKEN" }, metadata: { filename: "w2_1040.pdf", filetype: "application/pdf", intent: "client-document", }, onError: (error) => { throw error; }, onSuccess: () => { const uploadId = upload.url.split("/").filter(Boolean).pop(); console.log("upload ID:", uploadId); }, }); upload.start(); ``` ```python tuspy (Python) theme={null} from tusclient import client tus = client.TusClient( "https://web.apps.filed.com/api/uploads", headers={"Authorization": "Bearer YOUR_WORKSPACE_TOKEN"}, ) uploader = tus.uploader( "w2_1040.pdf", chunk_size=5 * 1024 * 1024, metadata={ "filename": "w2_1040.pdf", "filetype": "application/pdf", "intent": "client-document", }, ) uploader.upload() upload_id = uploader.url.rstrip("/").split("/")[-1] print("upload ID:", upload_id) ``` `tus-js-client` also runs in the browser: pass a `File` object instead of a stream and omit `uploadSize`. Install the clients with `npm i tus-js-client` and `pip install tuspy`. ## Direct upload The direct upload endpoint is **in development and not yet available**. Use resumable uploads today. This section describes the intended shape; the exact request and response contract may change before release. For small files where you do not need resumability, send the file bytes in a single `POST` to `/api/upload/direct`. It returns the same kind of **upload ID** that a resumable upload produces, which you attach to a client the same way. ```http theme={null} POST /api/upload/direct HTTP/1.1 Host: web.apps.filed.com Authorization: Bearer YOUR_WORKSPACE_TOKEN Content-Type: application/pdf ``` The response returns the upload ID in its JSON body. Pass that ID in `uploadIds` exactly as you would a resumable upload ID (see [Attach the upload IDs](#attach-the-upload-ids) below). Because it is a single request with no chunking or resume, direct upload is best for small files. For large files or unreliable networks, prefer the resumable endpoint above. ## Attach the upload IDs Pass the collected upload IDs to a GraphQL mutation. To create a **new** client from the files, use [`createClient`](/apis/clients#create-a-client) with `uploadIds`. To add files to an **existing** client's binder, use [`addClientDocuments`](/apis/clients#add-documents-to-a-client): ```graphql theme={null} mutation AddClientDocuments($input: AddClientDocumentsInput!) { addClientDocuments(input: $input) { taskId } } ``` ```json theme={null} { "input": { "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c", "uploadIds": ["018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"] } } ``` The mutation returns a `taskId` for the **binder ingestion task**. Filed converts, classifies, and files each document into the client's binder in the background. Track it with the [tasks API](/apis/tasks): poll the task until its `status` is `COMPLETED`. ## Troubleshooting **`POST` returns `413` or the upload is rejected** * The file exceeds the size cap for its `intent`, or its type is not allowed by that intent's validation policy. Use `intent=client-document` for client files. **`PATCH` returns `409` or `404`** * The `Upload-Offset` does not match the server's current offset, or the upload ID has expired. Re-create the upload with a fresh `POST`. **`createClient` / `addClientDocuments` rejects an upload ID** * The upload was never completed (no successful `PATCH`), belongs to a different workspace, or has expired. Re-upload and use the new ID. # Introduction Source: https://docs.apps.filed.com/index Filed's developer portal Explore the following sections: Step-by-step guides for building with Filed. Reference documentation for the Filed API. # Add users to workspace Source: https://docs.apps.filed.com/legacy/apis/endpoint/add-users-to-workspace Add tax firm users to a workspace by email addresses Add tax firm users (preparers, staff, etc.) to their workspace by email addresses. Users are immediately added to the workspace and can sign in right away. ```graphql theme={null} mutation AddUsersToWorkspace($workspaceId: ID!, $userEmails: [String!]!) { addUsersToTheWorkspace(workspaceId: $workspaceId, userEmails: $userEmails) { id email name role } } ``` ## Arguments Workspace identifier (corresponds to a tax firm). Add users to the workspace that corresponds to their tax firm. Array of email addresses for the tax firm's users. You can add multiple users in a single request. ## Returns Array of user objects that were added to the workspace. User identifier. User email address. User display name. May be null if not set. User role in workspace (e.g., "user"). ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation AddUsersToWorkspace($workspaceId: ID!, $userEmails: [String!]!) { addUsersToTheWorkspace(workspaceId: $workspaceId, userEmails: $userEmails) { id email name role } }", "variables": { "workspaceId": "workspace_123456", "userEmails": [ "test-1@firm.filed.com", "test-2@firm.filed.com" ] } }' ``` ```json theme={null} { "data": { "addUsersToTheWorkspace": [ { "id": "user_789", "email": "test-1@firm.filed.com", "name": null, "role": "user" }, { "id": "user_790", "email": "test-2@firm.filed.com", "name": null, "role": "user" } ] } } ``` Users are immediately added to the workspace - no email invitations are sent. Users can sign in using their email address right away to access the workspace. If a user already exists, they will be added to the workspace without creating a duplicate account. ## Troubleshooting **Problem**: Tax firm users unable to access workspace after being added **Solutions**: * Verify email addresses are valid and correctly formatted * Check that users haven't already been added to the workspace * Ensure workspace ID corresponds to the correct tax firm * Verify you're adding users to the correct workspace for their tax firm * Confirm users can sign in using their email address - no invitation email is required * Ensure users are using a valid deep link with the workspace slug format `/w/{workspaceSlug}/` (recommended) or `/w/redirect/` (convenience option) # Collect provider connection Source: https://docs.apps.filed.com/legacy/apis/endpoint/collect-provider-connection Verify a provider connection and provide additional configuration Verify the provider connection by allowing Filed to test the credentials you provided. Filed will make an API call to your platform using the credentials from the start step. You can also provide additional configuration parameters during this step. **Implementation Details**: The specific implementation of what happens on Filed's side when this mutation is called, including how Filed verifies the connection and makes API calls to your platform, will be discussed and coordinated between your team and Filed's engineering team during the integration process. ```graphql theme={null} mutation CollectProviderConnection($connectionId: ID!, $inputs: JSON) { collectProviderConnection(connectionId: $connectionId, inputs: $inputs) { id workspaceId providerKey status displayName settings updatedAt } } ``` ## Arguments The connection ID returned from the [start provider connection](/legacy/apis/endpoint/start-provider-connection) mutation. Optional JSON object containing additional configuration. Common fields include: * `firmReferenceId`: Your internal reference ID for the tax firm (useful for correlation) * Any other parameters you want Filed to store for this tax firm * Configuration that helps Filed communicate with your platform ## Returns Connection identifier. Workspace identifier this connection belongs to. The partner key used for this connection. Connection status. Will be `active` if the verification succeeds, or an error state if it fails. Display name for the connection. Timestamp when the connection was last updated. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation CollectProviderConnection($connectionId: ID!, $inputs: JSON) { collectProviderConnection(connectionId: $connectionId, inputs: $inputs) { id workspaceId providerKey status displayName settings updatedAt } }", "variables": { "connectionId": "connection_789", "inputs": { "firmReferenceId": "firm-ref-12345", "additionalConfig": { "timezone": "America/New_York", "region": "US" } } } }' ``` ```json theme={null} { "data": { "collectProviderConnection": { "id": "connection_789", "workspaceId": "workspace_123456", "providerKey": "", "status": "active", "displayName": "Canopy Integration", "settings": { "firmReferenceId": "firm-ref-12345", "additionalConfig": { "timezone": "America/New_York", "region": "US" } }, "updatedAt": "2024-01-15T10:35:00Z" } } } ``` Filed will use the credentials from the [start provider connection](/legacy/apis/endpoint/start-provider-connection) step to make an API call to your platform. If the API call succeeds, Filed marks the connection as `active`. If it fails, the connection status will reflect the error state. ## Troubleshooting **Problem**: Connection status remains `pending` or shows an error after collect **Solutions**: * Verify the credentials provided in the start step are valid and have the necessary permissions * Ensure your platform's API is accessible and responding correctly * Check that the API endpoint URLs are correct * Verify the API keys and tokens haven't expired * Review Filed's API call logs (contact support if needed) **Problem**: `connectionId` not found **Solutions**: * Ensure you're using the `connectionId` from the start provider connection response * Check that you're using the correct workspace's connection ID # Create connection job Source: https://docs.apps.filed.com/legacy/apis/endpoint/create-connection-job Create an import job that can be triggered to process all clients attached to it Create a connection job to organize and batch process multiple tax preparations. A connection job groups related works (clients) together and can be triggered to process all attached clients. ```graphql theme={null} mutation CreateConnectionJob($connectionId: ID!, $jobName: String!) { createConnectionJob(connectionId: $connectionId, jobName: $jobName) { id connectionId workspaceId name status createdAt updatedAt } } ``` ## Arguments The ID of the provider connection. This identifies which integration connection to create the job for. A descriptive name for the connection job. Use this to identify the batch of tax preparations (e.g., "2024 Tax Season Import", "Q1 Client Batch"). ## Returns Connection job identifier. Save this for creating works and triggering the import. The provider connection ID this job belongs to. The workspace ID this job belongs to. The name you provided for this connection job. Current status of the connection job (e.g., "pending", "active", "completed"). Timestamp when the connection job was created. Timestamp when the connection job was last updated. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation CreateConnectionJob($connectionId: ID!, $jobName: String!) { createConnectionJob(connectionId: $connectionId, jobName: $jobName) { id connectionId workspaceId name status createdAt updatedAt } }", "variables": { "connectionId": "connection_789", "jobName": "2024 Tax Season Import" } }' ``` ```json theme={null} { "data": { "createConnectionJob": { "id": "job_123456", "connectionId": "connection_789", "workspaceId": "workspace_123456", "name": "2024 Tax Season Import", "status": "pending", "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T10:30:00Z" } } } ``` After creating a connection job, use [createJobWorks](/legacy/apis/endpoint/create-works) to attach clients (works) to the job. Once all clients and job work artifacts are attached, you can initiate the import using [initiateTaxPrepsImport](/legacy/apis/endpoint/initiate-tax-preps-import). ## Troubleshooting **Problem**: `createConnectionJob` fails with invalid connection ID **Solutions**: * Verify the `connectionId` exists and belongs to your workspace * Ensure the connection status is `active` * Check that you're using the correct connection ID for the workspace **Problem**: Job created but cannot attach works **Solutions**: * Ensure you're using the `id` from the `createConnectionJob` response as the `connectionJobId` when creating works * Verify the job status allows adding works * Check that the connection job belongs to the correct connection # Create job work artifacts Source: https://docs.apps.filed.com/legacy/apis/endpoint/create-work-artifacts Attach files to a job work Attach one or more files (artifacts) to a job work. Artifacts represent documents, forms, or other files associated with a tax preparation. You can create multiple artifacts in a single request. ```graphql theme={null} mutation CreateJobWorkArtifacts($jobWorkId: ID!, $inputs: [CreateJobWorkArtifactsInput!]!) { createJobWorkArtifacts(jobWorkId: $jobWorkId, inputs: $inputs) { id createdAt updatedAt workspaceId artifactId connectionJobId workArtifact { id externalId } } } ``` ## Arguments The ID of the job work returned from [createJobWorks](/legacy/apis/endpoint/create-works). This identifies which client/tax preparation to attach files to. Array of artifact inputs. Each input represents one file to attach to the job work. File name or descriptive identifier for the artifact (e.g., "W-2 Form", "1099-INT", "receipts.pdf"). Type or category of the artifact (e.g., "w2", "1099", "receipt", "document", "form"). Optional URL where the file can be accessed. This should be a publicly accessible URL or a URL that Filed can access using the connection credentials. Your platform's unique identifier for this file/artifact. Used to correlate the artifact with your system. Optional metadata about the artifact. Can include file size, mime type, upload date, or any other relevant information. ## Returns Job artifact identifier. Timestamp when the job artifact was created. Timestamp when the job artifact was last updated. The workspace ID this job artifact belongs to. The artifact ID this job artifact references. The connection job ID this job artifact belongs to. The work artifact associated with this job artifact. The unique identifier of the work artifact. The external identifier you provided when creating the artifact. This matches the `externalId` from your input. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation CreateJobWorkArtifacts($jobWorkId: ID!, $inputs: [CreateJobWorkArtifactsInput!]!) { createJobWorkArtifacts(jobWorkId: $jobWorkId, inputs: $inputs) { id createdAt updatedAt workspaceId artifactId connectionJobId workArtifact { id externalId } } }", "variables": { "jobWorkId": "job_work_789", "inputs": [ { "name": "W-2 Form 2024", "type": "w2", "url": "https://api.example.com/files/w2-2024.pdf", "externalId": "file-12345", "metadata": { "fileSize": 245678, "mimeType": "application/pdf", "uploadDate": "2024-01-10T08:00:00Z" } }, { "name": "1099-INT Statement", "type": "1099", "url": "https://api.example.com/files/1099-int.pdf", "externalId": "file-67890", "metadata": { "fileSize": 189234, "mimeType": "application/pdf" } } ] } }' ``` ```json theme={null} { "data": { "createJobWorkArtifacts": [ { "id": "job_artifact_456", "createdAt": "2024-01-15T10:40:00Z", "updatedAt": "2024-01-15T10:40:00Z", "workspaceId": "workspace_123456", "artifactId": "artifact_456", "connectionJobId": "job_123456", "workArtifact": { "id": "artifact_456", "externalId": "file-12345" } } ] } } ``` **File URL Accessibility**: Ensure the URLs you provide are accessible to Filed. If your files require authentication, Filed will use the connection credentials provided during the connection setup. Test that Filed can access the URLs using those credentials. The `url` field should point to a file that Filed can download. Filed will fetch the file from this URL when processing the work. Ensure the URL is valid and the file is accessible at the time of import. ## Troubleshooting **Problem**: Artifacts created but files cannot be accessed **Solutions**: * Verify the URLs are publicly accessible or accessible with the connection credentials * Test the URLs manually to ensure they return the expected files * Check that file URLs haven't expired or been moved * Ensure the connection credentials have permission to access the file URLs **Problem**: Invalid URL format **Solutions**: * Ensure URLs use valid protocols (https\:// or http\://) * Verify URLs are properly encoded if they contain special characters * Check that URLs point to actual files, not directories or API endpoints that require additional parameters # Create job works Source: https://docs.apps.filed.com/legacy/apis/endpoint/create-works Attach clients to an import job Attach one or more clients (works) to a connection job. Each work represents a tax preparation for a specific client. You can create multiple works in a single request. ```graphql theme={null} mutation CreateJobWorks($connectionJobId: ID!, $inputs: [CreateJobWorksInput!]!) { createJobWorks(connectionJobId: $connectionJobId, inputs: $inputs) { id createdAt updatedAt workspaceId workId connectionJobId } } ``` ## Arguments The ID of the connection job returned from [createConnectionJob](/legacy/apis/endpoint/create-connection-job). This groups the works together for batch processing. Array of work inputs. Each input represents one client/tax preparation to attach to the job. Client name or tax preparation identifier. This is typically the client's name or a descriptive identifier for the tax prep (e.g., "John Smith - 2024 Tax Return", "client-12345"). Your platform's unique identifier for this client or tax preparation. This external ID is used to correlate the work with your system and should be unique within your platform. ## Returns Job work identifier. Save this for creating job work artifacts (attaching files). Timestamp when the job work was created. Timestamp when the job work was last updated. The workspace ID this job work belongs to. The work ID this job work references. The connection job ID this job work belongs to. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation CreateJobWorks($connectionJobId: ID!, $inputs: [CreateJobWorksInput!]!) { createJobWorks(connectionJobId: $connectionJobId, inputs: $inputs) { id createdAt updatedAt workspaceId workId connectionJobId } }", "variables": { "connectionJobId": "job_123456", "inputs": [ { "name": "John Smith - 2024 Tax Return", "externalId": "client-12345" }, { "name": "Jane Doe - 2024 Tax Return", "externalId": "client-67890" } ] } }' ``` ```json theme={null} { "data": { "createJobWorks": [ { "id": "job_work_789", "createdAt": "2024-01-15T10:35:00Z", "updatedAt": "2024-01-15T10:35:00Z", "workspaceId": "workspace_123456", "workId": "work_789", "connectionJobId": "job_123456" }, { "id": "job_work_790", "createdAt": "2024-01-15T10:35:00Z", "updatedAt": "2024-01-15T10:35:00Z", "workspaceId": "workspace_123456", "workId": "work_790", "connectionJobId": "job_123456" } ] } } ``` After creating job works, use [createJobWorkArtifacts](/legacy/apis/endpoint/create-job-work-artifacts) to attach files to each job work. The `externalId` should match your platform's client identifier to enable correlation between Filed and your system. ## Troubleshooting **Problem**: `createJobWorks` fails with invalid connection job ID **Solutions**: * Verify the `connectionJobId` exists and belongs to your connection * Ensure you're using the `id` from the `createConnectionJob` response * Check that the connection job status allows adding works **Problem**: Duplicate external IDs **Solutions**: * Ensure each `externalId` is unique within your platform * If you need to create multiple works for the same client, use different external IDs (e.g., add a suffix or timestamp) * Verify you're not accidentally reusing external IDs from previous imports # Create workspace Source: https://docs.apps.filed.com/legacy/apis/endpoint/create-workspace Create a new workspace for a tax firm customer Each workspace represents one tax firm. Create a separate workspace for each tax firm customer you onboard. ```graphql theme={null} mutation CreateWorkspace($input: CreateWorkspaceInput!) { createWorkspace(input: $input) { id slug displayName status workspaceType } } ``` ## Arguments Input object for creating a workspace. Tax firm's display name. Additional metadata (e.g., customer ID, partner info). Tax firm size classification. Valid values: "xs", "sm", "md", "lg", "xl". ## Returns Workspace identifier. Save this for adding users and other operations. URL-friendly workspace identifier. Essential for creating reliable deep links that route customers to the correct workspace. Workspace display name. Workspace status (e.g., "active"). Type of workspace. Will be automatically set to "partner" for partner accounts. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation CreateWorkspace($input: CreateWorkspaceInput!) { createWorkspace(input: $input) { id slug displayName status workspaceType } }", "variables": { "input": { "displayName": "Smith & Associates Tax Firm", "metadata": { "customerId": "customer-123", "partnerId": "acme-partner-001", "integrationVersion": "1.0" }, "firmSize": "md" } } }' ``` ```json theme={null} { "data": { "createWorkspace": { "id": "workspace_123456", "slug": "smith-associates-tax-firm", "displayName": "Smith & Associates Tax Firm", "status": "active", "workspaceType": "partner" } } } ``` Create one workspace per tax firm customer. If you manage 10 tax firms, create 10 workspaces. Save both the `workspaceId` and `slug` for each workspace - you'll need the workspace ID for adding users and the slug for generating deep links. ## Troubleshooting **Problem**: `createWorkspace` fails **Solutions**: * Verify your access token is valid and not expired * Check that `displayName` is provided and not empty * Ensure you have permission to create workspaces # Deep links Source: https://docs.apps.filed.com/legacy/apis/endpoint/deep-links Generate deep links to redirect tax firm users to Filed Deep links allow you to seamlessly redirect tax firm users to Filed. These links can be used at any time to send users from a specific tax firm to their workspace and specific pages within Filed. ## Base URL ``` https://app.filed.com/sign-in ``` ## Format **Recommended approach**: Use the workspace slug to ensure customers are routed to the correct workspace: ``` https://app.filed.com/sign-in?deep_link=/w/{workspaceSlug}/ ``` **Convenience option**: If you don't have the workspace slug available, you can use `/w/redirect/` instead: ``` https://app.filed.com/sign-in?deep_link=/w/redirect/ ``` **Important**: Always use the workspace slug format (`/w/{workspaceSlug}/`) when available to ensure customers are directed to the correct workspace. This prevents routing errors and ensures proper workspace isolation. The convenience option (`/w/redirect/`) automatically routes users to the correct workspace based on their authentication. However, using the workspace slug is the recommended approach for reliability. ## Parameters The path within Filed to redirect users to. Must be URL-encoded. **Format options:** * `/w/{workspaceSlug}/` - Recommended: Explicitly routes to a specific workspace * `/w/redirect/` - Convenience: Routes based on user authentication **Examples:** * `/w/smith-associates-tax-firm` - Workspace dashboard * `/w/smith-associates-tax-firm/connect/tax-software` - Tax software connection page * `/w/redirect/connect/tax-software` - Tax software connection (convenience format) ## Common Redirect Paths ### Tax Software Connection Page Redirect tax firm users to set up tax software connections. **Recommended (with workspace slug):** ``` https://app.filed.com/sign-in?deep_link=/w/{workspaceSlug}/connect/tax-software ``` **Example:** ``` https://app.filed.com/sign-in?deep_link=/w/smith-associates-tax-firm/connect/tax-software ``` **Convenience option (without workspace slug):** ``` https://app.filed.com/sign-in?deep_link=/w/redirect/connect/tax-software ``` ### Tax Prep Page Redirect to a specific tax preparation. **Recommended (with workspace slug):** ``` https://app.filed.com/sign-in?deep_link=/w/{workspaceSlug}/taxpreps/{taxPrepId}/files ``` **Example:** ``` https://app.filed.com/sign-in?deep_link=/w/smith-associates-tax-firm/taxpreps/prep_123456/files ``` **Convenience option (without workspace slug):** ``` https://app.filed.com/sign-in?deep_link=/w/redirect/taxpreps/{taxPrepId}/files ``` ### Workspace Dashboard Redirect to the main workspace dashboard. **Recommended (with workspace slug):** ``` https://app.filed.com/sign-in?deep_link=/w/{workspaceSlug} ``` **Example:** ``` https://app.filed.com/sign-in?deep_link=/w/smith-associates-tax-firm ``` **Convenience option (without workspace slug):** ``` https://app.filed.com/sign-in?deep_link=/w/redirect ``` ## Example ```javascript JavaScript theme={null} // Recommended: Generate deep link with workspace slug function generateDeepLink(workspaceSlug, path = '') { const baseUrl = 'https://app.filed.com/sign-in'; const deepLink = path ? `/w/${workspaceSlug}${path}` : `/w/${workspaceSlug}`; return `${baseUrl}?deep_link=${encodeURIComponent(deepLink)}`; } // Convenience option: Generate deep link without workspace slug (using redirect) function generateDeepLinkRedirect(path = '') { const baseUrl = 'https://app.filed.com/sign-in'; const deepLink = path ? `/w/redirect${path}` : `/w/redirect`; return `${baseUrl}?deep_link=${encodeURIComponent(deepLink)}`; } // Example usage (recommended - with workspace slug) const workspaceSlug = 'smith-associates-tax-firm'; const integrationLink = generateDeepLink(workspaceSlug, '/connect/tax-software'); const taxPrepLink = generateDeepLink(workspaceSlug, '/taxpreps/prep_123456/files'); const dashboardLink = generateDeepLink(workspaceSlug); console.log('Integration Link:', integrationLink); console.log('Tax Prep Link:', taxPrepLink); console.log('Dashboard Link:', dashboardLink); // Example usage (convenience - without workspace slug) // Only use this if you don't have the workspace slug available const integrationLinkRedirect = generateDeepLinkRedirect('/connect/tax-software'); ``` ```python Python theme={null} from urllib.parse import quote # Recommended: Generate deep link with workspace slug def generate_deep_link(workspace_slug, path=''): base_url = 'https://app.filed.com/sign-in' deep_link = f'/w/{workspace_slug}{path}' if path else f'/w/{workspace_slug}' return f'{base_url}?deep_link={quote(deep_link)}' # Convenience option: Generate deep link without workspace slug def generate_deep_link_redirect(path=''): base_url = 'https://app.filed.com/sign-in' deep_link = f'/w/redirect{path}' if path else '/w/redirect' return f'{base_url}?deep_link={quote(deep_link)}' # Example usage workspace_slug = 'smith-associates-tax-firm' integration_link = generate_deep_link(workspace_slug, '/connect/tax-software') tax_prep_link = generate_deep_link(workspace_slug, '/taxpreps/prep_123456/files') dashboard_link = generate_deep_link(workspace_slug) ``` ## Notes * Deep links can be generated at any time and don't expire * Always URL-encode the `deep_link` parameter * Tax firm users will be prompted to sign in if not already authenticated * Use the workspace slug format when available for reliable routing * The convenience option (`/w/redirect/`) relies on user authentication to route to the correct workspace ## Troubleshooting **Problem**: Deep links not redirecting correctly **Solutions**: * **Always use workspace slug format** (`/w/{workspaceSlug}/`) when available to ensure correct routing * Verify the workspace slug corresponds to the correct tax firm customer * Ensure `deep_link` parameter is properly URL-encoded * Check that the path exists in Filed's application * Double-check that you're using the correct workspace slug for each tax firm customer # Get client files Source: https://docs.apps.filed.com/legacy/apis/endpoint/get-client-files List files for a specific client from a provider connection Query the list of files available for a specific client from a provider connection. This allows Filed to discover files associated with a client. ```graphql theme={null} query GetClientFiles($workspaceId: ID!, $connectionId: ID!, $clientId: String!, $options: JSON) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id clients(options: { id: $clientId }) { id files(options: $options) { id fileName fileSize mimeType downloadUrl dateCreated } } } } } } ``` ## Arguments The ID of the workspace containing the provider connection. The ID of the provider connection. This identifies which integration connection to query files from. The client ID from your platform. This should match the `id` returned from [get connection clients](/legacy/apis/endpoint/get-connection-clients). Optional query parameters for filtering or pagination. The exact options depend on your platform's implementation and will be coordinated with Filed during integration setup. ## Returns File identifier from your platform. Name of the file. Size of the file in bytes. MIME type of the file (e.g., "application/pdf", "image/jpeg"). URL where the file can be downloaded. Filed will use this URL to fetch the file. Timestamp when the file was created in your platform. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "query GetClientFiles($workspaceId: ID!, $connectionId: ID!, $clientId: String!, $options: JSON) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id clients(options: { id: $clientId }) { id files(options: $options) { id fileName fileSize mimeType downloadUrl dateCreated } } } } } }", "variables": { "workspaceId": "workspace_123456", "connectionId": "connection_789", "clientId": "client-12345", "options": { "limit": 50, "offset": 0 } } }' ``` ```json theme={null} { "data": { "me": { "workspaces": [ { "id": "workspace_123456", "connections": [ { "id": "connection_789", "clients": [ { "id": "client-12345", "files": [ { "id": "file-001", "fileName": "W-2 Form 2024.pdf", "fileSize": 245678, "mimeType": "application/pdf", "downloadUrl": "https://api.example.com/files/file-001/download", "dateCreated": "2024-01-10T08:00:00Z" }, { "id": "file-002", "fileName": "1099-INT Statement.pdf", "fileSize": 189234, "mimeType": "application/pdf", "downloadUrl": "https://api.example.com/files/file-002/download", "dateCreated": "2024-01-12T14:30:00Z" } ] } ] } ] } ] } } } ``` **File URL Accessibility**: Ensure the `downloadUrl` values are accessible to Filed. Filed will use the connection credentials to download files from these URLs. Test that Filed can access the URLs using those credentials. To download a file, Filed will use the `downloadUrl` from the file object. Ensure these URLs are valid and accessible with the connection credentials provided during connection setup. ## Troubleshooting **Problem**: No files returned for client **Solutions**: * Verify the client ID is correct and exists in your platform * Check that the client has files associated with it * Review the `options` parameter to ensure filters aren't excluding all files * Verify the connection credentials have permission to access client files **Problem**: Files returned but download URLs are inaccessible **Solutions**: * Ensure download URLs are valid and use the correct protocol (https\://) * Verify the connection credentials have permission to access the file URLs * Test the download URLs manually to ensure they return the expected files * Check that file URLs haven't expired or been moved # Get connection clients Source: https://docs.apps.filed.com/legacy/apis/endpoint/get-connection-clients List clients from a provider connection Query the list of clients available from a provider connection. This allows Filed to discover clients from your platform. ```graphql theme={null} query GetConnectionClients($workspaceId: ID!, $connectionId: ID!, $options: JSON) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id clients(options: $options) { id name email phone type lastModified } } } } } ``` **Query Pattern**: Clients are accessed through the `ProviderConnection` type. Query connections from a workspace, then access the `clients` field on the connection. ## Arguments The ID of the workspace containing the provider connection. The ID of the provider connection. This identifies which integration connection to query clients from. Optional query parameters for filtering or pagination. The exact options depend on your platform's implementation and will be coordinated with Filed during integration setup. ## Returns Client identifier from your platform. Client name. Client email address. Client phone number. Client type or category. Timestamp when the client was last modified in your platform. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "query GetConnectionClients($workspaceId: ID!, $connectionId: ID!, $options: JSON) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id clients(options: $options) { id name email phone type lastModified } } } } }", "variables": { "workspaceId": "workspace_123456", "connectionId": "connection_789", "options": { "limit": 100, "offset": 0 } } }' ``` ```json theme={null} { "data": { "me": { "workspaces": [ { "id": "workspace_123456", "connections": [ { "id": "connection_789", "clients": [ { "id": "client-12345", "name": "John Smith", "email": "john.smith@example.com", "phone": "+1-555-0123", "type": "individual", "lastModified": "2024-01-15T08:00:00Z" }, { "id": "client-67890", "name": "Jane Doe", "email": "jane.doe@example.com", "phone": "+1-555-0456", "type": "individual", "lastModified": "2024-01-14T10:30:00Z" } ] } ] } ] } } } ``` To query files for a specific client, use [get client files](/legacy/apis/endpoint/get-client-files) with the client ID returned from this query. ## Troubleshooting **Problem**: No clients returned or empty list **Solutions**: * Verify the connection status is `active` * Check that your platform has clients available for this connection * Review the `options` parameter to ensure filters aren't excluding all clients * Verify the connection credentials have permission to access client data **Problem**: Connection not found **Solutions**: * Verify the `connectionId` exists and belongs to your workspace * Ensure you're using the correct connection ID from the connection setup * Check that the connection status allows querying clients # Get job work artifact status Source: https://docs.apps.filed.com/legacy/apis/endpoint/get-job-work-artifacts Query artifact status for job works via connection Retrieve the status of artifacts attached to job works. This allows you to monitor file processing progress, check upload status, and track the overall state of documents associated with tax preparations. ```graphql theme={null} query GetJobWorkArtifactStatus($workspaceId: ID!, $connectionId: ID!, $jobWorkFilters: JobWorkFilters) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id jobWorks(filters: $jobWorkFilters) { id work { id name externalId } jobWorkArtifacts { id createdAt updatedAt workspaceId artifactId connectionJobId jobWorkId workArtifact { id externalId createdAt updatedAt workId connectionId name type status uploadedUrl metadata } } } } } } } ``` ## Arguments The ID of the provider connection. Use this to scope the query to artifacts from a specific connection. Filters to identify which job works to retrieve artifacts for. Filter by specific job work IDs. Returns artifacts only for the specified job works. Filter by connection job IDs. Returns artifacts for all job works in the specified connection jobs. Filter by work IDs. Returns artifacts for all job works associated with the specified works. ## Returns ### JobArtifact The unique identifier of the job artifact. Timestamp when the job artifact was created. Timestamp when the job artifact was last updated. The workspace ID this job artifact belongs to. Reference to the underlying work artifact. The connection job ID this artifact is associated with. The job work ID this artifact is attached to. The underlying work artifact with detailed status information. The unique identifier of the work artifact. Your platform's unique identifier for this file/artifact. Timestamp when the artifact was created. Timestamp when the artifact was last updated. The work ID this artifact belongs to. The connection ID this artifact belongs to. The name of the artifact (e.g., "W-2 Form", "1099-INT"). The type/category of the artifact (e.g., "w2", "1099", "receipt"). The current processing status of the artifact. See [Artifact Status Reference](#artifact-status-reference) for details. The URL where the artifact file was uploaded, if available. Additional metadata about the artifact (e.g., file size, mime type, processing details). ## Example: Get Artifact Status by Job Work ID ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "query GetJobWorkArtifactStatus($workspaceId: ID!, $connectionId: ID!, $jobWorkFilters: JobWorkFilters) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id jobWorks(filters: $jobWorkFilters) { id work { id name externalId } jobWorkArtifacts { id createdAt updatedAt artifactId connectionJobId jobWorkId workArtifact { id externalId name type status uploadedUrl metadata } } } } } } }", "variables": { "workspaceId": "workspace_123456", "connectionId": "connection_789", "jobWorkFilters": { "ids": ["job_work_456"] } } }' ``` ```json theme={null} { "data": { "me": { "workspaces": [ { "id": "workspace_123456", "connections": [ { "id": "connection_789", "jobWorks": [ { "id": "job_work_456", "work": { "id": "work_789", "name": "John Smith - 2024 Tax Return", "externalId": "client-12345" }, "jobWorkArtifacts": [ { "id": "job_artifact_001", "createdAt": "2024-01-15T10:40:00Z", "updatedAt": "2024-01-15T10:42:00Z", "artifactId": "artifact_001", "connectionJobId": "job_123456", "jobWorkId": "job_work_456", "workArtifact": { "id": "artifact_001", "externalId": "file-w2-001", "name": "W-2 Form 2024", "type": "w2", "status": "completed", "uploadedUrl": "https://storage.filed.com/artifacts/artifact_001.pdf", "metadata": { "fileSize": 245678, "mimeType": "application/pdf", "pages": 2 } } }, { "id": "job_artifact_002", "createdAt": "2024-01-15T10:40:05Z", "updatedAt": "2024-01-15T10:40:05Z", "artifactId": "artifact_002", "connectionJobId": "job_123456", "jobWorkId": "job_work_456", "workArtifact": { "id": "artifact_002", "externalId": "file-1099-001", "name": "1099-INT Statement", "type": "1099", "status": "pending", "uploadedUrl": null, "metadata": { "fileSize": 189234, "mimeType": "application/pdf" } } } ] } ] } ] } ] } } } ``` ## Example: Monitor All Artifacts for a Connection Job Track the status of all artifacts across all job works in a connection job: ```graphql theme={null} query MonitorConnectionJobArtifacts($workspaceId: ID!, $connectionId: ID!, $connectionJobId: ID!) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id jobWorks(filters: { connectionJobIds: [$connectionJobId] }) { id work { name externalId } jobWorkArtifacts { id workArtifact { name type status externalId } } } } } } } ``` ## Artifact Status Reference Work artifacts have the following statuses that indicate their processing state: **Initial state** — The artifact has been created but has not yet been processed. This is the default status when an artifact is first created via `createJobWorkArtifacts`. When you see this status: * The artifact record exists in Filed * File fetching/processing has not started yet * The file URL provided is queued for download **Success state** — The artifact has been successfully processed. The file has been fetched from the provided URL and is now available in Filed. When you see this status: * The file was successfully downloaded from the source URL * The artifact is ready for use in tax preparation workflows * The `uploadedUrl` field will contain the Filed storage URL **Error state** — The artifact processing failed. This typically occurs when the file could not be fetched or processed. Common causes: * The source URL is inaccessible or returns an error * The file format is unsupported or corrupted * Authentication to the source URL failed * Network timeout during file download Check the `metadata` field for error details. **Terminated state** — The artifact was explicitly cancelled. This occurs when the artifact processing was stopped before completion. When you see this status: * The artifact will not be processed further * This may occur if the parent job was cancelled * The artifact can be recreated if needed ### Status Workflow ``` ┌───────────┐ ┌───▶│ completed │ │ └───────────┘ ┌─────────┐ │ │ pending │──── process ─┼───▶┌────────┐ └─────────┘ │ │ failed │ │ └────────┘ │ └───▶┌───────────┐ │ cancelled │ └───────────┘ ``` Artifacts start in `pending` status and transition to a terminal state (`completed`, `failed`, or `cancelled`) after processing. Poll periodically to track progress. ## Work Status Reference The parent `Work` entity (accessible via `jobWork.work.status`) has its own status values: **Initial state** — The work has been created but not yet imported into Filed's tax preparation system. **Success state** — The work has been successfully imported and is available for tax preparation. **Error state** — An error occurred during import processing. **Removed state** — The work has been deleted and is no longer active. ## Best Practices 1. **Batch status checks**: Query multiple job works at once rather than making individual requests for each artifact. 2. **Use filters efficiently**: When monitoring a specific import, use `connectionJobIds` filter to get all artifacts for that batch. 3. **Handle failures gracefully**: Check for `failed` status and review metadata for error details. 4. **Correlate with external IDs**: Use `workArtifact.externalId` to match artifacts back to your platform's file records. ## Troubleshooting **Problem**: Artifact status stuck on `pending` **Solutions**: * Verify the file URL provided in `createJobWorkArtifacts` is accessible * Check that the URL returns a valid file (not an error page) * Ensure the file format is supported * Review connection credentials if the file requires authentication * Trigger the import process via [initiateTaxPrepsImport](/legacy/apis/endpoint/initiate-tax-preps-import) if not already done **Problem**: Artifact status shows `failed` **Solutions**: * Check the `metadata` field for error details * Verify the file URL is still valid and accessible * Ensure the file is not corrupted or empty * Try re-creating the artifact with a fresh URL * Common causes: URL timeout, 404 errors, invalid file format **Problem**: Artifact status shows `cancelled` **Solutions**: * Check if the parent connection job was cancelled * Recreate the artifact if processing is still needed * Verify the job work and connection job are in valid states **Problem**: `uploadedUrl` is null **Solutions**: * Check the `status` field — URL is only populated when status is `completed` * For `pending` status, wait for processing to complete * For `failed` status, the file was not successfully fetched # Get job works Source: https://docs.apps.filed.com/legacy/apis/endpoint/get-job-works Query job works by ID via a provider connection Retrieve job works associated with a provider connection. Job works represent individual client/tax preparation entries that were created as part of an import job. You can filter by specific job work IDs, connection job IDs, or work IDs. ```graphql theme={null} query GetJobWorks($workspaceId: ID!, $connectionId: ID!, $filters: JobWorkFilters, $sortBy: SortBy, $offset: Int, $limit: Int) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id jobWorks(filters: $filters, sortBy: $sortBy, offset: $offset, limit: $limit) { id createdAt updatedAt workspaceId workId connectionJobId work { id name externalId status tag } jobWorkArtifacts { id artifactId workArtifact { id externalId name type status } } } } } } } ``` ## Arguments The ID of the provider connection to query job works from. This is the connection ID returned from [startProviderConnection](/legacy/apis/endpoint/start-provider-connection). Optional filters to narrow down the job works returned. Filter by specific job work IDs. Use this to retrieve one or more specific job works by their unique identifiers. Filter by connection job IDs. Returns all job works that belong to the specified connection jobs. Filter by work IDs. Returns all job works associated with the specified works. Optional sorting configuration. Field to sort by (e.g., "createdAt", "updatedAt"). Sort direction: `ASC` for ascending or `DESC` for descending. Number of records to skip for pagination. Defaults to 0. Maximum number of records to return. Use with offset for pagination. ## Returns The unique identifier of the job work. Timestamp when the job work was created. Timestamp when the job work was last updated. The workspace ID this job work belongs to. The work ID this job work references. The connection job ID this job work belongs to. The associated work (client/tax preparation) details. The unique identifier of the work. The name of the work (typically the client name). Your platform's unique identifier for this work. The current status of the work: * `pending` - Work created but not yet imported * `imported` - Work successfully imported into Filed * `error` - An error occurred during import * `deleted` - Work has been removed Optional tag associated with the work. List of artifacts attached to this job work. See [get-job-work-artifacts](/legacy/apis/endpoint/get-job-work-artifacts) for detailed artifact status information. ## Example: Get Job Work by ID ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "query GetJobWorks($workspaceId: ID!, $connectionId: ID!, $filters: JobWorkFilters) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id jobWorks(filters: $filters) { id createdAt updatedAt workspaceId workId connectionJobId work { id name externalId status tag } jobWorkArtifacts { id artifactId workArtifact { id externalId name type status } } } } } } }", "variables": { "workspaceId": "workspace_123456", "connectionId": "connection_789", "filters": { "ids": ["job_work_456"] } } }' ``` ```json theme={null} { "data": { "me": { "workspaces": [ { "id": "workspace_123456", "connections": [ { "id": "connection_789", "jobWorks": [ { "id": "job_work_456", "createdAt": "2024-01-15T10:35:00Z", "updatedAt": "2024-01-15T10:40:00Z", "workspaceId": "workspace_123456", "workId": "work_789", "connectionJobId": "job_123456", "work": { "id": "work_789", "name": "John Smith - 2024 Tax Return", "externalId": "client-12345", "status": "pending", "tag": "individual" }, "jobWorkArtifacts": [ { "id": "job_artifact_001", "artifactId": "artifact_001", "workArtifact": { "id": "artifact_001", "externalId": "file-w2-001", "name": "W-2 Form 2024", "type": "w2", "status": "completed" } } ] } ] } ] } ] } } } ``` ## Example: Get All Job Works for a Connection Job Query all job works that belong to a specific connection job: ```graphql theme={null} query GetJobWorksByConnectionJob($workspaceId: ID!, $connectionId: ID!, $connectionJobId: ID!) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: { ids: [$connectionId] }) { id jobWorks(filters: { connectionJobIds: [$connectionJobId] }) { id work { id name externalId status } } } } } } ``` The `jobWorks` field on a connection returns all job works across all connection jobs for that connection. Use the `connectionJobIds` filter to narrow down to a specific import batch. ## Troubleshooting **Problem**: No job works returned despite creating them **Solutions**: * Verify you're querying the correct connection ID * Check that the job work IDs in your filter are correct * Ensure the job works were successfully created (check for errors in the `createJobWorks` response) * Confirm you have access to the workspace containing the connection **Problem**: Missing artifact information **Solutions**: * Artifacts may not have been created yet - verify with `createJobWorkArtifacts` * Include the `jobWorkArtifacts` field in your query to fetch artifact details * Check artifact status to see if files are still being processed # Get workspace connections Source: https://docs.apps.filed.com/legacy/apis/endpoint/get-workspace-connections List provider connections for a workspace with their status Query the list of provider connections for a workspace. This allows you to see all integrations configured for a workspace and their current connection status. ```graphql theme={null} query GetWorkspaceConnections($workspaceId: ID!, $filters: ConnectionFilters, $limit: Int, $offset: Int, $sortBy: SortBy) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: $filters, limit: $limit, offset: $offset, sortBy: $sortBy) { id providerKey status provider { category } } } } } ``` **Query Pattern**: Connections are accessed through the `Workspace` type. Query workspaces, then access the `connections` field to get all provider connections. ## Arguments The ID of the workspace to query connections for. Optional filters to narrow down the connections returned. Filter by specific connection IDs. Filter by connection status (e.g., `active`, `pending`, `failed`). Filter by provider keys (e.g., `canopy`, `taxdome`, `karbon`). Filter by provider categories. Maximum number of connections to return. Number of connections to skip for pagination. Optional sorting configuration. Field to sort by (e.g., `createdAt`, `updatedAt`, `status`). Sort order: `ASC` for ascending, `DESC` for descending. ## Returns Unique identifier for the connection. The provider key identifying the integration type (e.g., `canopy`, `taxdome`, `karbon`, `google_drive`, `sharepoint`). Current status of the connection. Common values include: * `active` - Connection is working and authenticated * `pending` - Connection setup is in progress * `failed` - Connection has failed or credentials are invalid * `disconnected` - Connection has been disconnected The category of the provider. Common values include: * `practice_management` - Practice management systems (e.g., Canopy, TaxDome, Karbon) * `document_management` - Document storage providers (e.g., Google Drive, SharePoint) * `tax_software` - Tax preparation software (e.g., Drake, UltraTax, Lacerte) ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "query GetWorkspaceConnections($workspaceId: ID!, $filters: ConnectionFilters) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections(filters: $filters) { id providerKey status provider { category } } } } }", "variables": { "workspaceId": "workspace_123456", "filters": { "statuses": ["active"] } } }' ``` ```json theme={null} { "data": { "me": { "workspaces": [ { "id": "workspace_123456", "connections": [ { "id": "conn_abc123", "providerKey": "canopy", "status": "active", "provider": { "category": "practice_management" } }, { "id": "conn_def456", "providerKey": "google_drive", "status": "active", "provider": { "category": "document_management" } }, { "id": "conn_ghi789", "providerKey": "taxdome", "status": "pending", "provider": { "category": "practice_management" } } ] } ] } } } ``` ## Additional Fields The `ProviderConnection` type includes additional fields you can query: ```graphql theme={null} query GetWorkspaceConnectionsDetailed($workspaceId: ID!) { me { workspaces(filters: { ids: [$workspaceId] }) { id connections { id providerKey status displayName createdAt updatedAt provider { name category } } } } } ``` Human-readable name for the connection. Timestamp when the connection was created. Timestamp when the connection was last updated. Provider details including name and category. ## Troubleshooting **Problem**: No connections returned or empty list **Solutions**: * Verify the workspace ID is correct * Check that connections have been set up for this workspace * Review filters to ensure they aren't excluding all connections **Problem**: Connection shows `failed` status **Solutions**: * The connection credentials may have expired or been revoked * Re-authenticate the connection using [start provider connection](/legacy/apis/endpoint/start-provider-connection) * Check the provider's settings or permissions To set up a new connection, use [start provider connection](/legacy/apis/endpoint/start-provider-connection). To complete authentication, use [collect provider connection](/legacy/apis/endpoint/collect-provider-connection). # Login via API token Source: https://docs.apps.filed.com/legacy/apis/endpoint/login-via-personal-api-token Exchange an API key for an access token ```graphql theme={null} mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } } ``` ## Arguments Your API key provided by Filed. ## Returns Bearer token for API authentication. Use this token in the `Authorization` header for all subsequent requests. Format: `Authorization: Bearer ` All API requests, including the login request, should include a `source-platform` header with your platform identifier (e.g., `tax-firm-a`, `my-sample-platform`, `qount`). This header is used to uniquely identify API requests for analytical purposes and does not affect rate limits or any other API functionality. For partner API keys, the access token provides access to **all workspaces** created by that partner. You can use the same access token to call APIs against any workspace you've created, allowing you to manage multiple tax firm customers from a single authenticated session. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } }", "variables": { "apiKey": "your-api-key-here" } }' ``` ```json theme={null} { "data": { "loginViaPersonalApiToken": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } } ``` Store the `accessToken` securely and use it in the `Authorization` header for all subsequent requests. Access tokens are valid for a limited time; implement token refresh logic as needed. Remember to include the `source-platform` header in all API requests, including the login request. ## Troubleshooting **Problem**: `loginViaPersonalApiToken` returns an error **Solutions**: * Verify your API key is correct and hasn't expired * Check that you're using the correct GraphQL endpoint: `https://gateway.filed.com/graphql` * Ensure your partner account is active # Start provider connection Source: https://docs.apps.filed.com/legacy/apis/endpoint/start-provider-connection Initialize a provider connection for a workspace with partner credentials Initialize a new provider connection between Filed and your platform. This establishes the connection using your partner key and firm-specific credentials. **Implementation Details**: The specific implementation of what happens on Filed's side when this mutation is called will be discussed and coordinated between your team and Filed's engineering team during the integration process. ```graphql theme={null} mutation StartProviderConnection($workspaceId: ID!, $providerKey: String!, $inputs: JSON) { startProviderConnection(workspaceId: $workspaceId, providerKey: $providerKey, inputs: $inputs) { id workspaceId providerKey status action displayName createdAt } } ``` ## Arguments The ID of the workspace (tax firm) you're setting up the integration for. Your partner key provided by Filed. Examples: ``, `qount`. Your specific partner key will be provided during partner onboarding. Optional JSON object containing firm-specific credentials and configuration. Common fields include: * `apiKey`: Firm's API key for your platform * `securityToken`: Security token or authentication token * `baseUrl`: Base URL for API calls (if different from default) * Any other credentials or configuration your platform requires ## Returns Connection identifier. Save this for the collect step. Workspace identifier this connection belongs to. The partner key used to create this connection. Connection status. Will be `pending` until you complete the collect step. Next action required. Typically `collect` after starting a connection. Display name for the connection. Timestamp when the connection was created. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation StartProviderConnection($workspaceId: ID!, $providerKey: String!, $inputs: JSON) { startProviderConnection(workspaceId: $workspaceId, providerKey: $providerKey, inputs: $inputs) { id workspaceId providerKey status action displayName createdAt } }", "variables": { "workspaceId": "workspace_123456", "providerKey": "", "inputs": { "apiKey": "firm-specific-api-key-12345", "securityToken": "firm-security-token-abc", "baseUrl": "https://api.canopy.com" } } }' ``` ```json theme={null} { "data": { "startProviderConnection": { "id": "connection_789", "workspaceId": "workspace_123456", "providerKey": "", "status": "pending", "action": "collect", "displayName": null, "createdAt": "2024-01-15T10:30:00Z" } } } ``` Save the `id` from the response - you'll need it for the [collect provider connection](/legacy/apis/endpoint/collect-provider-connection) step. The connection status will be `pending` until you complete the collect step. ## Troubleshooting **Problem**: `startProviderConnection` fails with invalid partner key **Solutions**: * Verify your partner key is correct (contact Filed if unsure) * Ensure the partner key matches what was provided during partner onboarding * Check that the partner key is spelled correctly (case-sensitive) **Problem**: Connection created but credentials are invalid **Solutions**: * Verify the credentials in the `inputs` parameter are correct * Ensure API keys and tokens have the necessary permissions * Check that the credentials are for the correct tax firm/workspace # Upload connection file Source: https://docs.apps.filed.com/legacy/apis/endpoint/upload-connection-file Upload a file to a client in a provider connection Upload a file to a specific client in a provider connection. This allows Filed to send files back to your platform for a client. ```graphql theme={null} mutation UploadConnectionFile($connectionId: ID!, $clientKey: String!, $file: FileUploadInput!) { uploadConnectionFile(connectionId: $connectionId, clientKey: $clientKey, file: $file) { success message } } ``` ## Arguments The ID of the provider connection. This identifies which integration connection to upload the file to. The client identifier from your platform. This should match the `id` returned from [get connection clients](/legacy/apis/endpoint/get-connection-clients). File upload input containing file details. Name of the file to upload (e.g., "tax-return-2024.pdf", "notes.txt"). MIME type of the file (e.g., "application/pdf", "text/plain", "image/jpeg"). TUS (resumable upload) endpoint URL for uploading the file. Filed uses TUS protocol for reliable file uploads. ## Returns Whether the upload was successful. Success or error message describing the result of the upload operation. ## Example ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation UploadConnectionFile($connectionId: ID!, $clientKey: String!, $file: FileUploadInput!) { uploadConnectionFile(connectionId: $connectionId, clientKey: $clientKey, file: $file) { success message } }", "variables": { "connectionId": "connection_789", "clientKey": "client-12345", "file": { "fileName": "tax-return-2024.pdf", "contentType": "application/pdf", "tusdEndpoint": "https://api.example.com/files/upload" } } }' ``` ```json theme={null} { "data": { "uploadConnectionFile": { "success": true, "message": "File uploaded successfully" } } } ``` **TUS Protocol**: Filed uses the TUS (resumable upload) protocol for file uploads. Your platform needs to implement a TUS-compatible endpoint. The `tusdEndpoint` should point to your TUS upload endpoint that accepts file uploads for the specified client. After the mutation succeeds, Filed will upload the file to your platform using the TUS endpoint. Ensure your platform's TUS endpoint is configured to accept uploads for the specified client and connection. ## Troubleshooting **Problem**: Upload fails with invalid client key **Solutions**: * Verify the `clientKey` matches a valid client ID from your platform * Ensure the client exists and is accessible through this connection * Check that the connection credentials have permission to upload files for this client **Problem**: TUS endpoint not accessible or invalid **Solutions**: * Verify the `tusdEndpoint` URL is correct and accessible * Ensure your platform implements a TUS-compatible upload endpoint * Test the TUS endpoint manually to ensure it accepts uploads * Check that the endpoint accepts uploads for the specified client **Problem**: File upload fails after mutation succeeds **Solutions**: * Verify your TUS endpoint is properly configured * Check file size limits and ensure the file doesn't exceed them * Ensure the endpoint has proper error handling and returns appropriate TUS responses * Review TUS protocol implementation to ensure compatibility # Customer integration guide Source: https://docs.apps.filed.com/legacy/apis/guides/customer Learn how to integrate Filed's API as a customer and authenticate your requests **Are you a partner?** If you are an official Filed partner managing multiple tax firm customers, please use the [Partner integration guide](/legacy/apis/guides/partner) instead. This guide walks you through the complete API integration process for customers, from generating your API key to making your first authenticated request. Follow these steps to start building with Filed's API. ## Prerequisites Before you begin, ensure you have: * An active Filed account * Access to Developer Settings in your workspace * GraphQL endpoint: `https://gateway.filed.com/graphql` If you need assistance accessing Developer Settings or have questions about API access, please contact [support@filed.com](mailto:support@filed.com) ## Overview The API integration process consists of three simple steps: 1. **Generate API Key** from Developer Settings 2. **Exchange API Key** for an access token 3. **Make authenticated requests** to the Filed API *** ## Step 1: Generate API Key Navigate to Developer Settings in your Filed workspace to generate your API key. Log in to your Filed account and navigate to **Settings** → **Developer Settings** Click **Generate API Key** and provide a descriptive name for your key (e.g., "Production Integration", "Development Testing") Copy and securely store your API key immediately - you won't be able to see it again **Security Best Practice**: Store your API key securely and never commit it to version control. Use environment variables or secure secret management tools. *** ## Step 2: Exchange API Key for Access Token Use your API key to obtain an access token that will authenticate all your API requests. #### GraphQL Mutation ```graphql GraphQL theme={null} mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "source-platform: tax-firm-a" \ -d '{ "query": "mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } }", "variables": { "apiKey": "your-api-key-here" } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'source-platform': 'tax-firm-a' }, body: JSON.stringify({ query: ` mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } } `, variables: { apiKey: 'your-api-key-here' } }) }); const { data } = await response.json(); const accessToken = data.loginViaPersonalApiToken.accessToken; ``` ```python Python theme={null} import requests response = requests.post( 'https://gateway.filed.com/graphql', headers={ 'source-platform': 'tax-firm-a' }, json={ 'query': ''' mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } } ''', 'variables': { 'apiKey': 'your-api-key-here' } } ) data = response.json() access_token = data['data']['loginViaPersonalApiToken']['accessToken'] ``` ```json Response theme={null} { "data": { "loginViaPersonalApiToken": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } } ``` #### Implementation Notes * Store the `accessToken` securely and use it in the `Authorization` header for all subsequent requests * Access tokens are valid for a limited time; implement token refresh logic as needed * Format: `Authorization: Bearer ` * Include a `source-platform` header with a custom identifier for your integration (e.g., `tax-firm-a`, `my-sample-platform`, `qount`). This header is used to uniquely identify API requests for analytical purposes and does not affect rate limits or any other API functionality. *** ## Step 3: Verify Your Connection Make a test request to verify your API key works correctly. The `me` query returns information about the authenticated user. #### GraphQL Query ```graphql GraphQL theme={null} query Me { me { id name } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-access-token-here" \ -H "source-platform: tax-firm-a" \ -d '{ "query": "query Me { me { id name } }" }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'tax-firm-a' }, body: JSON.stringify({ query: ` query Me { me { id name } } ` }) }); const { data } = await response.json(); console.log('Current user:', data.me); ``` ```python Python theme={null} response = requests.post( 'https://gateway.filed.com/graphql', headers={ 'Authorization': f'Bearer {access_token}', 'source-platform': 'tax-firm-a' }, json={ 'query': ''' query Me { me { id name } } ''' } ) data = response.json() user = data['data']['me'] print(f"Current user ID: {user['id']}") ``` ```json Response theme={null} { "data": { "me": { "id": "user_123456", "name": "John Doe" } } } ``` #### Implementation Notes * The `me` query returns information about the currently authenticated user * A successful response confirms your API integration is working correctly * The `id` field uniquely identifies the authenticated user **Success!** If you receive a valid response with your user information, your API integration is working correctly and you're ready to start building. *** ## Complete Integration Example Here's a complete example showing all three steps together: ```javascript JavaScript theme={null} const GRAPHQL_ENDPOINT = 'https://gateway.filed.com/graphql'; const API_KEY = process.env.FILED_API_KEY; async function integrateWithFiled() { // Step 2: Exchange API Key for Access Token const authResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'source-platform': 'tax-firm-a' }, body: JSON.stringify({ query: ` mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } } `, variables: { apiKey: API_KEY } }) }); const { data: authData } = await authResponse.json(); const accessToken = authData.loginViaPersonalApiToken.accessToken; console.log('✓ Successfully authenticated'); // Step 3: Verify Connection const meResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'tax-firm-a' }, body: JSON.stringify({ query: ` query Me { me { id name } } ` }) }); const { data: meData } = await meResponse.json(); console.log('✓ Connection verified'); console.log('Current user:', meData.me); return { accessToken, user: meData.me }; } // Run the integration integrateWithFiled() .then(({ user }) => { console.log(`\n🎉 Integration successful! User ID: ${user.id}`); }) .catch(error => { console.error('Integration failed:', error); }); ``` ```typescript TypeScript theme={null} const GRAPHQL_ENDPOINT = 'https://gateway.filed.com/graphql'; const API_KEY = process.env.FILED_API_KEY!; interface AuthResponse { data: { loginViaPersonalApiToken: { accessToken: string; }; }; } interface MeResponse { data: { me: { id: string; name: string | null; }; }; } async function integrateWithFiled() { // Step 2: Exchange API Key for Access Token const authResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'source-platform': 'tax-firm-a' }, body: JSON.stringify({ query: ` mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } } `, variables: { apiKey: API_KEY } }) }); const { data: authData } = await authResponse.json() as AuthResponse; const accessToken = authData.loginViaPersonalApiToken.accessToken; console.log('✓ Successfully authenticated'); // Step 3: Verify Connection const meResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'tax-firm-a' }, body: JSON.stringify({ query: ` query Me { me { id name } } ` }) }); const { data: meData } = await meResponse.json() as MeResponse; console.log('✓ Connection verified'); console.log('Current user:', meData.me); return { accessToken, user: meData.me }; } // Run the integration integrateWithFiled() .then(({ user }) => { console.log(`\n🎉 Integration successful! User ID: ${user.id}`); }) .catch(error => { console.error('Integration failed:', error); }); ``` ```python Python theme={null} import os import requests from typing import Dict, Any GRAPHQL_ENDPOINT = 'https://gateway.filed.com/graphql' API_KEY = os.environ.get('FILED_API_KEY') def integrate_with_filed() -> Dict[str, Any]: # Step 2: Exchange API Key for Access Token auth_query = """ mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } } """ auth_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'source-platform': 'tax-firm-a' }, json={ 'query': auth_query, 'variables': {'apiKey': API_KEY} } ) auth_data = auth_response.json() access_token = auth_data['data']['loginViaPersonalApiToken']['accessToken'] print('✓ Successfully authenticated') # Step 3: Verify Connection me_query = """ query Me { me { id name } } """ me_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'Authorization': f'Bearer {access_token}', 'source-platform': 'tax-firm-a' }, json={'query': me_query} ) me_data = me_response.json() user = me_data['data']['me'] print('✓ Connection verified') print(f'Current user: {user}') return { 'access_token': access_token, 'user': user } if __name__ == '__main__': try: result = integrate_with_filed() print(f"\n🎉 Integration successful! User ID: {result['user']['id']}") except Exception as e: print(f'Integration failed: {e}') ``` *** ## Best Practices ### Security 1. **Never expose API keys**: Store API keys in environment variables or secure secret management systems 2. **Use HTTPS**: Always make API requests over HTTPS 3. **Validate inputs**: Always validate user inputs before making API calls ### Error handling 1. **Handle authentication errors**: Implement retry logic for expired tokens 2. **Validate responses**: Check for errors in GraphQL responses 3. **Log errors**: Maintain error logs for debugging and monitoring *** ## Next Steps Now that you've successfully integrated with Filed's API, you can: 1. **Explore the API**: Browse the full [API reference](/legacy/apis/introduction) to discover available queries and mutations 2. **Build features**: Start implementing features using Filed's GraphQL API 3. **Monitor usage**: Track your API usage and monitor for errors *** ## Support Need help with your integration? * **Email**: [support@filed.com](mailto:support@filed.com) * **Documentation**: Browse our [API documentation](/legacy/apis/introduction) for more details We're here to help you succeed with Filed's API! # Partner integration guide Source: https://docs.apps.filed.com/legacy/apis/guides/partner Learn how to integrate Filed's API as a partner and onboard your customers **Not an official partner?** If you are not an official Filed partner and want to integrate as a customer, please use the [Customer integration guide](/legacy/apis/guides/customer) instead. This guide walks you through the complete partner integration process, from receiving your API key to generating deep links for your customers. Follow these steps to start onboarding tax firms to Filed. **Important**: Each partner manages multiple customers (tax firms). Each workspace represents one customer/tax firm. You'll create a separate workspace for each tax firm customer you onboard. ## Prerequisites Before you begin, ensure you have: * Partner account with Filed * API access credentials (API Key) * Partner key (provider or partner key is a unique identifier from Filed for your platform) * Dedicated slack channel for your partner account * GraphQL endpoint: `https://gateway.filed.com/graphql` Contact your Filed account representative if you need assistance creating your partner account, API key, and partner key or have questions about the integration process. ## Overview The partner integration process enables you to: 1. **Authenticate** to Filed's API using your API key 2. **Verify your connection** by testing the API key with a sample query 3. **Create workspaces** for each customer (tax firm) - one workspace per tax firm 4. **Onboard tax firm users** by adding them to their respective workspaces 5. **Set up integrations** with tax software providers for each tax firm 6. **Generate deep links** that seamlessly redirect tax firm users to Filed ### Understanding the model * **Partner**: Your partner user account that manages multiple tax firm customers * **Workspace**: Each workspace represents one customer (tax firm) * One workspace = One customer = One tax firm * A workspace can have multiple users from the same tax firm * Partners like you can create multiple workspaces (one for each tax firm customer) ## Integration Workflow The partner integration process consists of six main steps: ### Step 1: Authenticate to Filed's API using your API key Exchange your API key for an access token that will be used for all subsequent API requests. #### GraphQL mutation ```graphql GraphQL theme={null} mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "source-platform: " \ -d '{ "query": "mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } }", "variables": { "apiKey": "your-api-key-here" } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'source-platform': '' }, body: JSON.stringify({ query: ` mutation LoginViaPersonalApiToken($apiKey: String!) { loginViaPersonalApiToken(token: $apiKey) { accessToken } } `, variables: { apiKey: 'your-api-key-here' } }) }); const { data } = await response.json(); const accessToken = data.loginViaPersonalApiToken.accessToken; ``` ```json Response theme={null} { "data": { "loginViaPersonalApiToken": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } } ``` #### Implementation notes * Store the `accessToken` securely and use it in the `Authorization` header for all subsequent requests * Access tokens are valid for a limited time; implement token refresh logic as needed * Format: `Authorization: Bearer ` * Include a `source-platform` header with your partner name or identifier (e.g., ``, `qount`, `partner-name`). This header is used to uniquely identify API requests for analytical purposes and does not affect rate limits or any other API functionality. **Important**: The access token obtained from your partner API key provides access to **all workspaces** you create as a partner. This means you can use the same access token to call APIs against any workspace you've created, allowing you to manage multiple tax firm customers from a single authenticated session. **Security Best Practice**: Store your API key securely and never commit it to version control. Use environment variables or secure secret management tools. *** ### Step 2: Verify your connection Make a test request to verify your API key works correctly. The `me` query returns information about the authenticated user. #### GraphQL Query ```graphql GraphQL theme={null} query Me { me { id name } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: " \ -d '{ "query": "query Me { me { id name } }" }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` query Me { me { id name } } ` }) }); const { data } = await response.json(); console.log('Current user:', data.me); ``` ```json Response theme={null} { "data": { "me": { "id": "user_123456", "name": "Partner Admin" } } } ``` #### Implementation notes * The `me` query returns information about the currently authenticated user * A successful response confirms your API integration is working correctly * The `id` field uniquely identifies the authenticated user **Success!** If you receive a valid response with your user information, your API key is working correctly and you're ready to proceed with creating workspaces. *** ### Step 3: Create workspace for each customer (tax firm) Create a workspace for each tax firm customer. Each workspace represents one tax firm and will be used to manage that tax firm's users and their tax preparations. **Note**: You'll create a separate workspace for each tax firm customer. If you have 10 tax firm customers, you'll create 10 workspaces. #### GraphQL mutation ```graphql GraphQL theme={null} mutation CreateWorkspace($input: CreateWorkspaceInput!) { createWorkspace(input: $input) { id slug displayName status workspaceType } } ``` ```graphql Input Type theme={null} input CreateWorkspaceInput { displayName: String! # Tax firm's display name metadata: JSON # Optional metadata (e.g., customer ID, partner info) firmSize: String # Optional: "xs", "sm", "md", "lg", "xl" } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: " \ -d '{ "query": "mutation CreateWorkspace($input: CreateWorkspaceInput!) { createWorkspace(input: $input) { id slug displayName status workspaceType } }", "variables": { "input": { "displayName": "Smith & Associates Tax Firm", "metadata": { "customerId": "customer-123", "partnerId": "acme-partner-001", "integrationVersion": "1.0" }, "firmSize": "md" } } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation CreateWorkspace($input: CreateWorkspaceInput!) { createWorkspace(input: $input) { id slug displayName status workspaceType } } `, variables: { input: { displayName: 'Smith & Associates Tax Firm', metadata: { customerId: 'customer-123', partnerId: 'acme-partner-001', integrationVersion: '1.0' }, firmSize: 'md' } } }) }); const { data } = await response.json(); const workspace = data.createWorkspace; ``` ```json Response theme={null} { "data": { "createWorkspace": { "id": "workspace_123456", "slug": "smith-associates-tax-firm", "displayName": "Smith & Associates Tax Firm", "status": "active", "workspaceType": "partner" } } } ``` #### Implementation notes * Save the `workspaceId` and `slug` for each workspace (customer) - you'll need the workspace ID for adding users and the slug for generating deep links * The `slug` is essential for creating reliable deep links that route customers to the correct workspace * Workspace type will be automatically set to `partner` for partner accounts * Create a new workspace for each tax firm customer you onboard * Store the mapping between your customer IDs, workspace IDs, and workspace slugs for reference *** ### Step 4: Add tax firm users to workspace Add the tax firm's users (preparers, staff, etc.) to their workspace by email addresses. This enables them to access Filed through deep links. #### GraphQL mutation ```graphql GraphQL theme={null} mutation AddUsersToWorkspace($workspaceId: ID!, $userEmails: [String!]!) { addUsersToTheWorkspace(workspaceId: $workspaceId, userEmails: $userEmails) { id email name role } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: " \ -d '{ "query": "mutation AddUsersToWorkspace($workspaceId: ID!, $userEmails: [String!]!) { addUsersToTheWorkspace(workspaceId: $workspaceId, userEmails: $userEmails) { id email name role } }", "variables": { "workspaceId": "workspace_123456", "userEmails": [ "test-1@firm.filed.com", "test-2@firm.filed.com" ] } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation AddUsersToWorkspace($workspaceId: ID!, $userEmails: [String!]!) { addUsersToTheWorkspace(workspaceId: $workspaceId, userEmails: $userEmails) { id email name role } } `, variables: { workspaceId: 'workspace_123456', userEmails: [ "test-1@firm.filed.com", "test-2@firm.filed.com" ] } }) }); const { data } = await response.json(); const users = data.addUsersToTheWorkspace; ``` ```json Response theme={null} { "data": { "addUsersToTheWorkspace": [ { "id": "user_789", "email": "test-1@firm.filed.com", "name": null, "role": "user" }, { "id": "user_790", "email": "test-2@firm.filed.com", "name": null, "role": "user" } ] } } ``` #### Implementation notes * No email invitations are sent - users are simply added to the workspace * If a user already exists, they will be added to the workspace without creating a duplicate account * You can add multiple users in a single request * Add users to the specific workspace that corresponds to their tax firm * After being added, users can sign in using their email address and access the workspace through deep links *** ### Step 5: Set up your integration with Filed Complete the integration setup between your platform and Filed by establishing a provider connection. This enables Filed to communicate with your platform using the credentials and configuration you provide. **Implementation Details**: The specific implementation of what happens on Filed's side when `startProviderConnection` and `verifyProviderConnection` are called will be discussed and coordinated between your team and Filed's engineering team during the integration process. The integration process consists of two steps: 1. **Start Provider Connection**: This is used to setup a connection between your platform and Filed. You can pass in credentials for the specific tax firm based on what our engineering teams agree on. The idea is after you call this, in Filed's integration section, the integration with your name on it will start showing up as pending. 2. **Verify Provider Connection**: This is where Filed will use the credentials and details you provided and call an endpoint at your end to test the connection between you and us. If this call succeeds, we will now start showing on our end that Filed and your platform has successfully connected. You can now start using the connection ID (the `id` field from the response) to send the tax prep to us. #### Step 5a: Start Provider Connection This is used to setup a connection between your platform and Filed. You can pass in credentials for the specific tax firm based on what our engineering teams agree on. The idea is after you call this, in Filed's integration section, the integration with your name on it will start showing up as pending. For complete API reference, see [Start provider connection](/legacy/apis/endpoint/start-provider-connection). **Partner Key**: Filed will provide you with a unique partner key identifier for your platform. Examples: * If Google was our partner we would use: `google` * Your specific partner key will be provided by Filed during partner onboarding ##### GraphQL mutation ```graphql GraphQL theme={null} mutation StartProviderConnection($workspaceId: ID!, $providerKey: String!, $inputs: JSON) { startProviderConnection(workspaceId: $workspaceId, providerKey: $providerKey, inputs: $inputs) { id workspaceId providerKey status action displayName createdAt } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: " \ -d '{ "query": "mutation StartProviderConnection($workspaceId: ID!, $providerKey: String!, $inputs: JSON) { startProviderConnection(workspaceId: $workspaceId, providerKey: $providerKey, inputs: $inputs) { id workspaceId providerKey status action displayName createdAt } }", "variables": { "workspaceId": "workspace_123456", "providerKey": "", "inputs": { "apiKey": "firm-specific-api-key-12345", "securityToken": "firm-security-token-abc", "baseUrl": "https://api.canopy.com" } } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation StartProviderConnection($workspaceId: ID!, $providerKey: String!, $inputs: JSON) { startProviderConnection(workspaceId: $workspaceId, providerKey: $providerKey, inputs: $inputs) { id workspaceId providerKey status action displayName createdAt } } `, variables: { workspaceId: 'workspace_123456', providerKey: '', inputs: { apiKey: 'firm-specific-api-key-12345', securityToken: 'firm-security-token-abc', baseUrl: 'https://api.canopy.com' } } }) }); const { data } = await response.json(); const connection = data.startProviderConnection; ``` ```json Response theme={null} { "data": { "startProviderConnection": { "id": "connection_789", "connectionId": "connection_789", "workspaceId": "workspace_123456", "providerKey": "", "status": "pending", "action": "collect", "displayName": null, "createdAt": "2024-01-15T10:30:00Z" } } } ``` ##### Implementation notes * **workspaceId**: The ID of the workspace (tax firm) you're setting up the integration for * **providerKey**: Your partner key provided by Filed (e.g., ``, `qount`) * **inputs**: Optional JSON object containing firm-specific credentials and configuration: * `apiKey`: Firm's API key for your platform * `securityToken`: Security token or authentication token * `baseUrl`: Base URL for API calls (if different from default) * Any other credentials or configuration your platform requires * Save the `id` from the response - you'll need it for the collect step * The connection status will be `pending` until you complete the collect step *** #### Step 5b: Verify Provider Connection This is where Filed will use the credentials and details you provided and call an endpoint at your end to test the connection between you and us. If this call succeeds, we will now start showing on our end that Filed and your platform has successfully connected. You can now start using the connectionId to send the tax prep to us. For complete API reference, see [Verify provider connection](/legacy/apis/endpoint/verify-provider-connection). **Deprecated**: The `collectProviderConnection` mutation is deprecated but will continue to work. It's recommended to use `verifyProviderConnection` instead, as it's the preferred method going forward. ##### GraphQL mutation ```graphql GraphQL theme={null} mutation VerifyProviderConnection($connectionId: ID!, $inputs: JSON) { verifyProviderConnection(connectionId: $connectionId, inputs: $inputs) { id workspaceId providerKey status displayName settings updatedAt } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: " \ -d '{ "query": "mutation VerifyProviderConnection($connectionId: ID!, $inputs: JSON) { verifyProviderConnection(connectionId: $connectionId, inputs: $inputs) { id workspaceId providerKey status displayName settings updatedAt } }", "variables": { "connectionId": "connection_789", "inputs": { "firmReferenceId": "firm-ref-12345", "additionalConfig": { "timezone": "America/New_York", "region": "US" } } } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation VerifyProviderConnection($connectionId: ID!, $inputs: JSON) { verifyProviderConnection(connectionId: $connectionId, inputs: $inputs) { id workspaceId providerKey status displayName settings updatedAt } } `, variables: { connectionId: 'connection_789', inputs: { firmReferenceId: 'firm-ref-12345', additionalConfig: { timezone: 'America/New_York', region: 'US' } } } }) }); const { data } = await response.json(); const connection = data.verifyProviderConnection; ``` ```json Response theme={null} { "data": { "verifyProviderConnection": { "id": "connection_789", "workspaceId": "workspace_123456", "providerKey": "", "status": "active", "displayName": "Canopy Integration", "settings": { "firmReferenceId": "firm-ref-12345", "additionalConfig": { "timezone": "America/New_York", "region": "US" } }, "updatedAt": "2024-01-15T10:35:00Z" } } } ``` ##### Implementation notes * **connectionId**: The connection ID (the `id` field) returned from the `startProviderConnection` mutation * **inputs**: Optional JSON object containing additional configuration: * `firmReferenceId`: Your internal reference ID for the tax firm (useful for correlation) * Any other parameters you want Filed to store for this tax firm * Configuration that helps Filed communicate with your platform * Filed will use the credentials from the start step to make an API call to your platform * If the API call succeeds, Filed marks the connection as `active` * If the API call fails, the connection status will reflect the error state **Connection Status**: After successful collection, the connection status will be `active`, indicating that Filed can successfully communicate with your platform using the provided credentials. **Security**: Ensure that the credentials you provide in the `startProviderConnection` mutation are valid and have the necessary permissions for Filed to make API calls to your platform on behalf of the tax firm. *** ### Step 6: Generate deep links and redirect URIs Generate deep links that seamlessly redirect tax firm users to Filed. These links can be used at any time to send users from a specific tax firm to their workspace and specific pages within Filed. **Important**: Always use the workspace slug format (`/w/{workspaceSlug}/`) when available to ensure customers are directed to the correct workspace. This prevents routing errors and ensures proper workspace isolation. For detailed information about deep link formats, common redirect paths, and implementation examples, see the [Deep links API reference](/legacy/apis/endpoint/deep-links). *** ## Complete Integration Example Here's a complete example showing all steps together: ```javascript JavaScript theme={null} const GRAPHQL_ENDPOINT = 'https://gateway.filed.com/graphql'; // Step 1: Authenticate const authResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'source-platform': '' }, body: JSON.stringify({ query: ` mutation LoginViaPersonalApiToken($token: String!) { loginViaPersonalApiToken(token: $token) { accessToken } } `, variables: { token: process.env.FILED_API_TOKEN } }) }); const { accessToken } = (await authResponse.json()).data.loginViaPersonalApiToken; // Step 2: Verify Connection const meResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` query Me { me { id name } } ` }) }); const { data: meData } = await meResponse.json(); console.log('✓ API key verified. Current user:', meData.me); // Step 3: Create Workspace const workspaceResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation CreateWorkspace($input: CreateWorkspaceInput!) { createWorkspace(input: $input) { id slug displayName } } `, variables: { input: { displayName: 'Smith & Associates Tax Firm', firmSize: 'md', metadata: { customerId: 'customer-123' } } } }) }); const { id: workspaceId, slug: workspaceSlug } = (await workspaceResponse.json()).data.createWorkspace; // Step 4: Add Tax Firm Users const addUsersResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation AddUsersToWorkspace($workspaceId: ID!, $userEmails: [String!]!) { addUsersToTheWorkspace(workspaceId: $workspaceId, userEmails: $userEmails) { id email } } `, variables: { workspaceId, userEmails: ['test-1@firm.filed.com', 'test-2@firm.filed.com'] } }) }); // Step 5a: Start Provider Connection const startConnectionResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation StartProviderConnection($workspaceId: ID!, $providerKey: String!, $inputs: JSON) { startProviderConnection(workspaceId: $workspaceId, providerKey: $providerKey, inputs: $inputs) { id status } } `, variables: { workspaceId, providerKey: '', // Your partner key inputs: { apiKey: 'firm-specific-api-key-12345', securityToken: 'firm-security-token-abc' } } }) }); const { id: connectionId } = (await startConnectionResponse.json()).data.startProviderConnection; // Step 5b: Verify Provider Connection const verifyConnectionResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation VerifyProviderConnection($connectionId: ID!, $inputs: JSON) { verifyProviderConnection(connectionId: $connectionId, inputs: $inputs) { id status } } `, variables: { connectionId, inputs: { firmReferenceId: 'firm-ref-12345' } } }) }); const { status: connectionStatus } = (await verifyConnectionResponse.json()).data.verifyProviderConnection; console.log(`✓ Integration connection status: ${connectionStatus}`); // Step 6: Generate Magic Link (using workspace slug - recommended) const magicLink = `https://app.filed.com/sign-in?deep_link=/w/${workspaceSlug}`; console.log('Send this link to the tax firm users:', magicLink); ``` ```typescript TypeScript theme={null} const GRAPHQL_ENDPOINT = 'https://gateway.filed.com/graphql'; interface AuthResponse { data: { loginViaPersonalApiToken: { accessToken: string; }; }; } interface WorkspaceResponse { data: { createWorkspace: { id: string; slug: string; displayName: string; }; }; } // Step 1: Authenticate const authResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'source-platform': '' }, body: JSON.stringify({ query: ` mutation LoginViaPersonalApiToken($token: String!) { loginViaPersonalApiToken(token: $token) { accessToken } } `, variables: { token: process.env.FILED_API_TOKEN } }) }); const { accessToken } = (await authResponse.json() as AuthResponse).data.loginViaPersonalApiToken; // Step 2: Verify Connection const meResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` query Me { me { id name } } ` }) }); const { data: meData } = await meResponse.json() as { data: { me: { id: string; name: string | null } } }; console.log('✓ API key verified. Current user:', meData.me); // Step 3: Create Workspace const workspaceResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation CreateWorkspace($input: CreateWorkspaceInput!) { createWorkspace(input: $input) { id slug displayName } } `, variables: { input: { displayName: 'Smith & Associates Tax Firm', firmSize: 'md', metadata: { customerId: 'customer-123' } } } }) }); const { id: workspaceId, slug: workspaceSlug } = (await workspaceResponse.json() as WorkspaceResponse).data.createWorkspace; // Step 4: Add Tax Firm Users await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation AddUsersToWorkspace($workspaceId: ID!, $userEmails: [String!]!) { addUsersToTheWorkspace(workspaceId: $workspaceId, userEmails: $userEmails) { id email } } `, variables: { workspaceId, userEmails: ['test-1@firm.filed.com', 'test-2@firm.filed.com'] } }) }); // Step 5a: Start Provider Connection interface ConnectionResponse { data: { startProviderConnection: { connectionId: string; status: string; }; }; } const startConnectionResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation StartProviderConnection($workspaceId: ID!, $providerKey: String!, $inputs: JSON) { startProviderConnection(workspaceId: $workspaceId, providerKey: $providerKey, inputs: $inputs) { id status } } `, variables: { workspaceId, providerKey: '', // Your partner key inputs: { apiKey: 'firm-specific-api-key-12345', securityToken: 'firm-security-token-abc' } } }) }); const { id: connectionId } = (await startConnectionResponse.json() as ConnectionResponse).data.startProviderConnection; // Step 5b: Verify Provider Connection const verifyConnectionResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': '' }, body: JSON.stringify({ query: ` mutation VerifyProviderConnection($connectionId: ID!, $inputs: JSON) { verifyProviderConnection(connectionId: $connectionId, inputs: $inputs) { status } } `, variables: { connectionId, inputs: { firmReferenceId: 'firm-ref-12345' } } }) }); const { status: connectionStatus } = (await verifyConnectionResponse.json() as { data: { verifyProviderConnection: { status: string } } }).data.verifyProviderConnection; console.log(`✓ Integration connection status: ${connectionStatus}`); // Step 6: Generate Magic Link (using workspace slug - recommended) const magicLink = `https://app.filed.com/sign-in?deep_link=/w/${workspaceSlug}`; console.log('Send this link to the tax firm users:', magicLink); ``` ```python Python theme={null} import os import requests from typing import Dict, Any GRAPHQL_ENDPOINT = 'https://gateway.filed.com/graphql' # Step 1: Authenticate auth_query = """ mutation LoginViaPersonalApiToken($token: String!) { loginViaPersonalApiToken(token: $token) { accessToken } } """ auth_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'source-platform': '' }, json={ 'query': auth_query, 'variables': {'token': os.environ.get('FILED_API_TOKEN')} } ) access_token = auth_response.json()['data']['loginViaPersonalApiToken']['accessToken'] # Step 2: Verify Connection me_query = """ query Me { me { id name } } """ me_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'Authorization': f'Bearer {access_token}', 'source-platform': '' }, json={'query': me_query} ) me_data = me_response.json()['data']['me'] print(f'✓ API key verified. Current user: {me_data}') # Step 3: Create Workspace create_workspace_query = """ mutation CreateWorkspace($input: CreateWorkspaceInput!) { createWorkspace(input: $input) { id slug displayName } } """ workspace_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'Authorization': f'Bearer {access_token}', 'source-platform': '' }, json={ 'query': create_workspace_query, 'variables': { 'input': { 'displayName': 'Smith & Associates Tax Firm', 'firmSize': 'md', 'metadata': { 'customerId': 'customer-123' } } } } ) workspace_data = workspace_response.json()['data']['createWorkspace'] workspace_id = workspace_data['id'] workspace_slug = workspace_data['slug'] # Step 4: Add Tax Firm Users add_users_query = """ mutation AddUsersToWorkspace($workspaceId: ID!, $userEmails: [String!]!) { addUsersToTheWorkspace(workspaceId: $workspaceId, userEmails: $userEmails) { id email } } """ requests.post( GRAPHQL_ENDPOINT, headers={ 'Authorization': f'Bearer {access_token}', 'source-platform': '' }, json={ 'query': add_users_query, 'variables': { 'workspaceId': workspace_id, 'userEmails': ['test-1@firm.filed.com', 'test-2@firm.filed.com'] } } ) # Step 5a: Start Provider Connection start_connection_query = """ mutation StartProviderConnection($workspaceId: ID!, $providerKey: String!, $inputs: JSON) { startProviderConnection(workspaceId: $workspaceId, providerKey: $providerKey, inputs: $inputs) { id status } } """ start_connection_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'Authorization': f'Bearer {access_token}', 'source-platform': '' }, json={ 'query': start_connection_query, 'variables': { 'workspaceId': workspace_id, 'providerKey': '', # Your partner key 'inputs': { 'apiKey': 'firm-specific-api-key-12345', 'securityToken': 'firm-security-token-abc' } } } ) connection_id = start_connection_response.json()['data']['startProviderConnection']['id'] # Step 5b: Verify Provider Connection verify_connection_query = """ mutation VerifyProviderConnection($connectionId: ID!, $inputs: JSON) { verifyProviderConnection(connectionId: $connectionId, inputs: $inputs) { status } } """ verify_connection_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'Authorization': f'Bearer {access_token}', 'source-platform': '' }, json={ 'query': verify_connection_query, 'variables': { 'connectionId': connection_id, 'inputs': { 'firmReferenceId': 'firm-ref-12345' } } } ) connection_status = verify_connection_response.json()['data']['verifyProviderConnection']['status'] print(f'✓ Integration connection status: {connection_status}') # Step 6: Generate Magic Link (using workspace slug - recommended) magic_link = f'https://app.filed.com/sign-in?deep_link=/w/{workspace_slug}' print(f'Send this link to the tax firm users: {magic_link}') ``` *** ## Best Practices ### Security 1. **Never expose API keys**: Store API keys in environment variables or secure secret management systems 2. **Use HTTPS**: Always make API requests over HTTPS 3. **Validate inputs**: Always validate user inputs before making API calls ### Error handling 1. **Handle authentication errors**: Implement retry logic for expired tokens 2. **Validate responses**: Check for errors in GraphQL responses 3. **Log errors**: Maintain error logs for debugging and monitoring ### User experience 1. **Provide clear instructions**: Guide tax firm users on what to expect when clicking deep links 2. **Handle edge cases**: Account for users who may already have Filed accounts 3. **Monitor onboarding**: Track tax firm user activation rates and identify bottlenecks 4. **Workspace isolation**: Ensure users from different tax firms are directed to their correct workspaces *** ## Support For additional support or questions: * **Slack**: Contact your dedicated partner Slack channel * **Email**: Reach out to your Filed account representative * **Documentation**: Check our [API documentation](/docs/apis/introduction) for more details *** ## Next Steps After completing the integration: 1. **Review API reference**: Check out the detailed [API reference](/legacy/apis/endpoint/login-via-personal-api-token) for complete endpoint documentation 2. **Test the flow**: Create a test workspace for a tax firm customer and verify all steps work correctly 3. **Monitor usage**: Track API usage and tax firm onboarding metrics per workspace 4. **Iterate**: Gather feedback and refine your integration based on tax firm needs 5. **Scale**: Automate the process to handle multiple tax firm customers efficiently 6. **Manage multiple workspaces**: Implement systems to track and manage workspaces for all your tax firm customers # Tax prep integration guide Source: https://docs.apps.filed.com/legacy/apis/guides/tax-prep Learn how to create tax preparations in Filed by pushing clients from your platform or allowing Filed to pull clients This guide covers two integration patterns for creating tax preparations in Filed: pushing clients from your platform to Filed, and allowing Filed to pull clients from your platform. **Integration Pattern Selection**: Choose the pattern that best fits your workflow. Pushing clients gives you full control over when and what data is sent. Pulling allows Filed to discover and fetch clients on-demand. ## Prerequisites Before you begin, ensure you have: * Completed the [partner integration guide](/legacy/apis/guides/partner) or [customer integration guide](/legacy/apis/guides/customer) setup * Active provider connection with status `active` * GraphQL endpoint: `https://gateway.filed.com/graphql` * Access token from API authentication **Integration Pattern Availability**: * **Push clients to Filed**: Available for both [partners](/legacy/apis/guides/partner) and [customers](/legacy/apis/guides/customer) * **Filed pulls clients**: Only available for [partners](/legacy/apis/guides/partner) If you haven't set up your provider connection yet, follow the [partner integration guide](/legacy/apis/guides/partner) to complete the connection setup first. ## Overview There are two ways to create tax preparations in Filed: 1. **Push clients to Filed**: You control when and what data is sent to Filed * Create a connection job to batch process clients * Attach clients (works) to the job * Attach files (artifacts) to each client * Trigger the import to process all clients 2. **Filed pulls clients**: Filed discovers and fetches clients from your platform * Filed queries your platform for clients * Filed queries files for each client * Filed downloads files as needed * Filed can upload files back to clients ## Integration Pattern 1: Pushing Clients to Filed This pattern gives you full control over when and what data is sent to Filed. You create connection jobs, attach clients and files, then trigger the import. ### Step 1: Create connection job Create an import job that groups related tax preparations together. A connection job can be triggered to process all attached clients. For complete API reference, see [Create connection job](/legacy/apis/endpoint/create-connection-job). #### GraphQL mutation ```graphql GraphQL theme={null} mutation CreateConnectionJob($connectionId: ID!, $jobName: String!) { createConnectionJob(connectionId: $connectionId, jobName: $jobName) { id connectionId workspaceId name status createdAt } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation CreateConnectionJob($connectionId: ID!, $jobName: String!) { createConnectionJob(connectionId: $connectionId, jobName: $jobName) { id connectionId workspaceId name status createdAt } }", "variables": { "connectionId": "connection_789", "jobName": "2024 Tax Season Import" } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'my-sample-platform' }, body: JSON.stringify({ query: ` mutation CreateConnectionJob($connectionId: ID!, $jobName: String!) { createConnectionJob(connectionId: $connectionId, jobName: $jobName) { id connectionId workspaceId name status createdAt } } `, variables: { connectionId: 'connection_789', jobName: '2024 Tax Season Import' } }) }); const { data } = await response.json(); const connectionJob = data.createConnectionJob; ``` ```python Python theme={null} import requests response = requests.post( 'https://gateway.filed.com/graphql', headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {access_token}', 'source-platform': 'my-sample-platform' }, json={ 'query': ''' mutation CreateConnectionJob($connectionId: ID!, $jobName: String!) { createConnectionJob(connectionId: $connectionId, jobName: $jobName) { id connectionId workspaceId name status createdAt } } ''', 'variables': { 'connectionId': 'connection_789', 'jobName': '2024 Tax Season Import' } } ) connection_job = response.json()['data']['createConnectionJob'] ``` ```json Response theme={null} { "data": { "createConnectionJob": { "id": "job_123456", "connectionId": "connection_789", "workspaceId": "workspace_123456", "name": "2024 Tax Season Import", "status": "pending", "createdAt": "2024-01-15T10:30:00Z" } } } ``` #### Implementation notes * Save the `id` from the response - you'll need it for creating works * Use descriptive job names to identify batches (e.g., "2024 Tax Season Import", "Q1 Client Batch") * Connection jobs group related tax preparations together for batch processing *** ### Step 2: Create works (attach clients) Attach clients to the connection job. Each work represents a tax preparation for a specific client. For complete API reference, see [Create job works](/legacy/apis/endpoint/create-works). #### GraphQL mutation ```graphql GraphQL theme={null} mutation CreateJobWorks($connectionJobId: ID!, $inputs: [CreateJobWorksInput!]!) { createJobWorks(connectionJobId: $connectionJobId, inputs: $inputs) { id createdAt updatedAt workspaceId workId connectionJobId } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation CreateJobWorks($connectionJobId: ID!, $inputs: [CreateJobWorksInput!]!) { createJobWorks(connectionJobId: $connectionJobId, inputs: $inputs) { id createdAt updatedAt workspaceId workId connectionJobId } }", "variables": { "connectionJobId": "job_123456", "inputs": [ { "name": "John Smith - 2024 Tax Return", "externalId": "client-12345" }, { "name": "Jane Doe - 2024 Tax Return", "externalId": "client-67890" } ] } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'my-sample-platform' }, body: JSON.stringify({ query: ` mutation CreateJobWorks($connectionJobId: ID!, $inputs: [CreateJobWorksInput!]!) { createJobWorks(connectionJobId: $connectionJobId, inputs: $inputs) { id createdAt updatedAt workspaceId workId connectionJobId } } `, variables: { connectionJobId: 'job_123456', inputs: [ { name: 'John Smith - 2024 Tax Return', externalId: 'client-12345' }, { name: 'Jane Doe - 2024 Tax Return', externalId: 'client-67890' } ] } }) }); const { data } = await response.json(); const jobWorks = data.createJobWorks; ``` ```python Python theme={null} response = requests.post( 'https://gateway.filed.com/graphql', headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {access_token}', 'source-platform': 'my-sample-platform' }, json={ 'query': ''' mutation CreateJobWorks($connectionJobId: ID!, $inputs: [CreateJobWorksInput!]!) { createJobWorks(connectionJobId: $connectionJobId, inputs: $inputs) { id createdAt updatedAt workspaceId workId connectionJobId } } ''', 'variables': { 'connectionJobId': 'job_123456', 'inputs': [ { 'name': 'John Smith - 2024 Tax Return', 'externalId': 'client-12345' }, { 'name': 'Jane Doe - 2024 Tax Return', 'externalId': 'client-67890' } ] } } ) job_works = response.json()['data']['createJobWorks'] ``` ```json Response theme={null} { "data": { "createJobWorks": [ { "id": "job_work_789", "createdAt": "2024-01-15T10:35:00Z", "updatedAt": "2024-01-15T10:35:00Z", "workspaceId": "workspace_123456", "workId": "work_789", "connectionJobId": "job_123456" }, { "id": "job_work_790", "createdAt": "2024-01-15T10:35:00Z", "updatedAt": "2024-01-15T10:35:00Z", "workspaceId": "workspace_123456", "workId": "work_790", "connectionJobId": "job_123456" } ] } } ``` #### Implementation notes * Save the `id` for each job work - you'll need it for attaching files * Save the `workId` to reference the underlying work * The `externalId` in the input should match your platform's client identifier for correlation * You can create multiple job works in a single request * Use descriptive names that identify the client and tax year *** ### Step 3: Create work artifacts (attach files) Attach files to each work. Artifacts represent documents, forms, or other files associated with a tax preparation. Before creating artifacts, you must first upload files to Filed's upload endpoint. For complete API reference, see [Create job work artifacts](/legacy/apis/endpoint/create-job-work-artifacts). #### Step 3a: Upload file to Filed First, upload your file to Filed's upload endpoint. This returns a `fileUrl` that you'll use when creating the artifact. You can upload files using either multipart/form-data or by providing a URL. **Important**: Partners and customers use **different upload endpoints**. Make sure you're using the correct endpoint for your integration type. **Partners**: Scroll down to the "FOR PARTNERS: Partner Upload Endpoint" section below. You **must** use the Partner Upload endpoint (`https://partner-upload.filed.com/upload`) - do NOT use the upload-proxy endpoint shown in the examples below. **Customers**: The examples below use the upload-proxy endpoint which is for customers. Partners have a separate endpoint - see the Partner Upload section below. ##### Option 1: Upload file using multipart/form-data ```bash cURL theme={null} curl --request POST \ --url https://upload-proxy.filed.com/upload \ --header 'Content-Type: multipart/form-data' \ --form file=@/path/to/your/file.pdf \ --form 'fileName=Phil Ed 2024 Return.PDF' ``` ```javascript JavaScript theme={null} const formData = new FormData(); formData.append('file', fileBlob, 'Phil Ed 2024 Return.PDF'); formData.append('fileName', 'Phil Ed 2024 Return.PDF'); const response = await fetch('https://upload-proxy.filed.com/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}` }, body: formData }); const uploadResult = await response.json(); // Returns: { fileUrl, fileName, mimeType } ``` ```python Python theme={null} import requests files = { 'file': ('Phil Ed 2024 Return.PDF', open('/path/to/file.pdf', 'rb'), 'application/pdf') } data = { 'fileName': 'Phil Ed 2024 Return.PDF' } response = requests.post( 'https://upload-proxy.filed.com/upload', headers={ 'Authorization': f'Bearer {access_token}' }, files=files, data=data ) upload_result = response.json() # Returns: { fileUrl, fileName, mimeType } ``` ```json Response theme={null} { "fileUrl": "https://upload.filed.com/files/305b8a45793a2d0fd12f67d4d977e686", "fileName": "Phil Ed 2024 Return.PDF", "mimeType": "application/pdf" } ``` ##### Option 2: Upload file using URL You can upload files by providing a URL to the file. The `mimeType` parameter is optional. ```bash cURL theme={null} curl --request POST \ --url https://upload-proxy.filed.com/upload \ --header 'Content-Type: multipart/form-data' \ --form 'fileName=document.pdf' \ --form 'url=https://example.com/file.pdf' \ --form 'mimeType=application/pdf' ``` ```javascript JavaScript theme={null} const formData = new FormData(); formData.append('fileName', 'document.pdf'); formData.append('url', 'https://example.com/file.pdf'); formData.append('mimeType', 'application/pdf'); // Optional const response = await fetch('https://upload-proxy.filed.com/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}` }, body: formData }); const uploadResult = await response.json(); // Returns: { fileUrl, fileName, mimeType } ``` ```python Python theme={null} import requests data = { 'fileName': 'document.pdf', 'url': 'https://example.com/file.pdf', 'mimeType': 'application/pdf' # Optional } response = requests.post( 'https://upload-proxy.filed.com/upload', headers={ 'Authorization': f'Bearer {access_token}' }, data=data ) upload_result = response.json() # Returns: { fileUrl, fileName, mimeType } ``` ```json Response theme={null} { "fileUrl": "https://upload.filed.com/files/305b8a45793a2d0fd12f67d4d977e686", "fileName": "document.pdf", "mimeType": "application/pdf" } ``` **URL Upload**: When uploading via URL, the `mimeType` parameter is optional. If not provided, Filed will attempt to detect the MIME type from the file URL or file content. *** ##### **FOR PARTNERS: Partner Upload Endpoint** (Required) **Partners Must Use This Endpoint**: If you're integrating as a partner, you **must** use the **Partner Upload endpoint** (`https://partner-upload.filed.com/upload`). Do NOT use the upload-proxy endpoint shown below. The Partner Upload endpoint is specifically designed for partner integrations and uses your partner JWT token for authentication. **Endpoint**: `https://partner-upload.filed.com/upload` **Authentication**: Use your partner JWT token (not your access token) **Upload file directly:** ```bash cURL theme={null} curl -X POST \ -H "Authorization: Bearer " \ -F "file=@./document.pdf" \ -F "fileName=document.pdf" \ -F "mimeType=application/pdf" \ https://partner-upload.filed.com/upload ``` ```javascript JavaScript theme={null} const formData = new FormData(); formData.append('file', fileBlob, 'document.pdf'); formData.append('fileName', 'document.pdf'); formData.append('mimeType', 'application/pdf'); const response = await fetch('https://partner-upload.filed.com/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${partnerJwtToken}` }, body: formData }); const uploadResult = await response.json(); // Returns: { fileUrl, fileName, mimeType } ``` ```python Python theme={null} import requests files = { 'file': ('document.pdf', open('./document.pdf', 'rb'), 'application/pdf') } data = { 'fileName': 'document.pdf', 'mimeType': 'application/pdf' } response = requests.post( 'https://partner-upload.filed.com/upload', headers={ 'Authorization': f'Bearer {partner_jwt_token}' }, files=files, data=data ) upload_result = response.json() # Returns: { fileUrl, fileName, mimeType } ``` You can also upload from a URL using the Partner Upload endpoint: ```bash theme={null} curl -X POST \ -H "Authorization: Bearer " \ -F "url=https://example.com/file.pdf" \ -F "fileName=document.pdf" \ -F "mimeType=application/pdf" \ https://partner-upload.filed.com/upload ``` **Partner-specific endpoint**: The Partner Upload endpoint uses partner-specific JWT tokens with scoped permissions, providing better security than the upload-proxy endpoint. *** ##### **FOR CUSTOMERS: Upload-proxy Endpoint** **Customers**: If you're integrating as a customer, you can use the `upload-proxy` endpoint (`https://upload-proxy.filed.com/upload`) shown in the examples above. This endpoint uses your access token for authentication. **Partners**: Partners should NOT use the upload-proxy endpoint. Partners must use the Partner Upload endpoint (`https://partner-upload.filed.com/upload`) shown in the section above. #### Step 3b: Create work artifacts After uploading the file, use the returned values to create work artifacts. Map the upload response fields to the artifact input: * Upload response `mimeType` → Artifact `type` * Upload response `fileName` → Artifact `name` * Upload response `fileUrl` → Artifact `url` ```graphql GraphQL theme={null} mutation CreateJobWorkArtifacts($jobWorkId: ID!, $inputs: [CreateJobWorkArtifactsInput!]!) { createJobWorkArtifacts(jobWorkId: $jobWorkId, inputs: $inputs) { id createdAt updatedAt workspaceId artifactId connectionJobId } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation CreateJobWorkArtifacts($jobWorkId: ID!, $inputs: [CreateJobWorkArtifactsInput!]!) { createJobWorkArtifacts(jobWorkId: $jobWorkId, inputs: $inputs) { id createdAt updatedAt workspaceId artifactId connectionJobId } }", "variables": { "jobWorkId": "job_work_789", "inputs": [ { "name": "Phil Ed 2024 Return.PDF", "type": "application/pdf", "url": "https://upload.filed.com/files/305b8a45793a2d0fd12f67d4d977e686", "externalId": "file-12345" } ] } }' ``` ```javascript JavaScript theme={null} // After uploading file and getting uploadResult const uploadResult = { fileUrl: "https://upload.filed.com/files/305b8a45793a2d0fd12f67d4d977e686", fileName: "Phil Ed 2024 Return.PDF", mimeType: "application/pdf" }; const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'my-sample-platform' }, body: JSON.stringify({ query: ` mutation CreateJobWorkArtifacts($jobWorkId: ID!, $inputs: [CreateJobWorkArtifactsInput!]!) { createJobWorkArtifacts(jobWorkId: $jobWorkId, inputs: $inputs) { id createdAt updatedAt workspaceId artifactId connectionJobId } } `, variables: { jobWorkId: 'job_work_789', inputs: [ { name: uploadResult.fileName, type: uploadResult.mimeType, url: uploadResult.fileUrl, externalId: 'file-12345' } ] } }) }); const { data } = await response.json(); const jobArtifacts = data.createJobWorkArtifacts; ``` ```python Python theme={null} # After uploading file and getting upload_result upload_result = { 'fileUrl': 'https://upload.filed.com/files/305b8a45793a2d0fd12f67d4d977e686', 'fileName': 'Phil Ed 2024 Return.PDF', 'mimeType': 'application/pdf' } response = requests.post( 'https://gateway.filed.com/graphql', headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {access_token}', 'source-platform': 'my-sample-platform' }, json={ 'query': ''' mutation CreateJobWorkArtifacts($jobWorkId: ID!, $inputs: [CreateJobWorkArtifactsInput!]!) { createJobWorkArtifacts(jobWorkId: $jobWorkId, inputs: $inputs) { id createdAt updatedAt workspaceId artifactId connectionJobId } } ''', 'variables': { 'jobWorkId': 'job_work_789', 'inputs': [ { 'name': upload_result['fileName'], 'type': upload_result['mimeType'], 'url': upload_result['fileUrl'], 'externalId': 'file-12345' } ] } } ) job_artifacts = response.json()['data']['createJobWorkArtifacts'] ``` ```json Response theme={null} { "data": { "createJobWorkArtifacts": [ { "id": "job_artifact_456", "createdAt": "2024-01-15T10:40:00Z", "updatedAt": "2024-01-15T10:40:00Z", "workspaceId": "workspace_123456", "artifactId": "artifact_456", "connectionJobId": "job_123456" } ] } } ``` #### Implementation notes * **Upload first**: You must upload files to Filed's upload endpoint before creating artifacts * **Field mapping**: Map upload response fields to artifact inputs: * `mimeType` → `type` * `fileName` → `name` * `fileUrl` → `url` * **Use descriptive types**: Use the MIME type from the upload response (e.g., "application/pdf", "image/jpeg") * **Multiple files**: Upload each file separately, then create artifacts for each uploaded file * **File URL**: The `fileUrl` from the upload response is what Filed uses to access the file **Field Mapping**: When creating artifacts, map the upload response fields as follows: * Upload `mimeType` → Artifact `type` * Upload `fileName` → Artifact `name` * Upload `fileUrl` → Artifact `url` *** ### Step 4: Initiate tax preps import After creating the connection job, attaching works, and adding artifacts, initiate the tax prep import to process all clients in the job. This triggers Filed to parse and import all tax preparations associated with the connection job. For complete API reference, see [Initiate tax preps import](/legacy/apis/endpoint/initiate-tax-preps-import). #### GraphQL mutation ```graphql GraphQL theme={null} mutation InitiateTaxPrepsImport($connectionJobId: ID!) { initiateTaxPrepsImport(connectionJobId: $connectionJobId) { id connectionId workspaceId name status createdAt updatedAt metadata } } ``` ```bash cURL theme={null} curl -X POST https://gateway.filed.com/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "source-platform: my-sample-platform" \ -d '{ "query": "mutation InitiateTaxPrepsImport($connectionJobId: ID!) { initiateTaxPrepsImport(connectionJobId: $connectionJobId) { id connectionId workspaceId name status createdAt updatedAt metadata } }", "variables": { "connectionJobId": "job_123456" } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://gateway.filed.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'my-sample-platform' }, body: JSON.stringify({ query: ` mutation InitiateTaxPrepsImport($connectionJobId: ID!) { initiateTaxPrepsImport(connectionJobId: $connectionJobId) { id connectionId workspaceId name status createdAt updatedAt metadata } } `, variables: { connectionJobId: 'job_123456' } }) }); const { data } = await response.json(); const connectionJob = data.initiateTaxPrepsImport; ``` ```python Python theme={null} import requests response = requests.post( 'https://gateway.filed.com/graphql', headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {access_token}', 'source-platform': 'my-sample-platform' }, json={ 'query': ''' mutation InitiateTaxPrepsImport($connectionJobId: ID!) { initiateTaxPrepsImport(connectionJobId: $connectionJobId) { id connectionId workspaceId name status createdAt updatedAt metadata } } ''', 'variables': { 'connectionJobId': 'job_123456' } } ) connection_job = response.json()['data']['initiateTaxPrepsImport'] ``` ```json Response theme={null} { "data": { "initiateTaxPrepsImport": { "id": "job_123456", "connectionId": "connection_789", "workspaceId": "workspace_123456", "name": "2024 Tax Season Import", "status": "processing", "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T10:45:00Z", "metadata": null } } } ``` #### Implementation notes * **Prerequisites**: Ensure you have completed Steps 1-3 before initiating the import: * Created a connection job * Attached works (clients) to the job * Attached work artifacts (files) to each work * **Status monitoring**: Monitor the `status` field to track import progress * **Metadata**: Check the `metadata` field for progress information or error details * **Async processing**: The import process runs asynchronously; the status will update as processing progresses *** ## Integration Pattern 2: Filed Pulling Clients This pattern allows Filed to discover and fetch clients from your platform on-demand. For this integration pattern, you need to provide API documentation for **your platform's APIs** that Filed engineers will use to build the integration. **Important**: This section is about documenting **your platform's APIs**, not Filed's APIs. Filed engineers will use your API documentation to build the integration that pulls clients from your platform. ### Required API Documentation You need to provide comprehensive API documentation for the following endpoints that Filed will use: #### 1. Authentication documentation Document how Filed should authenticate to your platform's API. This should include: * Authentication method (API key, OAuth, Bearer token, etc.) * How to obtain credentials * How to include credentials in requests (headers, query parameters, etc.) * Token refresh process (if applicable) * Rate limiting and quotas * Error responses for authentication failures **Example**: If your platform uses API keys, document the header format (e.g., `Authorization: Bearer ` or `X-API-Key: `). If using OAuth, document the token endpoint and refresh flow. #### 2. List of clients Document your API endpoint that returns a list of clients. Include: * **Endpoint URL**: Full URL path (e.g., `GET /api/v1/clients`) * **Authentication**: How to authenticate the request * **Query parameters**: Pagination, filtering, sorting options * **Request format**: Headers, body (if POST) * **Response format**: JSON schema with all fields * **Response fields**: * Client ID (unique identifier) * Client name * Email * Phone * Type/category * Last modified date * Any other relevant fields * **Pagination**: How pagination works (offset/limit, cursor-based, etc.) * **Error handling**: Error codes and messages **Example Response Format**: Document the exact JSON structure your API returns, including field names, types, and whether fields are required or optional. #### 3. List files for a client Document your API endpoint that returns files for a specific client. Include: * **Endpoint URL**: Full URL path (e.g., `GET /api/v1/clients/{clientId}/files`) * **Authentication**: How to authenticate the request * **Path parameters**: Client ID format and requirements * **Query parameters**: Pagination, filtering, file type filters * **Request format**: Headers, body (if POST) * **Response format**: JSON schema with all fields * **Response fields**: * File ID (unique identifier) * File name * File size (in bytes) * MIME type * Download URL (how Filed can download the file) * Date created * Date modified * Any other relevant metadata * **Pagination**: How pagination works * **Error handling**: Error codes and messages **Download URL**: The download URL in your response must be accessible to Filed using the connection credentials. Document how Filed should use these URLs to download files. #### 4. Download file Document your API endpoint that allows Filed to download a file. Include: * **Endpoint URL**: Full URL path (e.g., `GET /api/v1/files/{fileId}/download`) * **Authentication**: How to authenticate the request * **Path parameters**: File ID format * **Request format**: Headers required * **Response format**: * HTTP status codes * File content (binary or base64) * Content-Type header * Content-Disposition header (if applicable) * **Error handling**: Error codes and messages * **File access**: How long download URLs remain valid (if using signed URLs) **File Access**: Ensure your download endpoints accept the same authentication credentials used for other API calls. Document any special requirements for file downloads (e.g., signed URLs, temporary tokens). #### 5. Post notes on the client Document your API endpoint that allows Filed to post notes or comments on a client. Include: * **Endpoint URL**: Full URL path (e.g., `POST /api/v1/clients/{clientId}/notes`) * **Authentication**: How to authenticate the request * **Path parameters**: Client ID format * **Request format**: * Headers (Content-Type, etc.) * Request body schema: * Note content/text * Note type/category (if applicable) * Author/user information (if applicable) * Timestamp (if not auto-generated) * **Response format**: JSON schema with created note details * **Response fields**: * Note ID * Note content * Created date * Author information * Any other relevant fields * **Error handling**: Error codes and messages **Note Format**: Document the exact format for notes, including any formatting requirements (markdown, plain text, HTML), character limits, and required vs optional fields. #### 6. Upload file to the client Document your API endpoint that allows Filed to upload files to a client. Include: * **Endpoint URL**: Full URL path (e.g., `POST /api/v1/clients/{clientId}/files`) * **Authentication**: How to authenticate the request * **Path parameters**: Client ID format * **Request format**: * Headers (Content-Type, Content-Length, etc.) * Request body: multipart/form-data or binary * File metadata (name, type, description, etc.) * **Upload method**: * Standard HTTP POST * TUS (resumable upload) protocol support * Chunked upload support * **Response format**: JSON schema with uploaded file details * **Response fields**: * File ID * File name * File URL * File size * Upload status * Any other relevant fields * **File size limits**: Maximum file size, chunk size (if chunked) * **Error handling**: Error codes and messages **TUS Protocol**: If Filed uses TUS (resumable upload) protocol, document your TUS endpoint implementation, including the TUS protocol version, supported extensions, and any custom headers or parameters required. ### Documentation Best Practices When providing API documentation for Filed engineers: 1. **Be comprehensive**: Include all details needed to make API calls successfully 2. **Provide examples**: Include cURL, HTTP request/response examples for each endpoint 3. **Document errors**: List all possible error codes and their meanings 4. **Include schemas**: Provide JSON schemas or OpenAPI/Swagger specifications 5. **Test credentials**: Provide test/sandbox credentials for Filed to test the integration 6. **Versioning**: Document API versioning strategy and how to specify API versions 7. **Rate limits**: Document rate limits, quotas, and throttling behavior 8. **Webhooks** (if applicable): Document any webhooks your platform supports for real-time updates **Contact**: Share your API documentation with your Filed account representative or integration team. They will coordinate with Filed engineers to build the integration using your documented APIs. *** ## Complete Integration Example Here's a complete example showing the push pattern workflow: ```javascript JavaScript theme={null} const GRAPHQL_ENDPOINT = 'https://gateway.filed.com/graphql'; const connectionId = 'connection_789'; // Step 1: Create connection job const createJobResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'my-sample-platform' }, body: JSON.stringify({ query: ` mutation CreateConnectionJob($connectionId: ID!, $jobName: String!) { createConnectionJob(connectionId: $connectionId, jobName: $jobName) { id } } `, variables: { connectionId, jobName: '2024 Tax Season Import' } }) }); const { id: connectionJobId } = (await createJobResponse.json()).data.createConnectionJob; // Step 2: Create job works (attach clients) const createJobWorksResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'my-sample-platform' }, body: JSON.stringify({ query: ` mutation CreateJobWorks($connectionJobId: ID!, $inputs: [CreateJobWorksInput!]!) { createJobWorks(connectionJobId: $connectionJobId, inputs: $inputs) { id workId } } `, variables: { connectionJobId, inputs: [ { name: 'John Smith - 2024 Tax Return', externalId: 'client-12345' } ] } }) }); const [{ id: jobWorkId }] = (await createJobWorksResponse.json()).data.createJobWorks; // Step 3: Create job work artifacts (attach files) await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'my-sample-platform' }, body: JSON.stringify({ query: ` mutation CreateJobWorkArtifacts($jobWorkId: ID!, $inputs: [CreateJobWorkArtifactsInput!]!) { createJobWorkArtifacts(jobWorkId: $jobWorkId, inputs: $inputs) { id } } `, variables: { jobWorkId, inputs: [ { name: 'W-2 Form 2024', type: 'w2', url: 'https://api.example.com/files/w2-2024.pdf', externalId: 'file-12345' } ] } }) }); // Step 4: Initiate tax preps import const initiateImportResponse = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'source-platform': 'my-sample-platform' }, body: JSON.stringify({ query: ` mutation InitiateTaxPrepsImport($connectionJobId: ID!) { initiateTaxPrepsImport(connectionJobId: $connectionJobId) { id status } } `, variables: { connectionJobId } }) }); const importResult = (await initiateImportResponse.json()).data.initiateTaxPrepsImport; console.log(`Tax prep import initiated. Status: ${importResult.status}`); ``` ```python Python theme={null} import requests GRAPHQL_ENDPOINT = 'https://gateway.filed.com/graphql' connection_id = 'connection_789' # Step 1: Create connection job create_job_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {access_token}', 'source-platform': 'my-sample-platform' }, json={ 'query': ''' mutation CreateConnectionJob($connectionId: ID!, $jobName: String!) { createConnectionJob(connectionId: $connectionId, jobName: $jobName) { id } } ''', 'variables': { 'connectionId': connection_id, 'jobName': '2024 Tax Season Import' } } ) connection_job_id = create_job_response.json()['data']['createConnectionJob']['id'] # Step 2: Create job works (attach clients) create_job_works_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {access_token}', 'source-platform': 'my-sample-platform' }, json={ 'query': ''' mutation CreateJobWorks($connectionJobId: ID!, $inputs: [CreateJobWorksInput!]!) { createJobWorks(connectionJobId: $connectionJobId, inputs: $inputs) { id workId } } ''', 'variables': { 'connectionJobId': connection_job_id, 'inputs': [ { 'name': 'John Smith - 2024 Tax Return', 'externalId': 'client-12345' } ] } } ) job_work_id = create_job_works_response.json()['data']['createJobWorks'][0]['id'] # Step 3: Create job work artifacts (attach files) requests.post( GRAPHQL_ENDPOINT, headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {access_token}', 'source-platform': 'my-sample-platform' }, json={ 'query': ''' mutation CreateJobWorkArtifacts($jobWorkId: ID!, $inputs: [CreateJobWorkArtifactsInput!]!) { createJobWorkArtifacts(jobWorkId: $jobWorkId, inputs: $inputs) { id } } ''', 'variables': { 'jobWorkId': job_work_id, 'inputs': [ { 'name': 'W-2 Form 2024', 'type': 'w2', 'url': 'https://api.example.com/files/w2-2024.pdf', 'externalId': 'file-12345' } ] } } ) # Step 4: Initiate tax preps import initiate_import_response = requests.post( GRAPHQL_ENDPOINT, headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {access_token}', 'source-platform': 'my-sample-platform' }, json={ 'query': ''' mutation InitiateTaxPrepsImport($connectionJobId: ID!) { initiateTaxPrepsImport(connectionJobId: $connectionJobId) { id status } } ''', 'variables': { 'connectionJobId': connection_job_id } } ) import_result = initiate_import_response.json()['data']['initiateTaxPrepsImport'] print(f'Tax prep import initiated. Status: {import_result["status"]}') ``` *** ## Best Practices ### Security 1. **Protect file URLs**: Ensure file URLs are only accessible with proper authentication 2. **Validate credentials**: Verify connection credentials have appropriate permissions 3. **Use HTTPS**: Always use HTTPS for file URLs and API endpoints 4. **Secure storage**: Store access tokens securely and never commit them to version control ### Error handling 1. **Handle failures gracefully**: Implement retry logic for transient failures 2. **Validate responses**: Check for errors in GraphQL responses 3. **Log errors**: Maintain error logs for debugging and monitoring 4. **Test file access**: Verify file URLs are accessible before creating artifacts ### Performance 1. **Batch operations**: Create multiple works and artifacts in single requests when possible 2. **Pagination**: Implement pagination for large client and file lists 3. **Async processing**: Use async/await or background jobs for large imports 4. **Monitor progress**: Track connection job status and work completion ### Data management 1. **Use external IDs**: Always provide `externalId` values to enable correlation with your system 2. **Descriptive names**: Use clear, descriptive names for jobs, works, and artifacts 3. **Metadata**: Include relevant metadata (file size, mime type, dates) in artifacts 4. **Idempotency**: Design your integration to handle retries and duplicate prevention *** ## Support For additional support or questions: * **Slack**: Contact your dedicated partner Slack channel * **Email**: Reach out to your Filed account representative * **Documentation**: Check our [API documentation](/legacy/apis/introduction) for more details *** ## Next Steps After completing the integration: 1. **Review API reference**: Check out the detailed API reference for [create connection job](/legacy/apis/endpoint/create-connection-job), [create job works](/legacy/apis/endpoint/create-works), [create job work artifacts](/legacy/apis/endpoint/create-job-work-artifacts), and [initiate tax preps import](/legacy/apis/endpoint/initiate-tax-preps-import) 2. **Test the flow**: Create test connection jobs and verify all steps work correctly 3. **Monitor usage**: Track API usage and tax prep creation metrics 4. **Iterate**: Gather feedback and refine your integration based on needs 5. **Scale**: Automate the process to handle multiple tax preparations efficiently # Introduction Source: https://docs.apps.filed.com/legacy/apis/introduction Introduction to Filed APIs # GraphQL Introduction > Powerful GraphQL API for flexible server management and real-time data querying The Filed GraphQL API provides a flexible, efficient way to query and manage your taxpreps. Unlike REST APIs, GraphQL allows you to request exactly the data you need in a single request, reducing over-fetching and improving performance. ## Key Features * **Flexible Queries**: Request only the data you need * **Type Safety**: Strongly typed schema with comprehensive documentation * **Single Endpoint**: All operations through one GraphQL endpoint ## Getting Started ### Endpoint ``` https://gateway.filed.com/graphql ``` ### Authentication All GraphQL requests require authentication using a Bearer token in the Authorization header and a source-platform header to identify your integration. **The `source-platform` header is required for all requests, including the initial login request.** ```http theme={null} theme={null} Authorization: Bearer YOUR_JWT_ACCESS_TOKEN source-platform: your-platform-name ``` The `source-platform` header should be a custom identifier for your integration, such as: * A tax firm name: `tax-firm-a` * A partner name: `my-sample-platform`, `qount`, etc. * Any identifier that helps identify your platform The `source-platform` header is used to uniquely identify API requests for analytical purposes. This helps Filed track usage patterns and provide better support for your integration. The header does not affect rate limits or any other API functionality. ### Basic Query Example ```graphql theme={null} theme={null} query GetHealthResponse { health { api } } ``` ## Schema Overview The main types in the GraphQL schema: * **Workspaces**: Query and manage your workspaces * **User**: Access current logged in user details * **TaxPrep**: Query and manage your taxpreps * **InputDocument**: Query and manage the documents you submitted for taxprep ## GraphQL desktop client Explore the GraphQL API interactively using a desktop client: Test queries with this desktop client (Httpie) ## Benefits Over REST Fetch multiple related resources in a single request instead of multiple REST calls. Request only the fields you need, reducing payload size and improving performance. Strongly typed schema prevents runtime errors and improves developer experience. ## Next Steps * Customer integration guide: Check out our [Customer integration guide](/legacy/apis/guides/customer) to learn how to integrate Filed's API as a customer. * Partner integration guide: Check out our [Partner integration guide](/legacy/apis/guides/partner) to learn how to onboard and integrate your tax firm customers. * API reference: Check out our [API reference](/legacy/apis/endpoint) to learn about the available endpoints and how to use them.