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

# Map Upload

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`, `matterport`, `leica`, `navvis`, `xgrid`, `faro`, `insta360` |
| `fileType`         | `zip`, `e57`, `splat`, `360` (defaults to `zip`)                      |
| `coordinateSystem` | `LHS`, `RHS`, `RHS-Z-UP`, `RHS-Y-UP`                                  |

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` upload, `application/octet-stream` otherwise. Presigned URLs are valid for **1 hour**.
{% 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-ba07526960f765cebb0450c17643bfe399463f3b%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-ba07526960f765cebb0450c17643bfe399463f3b%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-ba07526960f765cebb0450c17643bfe399463f3b%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-ba07526960f765cebb0450c17643bfe399463f3b%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-ba07526960f765cebb0450c17643bfe399463f3b%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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
