> For the complete documentation index, see [llms.txt](https://docs.multiset.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.multiset.ai/fundamentals/rest-api-docs/map-upload.md).

# Map Upload

Upload a scan in parts to S3, with a standard flow and a resumable flow that survives an interrupted connection.

Map files are uploaded to S3 in parts, so a large scan can be sent as parallel chunks rather than one request. The process supports files up to **25 GB**, with retries on individual parts.

There are two ways to run an upload. Both use the same map metadata and the same `PUT` per part, and both finish with the same call.

| Flow          | How it works                                                                                     | Use it when                                                               |
| ------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| **Standard**  | The create call returns a presigned URL for every part up front.                                 | The upload is small, or the connection is reliable.                       |
| **Resumable** | Part URLs are signed on demand, and an interrupted upload can be continued instead of restarted. | Large scans, slow or unstable connections. **Recommended for big files.** |

{% hint style="success" %}
If you are unsure, use the **resumable** flow. It behaves the same as the standard flow on a clean run, and it saves re-uploading everything if the connection drops.
{% endhint %}

## Map metadata

Both flows start with `POST /v2/vps/map` and take the same body.

| Field                   | Type    | Required | Notes                                                                                                     |
| ----------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `mapName`               | string  | yes      | Display name for the map.                                                                                 |
| `fileSize`              | number  | yes      | Total upload size **in bytes**. Must be greater than 0.                                                   |
| `partSize`              | integer | no       | Part size in bytes. Recommended for resumable uploads, see [Choosing a part size](#choosing-a-part-size). |
| `coordinates.latitude`  | number  | yes      | -90 to 90.                                                                                                |
| `coordinates.longitude` | number  | yes      | -180 to 180.                                                                                              |
| `coordinates.altitude`  | number  | yes      | Metres.                                                                                                   |
| `heading`               | number  | no       | 0 to 360 degrees.                                                                                         |
| `source`                | object  | no       | Describes the scan, see below. Defaults to a `zip` upload.                                                |

### The source object

Set `source` to match the scan you are uploading:

```json
{
  "source": {
    "provider": "unity",
    "fileType": "zip",
    "coordinateSystem": "RHS"
  }
}
```

| Field              | Accepted values                                                                           |
| ------------------ | ----------------------------------------------------------------------------------------- |
| `provider`         | `unity`, `mobile`, `matterport`, `leica`, `navvis`, `xgrid`, `faro`, `insta360`           |
| `fileType`         | `zip`, `e57`, `splat`, `360`, `PosedRGB` (defaults to `zip`)                              |
| `coordinateSystem` | `LHS`, `RHS`, `RHS-Z-UP`, `RHS-Y-UP`                                                      |
| `metadata.assets`  | Optional deliverables to build alongside the map, see [Optional assets](#optional-assets) |

For a capture recorded on a phone or tablet (`provider: "mobile"`), see [Mobile Map Upload](/fundamentals/rest-api-docs/map-upload/mobile-map-upload.md) for the required `.zip` layout. A capture from a device without LiDAR uses `fileType: "PosedRGB"`.

For a Matterport or NavVis E57 scan:

```json
{
  "source": {
    "provider": "matterport",
    "fileType": "e57",
    "coordinateSystem": "RHS"
  }
}
```

```json
{
  "source": {
    "provider": "navvis",
    "fileType": "e57",
    "coordinateSystem": "RHS"
  }
}
```

{% hint style="info" %}
Upload each part with the `Content-Type` that matches your `fileType`: `application/zip` for a `zip` or `PosedRGB` upload, `application/octet-stream` otherwise. Presigned URLs are valid for **1 hour**.
{% endhint %}

### Optional assets

Some deliverables are not built by default. Request them with `source.metadata.assets`, an array of the extras you want:

```json
{
  "source": {
    "provider": "matterport",
    "fileType": "e57",
    "coordinateSystem": "RHS",
    "metadata": {
      "assets": ["pano"]
    }
  }
}
```

| Value      | What you get                                                                                                                         | Applies to                                    |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
| `pano`     | A navigable [360° Virtual Tour](/fundamentals/maps/output-asset/360-virtual-tour.md) built from the panoramic imagery in the capture | 360 video captures, `e57` and MatterPak scans |
| `panoMask` | Person masks alongside each panorama, so people captured in the scan can be hidden in the tour. Requires `pano`.                     | 360 video captures, `e57` and MatterPak scans |
| `splat`    | A [Gaussian Splat](/fundamentals/maps/output-asset/gaussian-splat.md) render of the space                                            | 360 video captures                            |

`panoMask` only makes sense together with a tour, so `["panoMask"]` on its own is rejected with `400`. Ask for `["pano", "panoMask"]`. The masks arrive as the `maskKey` on each panorama, see [360° Virtual Tour (Pano)](/fundamentals/rest-api-docs/pano-360-virtual-tour.md); without the opt-in, `maskKey` is `null`.

Omit the field, or send an empty array, and no extras are built. The [mesh](/fundamentals/maps/output-asset/mesh.md) and [point cloud](/fundamentals/maps/output-asset/mesh.md#point-cloud) are always produced, so the map itself is unaffected either way.

{% hint style="warning" %}
**`assets` is read once, at map creation.** It cannot be added to an existing map. If you upload an `.e57` or MatterPak scan without `assets: ["pano"]` and later want a tour, you have to create a new map and upload the scan again.
{% endhint %}

{% hint style="info" %}
Unrecognised entries are ignored, so a typo like `panos` builds nothing and does not fail the request. Check the spelling against the table above.
{% endhint %}

## Standard upload

### 1. Create the map

`POST /v2/vps/map` with the metadata above. The response includes a presigned URL for every part:

```json
{
  "message": "Map created successfully",
  "mapCode": "MAP_QQYKIBHXZE01",
  "mapId": "67e12d4bff7ecf561f2f8a0c",
  "key": "…",
  "uploadUrls": {
    "uploadId": "…",
    "signedUrls": [
      { "partNumber": 1, "signedUrl": "https://…" },
      { "partNumber": 2, "signedUrl": "https://…" }
    ]
  }
}
```

### 2. Upload the parts

Split the file and `PUT` each chunk to its matching `signedUrl`. Each successful `PUT` returns an **`ETag`** header. Keep every `ETag` with its part number, you need them to finish the upload.

### 3. Complete the upload

`POST /v2/vps/map/complete-upload/{mapId}` with the parts you uploaded:

```json
{
  "parts": [
    { "PartNumber": 1, "ETag": "\"a1b2c3…\"" },
    { "PartNumber": 2, "ETag": "\"d4e5f6…\"" }
  ]
}
```

The parts are assembled into a single file and the map enters the processing queue. Once processing finishes, the map is available for VPS queries.

{% openapi src="/files/KkX808qgMzlMCRvDY60Y" path="/map" method="post" %}
[map-upload.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-bb1d186be69381f2aed5e8893168bae1bb7cbfe5%2Fmap-upload.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/KkX808qgMzlMCRvDY60Y" path="/map/complete-upload/{id}" method="post" %}
[map-upload.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-bb1d186be69381f2aed5e8893168bae1bb7cbfe5%2Fmap-upload.yaml?alt=media)
{% endopenapi %}

## Resumable upload

### 1. Create the map in resumable mode

`POST /v2/vps/map?resumable=true` with the same metadata. Include `partSize` so the server can confirm the upload is complete before finalizing it.

The response is deliberately small, because no part URLs are issued yet:

```json
{
  "message": "Multipart upload initialised",
  "mapCode": "MAP_QQYKIBHXZE01",
  "mapId": "67e12d4bff7ecf561f2f8a0c"
}
```

### 2. Request URLs for the parts you are about to send

`POST /v2/vps/map/sign-part` with the `mapId` and the part numbers you want, up to **100 per call**:

```json
{
  "mapId": "67e12d4bff7ecf561f2f8a0c",
  "partNumbers": [1, 2, 3, 4]
}
```

```json
{
  "signedUrls": [
    { "partNumber": 1, "signedUrl": "https://…" },
    { "partNumber": 2, "signedUrl": "https://…" }
  ]
}
```

### 3. Upload the parts

`PUT` each chunk to its `signedUrl`, exactly as in the standard flow. You do not need to keep the `ETag` values here, because the server reads the uploaded parts when you complete.

### 4. Resume after an interruption

Ask which parts already arrived:

`GET /v2/vps/map/list-parts/{mapId}`

```json
{
  "active": true,
  "uploadedPartNumbers": [1, 2, 3, 5]
}
```

Skip those, request fresh URLs for the rest with **sign-part**, and upload only what is missing. Repeat as often as you need, the upload stays open.

### 5. Complete the upload

`POST /v2/vps/map/complete-upload/{mapId}` with an **empty body**:

```json
{}
```

The server assembles the upload from the parts it received. If you supplied `partSize` when creating the map, it first checks that every expected part is present and returns `409` if any are missing.

### Cancel an upload

`POST /v2/vps/map/abort-upload/{mapId}` discards the upload and removes the map.

{% openapi src="/files/KkX808qgMzlMCRvDY60Y" path="/map/sign-part" method="post" %}
[map-upload.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-bb1d186be69381f2aed5e8893168bae1bb7cbfe5%2Fmap-upload.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/KkX808qgMzlMCRvDY60Y" path="/map/list-parts/{id}" method="get" %}
[map-upload.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-bb1d186be69381f2aed5e8893168bae1bb7cbfe5%2Fmap-upload.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/KkX808qgMzlMCRvDY60Y" path="/map/abort-upload/{id}" method="post" %}
[map-upload.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-bb1d186be69381f2aed5e8893168bae1bb7cbfe5%2Fmap-upload.yaml?alt=media)
{% endopenapi %}

## Choosing a part size

`partSize` controls how the file is split. A smaller part means more requests but less to re-send after a failure. A larger part means fewer requests but more lost work when one fails.

* **8 MB to 16 MB** suits most uploads on a normal connection.
* Go smaller, around **5 MB**, on unstable or mobile connections so a dropped part costs little.
* The number of parts works out to `ceil(fileSize / partSize)`.

## Errors worth handling

| Status | Meaning                                                                                        |
| ------ | ---------------------------------------------------------------------------------------------- |
| `400`  | Missing or invalid metadata, maps limit reached, or the plan has expired.                      |
| `403`  | The map belongs to another account.                                                            |
| `409`  | No active upload for this map, or the upload is incomplete because expected parts are missing. |

{% hint style="info" %}
The upload location is managed for you. You work with the `mapId` and the presigned URLs, and never construct a storage path yourself.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.multiset.ai/fundamentals/rest-api-docs/map-upload.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
