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

> ## Agent Instructions
> These docs contain multiple Speckle product experiences. For Speckle 2026.9, prefer /next/ pages for the topics and changes they cover. The navigation label Current does not override this version-specific precedence.
> Coverage under /next/ is incremental. Use Current documentation for unchanged topics and when a /next/ page says coverage is unavailable or explicitly refers you to Current. Do not infer a feature was removed from a missing page.
> Read the page-specific documentation status, applicability, affected guidance and replacement links in agent-only content. Impacted, changed and superseded apply only to the stated scope; they do not mean the entire feature is deprecated. Historical connector guides apply only to the legacy connector described.
> Match guidance to the customer's deployment version, model data format and connector. Respect plan, permission and compatibility restrictions. If an unknown version or data format changes the answer, ask a focused clarification; do not infer deployment version from the date alone.

# Upload IFC and other files to Speckle

> Guide to uploading IFC, DWG, OBJ, and other file types using GraphQL mutations

Upload a supported file and create a model version using GraphQL and an HTTP upload to blob
storage. This guide covers requesting an upload URL, uploading the file, starting conversion,
and checking the result.

## Prerequisites

* A personal access token with the `streams:write` scope
* A Speckle project, where you have a `Can edit` role

<Info>
  Get your personal access token from: **Avatar → Profile → Personal Access Tokens** in the Speckle
  web app.
</Info>

For all GraphQL mutations, send the token in a request header named `Authorization`:

```bash theme={null}
Authorization: Bearer YOUR_TOKEN
```

<Note>
  On [app.speckle.systems](https://app.speckle.systems), this presigned-URL workflow is the default
  — no feature flag is required. On self-hosted public Speckle Server releases, set
  `FF_NEXT_GEN_FILE_IMPORTER_ENABLED=true` so the server queues jobs for `ifc-import-service`. Leave
  `FF_LEGACY_FILE_IMPORTS_ENABLED` off unless you need the legacy REST path — see the FAQ below.
</Note>

## Overview

Uploading a file and triggering Speckle's automatic ingestion pipeline is a multi-step process:

1. **Ask Speckle Server for a presigned upload URL**
   (GraphQL mutation: `fileUploadMutations.generateUploadUrl`)

2. **Upload the file directly to blob storage**
   (simple HTTP PUT to the presigned URL)

3. **Create a model (if needed)**
   (GraphQL mutation: `modelMutations.create` - optional if you already have a model)

4. **Tell Speckle to parse and ingest the file**
   (GraphQL mutation: `fileUploadMutations.startFileIngestion`)

5. **Check the ingestion status**
   (GraphQL query for the ingestion progress)

Important notes:

* Large files are never sent through the Speckle Server REST endpoints.
* The upload URL points to your server's configured blob storage (S3, S3 compatible, or Azure).
* The ETag returned from Blob storage is required for step 4 (file ingestion).
* Successful ingestion results in a new Model Version.

## Step 1: Generate an upload URL

Request an upload URL from Speckle Server. The response contains the URL and a unique ID for the file.

**POST** `/graphql`

Mutation:

```graphql theme={null}
mutation GenerateFileUploadUrl($input: GenerateFileUploadUrlInput!) {
  fileUploadMutations {
    generateUploadUrl(input: $input) {
      url
      fileId
    }
  }
}
```

Variables example:

```json theme={null}
{
  "input": {
    "fileName": "MyModel.ifc",
    "projectId": "your-project-id"
  }
}
```

Response example:

```json theme={null}
{
  "data": {
    "fileUploadMutations": {
      "generateUploadUrl": {
        "url": "https://your-s3-endpoint.example.org/presigned-upload-url",
        "fileId": "file-id-to-use-in-step-4"
      }
    }
  }
}
```

**Important:** Save the `fileId` from the response - you **must** use this exact value in Step 4 when triggering the file ingestion. Do not use the filename; use the `fileId` returned here.

## Step 2: Upload the file to the presigned URL

**PUT** `{presignedURL}`

Headers required:

```
Content-Type: application/octet-stream   # or appropriate type
```

Body:

```
<raw file bytes>
```

Important:

* Blob storage returns an ETag in the headers of the response.
* The ETag is a unique identifier for the uploaded file content, which Speckle uses to verify the file integrity before ingesting the file.
* The ETag is required for the final step.

Example curl:

```bash theme={null}
curl -X PUT \
  -H "Content-Type: application/octet-stream" \
  --data-binary @MyModel.ifc \
  "https://your-s3-endpoint.example.org/presigned-upload-url"
```

If the response is a successful `200` status code, the response headers will contain an `ETag` header. Record this value:

```
ETag: "ad13b92e173..."
```

Note: When using this ETag in Step 4, it must be passed as a double-quoted string (e.g., `"\"ad13b92e173...\""`).

## Step 3: Create a model (if needed)

This step requires a target model, where the file data should be ingested to. If you don't have a model yet, you can create one programmatically:

**POST** `/graphql`

Mutation:

```graphql theme={null}
mutation CreateModel($input: CreateModelInput!) {
  modelMutations {
    create(input: $input) {
      id
    }
  }
}
```

Variables example:

```json theme={null}
{
  "input": {
    "projectId": "your-project-id",
    "name": "model name"
  }
}
```

Response example:

```json theme={null}
{
  "data": {
    "modelMutations": {
      "create": {
        "id": "the-target-model-id"
      }
    }
  }
}
```

Store the `model.id` as it will be used as `modelId` in the next step.

## Step 4: Trigger the file ingestion

Once the file is uploaded, tell Speckle to parse, convert, index and create a new model version.

**POST** `/graphql`

Mutation:

```graphql theme={null}
mutation StartFileIngestion($input: StartFileImportInput!) {
  fileUploadMutations {
    startFileIngestion(input: $input) {
      id
      statusData {
        status
      }
    }
  }
}
```

Variables example:

```json theme={null}
{
  "input": {
    "etag": "\"ad13b92e173...\"",
    "fileId": "file-id-from-step-1-response",
    "modelId": "the-target-model-id",
    "projectId": "your-project-id"
  }
}
```

**Important:** The `fileId` must be the exact value returned from Step 1's `generateUploadUrl` response. Do not use the filename - use the `fileId` from that response.

Response example:

```json theme={null}
{
  "data": {
    "fileUploadMutations": {
      "startFileIngestion": {
        "id": "your-file-ingestion-id",
        "statusData": {
          "status": "queued"
        }
      }
    }
  }
}
```

**Note:** The `status` enum signals the job status where `queued`, `processing`, `success`, `failed`, and `cancelled` are possible values.

Your file is now in the ingestion pipeline. Once ingested, a new Version will appear under the referenced Model.

## Step 5: Check the ingestion status

Use the ID returned by `fileUploadMutations.startFileIngestion.id` to check progress. A successful result includes the new version ID; a failed result includes the error reason.

**POST** `/graphql`

Query:

```graphql theme={null}
query Ingestion($ingestionId: ID!, $projectId: String!) {
  project(id: $projectId) {
    ingestion(id: $ingestionId) {
      statusData {
        ... on ModelIngestionQueuedStatus {
          status
          progressMessage
        }
        ... on ModelIngestionSuccessStatus {
          status
          versionId
        }
        ... on ModelIngestionProcessingStatus {
          status
          progressMessage
        }
        ... on ModelIngestionFailedStatus {
          status
          errorReason
        }
      }
    }
  }
}
```

Variables example:

```json theme={null}
{
  "ingestionId": "your-file-ingestion-id",
  "projectId": "your-project-id"
}
```

Response example:

```json theme={null}
{
  "data": {
    "project": {
      "ingestion": {
        "statusData": {
          "status": "success",
          "versionId": "the-new-version-id-created-from-the-import"
        }
      }
    }
  }
}
```

In case of an error, the error response would look like:

```json theme={null}
{
  "data": {
    "project": {
      "ingestion": {
        "statusData": {
          "status": "failed",
          "errorReason": "the reason for the failure"
        }
      }
    }
  }
}
```

## What developers need to know

<AccordionGroup>
  <Accordion title="Which file formats can I upload?">
    <Warning>
      **2026.9 changes upload format support.** STL, DXF, and several other formats are no longer
      accepted by the default upload path. These formats remain supported for direct uploads in
      **legacy/compatibility mode**, until it ends on **1 November 2026**. See [2026.9 upload format changes and compatibility
      options](/next/connectors/file-uploads#breaking-change-supported-file-formats).
    </Warning>

    Before 2026.9, [app.speckle.systems](https://app.speckle.systems) supports IFC, RVT, NWC, NWD, DWG, DXF, OBJ, STL,
    3DM, and others — see [Supported Formats](/connectors/overview#supported-formats).

    Open-source self-hosted servers support IFC only.

    <Note>
      Extended formats on self-hosted deployments require Speckle Enterprise Server and a
      [license key](/developers/server/deployment/enterprise-license#license-key). See [File imports
      setup](/developers/server/deployment/enterprise-license#file-imports-conversion-runner).
    </Note>
  </Accordion>

  <Accordion title="Why are different formats supported in self-hosted vs cloud?">
    Self-hosted open-source servers use open-source dependencies, which limits supported formats.
    Formats such as RVT, NWC, NWD, DWG, DXF, and 3DM need proprietary libraries, so they are available
    on [app.speckle.systems](https://app.speckle.systems) and on Speckle Enterprise Server — not in
    the public open-source distribution.
  </Accordion>

  <Accordion title="Where did the StartFileImport mutation go?">
    `startFileImport` has been removed. Use
    `fileUploadMutations.startFileIngestion`, which gives more granular status updates and real-time
    subscription updates, with ingestion queries shared across file import and other Speckle
    integrations. As of the 2026.9 server release (1 September 2026), calling `startFileImport`
    fails with a 400 error (`UNSUPPORTED_FILE_TYPE`) rather than falling back to the old
    behavior — there is no compatibility period for this mutation.

    Update your code to use `fileUploadMutations.startFileIngestion` and the ingestion status
    queries in this guide.
  </Accordion>

  <Accordion title="Should I ever use the old REST upload?">
    For [app.speckle.systems](https://app.speckle.systems), no. The old REST upload is gone.

    For self-hosted instances, the old REST upload is only relevant if:

    * Your server runs with the `FF_LEGACY_FILE_IMPORTS_ENABLED` feature flag enabled (it is off by default), which proxies uploads through the server instead of sending them straight to blob storage
    * Your blob storage is not reachable by clients, so presigned URLs cannot work

    In these cases, you may need to use the legacy REST endpoints (`/api/file/...`).
    However, we recommend leaving `FF_LEGACY_FILE_IMPORTS_ENABLED` off and making your S3-compatible storage reachable by clients, so the modern workflow described above works.
  </Accordion>

  <Accordion title="Can I automate file uploads with SpecklePy?">
    SpecklePy centres on sending and receiving Speckle objects rather than this
    presigned-upload pipeline. You can still implement this guide in Python by combining GraphQL
    (`fileUploadMutations.generateUploadUrl`, model creation if needed,
    `fileUploadMutations.startFileIngestion`, ingestion status query) with HTTP `PUT` of file bytes
    to the presigned URL via `requests` or `httpx`. For GraphQL steps, use
    `SpeckleClient.execute_query()` as in [Custom GraphQL
    Queries](/developers/sdks/python/api-reference/client#custom-graphql-queries), or POST the same
    JSON payloads your tool would send to `{host}/graphql`.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.