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: