> 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/mobile-map-upload.md).

# Mobile Map Upload

Upload a mobile capture from a LiDAR or non-LiDAR phone or tablet (RGB frames and poses, plus depth images and a mesh when LiDAR is available) as a MultiSet VPS map via the REST API.

A mobile map is a capture recorded on a phone or tablet with your own AR app. Package the capture as a single `.zip` in the format below, then upload it in three calls: create the map, upload the zip, and complete the upload.

Any ARKit iPhone or iPad and any ARCore Android device can capture a map. What goes in the `.zip` depends on whether the device has a LiDAR sensor:

| Capture type              | Devices                            | Contents                                   | `fileType` |
| ------------------------- | ---------------------------------- | ------------------------------------------ | ---------- |
| **LiDAR**                 | iPhone Pro and iPad Pro with LiDAR | RGB frames, poses, depth images and a mesh | `zip`      |
| **Non-LiDAR (Posed RGB)** | Every other ARKit or ARCore device | RGB frames and poses only                  | `PosedRGB` |

If the device has LiDAR, upload a LiDAR capture: the depth images and mesh make the map more accurate.

## What to upload

### LiDAR capture

A **single `.zip`** with this layout:

```
MappingData.zip
├── CameraIntrinsics.json
├── RGB/
│   ├── Rgb_20260929101512034.jpg
│   └── …
├── Pose/
│   ├── CameraPose_20260929101512034.json
│   └── …
├── Depth/
│   ├── Depth_20260929101512034.exr
│   └── …
└── Mesh/
    └── Mesh.glb
```

| Item                    | Required | Format                                                                                  |
| ----------------------- | -------- | --------------------------------------------------------------------------------------- |
| `RGB/`                  | **Yes**  | Camera frames as `.jpg`.                                                                |
| `Pose/`                 | **Yes**  | One `.json` per RGB frame, in **Unity left-handed (LHS)** coordinates.                  |
| `Mesh/Mesh.glb`         | **Yes**  | Mesh of the captured space as a binary glTF, in the same coordinate frame as the poses. |
| `CameraIntrinsics.json` | **Yes**  | Camera intrinsics of the RGB frames.                                                    |
| `Depth/`                | **Yes**  | One depth image per RGB frame as a 16-bit or 32-bit float `.exr`.                       |

Keep the folders and `CameraIntrinsics.json` at the root of the archive.

### Non-LiDAR capture (Posed RGB)

On a device without LiDAR, upload the RGB frames, their poses and the camera intrinsics only, with this layout:

```
MappingData.zip
├── CameraIntrinsics.json
├── RGB/
│   ├── Rgb_20260929101512034.jpg
│   └── …
├── Pose/
│   ├── CameraPose_20260929101512034.json
│   └── …
```

| Item                    | Required | Format                                                                 |
| ----------------------- | -------- | ---------------------------------------------------------------------- |
| `RGB/`                  | **Yes**  | Camera frames as `.jpg`.                                               |
| `Pose/`                 | **Yes**  | One `.json` per RGB frame, in **Unity left-handed (LHS)** coordinates. |
| `CameraIntrinsics.json` | **Yes**  | Camera intrinsics of the RGB frames.                                   |

Leave out `Depth/` and `Mesh/`, and set `fileType` to `PosedRGB` when you create the map (see [`source` object](#source-object)). The RGB frames, poses and intrinsics use the same formats as a LiDAR capture.

### File naming

Each RGB frame, its pose and (on a LiDAR capture) its depth image share the same ID:

* `RGB/Rgb_<id>.jpg`
* `Pose/CameraPose_<id>.json`
* `Depth/Depth_<id>.exr`

The `<id>` must not contain an underscore. A UTC timestamp such as `20260929101512034` (`yyyyMMddHHmmssfff`) works well. Every RGB frame needs a pose file, and on a LiDAR capture a depth image too.

{% hint style="warning" %}
**The RGB frame, depth image and pose that share an ID must come from the exact same camera frame, with the same timestamp.** Take all three from one AR frame, and skip any frame where the depth image's timestamp does not match the RGB frame's. Even a small time offset between them misaligns the images and the poses, which causes drift in the map.
{% endhint %}

## Capture guidelines

* **Capture rate:** save frames at around **5 FPS** (one RGB frame, pose and, on LiDAR, depth image every 0.2 seconds) while the user is mapping.
* **Overlap:** each RGB frame should overlap with **2 to 3** neighbouring frames, so every part of the space is seen in several images.

## Pose

{% hint style="warning" %}
**Poses must be in Unity's left-handed coordinate system**: `+X` right, `+Y` up, `+Z` forward, in metres. Use the camera's world pose exactly as Unity reports it (`Camera.transform.position` and `Camera.transform.rotation`), without converting it to a right-handed frame.
{% endhint %}

```json
{
    "Position": { "x": 0.412, "y": 1.358, "z": -2.076 },
    "Rotation": { "x": 0.0121, "y": 0.9743, "z": -0.0452, "w": 0.2199 },
    "cameraParams": { "fx": 717.4, "fy": 717.4, "px": 478.9, "py": 362.1 }
}
```

| Field          | Format                                                                               |
| -------------- | ------------------------------------------------------------------------------------ |
| `Position`     | Camera position in world space, in metres.                                           |
| `Rotation`     | Camera rotation in world space as a unit quaternion `{x, y, z, w}`.                  |
| `cameraParams` | Focal length (`fx`, `fy`) and principal point (`px`, `py`) of this frame, in pixels. |

All poses, and the mesh on a LiDAR capture, must share the same world origin.

## Camera intrinsics

```json
{
    "camera_intrinsics": { "fx": 717.4, "fy": 717.4, "px": 478.9, "py": 362.1 },
    "resolution": { "width": 960, "height": 720 }
}
```

The intrinsics and `resolution` must match the RGB images as saved. If you resize the images, scale `fx`, `fy`, `px` and `py` by the same factor.

## Depth

LiDAR captures only.

* Single-channel OpenEXR (`.exr`), 16-bit (half) or 32-bit float, depth in **metres**.
* Same timestamp and orientation as its RGB frame (see [File naming](#file-naming)).
* It can be lower resolution than the RGB frame, with the same aspect ratio.

## `source` object

LiDAR capture:

```json
"source": {
  "provider": "mobile",
  "fileType": "zip",
  "coordinateSystem": "LHS"
}
```

Non-LiDAR capture:

```json
"source": {
  "provider": "mobile",
  "fileType": "PosedRGB",
  "coordinateSystem": "LHS"
}
```

| Field              | Value                                                                                           |
| ------------------ | ----------------------------------------------------------------------------------------------- |
| `provider`         | `"mobile"`                                                                                      |
| `fileType`         | `"zip"` for a LiDAR capture, `"PosedRGB"` for a non-LiDAR capture. The value is case-sensitive. |
| `coordinateSystem` | `"LHS"`                                                                                         |

## Step 1: create the map

```bash
curl --location 'https://api.multiset.ai/v2/vps/map' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--data '{
    "mapName": "Office Ground Floor",
    "fileSize": 412337152,
    "coordinates": {
        "latitude": 12.9130,
        "longitude": 77.6392,
        "altitude": 920.0
    },
    "source": {
        "provider": "mobile",
        "fileType": "zip",
        "coordinateSystem": "LHS"
    }
}'
```

This example is a LiDAR capture. For a non-LiDAR capture, set `"fileType": "PosedRGB"`. `fileSize` is the size of the `.zip` in bytes. The response contains the `mapCode` and an upload URL:

```json
{
  "message": "Map created successfully",
  "mapCode": "MAP_QQYKIBHXZE01",
  "uploadUrls": {
    "signedUrls": [
      { "partNumber": 1, "signedUrl": "https://…" }
    ]
  }
}
```

## Step 2: upload the zip

`PUT` the whole `.zip` to the `signedUrl`. The URL is valid for 1 hour.

```bash
curl --request PUT '<SIGNED_URL>' \
--header 'Content-Type: application/zip' \
--upload-file MappingData.zip
```

## Step 3: complete the upload

Use the `mapCode` from Step 1.

```bash
curl --location --request POST 'https://api.multiset.ai/v2/vps/map/complete-upload/<MAP_CODE>' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--data '{}'
```

The map then starts processing, and becomes available for VPS queries once its status is **active**. See the [Map Upload overview](/fundamentals/rest-api-docs/map-upload.md) for the full OpenAPI specs.


---

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