Skip to main content
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 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 until it completes. Everything here uses a workspaceToken (see Authentication) and goes to the single GraphQL endpoint:
https://router.apps.filed.com/graphql

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 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 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.
mutation CreateClient($input: CreateClientInput!) {
  createClient(input: $input) {
    client {
      id
      name
      externalId
      status
      returnType
      taxYear
    }
    taskId
  }
}
{
  "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.
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"]
      }
    }
  }'
{
  "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) 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. It takes an AddClientDocumentsInput with the clientId and one or more uploadIds, and returns a taskId for the binder ingestion task.
mutation AddClientDocuments($input: AddClientDocumentsInput!) {
  addClientDocuments(input: $input) {
    taskId
  }
}
{
  "input": {
    "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
    "uploadIds": ["018f9c2a-7b1e-7c3d-9a4e-2f6b1c8d0e5a"]
  }
}
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"]
      }
    }
  }'
{
  "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 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 with just the clientId and Filed re-runs ingestion over the documents already attached to the client.
mutation RetriggerIngestion($input: RetriggerIngestionInput!) {
  retriggerIngestion(input: $input) {
    taskId
  }
}
{
  "input": {
    "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c"
  }
}
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"
      }
    }
  }'
{
  "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).
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: 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.
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
            }
          }
        }
      }
    }
  }
}
{
  "clientId": "018f9c2a-3d5f-7a10-b2c4-9e8d7f6a5b4c",
  "type": "BINDER"
}
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"
    }
  }'
{
  "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: