> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apps.filed.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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 /<br/>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`        |

<Note>
  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)).
</Note>

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

<ParamField path="Upload-Length" type="integer" required>
  The exact size of the file in bytes.
</ParamField>

<ParamField path="Upload-Metadata" type="string" required>
  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.
</ParamField>

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

<raw file bytes>
```

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

<CodeGroup>
  ```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)
  ```
</CodeGroup>

<Note>
  `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`.
</Note>

## Direct upload

<Info>
  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.
</Info>

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

<raw file bytes>
```

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

<Note>
  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.
</Note>

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