WildMap API

Programmatic access to real-time environmental sensor data from deployed weather stations.
Built by Bonny Doon Labs

The WildMap API lets you query historical readings, stream live sensor data, manage alert thresholds, receive webhook notifications, and access shared data from partner organizations.

Interactive Docs: Visit /api-docs on your WildMap instance for a live Swagger UI. Append ?format=json for the raw OpenAPI 3.0.3 spec.

Quick Start

  1. Get an account — Register in the WildMap app and join or create an organization.
  2. Create an API key — Go to Settings → API Keys → Create. Select the scopes you need.
  3. Store the key securely — The plaintext key is shown once and cannot be retrieved again.
  4. Make your first request:
    curl "https://<project>.supabase.co/functions/v1/api-readings?limit=1" \
      -H "Authorization: Bearer wf_your_key_here"
    import requests
    
    resp = requests.get(
        "https://<project>.supabase.co/functions/v1/api-readings",
        headers={"Authorization": "Bearer wf_your_key_here"},
        params={"limit": 1}
    )
    print(resp.json())
    const resp = await fetch(
      "https://<project>.supabase.co/functions/v1/api-readings?limit=1",
      { headers: { Authorization: "Bearer wf_your_key_here" } }
    );
    const data = await resp.json();
    console.log(data);
    req, _ := http.NewRequest("GET",
        "https://<project>.supabase.co/functions/v1/api-readings?limit=1", nil)
    req.Header.Set("Authorization", "Bearer wf_your_key_here")
    resp, _ := http.DefaultClient.Do(req)
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
    require "net/http"
    require "json"
    
    uri = URI("https://<project>.supabase.co/functions/v1/api-readings?limit=1")
    req = Net::HTTP::Get.new(uri)
    req["Authorization"] = "Bearer wf_your_key_here"
    resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
    puts JSON.parse(resp.body)
  5. Set up streaming or webhooks for real-time data, or poll the readings endpoint on a schedule.

Authentication

All data endpoints use API key authentication. Keys are scoped to an organization and carry fine-grained permissions.

Using Your Key

Include your key in every request using either header format:

Authorization: Bearer wf_a1b2c3d4e5f6...
# or
X-API-Key: wf_a1b2c3d4e5f6...

Creating a Key via API

curl -X POST https://<project>.supabase.co/functions/v1/api-keys \
  -H "Authorization: Bearer <supabase_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "orgId": "your-org-uuid",
    "name": "Production Integration",
    "scopes": ["readings:read", "devices:read", "stream"],
    "expiresAt": "2027-01-01T00:00:00Z"
  }'
import requests

resp = requests.post(
    "https://<project>.supabase.co/functions/v1/api-keys",
    headers={
        "Authorization": "Bearer <supabase_jwt>",
        "Content-Type": "application/json",
    },
    json={
        "orgId": "your-org-uuid",
        "name": "Production Integration",
        "scopes": ["readings:read", "devices:read", "stream"],
        "expiresAt": "2027-01-01T00:00:00Z",
    }
)
api_key = resp.json()["key"]  # Store this securely — shown once only
print(f"API Key: {api_key}")
const resp = await fetch(
  "https://<project>.supabase.co/functions/v1/api-keys",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer <supabase_jwt>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      orgId: "your-org-uuid",
      name: "Production Integration",
      scopes: ["readings:read", "devices:read", "stream"],
      expiresAt: "2027-01-01T00:00:00Z",
    }),
  }
);
const { key } = await resp.json();
console.log(`API Key: ${key}`); // Store securely — shown once only
body := strings.NewReader(`{"orgId":"your-org-uuid","name":"Production Integration","scopes":["readings:read","devices:read","stream"],"expiresAt":"2027-01-01T00:00:00Z"}`)
req, _ := http.NewRequest("POST", "https://<project>.supabase.co/functions/v1/api-keys", body)
req.Header.Set("Authorization", "Bearer <supabase_jwt>")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
// Parse resp.Body for the "key" field — store securely
uri = URI("https://<project>.supabase.co/functions/v1/api-keys")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req["Authorization"] = "Bearer <supabase_jwt>"
req.body = { orgId: "your-org-uuid", name: "Production Integration",
             scopes: ["readings:read", "devices:read", "stream"],
             expiresAt: "2027-01-01T00:00:00Z" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
api_key = JSON.parse(resp.body)["key"] # Store securely
Important: The plaintext key is returned only once in the creation response. Store it immediately in a secure location (environment variable, secrets manager). It cannot be retrieved later.

Key Security

Scopes

ScopeAccess
readings:readQuery historical sensor readings
devices:readList devices and their metadata
streamSubscribe to real-time SSE data stream
alerts:readQuery alert history and current thresholds
alerts:writeUpdate alert threshold configuration
org:readQuery organization info, members, and device stats
webhooks:manageRegister, list, and delete webhooks

Endpoints

Sensor Readings

GET /api-readings readings:read

Query historical sensor readings with filtering, pagination, and optional cross-org data.

Parameters

ParameterTypeDefaultDescription
startISO 8601Filter readings after this time
endISO 8601Filter readings before this time
device_idUUIDFilter by device ID
device_serialstringFilter by serial (e.g., WX-000001)
include_sharedbooleanfalseInclude data from partner organizations
orderasc / descdescSort by timestamp
limit1–1000100Results per page
offsetinteger0Pagination offset

Example

curl "https://<project>.supabase.co/functions/v1/api-readings?\
start=2026-03-01T00:00:00Z&limit=10&order=desc" \
  -H "Authorization: Bearer wf_your_key_here"
resp = requests.get(
    f"{BASE}/api-readings",
    headers={"Authorization": f"Bearer {API_KEY}"},
    params={"start": "2026-03-01T00:00:00Z", "limit": 10, "order": "desc"}
)
for r in resp.json()["data"]:
    s = r["sensors"]
    print(f"{r['device']['serial']}: {s['temp_c']}°C, {s['wind_mps']} m/s")
const url = new URL(`${BASE}/api-readings`);
url.searchParams.set("start", "2026-03-01T00:00:00Z");
url.searchParams.set("limit", "10");
url.searchParams.set("order", "desc");

const resp = await fetch(url, {
  headers: { Authorization: `Bearer ${API_KEY}` }
});
const { data } = await resp.json();
data.forEach(r =>
  console.log(`${r.device.serial}: ${r.sensors.temp_c}°C, ${r.sensors.wind_mps} m/s`)
);
req, _ := http.NewRequest("GET",
    BASE+"/api-readings?start=2026-03-01T00:00:00Z&limit=10&order=desc", nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var result struct { Data []map[string]any }
json.NewDecoder(resp.Body).Decode(&result)
for _, r := range result.Data {
    fmt.Printf("%v: %v°C\n", r["device"], r["sensors"])
}
uri = URI("#{BASE}/api-readings?start=2026-03-01T00:00:00Z&limit=10&order=desc")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{API_KEY}"
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }

JSON.parse(resp.body)["data"].each do |r|
  s = r["sensors"]
  puts "#{r['device']['serial']}: #{s['temp_c']}°C, #{s['wind_mps']} m/s"
end

Response

{
  "data": [{
    "id": "reading-uuid", "time": "2026-03-20T14:30:00Z",
    "device": { "id": "device-uuid", "serial": "WX-000001", "label": "North Ridge Station" },
    "location": { "lat": 34.0522, "lon": -118.2437, "alt_m": 285.0 },
    "sensors": { "wind_mps": 4.7, "wind_dir_deg": 225, "temp_c": 28.3, "rh_pct": 35.0, "dewpoint_c": 11.8, "pm2_5": 18.5 },
    "org_id": "org-uuid"
  }],
  "meta": { "count": 1, "limit": 100, "offset": 0, "has_more": true }
}

Sensor Fields

FieldUnitDescription
wind_mpsm/sWind speed
wind_dir_degdegrees (0–360)Wind direction (0 = North)
temp_c°CTemperature
rh_pct%Relative humidity
dewpoint_c°CDewpoint (derived from temp + humidity)
pm1_0 / pm2_5 / pm10μg/m³Particulate matter

Devices

GET /api-devices devices:read

List weather stations registered to your organization.

// Response
{ "data": [{ "id": "uuid", "serial": "WX-000001", "label": "North Ridge",
    "status": "active", "firmware_version": "1.2.0", "last_seen_at": "2026-03-20T14:30:00Z" }] }

Real-Time Stream (SSE)

GET /api-stream stream

Subscribe to a Server-Sent Events stream for live readings as they arrive.

ParameterDescription
device_idComma-separated UUIDs to filter specific devices
include_sharedInclude data from partner organizations
curl -N "https://<project>.supabase.co/functions/v1/api-stream" \
  -H "Authorization: Bearer wf_your_key_here"

# Output:
# event: reading
# data: {"id":"...","device":{...},"sensors":{"temp_c":28.3,...}}
# id: reading-uuid
import requests, json

with requests.get(
    f"{BASE}/api-stream",
    headers={"Authorization": f"Bearer {API_KEY}"},
    stream=True
) as r:
    for line in r.iter_lines(decode_unicode=True):
        if line.startswith("data: "):
            reading = json.loads(line[6:])
            s = reading["sensors"]
            print(f"{reading['device']['serial']}: {s['temp_c']}°C")
const EventSource = require("eventsource");

const es = new EventSource(
  `${BASE}/api-stream`,
  { headers: { Authorization: `Bearer ${API_KEY}` } }
);

es.addEventListener("reading", (event) => {
  const r = JSON.parse(event.data);
  console.log(`${r.device.serial}: ${r.sensors.temp_c}°C`);
});
// Use github.com/r3labs/sse/v2
client := sse.NewClient(BASE + "/api-stream")
client.Headers["Authorization"] = "Bearer " + apiKey

client.SubscribeRaw(func(msg *sse.Event) {
    if string(msg.Event) == "reading" {
        fmt.Println(string(msg.Data))
    }
})
require "net/http"
require "json"

uri = URI("#{BASE}/api-stream")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  req = Net::HTTP::Get.new(uri)
  req["Authorization"] = "Bearer #{API_KEY}"
  http.request(req) do |resp|
    resp.read_body do |chunk|
      chunk.each_line do |line|
        if line.start_with?("data: ")
          puts JSON.parse(line[6..])
        end
      end
    end
  end
end

Alert History

GET /api-alert-history alerts:read

Query triggered alerts filtered by device, severity, type, and time range.

ParameterTypeDescription
device_idUUIDFilter by device
severitywarning / critical / emergencyFilter by severity
alert_typestringe.g., wind_direction_shift
start / endISO 8601Time range
include_sharedbooleanInclude partner org alerts
limit / offsetintegerPagination

Alert Thresholds

GET /api-alert-thresholds alerts:read

PUT /api-alert-thresholds alerts:write

Read or update your organization's alert threshold configuration.

# Update a threshold
curl -X PUT "https://<project>.supabase.co/functions/v1/api-alert-thresholds" \
  -H "Authorization: Bearer wf_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"parameter_path": "wind.speed.increaseMs", "value": 0.6}'
requests.put(
    f"{BASE}/api-alert-thresholds",
    headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
    json={"parameter_path": "wind.speed.increaseMs", "value": 0.6}
)
await fetch(`${BASE}/api-alert-thresholds`, {
  method: "PUT",
  headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ parameter_path: "wind.speed.increaseMs", value: 0.6 })
});
body := strings.NewReader(`{"parameter_path":"wind.speed.increaseMs","value":0.6}`)
req, _ := http.NewRequest("PUT", BASE+"/api-alert-thresholds", body)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)
uri = URI("#{BASE}/api-alert-thresholds")
req = Net::HTTP::Put.new(uri, "Content-Type" => "application/json")
req["Authorization"] = "Bearer #{API_KEY}"
req.body = { parameter_path: "wind.speed.increaseMs", value: 0.6 }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }

Organizations

GET /api-orgs org:read

Query organization info including member counts, device counts, and reading totals. Pass include_shared=true to list orgs sharing data with yours.

Webhooks

Receive push notifications to your server when events occur, instead of polling.

POST /api-webhooks webhooks:manage

GET /api-webhooks webhooks:manage

DELETE /api-webhooks webhooks:manage

Register a Webhook

curl -X POST "https://<project>.supabase.co/functions/v1/api-webhooks" \
  -H "Authorization: Bearer wf_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-server.com/wildmap-webhook", "events": ["new_reading", "alert_triggered"]}'
resp = requests.post(
    f"{BASE}/api-webhooks",
    headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
    json={"url": "https://your-server.com/wildmap-webhook",
          "events": ["new_reading", "alert_triggered"]}
)
secret = resp.json()["data"]["secret"]  # Store this — shown once only
const resp = await fetch(`${BASE}/api-webhooks`, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    url: "https://your-server.com/wildmap-webhook",
    events: ["new_reading", "alert_triggered"]
  })
});
const { data } = await resp.json();
console.log(`Secret: ${data.secret}`); // Store securely
body := strings.NewReader(`{"url":"https://your-server.com/wildmap-webhook","events":["new_reading","alert_triggered"]}`)
req, _ := http.NewRequest("POST", BASE+"/api-webhooks", body)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
// Parse resp for data.secret — store securely
uri = URI("#{BASE}/api-webhooks")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req["Authorization"] = "Bearer #{API_KEY}"
req.body = { url: "https://your-server.com/wildmap-webhook",
             events: ["new_reading", "alert_triggered"] }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
secret = JSON.parse(resp.body)["data"]["secret"] # Store securely
Store the secret: The response includes a signing secret shown only once. Use it to verify webhook deliveries.

Available Events

EventTriggered When
new_readingA new sensor reading is ingested
alert_triggeredAn alert threshold is crossed
device_connectedA device establishes a connection

Verifying Signatures

Every delivery includes an X-Webhook-Signature header with an HMAC-SHA256 signature of the request body.

# Verify with openssl
echo -n "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}'
# Compare output with X-Webhook-Signature header value
import hmac, hashlib

def verify_signature(payload_bytes, signature, secret):
    expected = hmac.new(
        secret.encode(), payload_bytes, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
const crypto = require("crypto");

function verifySignature(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(signature)
  );
}
func verifySignature(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}
def verify_signature(payload, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
  Rack::Utils.secure_compare(expected, signature)
end

Cross-Organization Data Sharing

WildMap supports secure data sharing between organizations. When a partner shares their data with you, their readings, devices, and alerts become accessible through the same API endpoints.

How It Works

  1. Source org admin creates a share — grants your organization access to their sensor data, with an optional expiration date.
  2. You query with include_shared=true — partner data appears alongside yours in readings, devices, stream, and alert endpoints.
  3. Access automatically expires when the set time limit is reached, or can be revoked immediately at any time.

Share Properties

ScopeFull read access to all readings, devices, and alerts from the source organization
Time LimitOptional expiration date — access auto-revokes when reached
RevocationImmediate — source org can cut access at any time
Audit Trailgranted_at, granted_by, revoked_at timestamps preserved
UniquenessOne active share per source–target org pair

Managing Shares

Share management requires JWT authentication (logged-in admin/owner):

# Create a share
curl -X POST ".../org-shares" -H "Authorization: Bearer <jwt>" \
  -d '{"sourceOrgId": "your-org", "targetOrgId": "partner-org", "expiresAt": "2026-06-01T00:00:00Z"}'

# Revoke immediately
curl -X DELETE ".../org-shares?id=share-uuid" -H "Authorization: Bearer <jwt>"

# Extend expiration
curl -X PATCH ".../org-shares" -H "Authorization: Bearer <jwt>" \
  -d '{"shareId": "share-uuid", "expiresAt": "2027-01-01T00:00:00Z"}'

Rate Limits

Limit100 requests per minute per API key
On exceedHTTP 429 with Retry-After header

Every response includes rate limit headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1711000000

Error Handling

All errors return JSON:

{ "error": "Human-readable error message" }
StatusMeaning
400Bad request — missing or invalid parameters
401Unauthorized — missing, invalid, expired, or revoked key
403Forbidden — valid key but insufficient scope
404Not found
429Rate limit exceeded — retry after Retry-After seconds
500Server error

Code Examples

Full working examples in each language. Set BASE and API_KEY variables first.

Fetch Recent Readings

# Last 24 hours of readings
curl "https://<project>.supabase.co/functions/v1/api-readings?\
start=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)&limit=500" \
  -H "X-API-Key: wf_your_key_here"

# List all devices
curl ".../api-devices" -H "X-API-Key: wf_your_key_here"

# Critical alerts from past week
curl ".../api-alert-history?severity=critical&\
start=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
  -H "X-API-Key: wf_your_key_here"
import requests
from datetime import datetime, timedelta

API_KEY = "wf_your_key_here"
BASE = "https://<project>.supabase.co/functions/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}

# Last 24 hours
since = (datetime.utcnow() - timedelta(hours=24)).isoformat() + "Z"
resp = requests.get(f"{BASE}/api-readings", headers=headers,
                    params={"start": since, "limit": 500})

for r in resp.json()["data"]:
    s = r["sensors"]
    print(f"{r['device']['serial']}: {s['temp_c']}°C, "
          f"{s['wind_mps']} m/s, PM2.5: {s['pm2_5']} µg/m³")
const API_KEY = "wf_your_key_here";
const BASE = "https://<project>.supabase.co/functions/v1";
const headers = { Authorization: `Bearer ${API_KEY}` };

// Last 24 hours
const since = new Date(Date.now() - 86400000).toISOString();
const resp = await fetch(
  `${BASE}/api-readings?start=${since}&limit=500`, { headers }
);
const { data } = await resp.json();

data.forEach(r => {
  const s = r.sensors;
  console.log(`${r.device.serial}: ${s.temp_c}°C, ${s.wind_mps} m/s, PM2.5: ${s.pm2_5}`);
});
package main

import (
    "encoding/json"; "fmt"; "net/http"; "time"
)

func main() {
    apiKey := "wf_your_key_here"
    base := "https://<project>.supabase.co/functions/v1"
    since := time.Now().Add(-24 * time.Hour).UTC().Format(time.RFC3339)

    req, _ := http.NewRequest("GET",
        fmt.Sprintf("%s/api-readings?start=%s&limit=500", base, since), nil)
    req.Header.Set("Authorization", "Bearer "+apiKey)
    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    var result struct { Data []map[string]any }
    json.NewDecoder(resp.Body).Decode(&result)
    for _, r := range result.Data {
        fmt.Println(r)
    }
}
require "net/http"
require "json"
require "time"

API_KEY = "wf_your_key_here"
BASE = "https://<project>.supabase.co/functions/v1"

since = (Time.now.utc - 86400).strftime("%Y-%m-%dT%H:%M:%SZ")
uri = URI("#{BASE}/api-readings?start=#{since}&limit=500")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{API_KEY}"
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }

JSON.parse(resp.body)["data"].each do |r|
  s = r["sensors"]
  puts "#{r['device']['serial']}: #{s['temp_c']}°C, #{s['wind_mps']} m/s"
end
Language: