GeoVivé Docs
API reference · v1Overview
GeoVivé stores places as features (standard GeoJSON) grouped into datasets. Each dataset has an owner and is either public or private. The API is JSON over HTTPS.
Base URL: https://api.geovive.link
All paths below are under /v1. Public data needs no key; writes need a signed-in user's token.
Authentication
GeoVivé uses Amazon Cognito (OpenID Connect) with Google sign-in. Authenticated calls send the user's access token:
Authorization: Bearer <access_token>
| Setting | Value |
|---|---|
| Issuer | https://cognito-idp.us-east-1.amazonaws.com/us-east-1_cLqbEZJhi |
| Sign-in domain | https://auth.geovive.link |
| Flow | Authorization code + PKCE |
| Token | Access token (token_use: access); the user is identified by its sub claim |
These tokens are issued only to GeoVivé's own web app. Other apps get access to a user's maps through account linking, with the user's consent.
Data model
The app calls a dataset a map; this reference and the API say dataset. GeoVivé stores datasets of features (standard GeoJSON) and displays layers it doesn't store. The authoritative definition is datamodel.yaml (JSON Schema). openapi.yaml references it, and every write is checked against it.
| Concept | Data model | API | In the app | AppConnect |
|---|---|---|---|---|
| Feature | Feature = geometry + FeatureProperties | /v1/datasets/{id}/features | A pin (point) or shape (line or area) | Pins in a user's datasets |
| Dataset | Dataset + its FeatureCollection | /v1/datasets | A "map": the Map menu and My maps | Created for a user by an Open in GeoVivé link or account linking |
| Layer | LayerList: a display contract (source, style, credits, license), not stored features | /v1/apps/{appId}/layers, relay | The Layers panel | The app's own layers |
| Dataset | Layer | |
|---|---|---|
| Whose data | Stored in GeoVivé, owned by a user or the GeoVivé library | A government agency's or an AppConnect app's |
| Editable | By its owner | No, display only |
| Geometry | Any: points, lines, areas | Any |
| Held to the data model | Yes, strictly | No: shown as published, after AppConnect's checks and approval |
Feature properties
| Field | Rules |
|---|---|
name | Required, up to 200 characters |
category | Required: location (a place), event (recorded activity) or alert (noteworthy activity) |
description | Markdown, up to 5,000 characters. Anything specific (address, hours, level) goes here. |
eventTime | When it happened; required for events and alerts |
status | active (default), inactive or archived |
severity | Alerts only: info, warning or critical |
source, externalId | Where the feature came from, and its ID there |
icon, color | Optional display overrides (emoji or icon name; #rrggbb) |
id, datasetId, createdAt, updatedAt | Set by the server |
No other properties are accepted. Tags describe datasets (up to 10, for search), never features.
Access rules
- Reads need no token. Public datasets are returned to everyone. Private datasets are returned only to their owner, when a valid token is sent; everyone else gets
404, so a private dataset's existence isn't revealed. - Writes (create, update, delete) always need a token, and only the dataset's owner may change it or its features.
- An invalid or expired token on a read returns
401rather than being ignored. - Browser calls are accepted from GeoVive's own sites. Server-to-server calls aren't limited by origin.
Admin API
/v1/admin/* is internal, not a public API surface: it's undocumented on purpose and returns 404 for anyone not in the admins Cognito group, so its existence isn't revealed to other signed-in users. Members still need to set up and verify an app-based TOTP authenticator (independent of Cognito MFA) to get a short-lived admin session before any admin route works. It backs the /admin/ console: dataset review/visibility/delete with an audit log, platform overview, and (once built) agent controls. All routes are served by one API Gateway route (ANY /v1/admin/{proxy+}, dispatched internally) to stay under the Lambda's resource-policy size limit — a pattern worth reusing for any future route group here rather than adding many explicit routes.
Datasets
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /v1/datasets/search | none | Search public datasets by name, description and tags: q, tag, limit (1–50, default 20) |
| GET | /v1/datasets?scope=mine&ids=a,b | optional | Your datasets, plus up to 25 other datasets you can read by id (what the map's menu uses) |
| GET | /v1/datasets | optional | Every public dataset plus your own. Fine for small catalogs; prefer search as the catalog grows |
| POST | /v1/datasets | required | Create a dataset (you become the owner). Returns 201 |
| GET | /v1/datasets/{datasetId} | optional | One dataset |
| PATCH | /v1/datasets/{datasetId} | owner | Update name, description, tags or visibility |
| DELETE | /v1/datasets/{datasetId} | owner | Delete the dataset and all its features. Returns 204 |
Create / update body
{
"name": "Scouting spots", // required on create
"description": "Optional text", // up to 1,000 characters
"tags": ["hiking", "colorado"], // up to 10; lowercase letters, numbers, spaces, dashes
"visibility": "private" // "private" (default) or "public"
}
Tags are normalized: lowercased, accents removed, duplicates dropped. A comma-separated string is accepted too.
Dataset object
{
"datasetId": "world-capitals",
"name": "World Capitals",
"description": "National capitals of the world (Natural Earth)…",
"tags": ["reference", "world", "capitals", "cities", "countries"],
"visibility": "public",
"ownerId": "system",
"source": "https://www.naturalearthdata.com/",
"featureCount": 215,
"createdAt": "2026-09-26T01:51:59.386Z",
"updatedAt": "2026-09-26T01:51:59.386Z"
}
Searching public datasets
GET /v1/datasets/search?q=colorado%20trails&tag=hiking&limit=20
{
"total": 3, // public datasets that matched
"complete": true,
"results": [ { …dataset… } ],
"tags": [ { "name": "hiking", "count": 3 } ] // tags among the matches, for filtering
}
Full-text search over each public dataset's name, tags and description. It matches partial words (trail finds "Trailheads"), simple word forms (schools finds "school"), small typos, and ignores accents and case. Results rank by relevance, with name matches weighted most, then tags, then description. When no dataset matches every word, datasets matching any word are returned. With no q, results are the largest datasets first. Only public datasets are searched.
Features
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /v1/datasets/{datasetId}/features | optional | A FeatureCollection. Query: limit (default 500, max 1000), nextToken |
| POST | /v1/datasets/{datasetId}/features | owner | Add a feature. GeoVivé assigns the ID. Returns 201 |
| GET | /v1/datasets/{datasetId}/features/{featureId} | optional | One feature, always with full-detail geometry. Query: detail=simplified for the preview |
| PUT | /v1/datasets/{datasetId}/features/{featureId} | owner | Replace a feature's geometry and properties |
| DELETE | /v1/datasets/{datasetId}/features/{featureId} | owner | Delete a feature. Returns 204 |
Pagination: when more results exist, the collection includes nextToken. Pass it back as a query parameter to get the next page.
Bulk delete, move and copy
POST /v1/datasets/{datasetId}/features/batch
{ "action": "move", "ids": ["f1", "f2"], "targetDatasetId": "d2" } // "delete" | "move" | "copy"
{ "action": "move", "done": ["f1", "f2"], "failed": [], "targetDatasetId": "d2" }
Owner only; for move and copy you must also own the target. Up to 100 IDs per call. Moves and copies keep full geometry and properties, with new feature IDs in the target. Each ID succeeds or fails on its own, and pin counts are updated.
Formats
Feature (request)
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-104.9903, 39.7392] },
"properties": {
"name": "Trailhead", // required
"category": "location", // event | location | alert (others are stored but not styled)
"description": "Parking for 10 cars"
// any other properties are stored and returned as-is
}
}
- Coordinates are [longitude, latitude] (WGS 84), as GeoJSON requires.
- Geometry types:
Point,MultiPoint,LineString,MultiLineString,Polygon,MultiPolygon. Polygon rings must be closed. - Large shapes: geometry up to 5 MB and 500,000 vertices. Shapes over 100 KB are stored in full, and lists return a simplified preview with
properties.geometryDetail: "simplified". Get the single feature for full detail.
Feature (response)
GeoVivé adds id, datasetId, createdAt and updatedAt to properties, and sets the feature's top-level id. These are ignored if sent in a request. Non-point features include a GeoJSON bbox of the full shape.
Errors
Errors return JSON: { "message": "…" }.
| Status | Meaning |
|---|---|
400 | Invalid body, geometry or query parameter |
401 | Missing, invalid or expired token |
403 | Signed in, but not the dataset's owner |
404 | Not found, or a private dataset you can't see |
413 | Geometry over 5 MB |
500 | Server error |
Examples
Read a public dataset
curl https://api.geovive.link/v1/datasets
curl "https://api.geovive.link/v1/datasets/world-capitals/features?limit=50"
Create a dataset and add a pin
TOKEN="<access token>"
curl -X POST https://api.geovive.link/v1/datasets \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Scouting spots","visibility":"private"}'
curl -X POST https://api.geovive.link/v1/datasets/<datasetId>/features \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"type":"Feature","geometry":{"type":"Point","coordinates":[-104.99,39.74]},
"properties":{"name":"Trailhead","category":"location"}}'
JavaScript
const res = await fetch("https://api.geovive.link/v1/datasets/world-capitals/features");
const collection = await res.json(); // GeoJSON FeatureCollection
map.getSource("places").setData(collection);
Imports
Load outside data into a dataset you own. Imports run in the background: start one, then poll it until status is succeeded or failed.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /v1/datasets/{datasetId}/imports/upload-url | owner | A presigned URL for uploading a GeoJSON file (PUT it within 15 minutes) |
| POST | /v1/datasets/{datasetId}/imports | owner | Start an import. Returns 202 with the import |
| GET | /v1/datasets/{datasetId}/imports/{importId} | owner | Status, counts and the first few problems |
// A state agency's ArcGIS layer, filtered, replacing what's in the map
{
"source": { "type": "arcgis", "url": "https://…/FeatureServer/0", "where": "STATE = 'CO'" },
"mode": "replace", // or "append" (default)
"nameField": "UNIT_NAME", // optional; otherwise name/title/…name fields are tried
"categoryField": "TYPE" // optional; copied into properties.category
}
// or { "source": { "type": "url", "url": "https://…/data.geojson" } }
// or { "source": { "type": "upload", "key": "<from upload-url>" } }
- Up to 20,000 features and 50 MB per import. GeoJSON must use longitude/latitude (WGS 84); ArcGIS layers are requested in WGS 84.
- Each feature is checked like
POST …/features. Invalid ones are skipped and counted. Large shapes are stored in full. - Imported features keep their original properties, plus
sourceIdwhen the source has one. - The dataset records each import in
imports: source URL or file name, layer title, attribution, time and counts.
Exports
POST /v1/datasets/{datasetId}/exports with { "format": "geojson" | "kml" | "gpx" } returns a download link valid for an hour. No sign-in is needed for public datasets; send your token for private ones.
- Files have full-detail geometry and all feature properties. GeoJSON adds a
geoviveblock with the dataset name, export time and the sources of imported data. - KML keeps pin colors and puts properties in
ExtendedData. - GPX: points become waypoints and lines become tracks. Areas become tracks along their outer boundaries, since GPX has no areas.
Layer lists (connected apps)
GeoVivé keeps no subject-matter layers of its own. A connected app brings its layers by publishing a layer list, a JSON file on the app's own verified domain, registered through AppConnect (for example https://hunt.example.com/geovive-layers.json). GeoVivé shows those layers in the AppConnect section of the Layers panel, only to people working with that app: users who arrived through the app's open link, or who open a map the app created.
No personal information. Labels and shown fields must not identify individuals (names, emails, phone numbers, home addresses). The connection checks look for it, and the AppConnect terms forbid it.
{
"version": 1,
"title": "Example layers",
"layers": [
{
"id": "co-gmu", // letters, numbers, - or _
"name": "GMUs (big game)",
"group": "Colorado", // heading in the Layers panel
"color": "#f59e0b",
"description": "Big game units with DAUs.",
"attribution": "Colorado Parks and Wildlife",
"license": "Official agency data",
"source": { "type": "arcgis", "url": "https://…/FeatureServer/6", "where": "1=1" },
"label": "GMU {GMUID}", // shown on the map; {FIELD} from the source
"fields": { "COUNTY": "County", "ELKDAU": "Elk DAU" }, // shown when clicked
"delivery": "relay", // or "direct"
"cacheHours": 24, // relay only (1–168)
"tolerance": 0.0005 // simplification in degrees (optional)
}
]
}
- Sources:
arcgis(a FeatureServer or MapServer layer),wfs(OGC WFS 2.0; addtypeNameand optionallygeometryField) orgeojson(a GeoJSON file, including one the app hosts). HTTPS only. - Delivery:
relay: GeoVivé's server fetches the source, simplifies it for display, keeps only the listed fields with readable names, and caches a compressed copy. Relayed source addresses aren't shown to browsers.direct: the browser loads the source itself. Use it when the data's terms don't allow a cached copy; the source must allow browser (CORS) requests.
- GeoVivé re-reads the list every few minutes and shows only approved layer versions. New or changed layers are checked automatically: passing ones show within minutes, ones a person must review within 5 business days. Checks also re-run weekly or whenever you ask.
| Method | Path | Description |
|---|---|---|
| GET | /v1/apps/{appId}/layers | The app's layers as GeoVivé shows them (display settings; sources only for direct layers) |
| GET | /v1/relay/{appId}/{layerId} | A one-hour link to the relayed layer file (GeoJSON, each feature with _label and the listed fields) |
Open in GeoVivé links
A registered app can send its signed-in users to their own GeoVivé map for one item in the app (for example, a hunt plan), then bring them back.
https://geovive.link/open?app=<appId>&ref=<itemId>
&title=<map name>
&area=<URL of a GeoJSON outline>
&return=<URL to come back to>
| Parameter | Required | Meaning |
|---|---|---|
app | yes | Your registered app ID |
ref | yes | Your ID for the item. The same user + app + ref always opens the same map. |
title | no | Name for the map when it's first created |
area | no | GeoJSON drawn as a read-only outline (never stored as pins). Must be served from one of your registered origins, with CORS allowing https://geovive.link. |
return | no | Where Done sends the user. Must be on one of your registered origins. |
GeoVivé signs the user in if needed, finds or creates their private map, applies your pin types, and shows a banner. Done returns them to return with ?geovive_map=<datasetId> added. Only the ID travels in the URL.
The map belongs to the user. Until connected apps ship (below), your app can't read it with its own credentials; the user sees it in GeoVivé under My maps.
Showing the app's layers
Add layers=<layerId,…> to turn on layers from your layer list, and focus=<layerId>:<label> to zoom to and outline one feature, for example focus=co-gmu:GMU 61. Without ref, the link is a view link: no sign-in and no map is created; GeoVivé just shows your layers, with a Done button back to return.
https://geovive.link/open?app=<appId>&layers=co-gmu,co-sheep&focus=co-gmu:GMU%2061&return=https%3A%2F%2Fhunt.example.com%2Funits%2F61
API used by the link
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /v1/apps/{appId} | none | App name, allowed origins and pin types |
| PUT | /v1/apps/{appId}/maps/{ref} | required | Find or create the caller's map for that item. Body: title, externalUrl, referenceArea. Returns 201 when created, 200 when it already existed. |
To connect an app, see AppConnect requirements for what to have ready.
AppConnect API
The AppConnect page uses these routes; all but the webhook need a signed-in user, who becomes the app's owner. See requirements and terms.
| Method | Path | Description |
|---|---|---|
| POST | /v1/appconnect/apps | Register an app: appId, name, domain, layersUrl, returnOrigins, contactEmail, acceptTerms. Returns the domain-verification token. |
| GET | /v1/appconnect/apps | Your apps and their stages |
| GET | /v1/appconnect/apps/{appId} | One app with its history |
| POST | /v1/appconnect/apps/{appId}/checks | Run the connection checks (202); the report is emailed and listed on the app |
| GET | /v1/appconnect/apps/{appId}/reports/{reportId} | A 10-minute link to a report or notice PDF |
| POST | /v1/appconnect/apps/{appId}/disconnect | Disconnect now or at term end (confirm must equal the app ID); DELETE cancels a scheduled one |
| POST | /v1/appconnect/apps/{appId}/reconnect | Reconnect within 90 days |
| GET | /v1/appconnect/apps/{appId}/export | Settings and history as JSON |
Stages: registered → checking → checks passed or needs fixes → awaiting payment → live, renewed yearly; plus sandbox, expired, suspended, disconnected (90-day reconnect window) and closed.
Account linking
A live or sandbox AppConnect app can ask a user to link their GeoVivé account. With the user's consent, your app can read the maps it created for them (and any they choose to share), and create maps and pins. It's standard OAuth 2.0: authorization code with PKCE, and your client secret stays on your server.
1. Set up
On your app's AppConnect page, under Account linking, add your redirect URIs (exact https addresses on your verified domain; http://localhost is allowed while in the sandbox) and create a client secret. The secret is shown once. Your client ID is your app ID.
2. Send the user to GeoVivé
https://geovive.link/connect/?response_type=code
&client_id=YOUR_APP_ID
&redirect_uri=https%3A%2F%2Fyourapp.example%2Fgeovive%2Fcallback
&scope=profile%20maps%3Aread%20maps%3Awrite
&state=RANDOM_PER_REQUEST
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256
The user signs in if needed, sees your app's name, domain and what it's asking for, and can share extra maps read-only. They come back to your redirect_uri with ?code=…&state=…, or ?error=access_denied if they cancel. Check that state matches.
| Scope | Lets your app |
|---|---|
profile | Get a user ID for your app and the user's display name |
maps:read (default) | Read maps your app created for this user, and maps the user shared with it |
maps:write | Create maps, and add, change and delete pins in maps your app created |
3. Swap the code for tokens (on your server)
POST https://api.geovive.link/v1/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=…&code_verifier=…
&redirect_uri=…&client_id=YOUR_APP_ID&client_secret=…
{ "access_token": "gva_…", "token_type": "Bearer", "expires_in": 3600,
"refresh_token": "gvr_…", "scope": "maps:read maps:write profile" }
Codes last 5 minutes and work once. Access tokens last an hour. Refresh tokens last 90 days and are replaced every time you use one (grant_type=refresh_token): store the new one. Using an old refresh token again is treated as theft and ends the link. HTTP Basic client authentication also works. Revoke a token with POST /v1/oauth/revoke (token, plus your client credentials).
4. Call the linked API
Authorization: Bearer gva_…
| Method | Path | Scope | Does |
|---|---|---|---|
| GET | /v1/linked/me | any | userId (stable for your app only), scopes, and displayName with profile |
| GET | /v1/linked/datasets | maps:read | Maps your app can see; createdByThisApp says which you can change |
| POST | /v1/linked/datasets | maps:write | Create a private map: { name, description?, tags?, externalRef? }. The same externalRef returns the existing map |
| GET | /v1/linked/datasets/{id}, …/features, …/features/{featureId} | maps:read | Same shapes as the public dataset and feature routes |
| POST, PUT, DELETE | /v1/linked/datasets/{id}/features[/{featureId}] | maps:write | Add, replace or delete pins, only in maps your app created |
What your app never gets: the user's email, password, GeoVivé account ID, or any map they didn't create through you or share with you. userId is different for every app, so apps can't match users with each other.
When access ends: the user removes your app in My data → Connected apps, your app leaves AppConnect or lapses, or a refresh token is reused. Calls then return 401; send the user through step 2 again. Maps your app created stay with the user.