Skip to content

magicplan/app-to-app-integration-example

Repository files navigation

magicplan Integration Field App Example

Minimal example app that shows an app-to-app customer app integration workflow with magicplan.

App-to-App_Demo.mov

What it does

  • Opens magicplan to create or reopen a linked project
  • Receives a single .magicplan package back from the iOS share sheet
  • Matches the package to a job by external_reference_id, with projectId as a fallback
  • Replaces the previous import and surfaces project, form, space, and media data

Handoff contract

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.

Demo flow

  1. Open JB-24018 Alder Street Duplex.
  2. Tap Create and open in magicplan.
  3. In magicplan, capture the plan and documentation.
  4. Use Share to Customer App and choose Field App.
  5. Return to the job and review the imported data.

JB-24027 Maple Crest Residence already includes a sample import for a faster walkthrough.


Receiving the .magicplan package (iOS configuration)

For your app to appear as a share target when magicplan opens the iOS share sheet, you need three things:

1. Import the Uniform Type Identifier (UTI)

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>

2. Register as a document handler

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>

3. Handle the incoming file URL

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:

  1. Copy the file into your sandbox (the source URL is a security-scoped temporary path).
  2. Unzip it (this example uses SSZipArchive).
  3. Read manifest.json from the extracted contents — this is the payload.
  4. Resolve referenced media and thumbnail files from the same extracted directory.

See AppDelegate.swift for the full native implementation.


Package format

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.


Payload reference (manifest.json)

The payload follows schema version 1.0. All timestamps use the format YYYY-MM-DDTHH:MM:SS.ssssss+00:00.

Top level

{
  "schema_version": "1.0",
  "data": { /* project + plan */ }
}

data — project

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)

data.media_files[] — media manifest

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

data.plan — plan

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)

data.plan.plan_data — structural data

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

statistics and statistics_formatted

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.

Floor

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

Room

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.)

Wall

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)

Object

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 Item

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

Affected Area

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

Value

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

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.

Attributes

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 ownership

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.


Run locally

npm install
bundle install
npm run ios:pods
npm start
npm run ios

Verification

npx tsc --noEmit
npm test -- --runInBand
xcodebuild -workspace ios/CustomerFieldApp.xcworkspace -scheme CustomerFieldApp -configuration Debug -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build

About

Example app to demonstrate sending magicplan project data offline to another app.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors