Skip to main content

Tutorial: Upload photos & assets

Add supplementary photos and media files to a published iGUIDE using the same direct-to-S3 upload pattern you learned for Stitch Data.

Time to complete: ~10 minutes

What you'll learn:

  • How to upload supplementary photos and media to an iGUIDE
  • How the S3 upload pattern works for assets
  • How to add uploaded assets to view galleries automatically
  • When and why to use asset uploads

Prerequisites

Before you begin, you need:

What are asset uploads?

Asset uploads let you add supplementary photos and media files to an existing iGUIDE after it's been processed. This is useful for:

  • Adding extra photos captured outside the iGUIDE camera—drone shots, detail photos, staging photos
  • Uploading marketing materials—brochures, floor plan overlays, property documents
  • Supplementing gallery images—before/after shots, seasonal photos, renovation progress

Assets are stored with the iGUIDE and can optionally be added to the viewer gallery automatically.

When to use asset uploads

Use asset uploads for supplementary media that wasn't captured by the iGUIDE camera. For standard gallery images captured during the scan, those are already included in the base iGUIDE deliverables—no separate upload needed.

The three-step flow

Asset uploads use the same S3 upload pattern as Stitch Data:

  1. Get an upload permit from the Portal API
  2. Upload directly to S3 using temporary credentials
  3. Trigger processing to finalize the upload

The only differences are shorter credential lifetime (2 hours vs 24 hours) and an optional appendToViews parameter to automatically add assets to galleries.

Step 1: Get upload permit

Request temporary S3 credentials for uploading your asset. You'll need the iGUIDE ID and information about the file you're uploading.

export APP_ID="your-app-id"
export APP_TOKEN="your-access-token"
export IGUIDE_ID="ig12345"

curl -X POST https://manage.youriguide.com/api/v1/iguides/$IGUIDE_ID/assets \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Plntr-App-Id: $APP_ID" \
-H "X-Plntr-App-Token: $APP_TOKEN" \
-d '{
"filename": "front-exterior.jpg",
"filesize": 4500000
}'
tip

The filesize value must be the exact size of the file in bytes. Use stat filename.tar to get it.

Response:

{
"name": "a3K9.jpg",
"uploadPermit": {
"region": "us-east-1",
"bucket": "iguides.plntr.ca",
"key": "gallery/ig12345/a3K9.jpg",
"accessKeyId": "ASIAXXX...",
"secretAccessKey": "xxx...",
"sessionToken": "FwoGZXIvYXdz..."
},
"uploadToken": "eyJhbGciOiJIUzI1NiIs..."
}

What just happened?

  • The Portal API generated a unique asset name (a3K9.jpg)—this is not your original filename
  • You received temporary AWS credentials scoped to a single S3 key
  • The uploadToken is a JWT you'll use to trigger processing in Step 3
Save these values

You need all three values from the response:

  • name — the generated asset name (for Step 3)
  • uploadPermit — temporary S3 credentials (for Step 2)
  • uploadToken — processing token (for Step 3)
export ASSET_NAME="a3K9.jpg"
export UPLOAD_TOKEN="eyJhbGciOiJIUzI1NiIs..."

Credential lifetime

Asset upload credentials expire after 2 hours. This is shorter than Stitch Data (24 hours) because asset files are typically much smaller—photos and documents usually upload in seconds or minutes.

If you're uploading many assets or very large video files, request credentials for each file individually rather than trying to batch uploads with a single permit.

Step 2: Upload to S3

Use the credentials from the upload permit to upload directly to AWS S3. The recommended approach is to use an official AWS SDK.

Using the AWS CLI:

# Set credentials from the upload permit
export AWS_ACCESS_KEY_ID="ASIAXXX..."
export AWS_SECRET_ACCESS_KEY="xxx..."
export AWS_SESSION_TOKEN="FwoGZXIvYXdz..."

# Upload to the exact bucket and key from the permit
aws s3 cp front-exterior.jpg \
s3://iguides.plntr.ca/gallery/ig12345/a3K9.jpg \
--acl bucket-owner-full-control \
--region us-east-1

Using the AWS JavaScript SDK:

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readFileSync } from "fs";

const { uploadPermit } = permitResponse;

const s3 = new S3Client({
region: uploadPermit.region,
credentials: {
accessKeyId: uploadPermit.accessKeyId,
secretAccessKey: uploadPermit.secretAccessKey,
sessionToken: uploadPermit.sessionToken,
},
});

await s3.send(new PutObjectCommand({
Bucket: uploadPermit.bucket,
Key: uploadPermit.key,
Body: readFileSync("front-exterior.jpg"),
ACL: "bucket-owner-full-control",
}));

Using the AWS Python SDK (boto3):

import boto3

permit = permit_response["uploadPermit"]

s3 = boto3.client("s3",
region_name=permit["region"],
aws_access_key_id=permit["accessKeyId"],
aws_secret_access_key=permit["secretAccessKey"],
aws_session_token=permit["sessionToken"],
)

s3.upload_file(
"front-exterior.jpg",
permit["bucket"],
permit["key"],
ExtraArgs={"ACL": "bucket-owner-full-control"},
)
ACL Required

You must include ACL: bucket-owner-full-control on every upload. The IAM policy enforces this—uploads without the ACL will fail with a 403 Forbidden error.

Step 3: Trigger processing

After uploading to S3, tell the Portal API to process the file:

curl -X POST "https://manage.youriguide.com/api/v1/iguides/$IGUIDE_ID/assets/$ASSET_NAME/process?uploadToken=$UPLOAD_TOKEN" \
-H "X-Plntr-App-Id: $APP_ID" \
-H "X-Plntr-App-Token: $APP_TOKEN"

Response:

{
"jid": "abc123",
"timestamp": 1708280400
}

This enqueues an asynchronous job to process the uploaded asset. Processing is typically very fast for photos (seconds to minutes).

Adding assets to view galleries

By default, uploaded assets are stored with the iGUIDE but not added to any viewer galleries. To automatically add the asset to galleries, include the appendToViews query parameter:

Add to default view only

curl -X POST "https://manage.youriguide.com/api/v1/iguides/$IGUIDE_ID/assets/$ASSET_NAME/process?uploadToken=$UPLOAD_TOKEN&appendToViews=default" \
-H "X-Plntr-App-Id: $APP_ID" \
-H "X-Plntr-App-Token: $APP_TOKEN"

The asset will appear in the gallery of the iGUIDE's default view only.

Add to all views

curl -X POST "https://manage.youriguide.com/api/v1/iguides/$IGUIDE_ID/assets/$ASSET_NAME/process?uploadToken=$UPLOAD_TOKEN&appendToViews=all" \
-H "X-Plntr-App-Id: $APP_ID" \
-H "X-Plntr-App-Token: $APP_TOKEN"

The asset will appear in the gallery of every view associated with the iGUIDE.

Omit appendToViews or pass an empty value to store the asset without adding it to any galleries:

curl -X POST "https://manage.youriguide.com/api/v1/iguides/$IGUIDE_ID/assets/$ASSET_NAME/process?uploadToken=$UPLOAD_TOKEN" \
-H "X-Plntr-App-Id: $APP_ID" \
-H "X-Plntr-App-Token: $APP_TOKEN"

The asset is stored and can be retrieved via the API, but won't appear in the viewer gallery.

When to append to views
  • Use appendToViews=default for most cases—supplementary photos should appear in the main viewer gallery
  • Use appendToViews=all if the iGUIDE has multiple views and the asset is relevant to all of them
  • Omit the parameter for assets that are for internal use only (documents, notes, etc.)

Complete example

Here's a full script that uploads a photo and adds it to the default view gallery:

#!/bin/bash
set -e

# Configuration
export APP_ID="your-app-id"
export APP_TOKEN="your-access-token"
IGUIDE_ID="ig12345"
ASSET_FILE="front-exterior.jpg"

# Step 1: Get upload permit
echo "Requesting upload permit..."
PERMIT_RESPONSE=$(curl -s -X POST https://manage.youriguide.com/api/v1/iguides/$IGUIDE_ID/assets \
-H "Content-Type: application/json" \
-H "X-Plntr-App-Id: $APP_ID" \
-H "X-Plntr-App-Token: $APP_TOKEN" \
-d "{
\"filename\": \"$ASSET_FILE\",
\"filesize\": $(stat -c%s "$ASSET_FILE")
}")

ASSET_NAME=$(echo $PERMIT_RESPONSE | jq -r '.name')
UPLOAD_TOKEN=$(echo $PERMIT_RESPONSE | jq -r '.uploadToken')
export AWS_ACCESS_KEY_ID=$(echo $PERMIT_RESPONSE | jq -r '.uploadPermit.accessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $PERMIT_RESPONSE | jq -r '.uploadPermit.secretAccessKey')
export AWS_SESSION_TOKEN=$(echo $PERMIT_RESPONSE | jq -r '.uploadPermit.sessionToken')
S3_BUCKET=$(echo $PERMIT_RESPONSE | jq -r '.uploadPermit.bucket')
S3_KEY=$(echo $PERMIT_RESPONSE | jq -r '.uploadPermit.key')
REGION=$(echo $UPLOAD_RESPONSE | jq -r '.uploadPermit.region')


echo "Asset name: $ASSET_NAME"

# Step 2: Upload to S3
echo "Uploading to S3..."
aws s3 cp $ASSET_FILE s3://$S3_BUCKET/$S3_KEY \
--acl bucket-owner-full-control \
--region $REGION

# Step 3: Trigger processing (add to default view gallery)
echo "Triggering processing..."
PROCESS_RESPONSE=$(curl -s -X POST "https://manage.youriguide.com/api/v1/iguides/$IGUIDE_ID/assets/$ASSET_NAME/process?uploadToken=$UPLOAD_TOKEN&appendToViews=default" \
-H "X-Plntr-App-Id: $APP_ID" \
-H "X-Plntr-App-Token: $APP_TOKEN")

JID=$(echo $PROCESS_RESPONSE | jq -r '.jid')
echo "Processing job: $JID"
echo "Asset uploaded successfully!"

Code examples

JavaScript (Node.js)

const axios = require('axios');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const fs = require('fs');

const APP_ID = 'your-app-id';
const APP_TOKEN = 'your-access-token';
const IGUIDE_ID = 'ig12345';
const ASSET_FILE = 'front-exterior.jpg';

async function uploadAsset() {
// Step 1: Get upload permit
const fileBuffer = fs.readFileSync(ASSET_FILE);
const fileStats = fs.statSync(ASSET_FILE);

const permitResponse = await axios.post(
`https://manage.youriguide.com/api/v1/iguides/${IGUIDE_ID}/assets`,
{
filename: ASSET_FILE,
filesize: fileStats.size
},
{
headers: {
'Content-Type': 'application/json',
'X-Plntr-App-Id': APP_ID,
'X-Plntr-App-Token': APP_TOKEN
}
}
);

const { name: assetName, uploadPermit, uploadToken } = permitResponse.data;
console.log('Asset name:', assetName);

// Step 2: Upload to S3
const s3 = new S3Client({
region: uploadPermit.region,
credentials: {
accessKeyId: uploadPermit.accessKeyId,
secretAccessKey: uploadPermit.secretAccessKey,
sessionToken: uploadPermit.sessionToken,
},
});

await s3.send(new PutObjectCommand({
Bucket: uploadPermit.bucket,
Key: uploadPermit.key,
Body: fileBuffer,
ACL: 'bucket-owner-full-control',
}));

console.log('Uploaded to S3');

// Step 3: Trigger processing
const processResponse = await axios.post(
`https://manage.youriguide.com/api/v1/iguides/${IGUIDE_ID}/assets/${assetName}/process`,
null,
{
params: {
uploadToken,
appendToViews: 'default'
},
headers: {
'X-Plntr-App-Id': APP_ID,
'X-Plntr-App-Token': APP_TOKEN
}
}
);

console.log('Processing job:', processResponse.data.jid);
console.log('Asset uploaded successfully!');
}

uploadAsset().catch(console.error);

Python

import requests
import boto3
import os

APP_ID = 'your-app-id'
APP_TOKEN = 'your-access-token'
IGUIDE_ID = 'ig12345'
ASSET_FILE = 'front-exterior.jpg'

def upload_asset():
# Step 1: Get upload permit
filesize = os.path.getsize(ASSET_FILE)

permit_response = requests.post(
f'https://manage.youriguide.com/api/v1/iguides/{IGUIDE_ID}/assets',
headers={
'Content-Type': 'application/json',
'X-Plntr-App-Id': APP_ID,
'X-Plntr-App-Token': APP_TOKEN
},
json={
'filename': ASSET_FILE,
'filesize': filesize
}
)
permit_response.raise_for_status()

permit_data = permit_response.json()
asset_name = permit_data['name']
upload_token = permit_data['uploadToken']
permit = permit_data['uploadPermit']

print(f'Asset name: {asset_name}')

# Step 2: Upload to S3
s3 = boto3.client('s3',
region_name=permit['region'],
aws_access_key_id=permit['accessKeyId'],
aws_secret_access_key=permit['secretAccessKey'],
aws_session_token=permit['sessionToken'],
)

s3.upload_file(
ASSET_FILE,
permit['bucket'],
permit['key'],
ExtraArgs={'ACL': 'bucket-owner-full-control'},
)

print('Uploaded to S3')

# Step 3: Trigger processing
process_response = requests.post(
f'https://manage.youriguide.com/api/v1/iguides/{IGUIDE_ID}/assets/{asset_name}/process',
headers={
'X-Plntr-App-Id': APP_ID,
'X-Plntr-App-Token': APP_TOKEN
},
params={
'uploadToken': upload_token,
'appendToViews': 'default'
}
)
process_response.raise_for_status()

print(f'Processing job: {process_response.json()["jid"]}')
print('Asset uploaded successfully!')

if __name__ == '__main__':
upload_asset()

Supported file types

The asset upload API accepts any file type—photos, videos, PDFs, documents. However, only image files (JPEG, PNG, GIF, HEIC) will render properly in the iGUIDE viewer gallery. Other file types can be uploaded and stored but won't display as gallery images.

Recommended file types for gallery display:

  • JPEG/JPG (most common)
  • PNG
  • GIF
  • HEIC (Apple formats)

Other supported types (stored but not displayed in gallery):

  • PDF documents
  • Video files (MP4, MOV)
  • Other document formats
Image optimization

For best performance in the viewer, use JPEG format with reasonable compression. Very large image files (over 10 MB) may slow down gallery loading—consider resizing or compressing before upload.

Error handling

Upload permit fails (400)

Problem: {"code": "invalid_argument"}

Common causes:

  • Missing filename or filesize in request body
  • filesize is too large (exceeds server limits)
  • iGUIDE not found or not accessible

Solution: Verify the request payload includes both required fields and the iGUIDE ID is correct.

S3 upload fails (403)

Problem: Access Denied when uploading to S3

Solutions:

  • Verify you're using all three credentials: accessKeyId, secretAccessKey and sessionToken
  • Ensure you're including --acl bucket-owner-full-control on the upload
  • Check if credentials expired (2-hour lifetime for assets)
  • Ensure you're uploading to the exact bucket and key from the uploadPermit response

Processing fails (400)

Problem: Invalid upload token

Solutions:

  • Verify you're passing the uploadToken from Step 1 exactly as-is
  • Check that you're using the correct asset name (from the permit response, not your original filename)
  • Ensure you uploaded the file to S3 before calling the process endpoint
  • Verify the upload token hasn't expired

Next steps

Now that you can upload supplementary assets, here's what to explore next: