SAR Map Canada
Developers
OpenAPI spec

Connect your app to SAR Map Canada

Any app or service (a drone app like Eagle Eyes, a detection tool like Mirada, a dispatch or CAD system, a tracker gateway) can work with team maps both ways:

There's one process for every integration. Nobody at the SAR team has to copy keys or edit settings:

  1. Your app asks us for a pairing code and shows it to the user.
  2. A team member opens /connect, types the code, and chooses which folder your app may use and what it may do (send positions, view, or view and edit).
  3. Your app collects its connection token and uses it as a Bearer token from then on.
  4. The team sees your app under Connected apps and can disconnect it at any time.

All endpoints are JSON over HTTPS and described in the OpenAPI document. Examples below use https://map.asar.team; a runnable example client is in the repository at examples/integration-example.mjs.

1. Connect: pairing code

This is the OAuth 2.0 device authorization pattern (RFC 8628), the same one TVs use for "enter this code on your phone". It suits drone controllers, field laptops and servers: no browser or redirect URL is needed inside your app.

Start

POST /api/connect/start
Content-Type: application/json

{ "app": "Eagle Eyes", "device": "M30T RC Plus" }

→ 200
{ "deviceCode": "…secret, keep it in your app…",
  "userCode": "7B7M-KKVM",
  "verificationUrl": "https://map.asar.team/connect",
  "verificationUrlComplete": "https://map.asar.team/connect?code=7B7M-KKVM",
  "expiresIn": 600, "interval": 5 }

Show the user the userCode and the verificationUrl. A QR code of verificationUrlComplete is the friendliest option on a device with a screen.

Wait for approval

Poll every interval seconds (never faster) until you get a token or a final error:

POST /api/connect/poll
{ "deviceCode": "…" }

→ 400 { "error": "authorization_pending" }   keep waiting
→ 400 { "error": "slow_down" }               you polled too fast: wait longer
→ 400 { "error": "access_denied" }           the user said no
→ 400 { "error": "expired_token" }           took more than 10 minutes: start again
→ 200 { "token": "sa.…", "org": { "id", "name" }, "folder": { "id", "name" } | null,
        "permission": "track" | "view" | "edit", "apiBase": "https://map.asar.team" }

The token is shown once. Store it securely (Android Keystore, iOS Keychain, an OS secret store). Send it on every request:

Authorization: Bearer sa.…

Scripts and servers

Tools that can't show a code can use an API key instead: a team admin creates one under My maps → Connected apps → Create an API key. It works exactly like a pairing token.

2. Read maps

GET /api/v1/maps
→ { "maps": [ { "id": "SQCWJAM", "title": "Task 26-1187", "orgName": "Arrowsmith SAR",
                "folderId": "…", "access": "write", "updatedAt": "…" } ] }

GET /api/v1/maps/SQCWJAM/features
→ { "type": "FeatureCollection", "head": 42, "features": [ … GeoJSON … ] }

Your app only ever sees maps in the folder it was connected to (and folders inside it).

Staying up to date cheaply

Every change has a sequence number. Remember head, then ask only for what changed:

GET /api/v1/maps/SQCWJAM/features?since=42
→ { "type": "FeatureCollection", "head": 45, "features": [ …changed… ], "deleted": [ "…ids…" ] }

3. Write features

POST /api/v1/maps/SQCWJAM/features
Idempotency-Key: 3f2c1e9a-…            (recommended: any unique string per change)
{ "title": "Possible PLS", "geometry": { "type": "Point", "coordinates": [-124.61, 49.23] },
  "properties": { "marker-color": "#c1121f", "description": "Thermal hit, 02:14" } }
→ 201 { "type": "Feature", "id": "…", … }

PATCH  /api/v1/maps/SQCWJAM/features/{id}   { "properties": { "description": "Confirmed: backpack" } }
DELETE /api/v1/maps/SQCWJAM/features/{id}   → 204

Always send an Idempotency-Key and reuse it when you retry after a dropped connection: the change is applied once, never twice. Field links are unreliable; queue changes while offline and send them in order when you're back.

properties are merged on PATCH; set a key to null to remove it. Keys you invent (prefix them, e.g. eagle_eyes:detection_id) are stored and returned untouched. The reserved keys class, title, folderId, updatedAt, updatedBy and seq are set by the server.

You may choose the feature id yourself (a UUID) on POST, which helps when your app tracks its own objects; posting an id that already exists returns 409 feature_exists. Geometry: Point, LineString or Polygon (closed rings), [lon, lat] or [lon, lat, alt], up to 10,000 positions.

4. Live updates (apps that stay connected)

A Server-Sent Events stream: one long HTTP GET. Any language can read it; browsers have EventSource.

GET /api/v1/maps/SQCWJAM/live?since=45
Authorization: Bearer sa.…

id: 46
event: ops
data: { "head": 46, "ops": [ { "seq": 46, "featureId": "…", "kind": "update",
        "actor": "…", "at": "…", "after": { …the feature… } } ] }

event: track
data: { "connectKeyId": "…", "label": "M30T-1", "points": [ { "lng", "lat", "alt", "at" } ] }

event: revoked          your connection was removed: stop and tell the user
: ping                  every 15 s

If the stream drops, reconnect with Last-Event-ID (or ?since=) set to the last id you saw: you get everything you missed, nothing twice.

5. Live positions

POST /api/v1/maps/SQCWJAM/positions
{ "device": "M30T-1",
  "points": [ { "lat": 49.2301, "lng": -124.6121, "alt": 1150, "at": "2026-10-30T02:14:05Z",
                "speed": 7.5, "heading": 280 } ],
  "props": { "eagle_eyes:public_drone_id": "…" } }
→ { "accepted": 1, "duplicates": 0 }

Send at as the time the position was measured, not when you send it: points queued offline land in the right place on the track. Re-sending the same point is harmless. Works with any permission, including send positions only.

6. Webhooks (servers)

If your service can't hold a stream open, register a URL and we'll POST every change to it.

POST /api/v1/webhooks
{ "url": "https://your.server/sar-map-hook", "mapId": "SQCWJAM" }   (omit mapId for every map you can see)
→ 201 { "id": "…", "secret": "…shown once…" }

Each delivery:
POST https://your.server/sar-map-hook
X-SARMap-Event: features.changed | positions.received
X-SARMap-Delivery: …unique id…
X-SARMap-Timestamp: …
X-SARMap-Signature: sha256=…hex HMAC-SHA256 of the raw body with your secret…

{ "id": "…", "type": "features.changed", "mapId": "SQCWJAM", "head": 47, "ops": [ … ], "at": "…" }

Verify the signature before trusting a delivery: it's an HMAC of the raw body only. Reply 2xx quickly. Changes made within a second are batched into one delivery, with at most 1000 ops; if head is past the last op you received, fetch /features?since=. Failed deliveries are retried after 5 s, 30 s, 2 min and 10 min (same delivery id, so you can de-duplicate); after 20 failures in a row the webhook is switched off. Redirects aren't followed, and production webhooks must use https on a public address.

// Node
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-sarmap-signature']));

# Python
expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers['X-SARMap-Signature'])

// Kotlin
val mac = Mac.getInstance("HmacSHA256").apply { init(SecretKeySpec(secret.toByteArray(), "HmacSHA256")) }
val expected = "sha256=" + mac.doFinal(rawBody).joinToString("") { "%02x".format(it) }
val ok = MessageDigest.isEqual(expected.toByteArray(), signatureHeader.toByteArray())

7. Photos

Attach photos to any marker, line or area, or create a marker straight from a photo. Send the image bytes as the request body: no multipart forms. JPEG, PNG or WebP, up to 25 MB. Photos keep their EXIF: the time taken and GPS position are read from it.

POST /api/v1/maps/SQCWJAM/features/{featureId}/photos?caption=Detection%201
Content-Type: image/jpeg
Idempotency-Key: …                     (retry-safe, like other writes)

<the JPEG bytes>

→ 201 { "id": "…", "featureId": "…", "width": 4000, "height": 3000, "takenAt": "…",
        "location": { "lat", "lng", "alt" } | null, "caption": "Detection 1",
        "url": "/api/v1/media/…", "mediumUrl": "…/medium", "thumbUrl": "…/thumb" }
POST /api/v1/maps/SQCWJAM/photos?title=Possible%20PLS        a new marker at the photo's GPS
POST /api/v1/maps/SQCWJAM/photos?lat=49.23&lng=-124.61      …or at a position you give
→ 201 { "feature": { …GeoJSON… }, "photo": { … } }
→ 400 { "error": "no_location" }    the photo has no GPS and you gave no lat/lng

The feature's properties.photos lists its photos, with url, mediumUrl (up to 1600 px) and thumbUrl (up to 320 px) to fetch them using your token. Adding, captioning or deleting a photo is an ordinary change, so it also arrives on the live stream and in webhooks.

GET    /api/v1/media/{id}             original      /medium   /thumb   /info (JSON)
PATCH  /api/v1/media/{id}             { "caption": "…" }
DELETE /api/v1/media/{id}             → 204

Uploading needs view and edit. Send positions only connections can't add photos.

8. Map overlays (mapsheets)

Lay a georeferenced image on the map: a scanned or exported mapsheet, an orthophoto, a drawing. Send the file bytes as the request body (no multipart): a GeoTIFF, a KMZ with a GroundOverlay (Google Earth, Avenza, CalTopo), a GeoPDF, or a PNG, JPEG or WebP. Up to 250 MB. The type is read from the file itself.

POST /api/v1/maps/SQCWJAM/overlays?title=92F%2F07
Content-Type: image/tiff
Idempotency-Key: …

<the GeoTIFF bytes>

→ 201 { "feature": { "properties": { "class": "Overlay", "overlay": { … } }, "geometry": { "type": "Polygon", … } },
        "overlay": { "mediaId": "…", "corners": [[lng,lat] TL, TR, BR, BL], "opacity": 0.75,
                     "width": 8192, "height": 6144, "georef": "geotiff", "crs": "EPSG:3005" } }

Where it goes, in order: the GeoTIFF's own tags; the KMZ LatLonBox (rotation included) or gx:LatLonQuad; a world file you pass as world=A,D,B,E,C,F (the six lines of the .pgw/.jgw/.tfw) with epsg=; otherwise it is centred in place=w,s,e,n (your current view) for the user to align by hand ("georef": "manual").

POST /api/v1/maps/SQCWJAM/overlays?world=2.5,0,0,-2.5,412001.25,5456998.75&epsg=26910     PNG + its .pgw
POST /api/v1/maps/SQCWJAM/overlays?place=-124.9,49.1,-124.3,49.4                       no georeference
→ 400 { "error": "no_location" }       no georeference and no place
→ 400 { "error": "crs_required" }      a world file in metres without epsg
→ 201 { …, "warning": "unsupported_crs", "crs": "EPSG:2056" }   placed in the view instead

GeoPDF. Send the PDF as is (Content-Type: application/pdf); add page= for a page other than the first. The server reads the page's georeference, either the ISO 32000 / Adobe geospatial encoding that Avenza, NRCan, USGS and QGIS/GDAL write, or the older OGC/TerraGo LGIDict. It then renders the page at 300 dpi (at most 8192 px a side), cropped to the map frame, with everything outside the neatline transparent: "georef": "geopdf", plus crs when the file names an EPSG code. A page without a georeference is rendered whole and placed in place= like a plain image, with "warning": "no_georeference". Every PDF answer includes "pages", the page count, so you can offer the other pages.

POST /api/v1/maps/SQCWJAM/overlays?page=2                                              a GeoPDF
→ 201 { …, "overlay": { …, "georef": "geopdf", "crs": "EPSG:26910" }, "pages": 3 }
→ 400 { "error": "bad_page" }          no such page
→ 400 { "error": "pdf_encrypted" }     password-protected or encrypted: save an unprotected copy
→ 400 { "error": "bad_pdf" }           unreadable, or took over 60 s to render

Coordinate systems: WGS 84 (4326), NAD83 (4269), NAD83(CSRS) (4617), Web Mercator (3857), Canada Atlas Lambert (3979), BC Albers (3005), WGS 84 UTM (32601–32660, 32701–32760), NAD83 UTM (26901–26923) and NAD83(CSRS) UTM 7N–22N. Draw the image from /api/v1/media/{mediaId}/medium (WebP, at most 8192 px, transparency kept) onto the four corners; /thumb is a small JPEG and /api/v1/media/{mediaId} the original file. Moving or re-aligning an overlay is an ordinary feature update of its geometry and properties.overlay. Uploading needs view and edit.

Feature format

classgeometryuseful properties
MarkerPointmarker-color, description
ShapeLineString, Polygonstroke, stroke-width, fill, description
RangeRingPoint (centre)radii (km, e.g. [1, 2, 5]), stroke
Foldernoneother features point at it with folderId

Coordinates are [longitude, latitude] or [lng, lat, altitude_m], WGS84.

Permissions and errors

Connection canRead mapsWrite features & photosSend positionsWebhooks
Send positions onlyyesnoyesyes
Viewyesnonoyes
View and edityesyesyesyes

Team data is stored in Canada by a Canadian-owned provider. Your app only gets the maps a team chooses to give it, and the team can see and remove every connection.