Skip to content

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 with id and name
  • totalCount: Total number of missions
  • pagination: Pagination metadata

listImages(missionId, options?)

List images for a specific mission with filtering and pagination.

Parameters:

  • missionId (string, required): Mission identifier
  • options.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 meters
  • options.altitudeMax (number, optional): Maximum altitude in meters
  • options.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 objects
  • totalCount: Total number of images matching filters
  • pagination: 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 image
  • options.w (number, optional): Width in pixels
  • options.h (number, optional): Height in pixels
  • options.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 processing
  • options.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 timestamp
  • metadata: 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 key
  • filename: Original filename
  • timestamp: 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 dimensions
  • contain: Fit within dimensions (letterbox)
  • inside: Resize to fit inside dimensions
  • outside: Resize to fit outside dimensions
  • fill: 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 rendering
  • thermal: Hot-to-cold temperature scale
  • viridis: Perceptually uniform, colorblind-friendly
  • plasma: Bright colormap for highlighting
  • inferno: 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

  1. Respect URL Expiration: Signed URLs expire after ~15 minutes. Download immediately or request a new URL.

  2. Use Appropriate Limits: Request only what you need. Max limits are 100 for missions, 1000 for images.

  3. Add Delays: When downloading many images, add small delays between requests to be respectful:

    javascript
    await new Promise(resolve => setTimeout(resolve, 500)); // 500ms delay
  4. Handle Errors Gracefully: Network issues happen. Implement retry logic with exponential backoff.

  5. Use Processing Options: Instead of downloading large originals and processing locally, use the API's built-in processing.

  6. Cache Metadata: If you need to reference image metadata multiple times, cache it locally rather than requesting repeatedly.

See Also