{"openapi":"3.1.0","info":{"title":"SAR Map Integration API","version":"1.0.0","description":"Two-way exchange with SAR Map maps for apps and servers (drone apps, detection tools, CAD systems, trackers).\n\n**Authentication.** Send `Authorization: Bearer <token>`. A token is either a *connection* token (`sa.<id>.<secret>`) — get one with the pairing flow (`/api/connect/start` → the user approves the code → `/api/connect/poll`) or from a team admin (Team settings ▸ Connected apps) — or a user session. A connection sees either the whole team or one folder (with its subfolders), with permission `view`, `edit` or `track` (read + post positions).\n\n**Writes** go through the same op log as the apps: they show up live for everyone, are audited, and can be retried safely with `Idempotency-Key`.\n\n**Changes.** Either hold the live stream open (`GET /api/v1/maps/{mapId}/live`, Server-Sent Events), poll `GET /api/v1/maps/{mapId}/features?since=<head>`, or register a webhook.\n\n**Errors** are JSON `{ \"error\": \"<code>\", \"message\"?: \"<text>\" }` with a 4xx/5xx status."},"servers":[{"url":"/"}],"security":[{"bearer":[]}],"tags":[{"name":"Pairing","description":"Get a connection token without copying secrets (OAuth 2.0 device flow style)."},{"name":"Maps"},{"name":"Features"},{"name":"Live"},{"name":"Positions"},{"name":"Photos","description":"Photos on features: upload raw bytes, read the original or JPEG sizes."},{"name":"Overlays","description":"Georeferenced map images (scanned mapsheets, GeoTIFFs, KMZ ground overlays) drawn on the map."},{"name":"Webhooks"},{"name":"Meta"}],"paths":{"/api/connect/start":{"post":{"tags":["Pairing"],"security":[],"summary":"Start pairing: get a code to show the user","description":"Show `userCode` (or a QR code of `verificationUrlComplete`) and ask the user to open `verificationUrl` and approve. Then poll `/api/connect/poll` every `interval` seconds with `deviceCode`. Limited to 30 starts per IP per hour.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["app"],"properties":{"app":{"type":"string","maxLength":80,"examples":["Eagle Eyes"]},"device":{"type":"string","maxLength":80,"examples":["RC Plus"]}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["deviceCode","userCode","verificationUrl","verificationUrlComplete","expiresIn","interval"],"properties":{"deviceCode":{"type":"string","description":"Secret; only for polling"},"userCode":{"type":"string","examples":["ABCD-EFGH"]},"verificationUrl":{"type":"string","format":"uri"},"verificationUrlComplete":{"type":"string","format":"uri"},"expiresIn":{"type":"integer","examples":[600]},"interval":{"type":"integer","examples":[5]}}}}}},"400":{"description":"Missing app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many pairing starts from this address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/connect/poll":{"post":{"tags":["Pairing"],"security":[],"summary":"Poll for the connection token","description":"Answers 400 with `authorization_pending` until the user decides, `slow_down` if polled faster than `interval`, `access_denied` if the user refused, `expired_token` after 10 minutes or once the token was collected. On approval answers 200 with the token — exactly once; store it securely.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["deviceCode"],"properties":{"deviceCode":{"type":"string"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["token","serviceAccountId","org","folder","permission","apiBase"],"properties":{"token":{"type":"string","examples":["sa.AB12CD34EF56.secret"]},"serviceAccountId":{"type":"string"},"org":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"folder":{"oneOf":[{"type":"null"},{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}}]},"permission":{"enum":["view","edit","track"]},"apiBase":{"type":"string","format":"uri"}}}}}},"400":{"description":"Not (yet) approved","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"enum":["authorization_pending","slow_down","access_denied","expired_token"]}}}}}}}}},"/api/v1/maps":{"get":{"tags":["Maps"],"summary":"Maps the caller can read","description":"Newest activity first. A folder-scoped connection sees only maps in its folder.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"maps":{"type":"array","items":{"$ref":"#/components/schemas/Map"}}}}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Maps"],"summary":"Create a map","description":"Team editors (and whole-team `edit` connections) anywhere; folder-scoped `edit` connections and members with an edit grant only inside their folder. `orgId` defaults to the connection's team (required for user sessions).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title"],"properties":{"title":{"type":"string","maxLength":200},"folderId":{"type":["string","null"],"format":"uuid"},"orgId":{"type":"string","format":"uuid"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Map"}}}},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/maps/{mapId}":{"get":{"tags":["Maps"],"summary":"One map","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Map"},{"type":"object","properties":{"head":{"type":"integer","description":"Latest op seq"}}}]}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/maps/{mapId}/features":{"get":{"tags":["Features"],"summary":"Features as a GeoJSON FeatureCollection","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""},{"name":"since","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Only features changed after this op seq (use the previous response's `head`); adds `deleted`."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureCollection"}}}},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Features"],"summary":"Create a feature","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Makes the write safe to retry: a repeated request with the same key (from the same caller) is applied once and answers like the first one, with the feature as it is now."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewFeature"}}}},"responses":{"201":{"description":"Created (or the original result of a retried Idempotency-Key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feature"}}}},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A feature with this id already exists (`feature_exists`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/maps/{mapId}/features/{featureId}":{"get":{"tags":["Features"],"summary":"One feature","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""},{"name":"featureId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feature"}}}},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Features"],"summary":"Change a feature","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""},{"name":"featureId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Makes the write safe to retry: a repeated request with the same key (from the same caller) is applied once and answers like the first one, with the feature as it is now."}],"description":"Only the fields sent change. `properties` are merged key by key; a `null` value removes the key. `geometry` replaces the old one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeaturePatch"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feature"}}}},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A feature with this id already exists (`feature_exists`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Features"],"summary":"Delete a feature","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""},{"name":"featureId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Makes the write safe to retry: a repeated request with the same key (from the same caller) is applied once and answers like the first one, with the feature as it is now."}],"responses":{"204":{"description":"Deleted"},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A feature with this id already exists (`feature_exists`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/maps/{mapId}/positions":{"post":{"tags":["Positions"],"summary":"Post live positions for a device","description":"For connections with `edit` or `track` (and editors). The device label identifies the track: the server keeps one tracker per connection, map and label, so there are no keys to manage. Points carry device time (`at`, ISO 8601 or epoch ms; default now); repeats of the same `at` are ignored. At most 1000 points per request.","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["device","points"],"properties":{"device":{"type":"string","maxLength":80,"examples":["M30T-1"]},"points":{"type":"array","minItems":1,"maxItems":1000,"items":{"$ref":"#/components/schemas/PositionIn"}},"props":{"type":"object","additionalProperties":true,"description":"Passed through to live listeners (battery, mode…)"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"accepted":{"type":"integer"},"duplicates":{"type":"integer"}}}}}},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A feature with this id already exists (`feature_exists`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/maps/{mapId}/live":{"get":{"tags":["Live"],"summary":"Live stream (Server-Sent Events)","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""},{"name":"since","in":"query","required":false,"schema":{"type":"integer"},"description":"Replay ops after this seq first (or send Last-Event-ID)"}],"description":"A `text/event-stream` that stays open. Events:\n\n- `ops` (id = last seq): `{ \"head\": 42, \"ops\": [Op, …] }` — see the `Op` schema; replays from `since` first.\n- `track`: `{ \"connectKeyId\", \"label\", \"points\": [{ \"lng\", \"lat\", \"alt\", \"at\" }], \"props\" }`.\n- `revoked`: `{ \"error\": \"access_revoked\" }`, then the stream ends (access was removed).\n\nComment lines (`: ping`) every 15 s keep proxies from closing it. Reconnect with `Last-Event-ID` to resume.","responses":{"200":{"description":"Event stream","content":{"text/event-stream":{"schema":{"type":"string"}}}},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"This connection's webhooks","description":"Connection tokens only.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Webhooks"],"summary":"Register a webhook","description":"Connection tokens only. `url` must be https (http is accepted for localhost / 127.0.0.1 during development); private-network addresses are refused in production. Omit `mapId` for every map the connection can read. The `secret` for verifying signatures is returned only here. See the `webhooks` section for payloads.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"mapId":{"type":"string"},"events":{"type":"array","items":{"enum":["features","positions"]},"default":["features","positions"]}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"mapId":{"type":["string","null"]},"events":{"type":"array","items":{"type":"string"}},"secret":{"type":"string"}}}]}}}},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks/{id}":{"delete":{"tags":["Webhooks"],"summary":"Delete a webhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted"},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/maps/{mapId}/features/{featureId}/photos":{"post":{"tags":["Photos"],"summary":"Attach a photo to a feature","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""},{"name":"featureId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Makes the write safe to retry: a repeated request with the same key (from the same caller) is applied once and answers like the first one, with the feature as it is now."},{"name":"caption","in":"query","required":false,"schema":{"type":"string","maxLength":1000}},{"name":"takenAt","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"When the photo was taken. Default: EXIF DateTimeOriginal (with OffsetTimeOriginal; without a zone it is read as UTC), else now."},{"name":"link","in":"query","required":false,"schema":{"type":"string"},"description":"Share-link token for guests (or the X-Map-Link header)"}],"description":"Stores the original untouched and makes JPEG derivatives (EXIF-rotated, metadata stripped). The feature's `properties.photos` gains `{ id, caption, takenAt, width, height }` (through the op log, so live listeners and webhooks see it). Needs write access. With `Idempotency-Key`, a retry answers 200 with the same photo instead of storing it twice.","requestBody":{"required":true,"description":"The raw image bytes (not multipart, not base64). JPEG, PNG or WebP up to 25 MB; convert HEIC to JPEG first.","content":{"image/jpeg":{"schema":{"type":"string","contentMediaType":"image/jpeg"}},"image/png":{"schema":{"type":"string","contentMediaType":"image/png"}},"image/webp":{"schema":{"type":"string","contentMediaType":"image/webp"}}}},"responses":{"200":{"description":"Replay of an earlier upload with the same Idempotency-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Photo"}}}},"201":{"description":"Stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Photo"}}}},"400":{"description":"Not an image (`bad_image`), bad parameters, or (standalone upload) `no_location`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only access (view and track connections, view links)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`too_many_photos`: a feature holds at most 50","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`too_large`: over 25 MB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"`unsupported_media_type`: not JPEG, PNG or WebP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`too_many_uploads`: 600 per hour","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/maps/{mapId}/photos":{"post":{"tags":["Photos"],"summary":"Drop a photo on the map as a new marker","description":"Creates a Marker at `lat`/`lng`, else at the photo's EXIF GPS position (400 `no_location` if there is neither), and attaches the photo to it.","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Makes the write safe to retry: a repeated request with the same key (from the same caller) is applied once and answers like the first one, with the feature as it is now."},{"name":"caption","in":"query","required":false,"schema":{"type":"string","maxLength":1000}},{"name":"takenAt","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"When the photo was taken. Default: EXIF DateTimeOriginal (with OffsetTimeOriginal; without a zone it is read as UTC), else now."},{"name":"link","in":"query","required":false,"schema":{"type":"string"},"description":"Share-link token for guests (or the X-Map-Link header)"},{"name":"title","in":"query","required":false,"schema":{"type":"string","default":"Photo"}},{"name":"lat","in":"query","required":false,"schema":{"type":"number"}},{"name":"lng","in":"query","required":false,"schema":{"type":"number"}},{"name":"folderId","in":"query","required":false,"schema":{"type":"string"},"description":"In-map folder (a Folder feature id)"},{"name":"color","in":"query","required":false,"schema":{"type":"string","examples":["#ff0000"]},"description":"Marker colour"}],"requestBody":{"required":true,"description":"The raw image bytes (not multipart, not base64). JPEG, PNG or WebP up to 25 MB; convert HEIC to JPEG first.","content":{"image/jpeg":{"schema":{"type":"string","contentMediaType":"image/jpeg"}},"image/png":{"schema":{"type":"string","contentMediaType":"image/png"}},"image/webp":{"schema":{"type":"string","contentMediaType":"image/webp"}}}},"responses":{"200":{"description":"Replay (Idempotency-Key)","content":{"application/json":{"schema":{"type":"object","properties":{"feature":{"$ref":"#/components/schemas/Feature"},"photo":{"$ref":"#/components/schemas/Photo"}}}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["feature","photo"],"properties":{"feature":{"$ref":"#/components/schemas/Feature"},"photo":{"$ref":"#/components/schemas/Photo"}}}}}},"400":{"description":"Not an image (`bad_image`), bad parameters, or (standalone upload) `no_location`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only access (view and track connections, view links)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`too_many_photos`: a feature holds at most 50","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`too_large`: over 25 MB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"`unsupported_media_type`: not JPEG, PNG or WebP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`too_many_uploads`: 600 per hour","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/maps/{mapId}/overlays":{"post":{"tags":["Overlays"],"summary":"Add a map image (mapsheet) as an Overlay feature","description":"The body is the raw file: a GeoTIFF, a KMZ with a GroundOverlay (Google Earth, Avenza, CalTopo exports), a GeoPDF, or a PNG/JPEG/WebP. The file type is read from its bytes. The georeference comes from, in order: the GeoTIFF tags (ModelTiepoint+ModelPixelScale or ModelTransformation, PixelIsArea/PixelIsPoint respected), the KMZ `LatLonBox` (with rotation) or `gx:LatLonQuad`, the GeoPDF page, or `world` + `epsg` (the 6 numbers of a .pgw/.jgw/.tfw world file).\n\nGeoPDF: page `page` (default 1) is rendered at 300 dpi (longest side at most 8192 px, at most 50 MP). Its georeference is the ISO 32000 / Adobe geospatial encoding (the largest `/VP` viewport with a `/Measure /GEO`: GPTS, LPTS, Bounds, GCS by EPSG code or WKT; Avenza, NRCan, USGS, QGIS/GDAL) or the older OGC/TerraGo `/LGIDict` (UTM or geographic). A georeferenced page is cropped to the map frame (the collar goes), drawn unrotated, and everything outside the neatline is transparent; `georef: \"geopdf\"`. A PDF page without one is rendered whole and placed like any image (`warning: \"no_georeference\"`). PDF answers include `pages` (the page count).\n\nWithout a usable georeference the image is centred in `place` (the current view) at ~60% of its size, `georef: \"manual\"`, for the user to align by hand (400 `no_location` without `place`). A coordinate system the server does not know falls back to `place` with `warning: \"unsupported_crs\"` (and `crs`). Supported EPSG: 4326, 4269, 4617, 3857, 3979, 3005, 32601–32660, 32701–32760, 26901–26923, NAD83(CSRS) UTM 7N–22N (3154–3160, 2955–2962, 3761).\n\nCreates a feature of class `Overlay` (through the op log, so open maps see it live): geometry = Polygon of the 4 image corners (top-left, top-right, bottom-right, bottom-left, closed), `properties.overlay` = OverlayProps. The image to draw is `/api/v1/media/{mediaId}/medium` (WebP, at most 8192 px, alpha kept). Moving it by hand is a normal feature update of geometry and `properties.overlay` (corners, `georef: \"manual\"`). Needs write access. Up to 250 MB.","parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"description":"Map id, e.g. \"K7Q2XRM\""},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Makes the write safe to retry: a repeated request with the same key (from the same caller) is applied once and answers like the first one, with the feature as it is now."},{"name":"title","in":"query","required":false,"schema":{"type":"string","maxLength":500},"description":"Default: the KMZ GroundOverlay name, else \"Map image\""},{"name":"folderId","in":"query","required":false,"schema":{"type":"string"},"description":"In-map folder (a Folder feature id)"},{"name":"place","in":"query","required":false,"schema":{"type":"string","examples":["-124.9,49.1,-124.3,49.4"]},"description":"w,s,e,n of the current map view; used only when the file has no usable georeference"},{"name":"world","in":"query","required":false,"schema":{"type":"string","examples":["2.5,0,0,-2.5,412001.25,5456998.75"]},"description":"World file numbers in file line order: A,D,B,E,C,F"},{"name":"epsg","in":"query","required":false,"schema":{"type":"integer","examples":[26910]},"description":"CRS of the world file (or of a GeoTIFF without CRS keys). Default 4326 when the numbers look like degrees, else 400 `crs_required`"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"PDFs only: the page to use (1 = the first); 400 `bad_page` past the last page"},{"name":"link","in":"query","required":false,"schema":{"type":"string"},"description":"Share-link token for guests (or the X-Map-Link header)"}],"requestBody":{"required":true,"description":"The raw file bytes (not multipart).","content":{"image/tiff":{"schema":{"type":"string","contentMediaType":"image/tiff"}},"application/vnd.google-earth.kmz":{"schema":{"type":"string","contentMediaType":"application/vnd.google-earth.kmz"}},"application/zip":{"schema":{"type":"string","contentMediaType":"application/zip"}},"application/pdf":{"schema":{"type":"string","contentMediaType":"application/pdf"}},"image/png":{"schema":{"type":"string","contentMediaType":"image/png"}},"image/jpeg":{"schema":{"type":"string","contentMediaType":"image/jpeg"}},"image/webp":{"schema":{"type":"string","contentMediaType":"image/webp"}}}},"responses":{"200":{"description":"Replay of an earlier upload with the same Idempotency-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverlayCreated"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverlayCreated"}}}},"400":{"description":"`bad_image`, `bad_kmz`, `no_location`, `crs_required`, `unsupported_crs` (no `place` to fall back to), `bad_place`, `bad_world_file`, `bad_epsg`, `bad_georeference`, `bad_pdf` (unreadable, or over 60 s to render), `pdf_encrypted`, `bad_page`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only access (view and track connections, view links)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`idempotency_key_reused`: the key was used for a different upload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`too_large`: over 250 MB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"`unsupported_media_type`: not a GeoTIFF, KMZ, PDF, PNG, JPEG or WebP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`too_many_uploads`: 600 per hour","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/{id}":{"get":{"tags":["Photos"],"summary":"The original photo, byte for byte","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"link","in":"query","required":false,"schema":{"type":"string"},"description":"Share-link token, for guests showing photos in <img> tags"}],"description":"Read access to the photo's map (a session cookie works for same-origin <img>). Photos of deleted features stay readable; deleted photos answer 404. ETag = the original's sha256.","responses":{"200":{"description":"The image","headers":{"ETag":{"schema":{"type":"string"}},"Cache-Control":{"schema":{"type":"string","examples":["private, max-age=86400"]}}},"content":{"image/jpeg":{"schema":{"type":"string","contentMediaType":"image/jpeg"}},"image/png":{"schema":{"type":"string","contentMediaType":"image/png"}},"image/webp":{"schema":{"type":"string","contentMediaType":"image/webp"}}}},"304":{"description":"Not modified"},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Photos"],"summary":"Change the caption","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"link","in":"query","required":false,"schema":{"type":"string"},"description":"Share-link token, for guests showing photos in <img> tags"}],"description":"Also updates the caption inside the feature's `properties.photos`. Needs write access.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["caption"],"properties":{"caption":{"type":["string","null"],"maxLength":1000}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Photo"}}}},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Photos"],"summary":"Delete a photo","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"link","in":"query","required":false,"schema":{"type":"string"},"description":"Share-link token, for guests showing photos in <img> tags"}],"description":"Removes it from the feature's `properties.photos`; the photo then answers 404. Needs write access.","responses":{"204":{"description":"Deleted"},"400":{"description":"Invalid request (`error` says what: bad_geometry, bad_request, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed (e.g. `read_only` for a view-only caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/{id}/medium":{"get":{"tags":["Photos","Overlays"],"summary":"JPEG that fits inside 1600×1600 (overlays: WebP inside 8192×8192)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"link","in":"query","required":false,"schema":{"type":"string"},"description":"Share-link token, for guests showing photos in <img> tags"}],"responses":{"200":{"description":"JPEG for photos, WebP with alpha for overlays","headers":{"ETag":{"schema":{"type":"string"}},"Cache-Control":{"schema":{"type":"string","examples":["private, max-age=86400"]}}},"content":{"image/jpeg":{"schema":{"type":"string","contentMediaType":"image/jpeg"}},"image/webp":{"schema":{"type":"string","contentMediaType":"image/webp"}}}},"304":{"description":"Not modified"},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/{id}/thumb":{"get":{"tags":["Photos"],"summary":"JPEG that fits inside 320×320","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"link","in":"query","required":false,"schema":{"type":"string"},"description":"Share-link token, for guests showing photos in <img> tags"}],"responses":{"200":{"description":"JPEG","headers":{"ETag":{"schema":{"type":"string"}},"Cache-Control":{"schema":{"type":"string","examples":["private, max-age=86400"]}}},"content":{"image/jpeg":{"schema":{"type":"string","contentMediaType":"image/jpeg"}}}},"304":{"description":"Not modified"},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/{id}/info":{"get":{"tags":["Photos"],"summary":"Photo details","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"link","in":"query","required":false,"schema":{"type":"string"},"description":"Share-link token, for guests showing photos in <img> tags"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Photo"}}}},"401":{"description":"No or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown map or feature, or one the caller can't see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/openapi.json":{"get":{"tags":["Meta"],"security":[],"summary":"This document","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"webhooks":{"features.changed":{"post":{"summary":"Features changed on a map","description":"Coalesced over about a second. `ops` are in seq order (at most 1000; if `head` is beyond the last op, fetch `/features?since=`). Verify `X-SARMap-Signature` before trusting the body; answer 2xx within 10 s. Failed deliveries are retried after 5 s, 30 s, 2 min and 10 min; after 20 consecutive failures the webhook is disabled.","parameters":[{"name":"X-SARMap-Event","in":"header","schema":{"type":"string","enum":["features.changed"]}},{"name":"X-SARMap-Delivery","in":"header","schema":{"type":"string","format":"uuid"},"description":"Same as body `id`; stays the same across retries"},{"name":"X-SARMap-Timestamp","in":"header","schema":{"type":"string"},"description":"Unix seconds when this attempt was sent"},{"name":"X-SARMap-Signature","in":"header","schema":{"type":"string"},"description":"`sha256=` + hex HMAC-SHA256 of the raw request body, keyed with the webhook secret. Compare in constant time."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeaturesChangedEvent"}}}},"responses":{"200":{"description":"Received"}}}},"positions.received":{"post":{"summary":"Live positions arrived on a map","description":"Same headers and retry rules as `features.changed`.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PositionsReceivedEvent"}}}},"responses":{"200":{"description":"Received"}}}}},"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"A connection token `sa.<id>.<secret>`, or a user session token"}},"schemas":{"PhotoEntry":{"type":"object","description":"An entry of a feature's properties.photos (URLs are relative to the server)","properties":{"id":{"type":"string","format":"uuid"},"caption":{"type":["string","null"]},"takenAt":{"type":["string","null"],"format":"date-time"},"width":{"type":"integer"},"height":{"type":"integer"},"url":{"type":"string","examples":["/api/v1/media/<id>"]},"mediumUrl":{"type":"string","examples":["/api/v1/media/<id>/medium"]},"thumbUrl":{"type":"string","examples":["/api/v1/media/<id>/thumb"]}}},"OverlayProps":{"type":"object","required":["mediaId","corners","opacity","width","height","georef"],"description":"properties.overlay of an Overlay feature","properties":{"mediaId":{"type":"string","format":"uuid","description":"Draw /api/v1/media/{mediaId}/medium"},"corners":{"type":"array","minItems":4,"maxItems":4,"items":{"type":"array","minItems":2,"maxItems":2,"items":{"type":"number"}},"description":"[lng, lat] of the image corners: top-left, top-right, bottom-right, bottom-left"},"opacity":{"type":"number","minimum":0,"maximum":1,"examples":[0.75]},"width":{"type":"integer","description":"Pixel width of the display image"},"height":{"type":"integer"},"georef":{"enum":["geotiff","worldfile","kmz","geopdf","manual"]},"crs":{"type":"string","examples":["EPSG:26910"],"description":"Source CRS (geotiff, worldfile, and geopdf when the file names an EPSG code)"}}},"OverlayCreated":{"type":"object","required":["feature","overlay"],"properties":{"feature":{"$ref":"#/components/schemas/Feature"},"overlay":{"$ref":"#/components/schemas/OverlayProps"},"warning":{"enum":["unsupported_crs","no_georeference"],"description":"`unsupported_crs`: the file's CRS is not supported; `no_georeference`: the PDF page has no georeference. Either way the image was placed in the view instead"},"crs":{"type":"string","description":"With warning: the unsupported CRS, e.g. EPSG:2056"},"pages":{"type":"integer","minimum":1,"description":"PDFs only: how many pages the PDF has"}}},"Photo":{"type":"object","required":["id","kind","mapId","featureId","contentType","bytes","width","height","takenAt","caption","location","url","mediumUrl","thumbUrl","uploadedBy","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"kind":{"enum":["photo","overlay"],"description":"overlay: the image of an Overlay feature"},"mapId":{"type":"string"},"featureId":{"type":["string","null"],"format":"uuid"},"contentType":{"enum":["image/jpeg","image/png","image/webp","image/tiff","application/vnd.google-earth.kmz"]},"bytes":{"type":"integer"},"width":{"type":"integer","description":"After EXIF rotation"},"height":{"type":"integer"},"takenAt":{"type":["string","null"],"format":"date-time"},"caption":{"type":["string","null"]},"location":{"oneOf":[{"type":"null"},{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"alt":{"type":["number","null"]}}}],"description":"From the photo's EXIF GPS"},"url":{"type":"string","description":"Relative: /api/v1/media/<id>"},"mediumUrl":{"type":"string"},"thumbUrl":{"type":"string"},"uploadedBy":{"type":"string","description":"Person or app name"},"createdAt":{"type":"string","format":"date-time"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"}}},"Map":{"type":"object","required":["id","title","orgId","orgName","folderId","access","updatedAt"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"orgId":{"type":"string","format":"uuid"},"orgName":{"type":"string"},"folderId":{"type":["string","null"],"description":"Library folder"},"access":{"enum":["read","write"]},"updatedAt":{"type":"string","format":"date-time"}}},"Geometry":{"description":"GeoJSON Point, LineString or Polygon in WGS84 (Multi* types are not accepted)","oneOf":[{"type":"object","required":["type","coordinates"],"properties":{"type":{"const":"Point"},"coordinates":{"type":"array","minItems":2,"maxItems":3,"items":{"type":"number"},"description":"[longitude, latitude] or [longitude, latitude, altitude metres], WGS84"}}},{"type":"object","required":["type","coordinates"],"properties":{"type":{"const":"LineString"},"coordinates":{"type":"array","minItems":2,"items":{"type":"array","minItems":2,"maxItems":3,"items":{"type":"number"},"description":"[longitude, latitude] or [longitude, latitude, altitude metres], WGS84"}}}},{"type":"object","required":["type","coordinates"],"properties":{"type":{"const":"Polygon"},"coordinates":{"type":"array","minItems":1,"items":{"type":"array","minItems":4,"items":{"type":"array","minItems":2,"maxItems":3,"items":{"type":"number"},"description":"[longitude, latitude] or [longitude, latitude, altitude metres], WGS84"}},"description":"Closed rings"}}}]},"Feature":{"type":"object","required":["type","id","geometry","properties"],"properties":{"type":{"const":"Feature"},"id":{"type":"string","format":"uuid"},"geometry":{"oneOf":[{"$ref":"#/components/schemas/Geometry"},{"type":"null"}],"description":"null for folders"},"properties":{"type":"object","additionalProperties":true,"required":["class","title","folderId","updatedAt","updatedBy","seq"],"properties":{"class":{"type":"string","examples":["Marker","Shape","Folder","Assignment","Clue"]},"title":{"type":"string"},"folderId":{"type":["string","null"],"description":"In-map folder (a Folder feature id)"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"type":"string","description":"Person or app name"},"seq":{"type":"integer","description":"Op seq of the last change"},"photos":{"type":"array","items":{"$ref":"#/components/schemas/PhotoEntry"},"description":"Photos attached to the feature, with relative URLs"}},"description":"Your own properties sit alongside these (e.g. marker-color, description)."}}},"FeatureCollection":{"type":"object","required":["type","head","features"],"properties":{"type":{"const":"FeatureCollection"},"head":{"type":"integer","description":"Latest op seq; pass as `since` next time"},"features":{"type":"array","items":{"$ref":"#/components/schemas/Feature"}},"deleted":{"type":"array","items":{"type":"string","format":"uuid"},"description":"With `since` only: ids deleted since then"}}},"NewFeature":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Optional; the server makes one if omitted"},"class":{"type":"string","description":"Default Marker for a Point, else Shape. Use Folder (no geometry) for an in-map folder."},"title":{"type":"string","maxLength":500},"folderId":{"type":["string","null"]},"geometry":{"$ref":"#/components/schemas/Geometry"},"properties":{"type":"object","additionalProperties":true}}},"FeaturePatch":{"type":"object","properties":{"title":{"type":"string","maxLength":500},"folderId":{"type":["string","null"]},"geometry":{"$ref":"#/components/schemas/Geometry"},"properties":{"type":"object","additionalProperties":true,"description":"Merged; null removes a key"}}},"PositionIn":{"type":"object","required":["lat","lng"],"properties":{"lat":{"type":"number"},"lng":{"type":"number"},"alt":{"type":"number","description":"Metres"},"at":{"oneOf":[{"type":"string","format":"date-time"},{"type":"integer","description":"Epoch ms"}]},"speed":{"type":"number","description":"m/s"},"heading":{"type":"number","description":"Degrees"},"accuracy":{"type":"number","description":"Metres"}}},"Op":{"type":"object","properties":{"seq":{"type":"integer"},"featureId":{"type":"string","format":"uuid"},"kind":{"enum":["create","update","delete"]},"actor":{"type":"string","description":"'user:<id>', 'sa:<id>' or 'link:<id>'"},"at":{"type":"string","format":"date-time"},"after":{"type":["object","null"],"description":"The feature after the op (engine shape: id, class, folderId, title, geometry, props, deleted, …)"}}},"Webhook":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string"},"mapId":{"type":["string","null"]},"events":{"type":"array","items":{"enum":["features","positions"]}},"createdAt":{"type":"string","format":"date-time"},"disabledAt":{"type":["string","null"],"format":"date-time"},"failureCount":{"type":"integer"},"lastError":{"type":["string","null"]},"lastSuccessAt":{"type":["string","null"],"format":"date-time"}}},"FeaturesChangedEvent":{"type":"object","required":["id","type","mapId","head","ops","at"],"properties":{"id":{"type":"string","format":"uuid"},"type":{"const":"features.changed"},"mapId":{"type":"string"},"head":{"type":"integer"},"ops":{"type":"array","items":{"$ref":"#/components/schemas/Op"}},"at":{"type":"string","format":"date-time"}}},"PositionsReceivedEvent":{"type":"object","required":["id","type","mapId","positions","at"],"properties":{"id":{"type":"string","format":"uuid"},"type":{"const":"positions.received"},"mapId":{"type":"string"},"positions":{"type":"array","items":{"type":"object","properties":{"connectKeyId":{"type":"string"},"label":{"type":"string"},"props":{"type":"object"},"points":{"type":"array","items":{"type":"object","properties":{"lng":{"type":"number"},"lat":{"type":"number"},"alt":{"type":["number","null"]},"at":{"type":"string","format":"date-time"}}}}}}},"at":{"type":"string","format":"date-time"}}}}}}