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:
- App ID and Access Token (see Your First iGUIDE for setup)
- A published iGUIDE with a completed initial work order
- Understanding of the S3 Upload Pattern
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.
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:
- Get an upload permit from the Portal API
- Upload directly to S3 using temporary credentials
- 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
}'
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
uploadTokenis a JWT you'll use to trigger processing in Step 3
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"},
)
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.
No gallery addition
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.
- Use
appendToViews=defaultfor most cases—supplementary photos should appear in the main viewer gallery - Use
appendToViews=allif 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
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
filenameorfilesizein request body filesizeis 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,secretAccessKeyandsessionToken - Ensure you're including
--acl bucket-owner-full-controlon the upload - Check if credentials expired (2-hour lifetime for assets)
- Ensure you're uploading to the exact bucket and key from the
uploadPermitresponse
Processing fails (400)
Problem: Invalid upload token
Solutions:
- Verify you're passing the
uploadTokenfrom 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:
- S3 Upload Pattern—Deep dive on the upload pattern, multipart uploads and SDK examples
- Listen for Webhooks—Get notified when processing completes instead of polling
- Download Deliverables—Retrieve all iGUIDE deliverables including gallery images