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: tokenRequest 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: tokenReturns
{
"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: tokenReturns N day rollups including total stopped time, stop count, and the top label.
GET
/api/places?token=...&since=YYYY-MM-DDauth: tokenAggregated recurring locations clustered on a ~11m grid.
GET
/api/evidence/[date]?token=...auth: tokenReturns
{
"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: tokenRequest 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: tokenDownloads a JSON dump of everything for the device. Use this for backup or right-to-portability.
GET
/api/route?token=...&date=YYYY-MM-DDauth: tokenReturns
{
"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: tokenRequest body
{ "confirmation": "I understand this deletes everything" }Permanently deletes the device and all its data. Right-to-erasure compliance.
GET
/api/attorneys?token=...auth: tokenLists attorney/PO grants for this device.
POST
/api/attorneys?token=...auth: tokenRequest 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: tokenRevokes an attorney grant. Portal stops working immediately.
GET
/api/portal/[grant_token]auth: grant_tokenPortal 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.