Skip to content
EN

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.

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.

Terminal window
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": "…" }
Terminal window
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 token
AUTH="Authorization: Bearer $TOKEN"

(Examples pipe through jq for readability — optional.)

Terminal window
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”$ROOM doesn’t match the token’s room.
  • 403 “API token lacks ‘read’ permission” → re-mint including read.

GeoJSON, zipped shapefile, or GeoPackage. Larger vector processing is async → you get a jobId.

Terminal window
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}'

Same endpoint; a .tif/.tiff is routed to the raster pipeline. Set the display up front:

Terminal window
# single-band: colormap + stretch
curl -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 composite
curl -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]}'

Synchronous — returns the created marker with its id.

Terminal window
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.

Terminal window
# read one back
curl -s "$API/api/rooms/$ROOM/markers/$MARKER" -H "$AUTH" | jq '.marker.title'
# change only what you name; null clears a field
curl -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 ”
Terminal window
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 ”
Terminal window
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}'
Terminal window
curl -s "$API/api/layers/$ROOM" -H "$AUTH" | jq '.layers[].name' # layers
curl -s "$API/api/rooms/$ROOM/markers" -H "$AUTH" | jq '.count' # markers
curl -s "$API/api/rooms/$ROOM/raster-layers" -H "$AUTH" | jq '.rasterLayers[] | {id,name,display}'
Terminal window
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 ”
Terminal window
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 ”
Terminal window
curl -X POST "$API/api/rooms/$ROOM/export" -H "$AUTH" -H "Content-Type: application/json" -d '{}' -o room.gpkg
curl -X POST "$API/api/rooms/$ROOM/import" -H "$AUTH" -F "file=@room.gpkg" # ≤ 2 GiB

Big rooms: probe with GET /api/rooms/$ROOM/export-size, then use the async export jobs.

scripts/import_geo_data.mjs auto-detects formats, chunks large files, and polls jobs for you:

Terminal window
COMAP_API_TOKEN=$TOKEN node scripts/import_geo_data.mjs --room $ROOM data/my-folder
COMAP_API_TOKEN=$TOKEN node scripts/import_geo_data.mjs --room $ROOM points.csv imagery.tiff
node scripts/import_geo_data.mjs --room $ROOM --dry-run data/my-folder # preview only
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