API reference

Wovenmap exposes a small REST surface. Every authenticated endpoint accepts the device's ingest_token via the ?token= query parameter. Bodies and responses are JSON.

The native Wovenmap iOS app is the primary caller of the location ingest endpoint.

POST/api/ingestauth: token
Request body
{
  "_type": "location",
  "lat": 32.7767,
  "lon": -96.7970,
  "tst": 1780118400,
  "acc": 12,
  "batt": 84,
  "vel": 0
}
Returns
[]   // Empty array on success
Location ingest endpoint. Accepts a single location object or an array of them. Called by the native Wovenmap iOS app. Server runs stop detection on every accepted ping.
GET/api/day?token=...&date=YYYY-MM-DDauth: token
Returns
{
  "device": { "id": "...", "name": "..." },
  "pings": [ { "ts": "...", "lat": 32.78, "lon": -96.79, ... } ],
  "stops": [ { "id": 12, "started_at": "...", "ended_at": "...", "lat": ..., "lon": ..., "duration_seconds": 1800, "place_label": "..." } ]
}
GET/api/week?token=...&end=YYYY-MM-DD&days=7auth: token
Returns N day rollups including total stopped time, stop count, and the top label.
GET/api/places?token=...&since=YYYY-MM-DDauth: token
Aggregated recurring locations clustered on a ~11m grid.
GET/api/evidence/[date]?token=...auth: token
Returns
{
  "body": { "device": ..., "date": "...", "ping_count": 17, "first_hash": "...", "root_hash": "...", "pings": [...], "stops": [...] },
  "signature": "hmac-sha256-hex",
  "signed_at": "ISO 8601",
  "key_id": "first 8 hex chars of sha256(SIGNING_KEY)"
}
Returns the day's full data plus a tamper-evident SHA-256 hash chain on every ping. The HMAC signature is over JSON(body) + '|' + signed_at.
POST/api/evidence/verifyauth: none (public)
Request body
{ "body": {...}, "signature": "...", "signed_at": "...", "key_id": "..." }
Returns
{ "valid": true, "key_id": "94c7e1d2", "signed_at": "..." }
Public verification endpoint. Anyone can verify any signed bundle against our key. No authentication required.
POST/api/share?token=...auth: token
Request body
{ "date": "YYYY-MM-DD", "snap_radius_m": 250, "ttl_hours": 168 }
Returns
{ "id": "9-byte-hex", "expires_at": "..." }
Mints a public read link with coordinates snapped to the given radius and a TTL.
GET/api/share/[id]auth: none (public)
Returns the snapped day data for a share link. No token required.
GET/api/export?token=...auth: token
Downloads a JSON dump of everything for the device. Use this for backup or right-to-portability.
GET/api/route?token=...&date=YYYY-MM-DDauth: token
Returns
{
  "geometry": { "type": "LineString", "coordinates": [[lon,lat], ...] },
  "source": "osrm",
  "evidence_grade": false,
  "disclaimer": "..."
}
VISUALIZATION ONLY. Map-matches the day's raw pings to OpenStreetMap road geometry via OSRM and returns an inferred route as a GeoJSON LineString. This is decorative — it is NOT included in /api/evidence, /api/export, or /api/export.csv, and is NOT covered by the hash chain. Court-admissible records come from /api/evidence/[date] only.
POST/api/purge?token=...auth: token
Request body
{ "confirmation": "I understand this deletes everything" }
Permanently deletes the device and all its data. Right-to-erasure compliance.
GET/api/attorneys?token=...auth: token
Lists attorney/PO grants for this device.
POST/api/attorneys?token=...auth: token
Request body
{ "attorney_email": "...", "attorney_name": "...", "scopes": ["view_evidence","view_history"], "ttl_days": 365 }
Returns
{ "id": "uuid", "grant_token": "tat_...", "expires_at": "..." }
DELETE/api/attorneys?token=...&grant_id=...auth: token
Revokes an attorney grant. Portal stops working immediately.
GET/api/portal/[grant_token]auth: grant_token
Portal API used by the attorney portal page. Returns session info and a 90-day day summary.
GET/api/healthauth: none (public)
Exercises the DB + RPC chain. Hit by Vercel Cron hourly.

Rate limits

None today. We rate-limit per IP via Vercel's edge network if/when abuse becomes an issue.

Error responses

All errors return {"error": "message"} with an appropriate HTTP status code. 401 = missing/invalid token. 400 = malformed request. 503 = optional service (Stripe, Sentry) not configured.