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.