Quick Start

Get up and running in 3 steps:

1. Get your API key

Sign up to get your API key instantly. Then purchase credits or redeem a voucher to get started.

2. Make your first query

Query NO₂ air quality data for Paris, January 2023:

bash
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"

3. Parse the response

json
{
  "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:

json
{
  "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..."
}

Authentication

All API requests require an API key. You can pass it in two ways:

Header (recommended)

http
X-API-Key: sk_live_your_key_here

Query parameter

http
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.

Credits

Each query costs credits based on the data it touches:

Formula
credits = geographic_regions × time_months × pollutants
FactorDescriptionExample
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.

Query Endpoint

GET /api/v1/climate/query

Query climate data for a geographic region and time range.

Parameters

ParameterTypeRequiredDescription
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.

Response

json
{
  "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.

Output Formats

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.

ModeParametersOutput rowsUse case
Statsformat=stats (default)4 scalar linesQuick min / max / avg summary
Hourlyformat=csv1 per grid point × hourRaw time series, full resolution
Dailyformat=csv&aggregate=daily1 per grid point × dayDay-by-day means
Monthlyformat=csv&aggregate=monthly1 per grid point × monthMonth-by-month means
Annualformat=csv&aggregate=annual1 per grid point × yearYear-over-year comparison
Area hourlyaggregate=area_hourly1 per hourRegional mean time series
Area dailyaggregate=area_daily1 per dayRegional daily trend
Area monthlyaggregate=area_monthly1 per monthRegional monthly trend
Diurnalaggregate=diurnal24 (one per hour-of-day)Typical daily cycle
Exceedancethreshold=N1 per grid pointHours above a µg/m³ limit
Percentilepercentile=N1 per grid pointP50, P95, P98 … per location
GeoJSONformat=geojsonsame rows as CSVMap-ready output for Mapbox / Leaflet / QGIS

format=stats (default)

Returns aggregate statistics over the entire query region and time range. Useful for quick summaries.

text
Rows matched: 18200
Min: 1.230000
Max: 42.500000
Average: 9.870000

format=csv & aggregate=hourly (default for csv)

Returns one row per grid point per hour. Time is ISO 8601 UTC.

bash
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"
csv
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
...

format=csv & aggregate=daily

Returns the daily mean per grid point. 24× fewer rows than hourly.

bash
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"
csv
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
...

format=csv & aggregate=monthly

Returns the monthly mean per grid point. Great for trend analysis across months.

bash
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"
csv
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
...

Row limit

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.

Point Query

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.

bash
# 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"
csv
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.

Exceedance Analysis

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.

bash
# 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"
csv
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
...
ColumnDescription
hours_aboveNumber of hours the pollutant exceeded the threshold
total_hoursTotal hours in the queried period for this grid point
pct_abovePercentage of hours above the threshold (0–100)

Area Mean (Spatial Average)

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.

bash
# 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"
csv
date,no2_mean
2023-01-01,9.41
2023-01-02,8.73
2023-01-03,11.20
...
aggregate valueTime columnRows per pollutant
area_hourlytime (ISO 8601)1 per hour
area_dailydate (YYYY-MM-DD)1 per day
area_monthlyyear_month (YYYY-MM)1 per month

Annual Aggregate

Use aggregate=annual with format=csv to get the annual mean per grid point. Useful for comparing year-over-year trends across a region.

bash
# 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"
csv
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
...

Diurnal Profile

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.

bash
# 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"
csv
hour,no2_mean
0,12.79
1,11.43
...
12,14.20
...
23,13.55

Percentile Analysis

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).

bash
# 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"
csv
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).

GeoJSON Output

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.

bash
# 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"
json
{
  "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.

Health Check

GET /health

Check if the API is running. No authentication required.

json
{
  "status": "healthy",
  "service": "climate-api",
  "auth_enabled": true
}

Redeem Voucher

POST /api/v1/redeem

Redeem a voucher code to add credits to your account. Requires authentication.

Request Body

json
{
  "code": "WELCOME-2025-ABC"
}

Response

json
{
  "status": "success",
  "credits_added": 500,
  "credits_remaining": 500,
  "description": "Welcome bonus"
}
StatusMeaning
200Voucher redeemed successfully
404Invalid voucher code
409Voucher already redeemed
410Voucher has expired

Buy Credits

POST /api/v1/checkout

Create a Stripe checkout session to purchase credits.

Request Body

json
{
  "package": "starter",
  "api_key_id": "your-api-key-uuid"
}
FieldTypeDescription
package string One of: starter (€10), professional (€50), enterprise (€200)
api_key_id string Your API key UUID (from account setup)

Response

json
{
  "checkout_url": "https://checkout.stripe.com/c/pay/..."
}

Redirect the user to checkout_url to complete payment. Credits are added automatically after successful payment.

Stripe Webhook

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.

Setup (Stripe Dashboard)

  1. Go to Developers → Webhooks in your Stripe Dashboard
  2. Click Add endpoint
  3. Set URL to your API's webhook path (e.g. https://your-api.com/api/v1/webhook/stripe)
  4. Select event: checkout.session.completed
  5. Copy the Signing secret (whsec_...) and set it as STRIPE_WEBHOOK_SECRET env var

Signature Verification

If STRIPE_WEBHOOK_SECRET is configured, the API verifies the Stripe-Signature header using HMAC-SHA256 to prevent spoofed events.

Event Flow

text
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

Regenerate API Key

If your API key is compromised or you need a fresh key, you can regenerate it from the Dashboard.

How it works

  • Click "Regenerate Key" on the dashboard
  • Your old key is immediately deactivated — any requests using it will return 401
  • A new sk_live_ key is generated
  • Your credit balance is fully preserved (transferred to the new key)
  • The new key is shown once — copy it and store it safely

Technical Details (Supabase RPC)

For advanced integrations, the regeneration is powered by a Supabase RPC function called from the authenticated client:

javascript
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.

Code Examples

Python

python
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"])

JavaScript (Node.js)

javascript
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' }

cURL

bash
# 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'])"

Error Handling

The API uses standard HTTP status codes:

StatusMeaningAction
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 response format

json
{
  "error": "Insufficient credits",
  "credits_remaining": 3,
  "credits_required": 8
}