Minimal example app that shows an app-to-app customer app integration workflow with magicplan.
App-to-App_Demo.mov
- Opens magicplan to create or reopen a linked project
- Receives a single
.magicplanpackage back from the iOS share sheet - Matches the package to a job by
external_reference_id, withprojectIdas a fallback - Replaces the previous import and surfaces project, form, space, and media data
Outbound from Field App:
magicplanstd://project/{projectId}
magicplanstd://create-project?name={jobTitle}&external_reference_id={jobId}&address_1={street}&city={city}&zip={postalCode}&country={country}&latitude={latitude}&longitude={longitude}
Inbound from magicplan:
application/vnd.magicplan.project-package+zip
The shared .magicplan archive contains manifest.json plus the referenced media files.
- Open
JB-24018 Alder Street Duplex. - Tap
Create and open in magicplan. - In magicplan, capture the plan and documentation.
- Use
Share to Customer Appand chooseField App. - Return to the job and review the imported data.
JB-24027 Maple Crest Residence already includes a sample import for a faster walkthrough.
For your app to appear as a share target when magicplan opens the iOS share sheet, you need three things:
In your app's Info.plist, declare that your app understands the com.magicplan.project-package type. Use UTImportedTypeDeclarations (not Exported — magicplan owns the type definition):
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>com.magicplan.project-package</string>
<key>UTTypeDescription</key>
<string>magicplan Project Package</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.zip-archive</string>
<string>public.data</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>magicplan</string>
</array>
<key>public.mime-type</key>
<string>application/vnd.magicplan.project-package+zip</string>
</dict>
</dict>
</array>Add a CFBundleDocumentTypes entry so iOS routes .magicplan files to your app:
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>magicplan Project Package</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>com.magicplan.project-package</string>
</array>
</dict>
</array>When the user picks your app from the share sheet, iOS delivers a file URL to your AppDelegate via application(_:open:options:). The file is a zip archive with a .magicplan extension. Your app should:
- Copy the file into your sandbox (the source URL is a security-scoped temporary path).
- Unzip it (this example uses SSZipArchive).
- Read
manifest.jsonfrom the extracted contents — this is the payload. - Resolve referenced media and thumbnail files from the same extracted directory.
See AppDelegate.swift for the full native implementation.
A .magicplan file is a standard zip archive containing:
| File | Description |
|---|---|
manifest.json |
The project payload (described below) |
floor_thumbnail_<floorUid>.png |
Generated floor plan thumbnail (one per floor) |
room_thumbnail_<roomUid>.png |
Generated room rendering (one per room) |
<media_filename> |
User photos, 360 photos, and videos referenced by the payload |
Floor and room thumbnails are generated assets — they are not part of media_files. They are referenced by the image field on floor and room objects.
The payload follows schema version 1.0. All timestamps use the format YYYY-MM-DDTHH:MM:SS.ssssss+00:00.
| Field | Type | Description |
|---|---|---|
id |
string |
magicplan project ID |
plan_id |
string |
Plan ID within the project |
external_reference_id |
string? |
Your app's ID passed during project creation. Use this to match back to a job. |
name |
string |
Project name |
description |
string |
Project notes |
cloud_url |
string? |
Plain project URL (https://<cloud>/estimator/projects/<id>). No auth token. |
thumbnail_url |
string? |
Reserved |
user |
object? |
{ id, email, firstname, lastname } or null |
address |
object? |
{ street, city, country, postal_code, latitude, longitude } or null |
user_created |
string |
When the project was created |
user_modified |
string |
When the project was last modified |
archived_at |
string? |
Archive timestamp or null |
media_ids |
string[]? |
IDs of project-level media (photos not attached to a specific entity) |
media_files |
MediaFile[]? |
Manifest of all media files included in the package |
plan |
object |
The plan object (see below) |
Every user-captured photo, 360 photo, and video in the package appears here. Custom-form image attachments and autoscan/model media are excluded.
| Field | Type | Description |
|---|---|---|
id |
string |
Unique media ID. Entities reference media via media_ids pointing to these. |
filename |
string |
Filename of the file in the zip archive. Use this for resolving the actual file. |
mime_type |
string |
MIME type (e.g. image/jpeg, video/mp4) |
media_type |
string |
One of photo, photo_360, video |
caption |
string? |
User-provided description, trimmed, or null |
user_created |
string |
When the media was captured |
| Field | Type | Description |
|---|---|---|
id |
string |
Plan ID |
unit |
string? |
"metric", "feet", "inches", or null |
user_created |
string |
Plan creation timestamp |
user_modified |
string |
Plan last-modified timestamp |
plan_data |
object |
Structural data (floors, rooms, etc.) |
attributes |
Attribute[] |
Custom attribute forms from the Details tab |
forms |
FormData[] |
Forms from the Forms tab (only user-filled fields) |
| Field | Type | Description |
|---|---|---|
floor_count |
number |
Number of floors |
room_count |
number |
Total number of rooms |
door_count |
number? |
Total doors (parsed from statistics) or null |
window_count |
number? |
Total windows (parsed from statistics) or null |
statistics |
object? |
Numeric measurements in base units (meters / sq meters / cubic meters) |
statistics_formatted |
object? |
Same measurements as pre-formatted display strings with units |
floors |
Floor[] |
Array of floors |
These objects appear at plan, floor, and room level. They share the same keys:
| Key | Description |
|---|---|
area_without_walls |
Floor area excluding walls |
area_with_walls |
Floor area including walls |
area_with_interior_walls_only |
Floor area with interior walls only |
perimeter |
Ceiling perimeter |
ground_perimeter |
Floor/ground perimeter |
volume |
Room volume |
walls_surface |
Total wall surface |
walls_surface_without_openings |
Wall surface minus doors/windows |
ceiling_area |
Ceiling area |
height |
Height (room-level only) |
In statistics, values are numbers in metric base units. In statistics_formatted, values are display strings (e.g. "25 m2", "82 sq ft"). Both are sparse — only populated keys are present.
| Field | Type | Description |
|---|---|---|
uid |
string |
Floor identifier |
name |
string |
Floor name (e.g. "Ground Floor") |
image |
string? |
Package-local filename of the floor thumbnail (e.g. floor_thumbnail_<uid>.png), or null. Not a URL. Not in media_files. |
media_ids |
string[]? |
IDs into media_files for floor-level photos |
values |
Value[] |
Floor-level values (e.g. ceilingHeight, notes) |
statistics |
object? |
Floor-level statistics |
statistics_formatted |
object? |
Floor-level formatted statistics |
line_items |
LineItem[] |
Scope of work line items for this floor |
objects |
Object[] |
Objects placed directly on the floor (not in a room) |
rooms |
Room[] |
Rooms on this floor |
| Field | Type | Description |
|---|---|---|
uid |
string |
Room identifier |
name |
string |
Room name (e.g. "Living Room") |
image |
string? |
Package-local filename of the room rendering, or null. Not a URL. Not in media_files. |
formatted_dimensions |
string? |
Pre-formatted dimension string, or null |
media_ids |
string[]? |
IDs into media_files |
values |
Value[] |
Room-level values (e.g. roomType, ceilingHeight, notes) |
statistics |
object? |
Room-level statistics |
statistics_formatted |
object? |
Room-level formatted statistics |
line_items |
LineItem[] |
Scope of work line items for this room |
affected_areas |
AffectedArea[] |
Damage/markup areas within the room |
walls |
Wall[] |
Walls of the room |
objects |
Object[] |
Objects in the room (doors, windows, equipment, etc.) |
| Field | Type | Description |
|---|---|---|
uid |
string |
Wall identifier |
name |
string |
Wall name |
media_ids |
string[]? |
IDs into media_files |
values |
Value[] |
Wall values (e.g. length, notes) |
| Field | Type | Description |
|---|---|---|
uid |
string |
Object identifier |
name |
string |
Object name (e.g. "Door", "Window") |
symbol_id |
string |
Symbol type identifier (e.g. door, window) |
wall_uid |
string? |
Reserved |
media_ids |
string[]? |
IDs into media_files |
values |
Value[] |
Object values (e.g. width, height, depth, notes) |
Line items come from the project's scope of work. They are keyed to the floor or room they belong to.
| Field | Type | Description |
|---|---|---|
id |
string |
Line item identifier |
name |
string |
Item name |
sku |
string? |
Xactimate or vendor SKU code |
description |
string? |
Item description |
notes |
string? |
Additional notes |
quantity |
number? |
Calculated quantity |
unit_symbol |
string? |
Unit abbreviation (e.g. SF, LF) |
unit_name |
string? |
Full unit name (e.g. Square Foot) |
sort |
number |
Sort order within the scope |
| Field | Type | Description |
|---|---|---|
uid |
string |
Area identifier |
name |
string |
Area name (e.g. "Wet wall") |
type |
string |
"wall" or "floor" |
color |
string |
Color hex string (e.g. "#e22411FF") |
area |
number |
Surface area in square meters |
values |
Value[] |
Values including notes, partial.area.dimensions.area, partialarea.general.fill.color |
A simple key-value pair used for entity-level fields. Notes are always delivered through values (not top-level fields).
{ "id": "notes", "value": "Water damage along baseboard" }Common value IDs:
| ID | Appears on | Description |
|---|---|---|
notes |
floor, room, wall, object, affected area | User notes |
ceilingHeight |
floor, room | Ceiling height in meters |
roomType |
room | Room type label |
length |
wall | Wall length in meters |
width |
object | Object width in meters |
height |
object | Object height in meters |
depth |
object | Object depth in meters |
partial.area.dimensions.area |
affected area | Surface area as string |
partialarea.general.fill.color |
affected area | Color hex |
ground.color |
room | Floor color |
Forms from the Forms tab. Only fields with user-filled values are included.
{
"symbol_instance_id": "plan", // which entity this form is attached to
"symbol_type": "plan", // "plan", "floor", "room", "wall_item", "furniture"
"symbol_name": "My Project",
"forms": [
{
"id": "abc-123", // form template UUID
"title": "Mitigation Checklist",
"sections": [
{
"id": "abc-123s1",
"name": "General",
"fields": [
{
"id": "qf.moisture_level",
"type_as_string": "number",
"label": "Moisture level",
"is_required": false,
"value": {
"has_value": true,
"value": "42",
"values": [],
"formatted": null
}
}
]
}
]
}
]
}Field type_as_string values: text, multitext, list, multilist, number, bool, distance, area, date, time, color, label, signature, image, image360, disclosure, illustration.
For multi-select fields, individual selections are in value.values[] and value.value is the comma-joined string.
Custom attribute forms from the Details tab (distinct from Forms tab forms). These map to the magicplan Cloud API's /projects/{id}/plan attributes endpoint.
{
"symbol_instance_id": "room-1",
"symbol_type": "room",
"symbol_name": "Living Room",
"custom_attributes": {
"id": "qfield_title.abc-123",
"title": "Room Details",
"fields": [
{
"id": "qcustomfield.flooring_type",
"type_as_string": "list",
"label": "Flooring type",
"is_required": false,
"value": {
"has_value": true,
"value": "Hardwood",
"values": [],
"formatted": null
}
}
]
}
}Media files are assigned to entities using media_ids arrays that reference id values in data.media_files[]. The ownership model follows magicplan's Photos UI:
- Project-level:
data.media_ids— photos not tied to a specific entity - Entity-level:
floor.media_ids,room.media_ids,wall.media_ids,object.media_ids— photos attached to that entity - Fallback: If a photo's target entity no longer exists in the plan, it falls back to project-level
To resolve a media file to its actual bytes, look up the filename from media_files[] and find the matching file in the extracted zip directory.
npm install
bundle install
npm run ios:pods
npm start
npm run iosnpx tsc --noEmit
npm test -- --runInBand
xcodebuild -workspace ios/CustomerFieldApp.xcworkspace -scheme CustomerFieldApp -configuration Debug -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
{ "schema_version": "1.0", "data": { /* project + plan */ } }