Quick Start: driving a room with cURL
This is the “just give me the commands” guide to doing everything in one room over HTTP with a
room-scoped token (cmap_…) — no browser session, no user login. For the full endpoint list see the
Reference.
1. Get a token
Section titled “1. Get a token”Mint a token as the room owner — in the app (My Rooms → API button on the room card) or over HTTP with your owner JWT. See Room API Tokens for details.
curl -X POST "$API/api/rooms/$ROOM/api-tokens" \ -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \ -d '{"name":"my-script","permissions":["read","write","upload"],"expiresInDays":30}'# → { "token": "cmap_…", "id": "…" }2. Set up your shell
Section titled “2. Set up your shell”export API=https://collmap.com # production (use http://localhost:3001 for local dev)export ROOM=aB3dEfGhIj # the 10-char room id — it's in the map URL: /map/<ROOM>export TOKEN=cmap_xxxxxxxxxxxx # your room tokenAUTH="Authorization: Bearer $TOKEN"(Examples pipe through jq for readability — optional.)
3. Sanity check
Section titled “3. Sanity check”curl -s "$API/api/rooms/$ROOM" -H "$AUTH" | jq . # → room metadata (200) for a read token- 200 → you’re wired up.
- 403 “API token is not scoped to this room” →
$ROOMdoesn’t match the token’s room. - 403 “API token lacks ‘read’ permission” → re-mint including
read.
4. Push data into the room
Section titled “4. Push data into the room”Upload a vector layer upload
Section titled “Upload a vector layer ”GeoJSON, zipped shapefile, or GeoPackage. Larger vector processing is async → you get a jobId.
curl -X POST "$API/api/layers/upload" -H "$AUTH" \ -F "roomName=$ROOM" -F "layerName=my-layer" -F "file=@data/my-layer.geojson"# → { "jobId": "job-…", ... } then poll:curl -s "$API/api/layers/jobs/<jobId>" -H "$AUTH" | jq '{stage,progress,message}'Upload a GeoTIFF upload
Section titled “Upload a GeoTIFF ”Same endpoint; a .tif/.tiff is routed to the raster pipeline. Set the display up front:
# single-band: colormap + stretchcurl -X POST "$API/api/layers/upload" -H "$AUTH" \ -F "roomName=$ROOM" -F "layerName=elevation" -F "file=@data/dem.tif" \ -F 'display={"colormap":"terrain","band":1,"vmin":0,"vmax":1500,"opacity":0.8}'
# multi-band: an RGB compositecurl -X POST "$API/api/layers/upload" -H "$AUTH" \ -F "roomName=$ROOM" -F "layerName=truecolor" -F "file=@data/scene.tif" \ -F 'display={"renderMode":"rgb","rgbBands":[1,2,3]}'Add a single marker write
Section titled “Add a single marker ”Synchronous — returns the created marker with its id.
curl -X POST "$API/api/rooms/$ROOM/markers" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"latitude":47.5596,"longitude":7.5886,"title":"Site A","description":"first visit","tags":["survey"]}'# → 201 { "success": true, "marker": { "id": "marker-…", ... } }Only latitude/longitude are required. Optional: title, description, color (hex), tags,
eventDate (epoch ms), markerIconId, markerIconSize, imageKey/thumbnailKey,
embeddedMediaUrl, mediaSize.
Update or delete a marker write
Section titled “Update or delete a marker ”# read one backcurl -s "$API/api/rooms/$ROOM/markers/$MARKER" -H "$AUTH" | jq '.marker.title'
# change only what you name; null clears a fieldcurl -X PATCH "$API/api/rooms/$ROOM/markers/$MARKER" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"title":"Site A (revisited)","description":null,"color":"#ef4444"}'
curl -X DELETE "$API/api/rooms/$ROOM/markers/$MARKER" -H "$AUTH"# → 200 { "success": true, "markerId": "marker-…" }latitude, longitude, title and color can be changed but not cleared; imageKey and
thumbnailKey must be sent together. An empty body is rejected as empty_update. Both calls
show up live in open browsers and land in the room’s history.
Bulk-add markers from point GeoJSON write
Section titled “Bulk-add markers from point GeoJSON ”curl -X POST "$API/api/rooms/$ROOM/bulk-import" -H "$AUTH" -F "file=@data/points.geojson"# → { "jobId": "bulk-import-…" }; poll: curl -s "$API/api/jobs/<jobId>" -H "$AUTH" | jq .Create a layer from inline GeoJSON write
Section titled “Create a layer from inline GeoJSON ”curl -X POST "$API/api/rooms/$ROOM/layers" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"name":"drawn","description":"study area","geojson":{"type":"FeatureCollection","features":[]}, "color":"e11d48","opacity":0.35,"strokeWeight":3,"visible":true}'5. Read data back
Section titled “5. Read data back”curl -s "$API/api/layers/$ROOM" -H "$AUTH" | jq '.layers[].name' # layerscurl -s "$API/api/rooms/$ROOM/markers" -H "$AUTH" | jq '.count' # markerscurl -s "$API/api/rooms/$ROOM/raster-layers" -H "$AUTH" | jq '.rasterLayers[] | {id,name,display}'6. Comments readwrite
Section titled “6. Comments ”curl -s "$API/api/rooms/$ROOM/comments?resolved=false" -H "$AUTH" | jq '.comments[].body'curl -X POST "$API/api/rooms/$ROOM/comments" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"objectId":"marker-1","objectType":"marker","body":"needs review","lat":47.56,"lng":7.59}'7. Change a raster’s display write
Section titled “7. Change a raster’s display ”curl -X PATCH "$API/api/rooms/$ROOM/raster-layers/<layerId>/display" \ -H "$AUTH" -H "Content-Type: application/json" \ -d '{"colormap":"viridis","band":1,"vmin":0,"vmax":3000}'8. Export / import readwrite
Section titled “8. Export / import ”curl -X POST "$API/api/rooms/$ROOM/export" -H "$AUTH" -H "Content-Type: application/json" -d '{}' -o room.gpkgcurl -X POST "$API/api/rooms/$ROOM/import" -H "$AUTH" -F "file=@room.gpkg" # ≤ 2 GiBBig rooms: probe with GET /api/rooms/$ROOM/export-size, then use the async
export jobs.
9. The easy button: the import script
Section titled “9. The easy button: the import script”scripts/import_geo_data.mjs auto-detects formats, chunks large files, and polls jobs for you:
COMAP_API_TOKEN=$TOKEN node scripts/import_geo_data.mjs --room $ROOM data/my-folderCOMAP_API_TOKEN=$TOKEN node scripts/import_geo_data.mjs --room $ROOM points.csv imagery.tiffnode scripts/import_geo_data.mjs --room $ROOM --dry-run data/my-folder # preview only10. Troubleshooting
Section titled “10. Troubleshooting”| Symptom | Cause → fix |
|---|---|
403 api_token_not_allowed on a data endpoint |
You hit an owner/admin endpoint (see what a token can’t do); in prod, a new deploy may not be live yet |
403 "API token is not scoped to this room" |
roomName/$ROOM ≠ the token’s room |
403 "API token lacks 'X' permission" |
Re-mint the token including permission X |
401 Invalid or expired API token |
Token revoked/expired — mint a new one |
413 quota_exceeded |
Room owner’s storage cap is full — free space or raise the plan |
413 file_too_large |
File > 50 MB in one request — use the import script / chunked upload |

