Get up and running in 3 steps:
Sign up to get your API key instantly. Then purchase credits or redeem a voucher to get started.
Query NO₂ air quality data for Paris, January 2023:
curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.2&\ lon_min=2.0&lon_max=2.8&\ time_start=2023-01-01&time_end=2023-02-01&\ pollutants=no2" \ -H "X-API-Key: sk_live_your_key_here"
{
"status": "success",
"query_time_ms": 18,
"tiles_scanned": 2,
"credits_used": 2,
"credits_remaining": 198,
"output": "Rows matched: 18200\nMin: 1.23\nMax: 42.5\nAverage: 9.87\n"
}
Add format=csv to get raw hourly data instead:
{
"status": "success",
"query_time_ms": 22,
"tiles_scanned": 2,
"credits_used": 2,
"credits_remaining": 196,
"format": "csv",
"aggregate": "hourly",
"output": "lat,lon,time,no2\n48.6500,2.1500,2023-01-01T00:00:00Z,12.34\n48.6500,2.1500,2023-01-01T01:00:00Z,11.87\n..."
}
All API requests require an API key. You can pass it in two ways:
X-API-Key: sk_live_your_key_here
GET /api/v1/climate/query?api_key=sk_live_your_key_here&...
Security note: API keys are hashed with SHA-256 before storage. We never store your raw key.
Each query costs credits based on the data it touches:
credits = geographic_regions × time_months × pollutants
| Factor | Description | Example |
|---|---|---|
geographic_regions |
Number of geographic areas (5°×5°) covering your lat/lon bounds | Paris region: 2 regions |
time_months |
Number of months in your time range | Jan 2023: 1 month |
pollutants |
Number of pollutants queried | no2: 1 pollutant |
Your remaining balance is returned in every response as credits_remaining. If you don't have enough credits, the API returns 402 Payment Required.
GET /api/v1/climate/query
Query climate data for a geographic region and time range.
| Parameter | Type | Required | Description |
|---|---|---|---|
lat_min |
float | required* | Minimum latitude (-90 to 90). *Not required when using lat/lon point shorthand. |
lat_max |
float | required* | Maximum latitude (-90 to 90) |
lon_min |
float | required* | Minimum longitude (-180 to 180) |
lon_max |
float | required* | Maximum longitude (-180 to 180) |
lat |
float | optional | Point query latitude. Use with lon instead of bbox params. Snaps to the nearest 0.1° grid point. See Point Query. |
lon |
float | optional | Point query longitude. Use with lat. |
time_start |
string | required | Start time. Accepts YYYY-MM-DD, YYYY-MM, YYYY-MM-DDTHH:MM:SSZ, or Unix timestamp (seconds) |
time_end |
string | required | End time. Same formats as time_start. Both bounds are inclusive. |
pollutants |
string | optional | Comma-separated list. Values: no2, pm2p5. Default: no2 |
format |
string | optional | stats (default) — summary statistics. csv — data rows. geojson — GeoJSON FeatureCollection. See Output Formats. |
aggregate |
string | optional | Controls time/space aggregation. Per-point: hourly (default), daily, monthly, annual. Spatial averages: area_hourly, area_daily, area_monthly, diurnal. See Output Formats. |
threshold |
float | optional | µg/m³ threshold for exceedance analysis. When set, returns hours-above-threshold per grid point instead of values. See Exceedance. |
percentile |
float | optional | Percentile to compute (1–100) per grid point over the queried period. E.g. percentile=95 for the 95th percentile. See Percentile. |
{
"status": "success",
"query_time_ms": 18,
"tiles_scanned": 2,
"credits_used": 2,
"credits_remaining": 6498,
"format": "csv", // present only for format=csv or threshold queries
"aggregate": "hourly", // present only for format=csv or threshold queries
"output": "..." // see Output Formats below
}
The output field contains the query result. Its format depends on the format and aggregate parameters — see Output Formats below.
The format and aggregate parameters together control the shape of the output field. All modes cost the same credits — only the queried tile count matters.
| Mode | Parameters | Output rows | Use case |
|---|---|---|---|
| Stats | format=stats (default) | 4 scalar lines | Quick min / max / avg summary |
| Hourly | format=csv | 1 per grid point × hour | Raw time series, full resolution |
| Daily | format=csv&aggregate=daily | 1 per grid point × day | Day-by-day means |
| Monthly | format=csv&aggregate=monthly | 1 per grid point × month | Month-by-month means |
| Annual | format=csv&aggregate=annual | 1 per grid point × year | Year-over-year comparison |
| Area hourly | aggregate=area_hourly | 1 per hour | Regional mean time series |
| Area daily | aggregate=area_daily | 1 per day | Regional daily trend |
| Area monthly | aggregate=area_monthly | 1 per month | Regional monthly trend |
| Diurnal | aggregate=diurnal | 24 (one per hour-of-day) | Typical daily cycle |
| Exceedance | threshold=N | 1 per grid point | Hours above a µg/m³ limit |
| Percentile | percentile=N | 1 per grid point | P50, P95, P98 … per location |
| GeoJSON | format=geojson | same rows as CSV | Map-ready output for Mapbox / Leaflet / QGIS |
Returns aggregate statistics over the entire query region and time range. Useful for quick summaries.
Rows matched: 18200 Min: 1.230000 Max: 42.500000 Average: 9.870000
Returns one row per grid point per hour. Time is ISO 8601 UTC.
curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.0&lon_min=2.0&lon_max=2.5&\ time_start=2023-01-01&time_end=2023-01-02&\ pollutants=no2&format=csv&aggregate=hourly" \ -H "X-API-Key: sk_live_your_key_here"
lat,lon,time,no2 48.5500,2.0500,2023-01-01T00:00:00Z,8.32 48.5500,2.0500,2023-01-01T01:00:00Z,7.14 48.5500,2.1500,2023-01-01T00:00:00Z,9.01 ...
Returns the daily mean per grid point. 24× fewer rows than hourly.
curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.0&lon_min=2.0&lon_max=2.5&\ time_start=2023-01-01&time_end=2023-01-31&\ pollutants=no2&format=csv&aggregate=daily" \ -H "X-API-Key: sk_live_your_key_here"
lat,lon,date,no2_mean 48.5500,2.0500,2023-01-01,8.93 48.5500,2.0500,2023-01-02,7.65 48.5500,2.1500,2023-01-01,9.12 ...
Returns the monthly mean per grid point. Great for trend analysis across months.
curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.0&lon_min=2.0&lon_max=2.5&\ time_start=2023-01&time_end=2023-12&\ pollutants=no2&format=csv&aggregate=monthly" \ -H "X-API-Key: sk_live_your_key_here"
lat,lon,year_month,no2_mean 48.5500,2.0500,2023-01,9.87 48.5500,2.0500,2023-02,8.34 48.5500,2.1500,2023-01,10.23 ...
Queries that would return more than 500,000 rows are rejected with 400 Bad Request and a message suggesting to use aggregate=daily or aggregate=monthly, or to reduce the time range.
Use lat and lon instead of the four bbox params to query a single grid point. The coordinates are automatically snapped to the nearest 0.1° grid point.
# Hourly NO₂ at the Eiffel Tower, January 2023 curl "https://api.jiskta.com/api/v1/climate/query?\ lat=48.8566&lon=2.3522&\ time_start=2023-01-01&time_end=2023-01-31&\ pollutants=no2&format=csv" \ -H "X-API-Key: sk_live_your_key_here"
lat,lon,time,no2 48.8500,2.3500,2023-01-01T00:00:00Z,12.34 48.8500,2.3500,2023-01-01T01:00:00Z,11.87 ...
Grid snapping: The data grid has cell centres at …, −0.05°, 0.05°, 0.15°, 0.25°, … The API snaps your coordinates to the nearest cell centre so you always get exactly one point back, regardless of how precisely you specify the coordinates.
Set threshold to a µg/m³ value to get a per-grid-point count of hours above that threshold over the queried period. Useful for air quality index calculations and health exposure studies.
# Hours of NO₂ above 25 µg/m³ in Paris, January 2023 curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.0&lon_min=2.0&lon_max=2.5&\ time_start=2023-01-01&time_end=2023-01-31&\ pollutants=no2&threshold=25" \ -H "X-API-Key: sk_live_your_key_here"
lat,lon,hours_above,total_hours,pct_above 48.5500,2.0500,22,744,2.96 48.5500,2.1500,31,744,4.17 48.6500,2.0500,18,744,2.42 ...
| Column | Description |
|---|---|
hours_above | Number of hours the pollutant exceeded the threshold |
total_hours | Total hours in the queried period for this grid point |
pct_above | Percentage of hours above the threshold (0–100) |
Use aggregate=area_hourly, area_daily, or area_monthly to get a single spatially-averaged time series for the entire queried bounding box. Instead of one row per grid point, you get one row per time step — ideal for regional trend analysis.
# Daily spatial average for Paris bounding box, January 2023 curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.0&lon_min=2.0&lon_max=2.5&\ time_start=2023-01-01&time_end=2023-01-31&\ pollutants=no2&format=csv&aggregate=area_daily" \ -H "X-API-Key: sk_live_your_key_here"
date,no2_mean 2023-01-01,9.41 2023-01-02,8.73 2023-01-03,11.20 ...
| aggregate value | Time column | Rows per pollutant |
|---|---|---|
area_hourly | time (ISO 8601) | 1 per hour |
area_daily | date (YYYY-MM-DD) | 1 per day |
area_monthly | year_month (YYYY-MM) | 1 per month |
Use aggregate=annual with format=csv to get the annual mean per grid point. Useful for comparing year-over-year trends across a region.
# Annual mean per grid point, 2023 full year curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.0&lon_min=2.0&lon_max=2.5&\ time_start=2023-01-01&time_end=2023-12-31&\ pollutants=no2&format=csv&aggregate=annual" \ -H "X-API-Key: sk_live_your_key_here"
lat,lon,year,no2_mean 48.5500,2.0500,2023,8.72 48.5500,2.1500,2023,9.10 48.6500,2.0500,2023,7.95 ...
Use aggregate=diurnal to get the average value per hour-of-day (0–23), spatially averaged over the bounding box. Shows the typical daily cycle of a pollutant over your chosen period.
# Hourly-of-day NO₂ profile for Paris, January 2023 curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.0&lon_min=2.0&lon_max=2.5&\ time_start=2023-01-01&time_end=2023-01-31&\ pollutants=no2&format=csv&aggregate=diurnal" \ -H "X-API-Key: sk_live_your_key_here"
hour,no2_mean 0,12.79 1,11.43 ... 12,14.20 ... 23,13.55
Add percentile=N (1–100) to compute the Nth percentile of all hourly values per grid point over the queried period. Commonly used for regulatory compliance (e.g., the 98th percentile for NO₂ limit value assessments).
# 95th percentile NO₂ per grid point, January 2023 curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.0&lon_min=2.0&lon_max=2.5&\ time_start=2023-01-01&time_end=2023-01-31&\ pollutants=no2&percentile=95" \ -H "X-API-Key: sk_live_your_key_here"
lat,lon,p95 48.5500,2.0500,23.82 48.5500,2.1500,21.47 48.6500,2.0500,19.63 ...
The aggregate field in the response is set to percentile_N (e.g. percentile_95).
Set format=geojson to receive a GeoJSON FeatureCollection instead of CSV. Each feature is a Point with coordinates [lon, lat] and all data values in properties. Compatible with Mapbox, Leaflet, QGIS, and any GeoJSON-aware tool.
# Monthly mean in GeoJSON format curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=48.5&lat_max=49.0&lon_min=2.0&lon_max=2.5&\ time_start=2023-01&time_end=2023-01&\ pollutants=no2&format=geojson&aggregate=monthly" \ -H "X-API-Key: sk_live_your_key_here"
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [2.05, 48.55] },
"properties": { "year_month": "2023-01", "no2_mean": 9.87 }
},
...
]
}
Note: GeoJSON output is supported for all per-point modes (hourly, daily, monthly, annual, exceedance, percentile). Area and diurnal aggregates have no spatial dimension and are returned as CSV regardless of the format parameter.
GET /health
Check if the API is running. No authentication required.
{
"status": "healthy",
"service": "climate-api",
"auth_enabled": true
}
POST /api/v1/redeem
Redeem a voucher code to add credits to your account. Requires authentication.
{
"code": "WELCOME-2025-ABC"
}
{
"status": "success",
"credits_added": 500,
"credits_remaining": 500,
"description": "Welcome bonus"
}
| Status | Meaning |
|---|---|
200 | Voucher redeemed successfully |
404 | Invalid voucher code |
409 | Voucher already redeemed |
410 | Voucher has expired |
POST /api/v1/checkout
Create a Stripe checkout session to purchase credits.
{
"package": "starter",
"api_key_id": "your-api-key-uuid"
}
| Field | Type | Description |
|---|---|---|
package |
string | One of: starter (€10), professional (€50), enterprise (€200) |
api_key_id |
string | Your API key UUID (from account setup) |
{
"checkout_url": "https://checkout.stripe.com/c/pay/..."
}
Redirect the user to checkout_url to complete payment. Credits are added automatically after successful payment.
POST /api/v1/webhook/stripe
Handles Stripe payment events. Called by Stripe — not by your code. When a customer completes a checkout session, Stripe sends a checkout.session.completed event to this endpoint, which automatically adds credits to the customer's account.
https://your-api.com/api/v1/webhook/stripe)checkout.session.completedwhsec_...) and set it as STRIPE_WEBHOOK_SECRET env varIf STRIPE_WEBHOOK_SECRET is configured, the API verifies the Stripe-Signature header using HMAC-SHA256 to prevent spoofed events.
Customer clicks "Buy Credits" on dashboard → Stripe Checkout page opens → Customer pays with card → Stripe sends checkout.session.completed webhook → API reads metadata (api_key_id, package, credits) → Credits added atomically to customer's account → Customer returns to dashboard with updated balance
If your API key is compromised or you need a fresh key, you can regenerate it from the Dashboard.
401sk_live_ key is generatedFor advanced integrations, the regeneration is powered by a Supabase RPC function called from the authenticated client:
const { data, error } = await supabase.rpc('regenerate_api_key');
// Returns: [{ raw_key: "sk_live_...", new_prefix: "sk_live_abcd" }]
Important: This function requires an authenticated Supabase session. It uses auth.email() to identify the user, so it can only regenerate your own key.
import requests
import io, csv
API_KEY = "sk_live_your_key_here"
BASE_URL = "https://api.jiskta.com"
# Get daily NO₂ means for Paris, full year 2023
response = requests.get(
f"{BASE_URL}/api/v1/climate/query",
headers={"X-API-Key": API_KEY},
params={
"lat_min": 48.5, "lat_max": 49.2,
"lon_min": 2.0, "lon_max": 2.8,
"time_start": "2023-01-01",
"time_end": "2023-12-31",
"pollutants": "no2",
"format": "csv",
"aggregate": "daily",
}
)
data = response.json()
print(f"Query took {data['query_time_ms']}ms, {data['credits_used']} credits used")
# Parse the CSV output
reader = csv.DictReader(io.StringIO(data["output"]))
rows = list(reader)
print(f"{len(rows)} daily observations")
print(rows[0]) # {'lat': '48.6500', 'lon': '2.1500', 'date': '2023-01-01', 'no2_mean': '9.87'}
# Single-point query
pt = requests.get(
f"{BASE_URL}/api/v1/climate/query",
headers={"X-API-Key": API_KEY},
params={
"lat": 48.8566, "lon": 2.3522, # Eiffel Tower — snapped to nearest grid point
"time_start": "2023-06-01", "time_end": "2023-06-30",
"pollutants": "no2", "format": "csv", "aggregate": "monthly",
}
).json()
print(pt["output"])
const API_KEY = "sk_live_your_key_here";
const BASE_URL = "https://api.jiskta.com";
// Monthly means for London, 2023
const params = new URLSearchParams({
lat_min: 51.3, lat_max: 51.7,
lon_min: -0.5, lon_max: 0.3,
time_start: "2023-01", time_end: "2023-12",
pollutants: "no2",
format: "csv",
aggregate: "monthly",
});
const res = await fetch(
`${BASE_URL}/api/v1/climate/query?${params}`,
{ headers: { "X-API-Key": API_KEY } }
);
const data = await res.json();
console.log(`${data.credits_used} credits, ${data.query_time_ms}ms`);
// Parse CSV rows
const [header, ...rows] = data.output.trim().split("\n");
const cols = header.split(",");
const parsed = rows.map(r => Object.fromEntries(r.split(",").map((v, i) => [cols[i], v])));
console.log(parsed[0]); // { lat: '51.4500', lon: '-0.4500', year_month: '2023-01', no2_mean: '14.2' }
# Hourly PM2.5 in London, March 2023 — save to CSV curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=51.3&lat_max=51.7&\ lon_min=-0.5&lon_max=0.3&\ time_start=2023-03-01&time_end=2023-04-01&\ pollutants=pm2p5&format=csv&aggregate=daily" \ -H "X-API-Key: sk_live_your_key_here" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['output'])" > london_pm25_march.csv # Exceedance: hours of NO₂ above WHO guideline (25 µg/m³), Berlin, 2023 curl "https://api.jiskta.com/api/v1/climate/query?\ lat_min=52.3&lat_max=52.7&\ lon_min=13.0&lon_max=13.7&\ time_start=2023-01-01&time_end=2023-12-31&\ pollutants=no2&threshold=25" \ -H "X-API-Key: sk_live_your_key_here" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['output'])"
The API uses standard HTTP status codes:
| Status | Meaning | Action |
|---|---|---|
200 |
Success | Parse the response data |
400 |
Bad Request | Check your query parameters; or result exceeded 500K rows — use aggregate=daily or smaller range |
401 |
Unauthorized | Check your API key |
402 |
Payment Required | Insufficient credits — buy more |
429 |
Too Many Requests | Server under load and your key is above its fair share — retry after 500ms with exponential backoff |
500 |
Server Error | Retry after a moment; contact support if persistent |
{
"error": "Insufficient credits",
"credits_remaining": 3,
"credits_required": 8
}