Appearance
Imagery SDK Guide
The Urban Sky SDK now includes built-in support for accessing mission imagery through the Data Explorer API.
Quick Start
JavaScript
javascript
const { UrbanSkySDK } = require('@urbansky/sdk')
const sdk = await UrbanSkySDK.init({
apiToken: 'your-api-token',
baseUrl: 'https://api.ops.urbansky.com',
})
// List missions
const missions = await sdk.listMissions({ page: 1, limit: 50 })
// List images for a mission
const images = await sdk.listImages('mission-id', {
page: 1,
limit: 100,
dateFrom: '2024-06-01T00:00:00Z',
dateTo: '2024-06-30T23:59:59Z',
altitudeMin: 15000,
altitudeMax: 20000,
})
// Get signed URL for original image (valid 15 minutes)
const { url, expiresAt } = await sdk.getImageUrl('mission-id/path/image.jpg', {
original: 'true',
})
// Get processed image URL
const thumbnail = await sdk.getImageUrl('mission-id/path/image.jpg', {
w: 400,
h: 300,
fit: 'cover',
format: 'webp',
quality: 85,
})
// Get image metadata
const metadata = await sdk.getImageMetadata('mission-id/path/image.jpg')Python
python
from urbansky_sdk import (
UrbanSkySDK,
ImageListOptions,
ImageProcessingOptions,
MissionListOptions
)
sdk = await UrbanSkySDK.init({
'apiToken': 'your-api-token',
'baseUrl': 'https://api.ops.urbansky.com'
})
# List missions
missions = sdk.list_missions(MissionListOptions(page=1, limit=50))
# List images for a mission
images = sdk.list_images(
'mission-id',
ImageListOptions(
page=1,
limit=100,
date_from='2024-06-01T00:00:00Z',
date_to='2024-06-30T23:59:59Z',
altitude_min=15000,
altitude_max=20000
)
)
# Get signed URL for original image
signed_url = sdk.get_image_url(
'mission-id/path/image.jpg',
ImageProcessingOptions(original='true')
)
# Get processed image URL
thumbnail = sdk.get_image_url(
'mission-id/path/image.jpg',
ImageProcessingOptions(
w=400,
h=300,
fit='cover',
format='webp',
quality=85
)
)
# Get image metadata
metadata = sdk.get_image_metadata('mission-id/path/image.jpg')Complete End-to-End Example
This example shows the entire flow from listing missions to downloading an image file. Perfect for testing in the browser console on ops.urbansky.com.
Browser Console (Complete Workflow)
javascript
// ============================================
// COMPLETE IMAGE DOWNLOAD WORKFLOW
// ============================================
const API_TOKEN = 'your-32-character-api-token-here'
const BASE_URL = 'https://api.ops.urbansky.com'
const MISSION_ID = 'your-mission-id'
;(async function downloadImageComplete() {
try {
console.log('🚀 Starting complete image download flow...\n')
// STEP 1: List available missions
console.log('📋 STEP 1: Listing missions...')
const missionsResponse = await fetch(`${BASE_URL}/v1/images/missions`, {
headers: {
'x-api-token': API_TOKEN,
'Content-Type': 'application/json',
},
})
const missionsData = await missionsResponse.json()
console.log(`✅ Found ${missionsData.totalCount} missions`)
console.table(missionsData.missions)
// STEP 2: List images for specific mission
console.log(`\n🖼️ STEP 2: Listing images for mission: ${MISSION_ID}`)
const imagesResponse = await fetch(
`${BASE_URL}/v1/images/missions/${MISSION_ID}/images?limit=5`,
{
headers: {
'x-api-token': API_TOKEN,
'Content-Type': 'application/json',
},
},
)
const imagesData = await imagesResponse.json()
console.log(
`✅ Found ${imagesData.totalCount} images (showing first ${imagesData.images.length})`,
)
// Show image details
imagesData.images.forEach((img, i) => {
console.log(`\n Image ${i + 1}: ${img.filename}`)
console.log(` Location: ${img.latitude}, ${img.longitude}`)
console.log(` Altitude: ${img.altitude}m`)
console.log(` Size: ${(img.size / 1024 / 1024).toFixed(2)} MB`)
})
// STEP 3: Get signed URL for first image
const firstImage = imagesData.images[0]
console.log(`\n🔗 STEP 3: Getting signed URL for: ${firstImage.filename}`)
const signedUrlResponse = await fetch(
`${BASE_URL}/v1/images?key=${encodeURIComponent(firstImage.key)}&original=true`,
{
headers: {
'x-api-token': API_TOKEN,
'Content-Type': 'application/json',
},
},
)
const signedUrlData = await signedUrlResponse.json()
console.log('✅ Signed URL generated successfully!')
console.log(` Expires at: ${signedUrlData.expiresAt}`)
// STEP 4: Download image from S3
console.log(`\n💾 STEP 4: Downloading image from S3...`)
console.log(' Making direct request to S3 (no API token needed!)...')
const imageResponse = await fetch(signedUrlData.url)
const blob = await imageResponse.blob()
console.log(`✅ Image downloaded! Size: ${(blob.size / 1024 / 1024).toFixed(2)} MB`)
// STEP 5: Save to browser
console.log(`\n💿 STEP 5: Saving to your computer...`)
// Create a download link
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = firstImage.filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
console.log(`✅ File saved as: ${firstImage.filename}`)
console.log(' Check your Downloads folder!')
// SUMMARY
console.log('\n' + '='.repeat(50))
console.log('🎉 COMPLETE! Summary:')
console.log('='.repeat(50))
console.log(`✅ API Authentication: Success`)
console.log(`✅ Missions Found: ${missionsData.totalCount}`)
console.log(`✅ Images Found: ${imagesData.totalCount}`)
console.log(`✅ Signed URL Generated: Valid for ~15 minutes`)
console.log(`✅ File Saved: ${firstImage.filename}`)
console.log('='.repeat(50))
return {
mission: MISSION_ID,
image: firstImage,
signedUrl: signedUrlData,
downloadedSize: blob.size,
}
} catch (error) {
console.error('\n❌ ERROR:', error.message)
throw error
}
})()Expected Output:
🚀 Starting complete image download flow...
📋 STEP 1: Listing missions...
✅ Found 9 missions
┌─────────┬──────────────┬─────────────────┐
│ (index) │ id │ name │
├─────────┼──────────────┼─────────────────┤
│ 0 │ 'eLHyAqt6fy' │ 'AEROS1' │
│ 1 │ 'raoaiVxYim' │ 'AEROS2' │
└─────────┴──────────────┴─────────────────┘
🖼️ STEP 2: Listing images for mission: eLHyAqt6fy
✅ Found 234 images (showing first 5)
Image 1: IMG_0001.jpg
Location: 39.7392, -104.9903
Altitude: 15240m
Size: 5.24 MB
🔗 STEP 3: Getting signed URL for: IMG_0001.jpg
✅ Signed URL generated successfully!
Expires at: 2024-12-30T18:39:45.000Z
💾 STEP 4: Downloading image from S3...
✅ Image downloaded! Size: 5.24 MB
💿 STEP 5: Saving to your computer...
✅ File saved as: IMG_0001.jpg
Check your Downloads folder!
==================================================
🎉 COMPLETE! Summary:
==================================================
✅ API Authentication: Success
✅ Missions Found: 9
✅ Images Found: 234
✅ Signed URL Generated: Valid for ~15 minutes
✅ File Saved: IMG_0001.jpg
==================================================Key Points:
- ✅ Shows every step in the download process
- ✅ No external dependencies (works in browser console)
- ✅ Demonstrates API → Signed URL → S3 download flow
- ✅ Automatically saves file to Downloads folder
- ✅ Great for testing and understanding the complete workflow
Core Methods
listMissions(options?)
List available missions with pagination.
Parameters:
options.page(number, optional): Page number (default: 1)options.limit(number, optional): Items per page, max 100 (default: 50)
Returns: MissionListResponse
missions: Array of mission objects withidandnametotalCount: Total number of missionspagination: Pagination metadata
listImages(missionId, options?)
List images for a specific mission with filtering and pagination.
Parameters:
missionId(string, required): Mission identifieroptions.page(number, optional): Page number (default: 1)options.limit(number, optional): Items per page, max 1000 (default: 50)options.dateFrom(string, optional): Start date filter (ISO 8601)options.dateTo(string, optional): End date filter (ISO 8601)options.altitudeMin(number, optional): Minimum altitude in metersoptions.altitudeMax(number, optional): Maximum altitude in metersoptions.latitudeMin(number, optional): Minimum latitude (-90 to 90)options.latitudeMax(number, optional): Maximum latitude (-90 to 90)options.longitudeMin(number, optional): Minimum longitude (-180 to 180)options.longitudeMax(number, optional): Maximum longitude (-180 to 180)
Returns: ImageListResponse
images: Array of image metadata objectstotalCount: Total number of images matching filterspagination: Pagination metadata
getImageUrl(key, options?)
Get a pre-signed S3 URL to download or process an image. URLs are valid for approximately 15 minutes.
Parameters:
key(string, required): Image key (format:missionId/path/to/image.jpg)options.original(string, optional): Set to'true'for original unprocessed imageoptions.w(number, optional): Width in pixelsoptions.h(number, optional): Height in pixelsoptions.fit(string, optional): Resize mode:'cover','contain','inside','outside','fill'options.format(string, optional): Output format:'jpeg','png','webp','avif','tiff','gif'options.quality(number, optional): Quality 1-100 (higher is better)options.colormap(string, optional): Color map for scientific visualization:'normal','thermal','viridis','plasma','inferno'options.preserveValues(boolean, optional): Preserve measurement values during processingoptions.displayMode(string, optional): Display mode:'auto','scientific','standard'options.targetRange(string, optional): Target range for value mapping
Returns: SignedUrlResponse
url: Pre-signed S3 URL (valid ~15 minutes)expiresAt: Expiration timestampmetadata: Additional metadata including the original key
getImageMetadata(key)
Get metadata for an image without downloading it.
Parameters:
key(string, required): Image key (format:missionId/path/to/image.jpg)
Returns: ImageMetadata
key: Image keyfilename: Original filenametimestamp: Capture timestamp (ISO 8601)latitude: Capture latitude (optional)longitude: Capture longitude (optional)altitude: Capture altitude in meters (optional)size: File size in bytes (optional)
Image Processing Options
Resize and Format Conversion
javascript
// Resize to 800x600 WebP with 85% quality
const url = await sdk.getImageUrl('mission-id/image.jpg', {
w: 800,
h: 600,
fit: 'cover',
format: 'webp',
quality: 85,
})
// Create thumbnail maintaining aspect ratio
const thumb = await sdk.getImageUrl('mission-id/image.jpg', {
w: 400,
format: 'jpeg',
quality: 80,
})Fit Modes:
cover: Crop to fill dimensionscontain: Fit within dimensions (letterbox)inside: Resize to fit inside dimensionsoutside: Resize to fit outside dimensionsfill: Stretch to fill dimensions
Scientific Visualization
javascript
// Apply thermal colormap to temperature data
const thermal = await sdk.getImageUrl('mission-id/thermal-image.tiff', {
colormap: 'thermal',
format: 'png',
})
// Preserve exact measurement values
const scientific = await sdk.getImageUrl('mission-id/data-image.tiff', {
displayMode: 'scientific',
preserveValues: true,
format: 'tiff',
})Available Colormaps:
normal: Standard RGB renderingthermal: Hot-to-cold temperature scaleviridis: Perceptually uniform, colorblind-friendlyplasma: Bright colormap for highlightinginferno: Dark-to-bright intensity scale
Filtering Examples
Filter by Date Range
javascript
const images = await sdk.listImages('mission-id', {
dateFrom: '2024-06-01T00:00:00Z',
dateTo: '2024-06-30T23:59:59Z',
})Filter by Altitude
javascript
const images = await sdk.listImages('mission-id', {
altitudeMin: 15000, // 15km
altitudeMax: 20000, // 20km
})Filter by Geographic Bounding Box
javascript
const images = await sdk.listImages('mission-id', {
latitudeMin: 39.0,
latitudeMax: 40.0,
longitudeMin: -105.5,
longitudeMax: -104.5,
})Combine Multiple Filters
javascript
const images = await sdk.listImages('mission-id', {
dateFrom: '2024-06-15T00:00:00Z',
altitudeMin: 15000,
latitudeMin: 39.0,
latitudeMax: 40.0,
page: 1,
limit: 100,
})Pagination
Results are paginated. Use the pagination metadata to iterate through pages:
javascript
let page = 1
let allImages = []
while (true) {
const response = await sdk.listImages('mission-id', {
page,
limit: 100,
})
allImages.push(...response.images)
if (!response.pagination.hasNextPage) {
break
}
page++
}
console.log(`Total images downloaded: ${allImages.length}`)Error Handling
javascript
try {
const images = await sdk.listImages('mission-id')
} catch (error) {
if (error.message.includes('401')) {
console.error('Authentication failed - check API token')
} else if (error.message.includes('403')) {
console.error('Access denied - insufficient permissions')
} else if (error.message.includes('404')) {
console.error('Mission not found')
} else if (error.message.includes('429')) {
console.error('Rate limit exceeded - slow down requests')
} else {
console.error('Unexpected error:', error)
}
}Complete Download Example
javascript
const https = require('https')
const fs = require('fs')
async function downloadAllImages(missionId, outputDir) {
let page = 1
let totalDownloaded = 0
while (true) {
const response = await sdk.listImages(missionId, { page, limit: 100 })
for (const image of response.images) {
const signedUrl = await sdk.getImageUrl(image.key, { original: 'true' })
const outputPath = `${outputDir}/${image.filename}`
await downloadFile(signedUrl.url, outputPath)
totalDownloaded++
console.log(`Downloaded ${totalDownloaded}/${response.totalCount}: ${image.filename}`)
}
if (!response.pagination.hasNextPage) {
break
}
page++
}
return totalDownloaded
}
function downloadFile(url, outputPath) {
return new Promise((resolve, reject) => {
https
.get(url, response => {
const fileStream = fs.createWriteStream(outputPath)
response.pipe(fileStream)
fileStream.on('finish', () => {
fileStream.close()
resolve()
})
fileStream.on('error', reject)
})
.on('error', reject)
})
}
// Usage
await downloadAllImages('mission-id', './downloads')TypeScript Support
Full TypeScript definitions are included:
typescript
import {
UrbanSkySDK,
Mission,
ImageMetadata,
ImageListOptions,
ImageProcessingOptions,
SignedUrlResponse,
} from '@urbansky/sdk'
const sdk = await UrbanSkySDK.init({
apiToken: process.env.API_TOKEN!,
baseUrl: 'https://api.ops.urbansky.com',
})
const missions: Mission[] = (await sdk.listMissions()).missions
const images: ImageMetadata[] = (await sdk.listImages(missions[0].id)).images
const signedUrl: SignedUrlResponse = await sdk.getImageUrl(images[0].key)Best Practices
Respect URL Expiration: Signed URLs expire after ~15 minutes. Download immediately or request a new URL.
Use Appropriate Limits: Request only what you need. Max limits are 100 for missions, 1000 for images.
Add Delays: When downloading many images, add small delays between requests to be respectful:
javascriptawait new Promise(resolve => setTimeout(resolve, 500)) // 500ms delayHandle Errors Gracefully: Network issues happen. Implement retry logic with exponential backoff.
Use Processing Options: Instead of downloading large originals and processing locally, use the API's built-in processing.
Cache Metadata: If you need to reference image metadata multiple times, cache it locally rather than requesting repeatedly.