Skip to main content
Solved

Sandbox Data - Skysat

  • March 27, 2026
  • 1 reply
  • 297 views

Lancelloti
Forum|alt.badge.img

Hello All. How you doing?

I'm trying to develop an JS node application using Planet API but I'm falling to get any data from SkySat Sandbox.

According to sandbox docs, the city of São Mateus - SP, Brazil should be accessible in Sandbox from 2021 - 2022. Altought, everytime I try to set and order it fails saying: "message": "no access to assets: SkySatScene/20210105_131542_ssc12d3_0014/[basic_analytic basic_analytic_rpc basic_analytic_udm]", I've tried a bunch of visual names and none of it seems to work.

Here is my search function:

export async function searchImages() {
const geometry: {
"type": "Polygon",
"coordinates": [
[
[
-46.480481779234,
-23.611431091069008
],
[
-46.480481779234,
-23.613647803458363
],
[
-46.47597059136797,
-23.613647803458363
],
[
-46.47597059136797,
-23.611431091069008
],
[
-46.480481779234,
-23.611431091069008
]
]
],
},
const startDate: '2021-01-05',
const endDate: '2021-12-31'
const url = 'https://api.planet.com/data/v1/quick-search';
const requestBody = {
"item_types": ["SkySatScene"],
"filter": {
"type": "AndFilter",
"config": [
{
"type": "GeometryFilter",
"field_name": "geometry",
"config": geometry
},
{
"type": "DateRangeFilter",
"field_name": "acquired",
"config": {
"gte": `${startDate}T00:00:00Z`,
"lte": `${endDate}T23:59:59Z`
}
}
]
}
};

And here my order function:

 

import 'dotenv/config';

const API_KEY = process.env.PLANET_API_KEY;
const authHeader = 'Basic ' + Buffer.from(`${API_KEY}:`).toString('base64');

export async function createOrder(imageIds, geometry, orderName) {
const url = 'https://api.planet.com/compute/ops/orders/v2';

if (!imageIds || imageIds.length === 0) {
throw new Error("Erro: Lista de IDs de imagem está vazia.");
}

const orderBody = {
name: orderName,
products: [
{
item_ids: imageIds,
item_type: "SkySatScene",
product_bundle: "basic_analytic"
}
],
};

const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json'
},
body: JSON.stringify(orderBody)
});

if (!response.ok) {
// A Planet envia o motivo do erro no corpo da resposta
const errorDetail = await response.json();
console.error("Detalhes do erro da Planet:", JSON.stringify(errorDetail, null, 2));
throw new Error(`Erro no pedido: ${response.status} ${response.statusText}`);
}

return await response.json();
}

Can someone gitve me hint to make it work?

Best answer by matt.ballard

Hey ​@Lancelloti - sorry to hear that you ran into an issue here. The issue is that these scripts are for searching and ordering imagery. With the Sandbox Data, the imagery has already been ordered and delivered to a data collection that you can access directly. You can view this imagery for the area you shared here

If you want to build this imagery into a JavaScript application, you can use the Processing API with the SkySat Sandbox Data collection ID: `BYOC-fc704520-fc81-439f-9016-5e162c32e736`. In the Processing API request, you can use the true color evalscript. You can get help with API requests for you application from our requests builder tool or by asking our AI assistant in our documentation. You may also need to use the Catalog API to find data where there is imagery available.

Here is some example code that works - you can expand the code in the hidden cell below - there is a server.py and index.html file

"""
Tiny local server: OAuth (client credentials), Catalog distinct dates, Process API map tiles.
Run from repo root: python server.py
Requires SH_CLIENT_ID and SH_CLIENT_SECRET in .env
"""

import json
import math
import os
import time
from datetime import datetime, timedelta, timezone

import requests
from dotenv import load_dotenv
from flask import Flask, Response, jsonify, request, send_from_directory

load_dotenv()

BASE = os.environ.get("SH_BASE", "https://services.sentinel-hub.com").rstrip("/")
TOKEN_URL = f"{BASE}/auth/realms/main/protocol/openid-connect/token"
CATALOG_SEARCH = f"{BASE}/api/v1/catalog/1.0.0/search"
PROCESS_URL = f"{BASE}/api/v1/process"

BYOC_TYPE = "byoc-fc704520-fc81-439f-9016-5e162c32e736"
COLLECTION = "byoc-fc704520-fc81-439f-9016-5e162c32e736"

AOI = {
"type": "Polygon",
"coordinates": [
[
[-46.480481779234, -23.611431091069008],
[-46.480481779234, -23.613647803458363],
[-46.47597059136797, -23.613647803458363],
[-46.47597059136797, -23.611431091069008],
[-46.480481779234, -23.611431091069008],
]
],
}

EVALSCRIPT = """//VERSION=3
function setup() {
return {
input: [{ bands: ["blue", "red", "green", "dataMask"] }],
output: { bands: 4 },
};
}
var f = 2.5 / 10000;
function evaluatePixel(sample) {
return [sample.red * f, sample.green * f, sample.blue * f, sample.dataMask];
}
"""

app = Flask(__name__, static_folder="static", static_url_path="")

_token = None
_token_expires_at = 0.0


def get_token():
global _token, _token_expires_at
if _token and time.time() < _token_expires_at - 60:
return _token
cid = os.environ.get("SH_CLIENT_ID", "").strip()
csec = os.environ.get("SH_CLIENT_SECRET", "").strip()
if not cid or not csec:
raise RuntimeError("Set SH_CLIENT_ID and SH_CLIENT_SECRET in .env")
r = requests.post(
TOKEN_URL,
data={
"grant_type": "client_credentials",
"client_id": cid,
"client_secret": csec,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=60,
)
r.raise_for_status()
data = r.json()
_token = data["access_token"]
# JWT exp claim optional; fall back to expires_in
exp = data.get("expires_in", 3600)
_token_expires_at = time.time() + float(exp)
return _token


def tile_lonlat_bounds(z: int, x: int, y: int):
"""Web Mercator XYZ tile corners in WGS84 (min_lon, min_lat, max_lon, max_lat)."""
n = 2.0**z
min_lon = x / n * 360.0 - 180.0
max_lon = (x + 1) / n * 360.0 - 180.0
max_lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * y / n))))
min_lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * (y + 1) / n))))
return (min_lon, min_lat, max_lon, max_lat)


def _iso_date_prefix(value):
if value is None:
return None
s = str(value).strip()
return s[:10] if len(s) >= 10 else None


def _dates_from_catalog_features(features):
"""Catalog distinct 'features' may be GeoJSON features or plain date strings."""
out = []
for feat in features:
if isinstance(feat, str):
d = _iso_date_prefix(feat)
if d:
out.append(d)
continue
if not isinstance(feat, dict):
continue
props = feat.get("properties") or {}
dt = props.get("datetime") or props.get("start_datetime")
if not dt:
dt = feat.get("datetime")
d = _iso_date_prefix(dt)
if d:
out.append(d)
return out


def _collect_dates_paginated(token, page_limit=100, max_pages=50):
"""Fallback: paginate STAC search and unique dates (if distinct shape differs)."""
seen = set()
next_token = None
for _ in range(max_pages):
body = {
"collections": [COLLECTION],
"datetime": "2021-01-01T00:00:00Z/2022-12-31T23:59:59Z",
"intersects": AOI,
"limit": page_limit,
"fields": {"include": ["properties.datetime"]},
}
if next_token is not None:
body["next"] = next_token
r = requests.post(
CATALOG_SEARCH,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json=body,
timeout=120,
)
r.raise_for_status()
payload = r.json()
for d in _dates_from_catalog_features(payload.get("features") or []):
seen.add(d)
ctx = payload.get("context") or {}
next_token = ctx.get("next")
if next_token is None:
break
return list(seen)


def day_range_utc(day_str: str):
"""ISO date YYYY-MM-DD -> (from, to) for that UTC calendar day."""
d = datetime.strptime(day_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
start = d
end = d + timedelta(days=1) - timedelta(seconds=1)
return (
start.strftime("%Y-%m-%dT%H:%M:%SZ"),
end.strftime("%Y-%m-%dT%H:%M:%SZ"),
)


@app.get("/")
def index():
return send_from_directory(app.static_folder, "index.html")


@app.get("/api/dates")
def api_dates():
try:
token = get_token()
except Exception as e:
return jsonify({"error": str(e)}), 500

dates = []
next_token = None
for _ in range(50):
body = {
"collections": [COLLECTION],
"datetime": "2021-01-01T00:00:00Z/2022-12-31T23:59:59Z",
"intersects": AOI,
"distinct": "date",
"limit": 100,
}
if next_token is not None:
body["next"] = next_token
r = requests.post(
CATALOG_SEARCH,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json=body,
timeout=120,
)
if not r.ok:
return (
jsonify({"error": r.text, "status": r.status_code}),
r.status_code,
)
payload = r.json()
dates.extend(_dates_from_catalog_features(payload.get("features") or []))
ctx = payload.get("context") or {}
next_token = ctx.get("next")
if next_token is None:
break

dates = list(set(dates))

if not dates:
dates = _collect_dates_paginated(token)

dates = sorted(set(dates))
return jsonify({"dates": dates})


@app.get("/tiles/<int:z>/<int:x>/<int:y>.png")
def tile(z, x, y):
day = request.args.get("date")
if not day:
return Response("missing date", status=400)

try:
token = get_token()
t_from, t_to = day_range_utc(day)
except Exception as e:
return Response(str(e), status=500)

min_lon, min_lat, max_lon, max_lat = tile_lonlat_bounds(z, x, y)
req_body = {
"input": {
"bounds": {
"bbox": [min_lon, min_lat, max_lon, max_lat],
"properties": {
"crs": "http://www.opengis.net/def/crs/EPSG/0/4326",
},
},
"data": [
{
"type": BYOC_TYPE,
"dataFilter": {
"timeRange": {"from": t_from, "to": t_to},
},
}
],
},
"output": {
"width": 256,
"height": 256,
"responses": [
{
"identifier": "default",
"format": {"type": "image/png"},
}
],
},
"evalscript": EVALSCRIPT,
}

r = requests.post(
PROCESS_URL,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "image/png",
},
data=json.dumps(req_body),
timeout=120,
)
if not r.ok:
return Response(r.text, status=r.status_code, mimetype="text/plain")

return Response(r.content, mimetype="image/png")


if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SkySat sandbox viewer</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }
#bar {
padding: 8px 12px;
border-bottom: 1px solid #ccc;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
#map { height: calc(100vh - 49px); width: 100%; }
label { font-size: 14px; }
select { min-width: 200px; padding: 4px 8px; }
#status { font-size: 13px; color: #666; }
.err { color: #a00; }
</style>
</head>
<body>
<div id="bar">
<label for="dates">Acquisition date</label>
<select id="dates" disabled></select>
<span id="status">Loading dates…</span>
</div>
<div id="map"></div>
<script>
const aoi = {
type: "Polygon",
coordinates: [[
[-46.480481779234, -23.611431091069008],
[-46.480481779234, -23.613647803458363],
[-46.47597059136797, -23.613647803458363],
[-46.47597059136797, -23.611431091069008],
[-46.480481779234, -23.611431091069008],
]],
};

const ring = aoi.coordinates[0];
const lats = ring.map((c) => c[1]);
const lons = ring.map((c) => c[0]);
const center = [
(Math.min(...lats) + Math.max(...lats)) / 2,
(Math.min(...lons) + Math.max(...lons)) / 2,
];

const map = L.map("map").setView(center, 17);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "&copy; OpenStreetMap",
}).addTo(map);

L.geoJSON(aoi, {
style: { color: "#e91e63", weight: 2, fillOpacity: 0.05 },
}).addTo(map);
map.fitBounds(
[[Math.min(...lats), Math.min(...lons)], [Math.max(...lats), Math.max(...lons)]],
{ padding: [24, 24] },
);

let overlay = null;
const sel = document.getElementById("dates");
const statusEl = document.getElementById("status");

function setOverlay(dateStr) {
if (overlay) map.removeLayer(overlay);
if (!dateStr) return;
overlay = L.tileLayer(
`/tiles/{z}/{x}/{y}.png?date=${encodeURIComponent(dateStr)}`,
{ tms: false, maxZoom: 20, opacity: 1, zIndex: 10 },
);
overlay.addTo(map);
}

sel.addEventListener("change", () => {
setOverlay(sel.value);
});

fetch("/api/dates")
.then((r) => r.json())
.then((data) => {
if (data.error) {
statusEl.textContent = data.error;
statusEl.classList.add("err");
return;
}
const dates = data.dates || [];
if (dates.length === 0) {
statusEl.textContent = "No dates returned (check collection / AOI).";
statusEl.classList.add("err");
return;
}
statusEl.textContent = dates.length + " date(s)";
sel.innerHTML = dates
.map((d) => `<option value="${d}">${d}</option>`)
.join("");
sel.disabled = false;
sel.value = dates[dates.length - 1];
setOverlay(sel.value);
})
.catch((e) => {
statusEl.textContent = String(e);
statusEl.classList.add("err");
});
</script>
</body>
</html>

 

If you decide to purchase SkySat data, you’ll then have access to order imagery yourself, and you would use functions similar to what you shared above using the Data API to search, and the Orders API to use the imagery.

1 reply

matt.ballard
Planeteer 🌎
Forum|alt.badge.img+5
  • Planeteer 🌎
  • Answer
  • March 30, 2026

Hey ​@Lancelloti - sorry to hear that you ran into an issue here. The issue is that these scripts are for searching and ordering imagery. With the Sandbox Data, the imagery has already been ordered and delivered to a data collection that you can access directly. You can view this imagery for the area you shared here

If you want to build this imagery into a JavaScript application, you can use the Processing API with the SkySat Sandbox Data collection ID: `BYOC-fc704520-fc81-439f-9016-5e162c32e736`. In the Processing API request, you can use the true color evalscript. You can get help with API requests for you application from our requests builder tool or by asking our AI assistant in our documentation. You may also need to use the Catalog API to find data where there is imagery available.

Here is some example code that works - you can expand the code in the hidden cell below - there is a server.py and index.html file

"""
Tiny local server: OAuth (client credentials), Catalog distinct dates, Process API map tiles.
Run from repo root: python server.py
Requires SH_CLIENT_ID and SH_CLIENT_SECRET in .env
"""

import json
import math
import os
import time
from datetime import datetime, timedelta, timezone

import requests
from dotenv import load_dotenv
from flask import Flask, Response, jsonify, request, send_from_directory

load_dotenv()

BASE = os.environ.get("SH_BASE", "https://services.sentinel-hub.com").rstrip("/")
TOKEN_URL = f"{BASE}/auth/realms/main/protocol/openid-connect/token"
CATALOG_SEARCH = f"{BASE}/api/v1/catalog/1.0.0/search"
PROCESS_URL = f"{BASE}/api/v1/process"

BYOC_TYPE = "byoc-fc704520-fc81-439f-9016-5e162c32e736"
COLLECTION = "byoc-fc704520-fc81-439f-9016-5e162c32e736"

AOI = {
"type": "Polygon",
"coordinates": [
[
[-46.480481779234, -23.611431091069008],
[-46.480481779234, -23.613647803458363],
[-46.47597059136797, -23.613647803458363],
[-46.47597059136797, -23.611431091069008],
[-46.480481779234, -23.611431091069008],
]
],
}

EVALSCRIPT = """//VERSION=3
function setup() {
return {
input: [{ bands: ["blue", "red", "green", "dataMask"] }],
output: { bands: 4 },
};
}
var f = 2.5 / 10000;
function evaluatePixel(sample) {
return [sample.red * f, sample.green * f, sample.blue * f, sample.dataMask];
}
"""

app = Flask(__name__, static_folder="static", static_url_path="")

_token = None
_token_expires_at = 0.0


def get_token():
global _token, _token_expires_at
if _token and time.time() < _token_expires_at - 60:
return _token
cid = os.environ.get("SH_CLIENT_ID", "").strip()
csec = os.environ.get("SH_CLIENT_SECRET", "").strip()
if not cid or not csec:
raise RuntimeError("Set SH_CLIENT_ID and SH_CLIENT_SECRET in .env")
r = requests.post(
TOKEN_URL,
data={
"grant_type": "client_credentials",
"client_id": cid,
"client_secret": csec,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=60,
)
r.raise_for_status()
data = r.json()
_token = data["access_token"]
# JWT exp claim optional; fall back to expires_in
exp = data.get("expires_in", 3600)
_token_expires_at = time.time() + float(exp)
return _token


def tile_lonlat_bounds(z: int, x: int, y: int):
"""Web Mercator XYZ tile corners in WGS84 (min_lon, min_lat, max_lon, max_lat)."""
n = 2.0**z
min_lon = x / n * 360.0 - 180.0
max_lon = (x + 1) / n * 360.0 - 180.0
max_lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * y / n))))
min_lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * (y + 1) / n))))
return (min_lon, min_lat, max_lon, max_lat)


def _iso_date_prefix(value):
if value is None:
return None
s = str(value).strip()
return s[:10] if len(s) >= 10 else None


def _dates_from_catalog_features(features):
"""Catalog distinct 'features' may be GeoJSON features or plain date strings."""
out = []
for feat in features:
if isinstance(feat, str):
d = _iso_date_prefix(feat)
if d:
out.append(d)
continue
if not isinstance(feat, dict):
continue
props = feat.get("properties") or {}
dt = props.get("datetime") or props.get("start_datetime")
if not dt:
dt = feat.get("datetime")
d = _iso_date_prefix(dt)
if d:
out.append(d)
return out


def _collect_dates_paginated(token, page_limit=100, max_pages=50):
"""Fallback: paginate STAC search and unique dates (if distinct shape differs)."""
seen = set()
next_token = None
for _ in range(max_pages):
body = {
"collections": [COLLECTION],
"datetime": "2021-01-01T00:00:00Z/2022-12-31T23:59:59Z",
"intersects": AOI,
"limit": page_limit,
"fields": {"include": ["properties.datetime"]},
}
if next_token is not None:
body["next"] = next_token
r = requests.post(
CATALOG_SEARCH,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json=body,
timeout=120,
)
r.raise_for_status()
payload = r.json()
for d in _dates_from_catalog_features(payload.get("features") or []):
seen.add(d)
ctx = payload.get("context") or {}
next_token = ctx.get("next")
if next_token is None:
break
return list(seen)


def day_range_utc(day_str: str):
"""ISO date YYYY-MM-DD -> (from, to) for that UTC calendar day."""
d = datetime.strptime(day_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
start = d
end = d + timedelta(days=1) - timedelta(seconds=1)
return (
start.strftime("%Y-%m-%dT%H:%M:%SZ"),
end.strftime("%Y-%m-%dT%H:%M:%SZ"),
)


@app.get("/")
def index():
return send_from_directory(app.static_folder, "index.html")


@app.get("/api/dates")
def api_dates():
try:
token = get_token()
except Exception as e:
return jsonify({"error": str(e)}), 500

dates = []
next_token = None
for _ in range(50):
body = {
"collections": [COLLECTION],
"datetime": "2021-01-01T00:00:00Z/2022-12-31T23:59:59Z",
"intersects": AOI,
"distinct": "date",
"limit": 100,
}
if next_token is not None:
body["next"] = next_token
r = requests.post(
CATALOG_SEARCH,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json=body,
timeout=120,
)
if not r.ok:
return (
jsonify({"error": r.text, "status": r.status_code}),
r.status_code,
)
payload = r.json()
dates.extend(_dates_from_catalog_features(payload.get("features") or []))
ctx = payload.get("context") or {}
next_token = ctx.get("next")
if next_token is None:
break

dates = list(set(dates))

if not dates:
dates = _collect_dates_paginated(token)

dates = sorted(set(dates))
return jsonify({"dates": dates})


@app.get("/tiles/<int:z>/<int:x>/<int:y>.png")
def tile(z, x, y):
day = request.args.get("date")
if not day:
return Response("missing date", status=400)

try:
token = get_token()
t_from, t_to = day_range_utc(day)
except Exception as e:
return Response(str(e), status=500)

min_lon, min_lat, max_lon, max_lat = tile_lonlat_bounds(z, x, y)
req_body = {
"input": {
"bounds": {
"bbox": [min_lon, min_lat, max_lon, max_lat],
"properties": {
"crs": "http://www.opengis.net/def/crs/EPSG/0/4326",
},
},
"data": [
{
"type": BYOC_TYPE,
"dataFilter": {
"timeRange": {"from": t_from, "to": t_to},
},
}
],
},
"output": {
"width": 256,
"height": 256,
"responses": [
{
"identifier": "default",
"format": {"type": "image/png"},
}
],
},
"evalscript": EVALSCRIPT,
}

r = requests.post(
PROCESS_URL,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "image/png",
},
data=json.dumps(req_body),
timeout=120,
)
if not r.ok:
return Response(r.text, status=r.status_code, mimetype="text/plain")

return Response(r.content, mimetype="image/png")


if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SkySat sandbox viewer</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }
#bar {
padding: 8px 12px;
border-bottom: 1px solid #ccc;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
#map { height: calc(100vh - 49px); width: 100%; }
label { font-size: 14px; }
select { min-width: 200px; padding: 4px 8px; }
#status { font-size: 13px; color: #666; }
.err { color: #a00; }
</style>
</head>
<body>
<div id="bar">
<label for="dates">Acquisition date</label>
<select id="dates" disabled></select>
<span id="status">Loading dates…</span>
</div>
<div id="map"></div>
<script>
const aoi = {
type: "Polygon",
coordinates: [[
[-46.480481779234, -23.611431091069008],
[-46.480481779234, -23.613647803458363],
[-46.47597059136797, -23.613647803458363],
[-46.47597059136797, -23.611431091069008],
[-46.480481779234, -23.611431091069008],
]],
};

const ring = aoi.coordinates[0];
const lats = ring.map((c) => c[1]);
const lons = ring.map((c) => c[0]);
const center = [
(Math.min(...lats) + Math.max(...lats)) / 2,
(Math.min(...lons) + Math.max(...lons)) / 2,
];

const map = L.map("map").setView(center, 17);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "&copy; OpenStreetMap",
}).addTo(map);

L.geoJSON(aoi, {
style: { color: "#e91e63", weight: 2, fillOpacity: 0.05 },
}).addTo(map);
map.fitBounds(
[[Math.min(...lats), Math.min(...lons)], [Math.max(...lats), Math.max(...lons)]],
{ padding: [24, 24] },
);

let overlay = null;
const sel = document.getElementById("dates");
const statusEl = document.getElementById("status");

function setOverlay(dateStr) {
if (overlay) map.removeLayer(overlay);
if (!dateStr) return;
overlay = L.tileLayer(
`/tiles/{z}/{x}/{y}.png?date=${encodeURIComponent(dateStr)}`,
{ tms: false, maxZoom: 20, opacity: 1, zIndex: 10 },
);
overlay.addTo(map);
}

sel.addEventListener("change", () => {
setOverlay(sel.value);
});

fetch("/api/dates")
.then((r) => r.json())
.then((data) => {
if (data.error) {
statusEl.textContent = data.error;
statusEl.classList.add("err");
return;
}
const dates = data.dates || [];
if (dates.length === 0) {
statusEl.textContent = "No dates returned (check collection / AOI).";
statusEl.classList.add("err");
return;
}
statusEl.textContent = dates.length + " date(s)";
sel.innerHTML = dates
.map((d) => `<option value="${d}">${d}</option>`)
.join("");
sel.disabled = false;
sel.value = dates[dates.length - 1];
setOverlay(sel.value);
})
.catch((e) => {
statusEl.textContent = String(e);
statusEl.classList.add("err");
});
</script>
</body>
</html>

 

If you decide to purchase SkySat data, you’ll then have access to order imagery yourself, and you would use functions similar to what you shared above using the Data API to search, and the Orders API to use the imagery.