{"openapi":"3.1.0","info":{"title":"SnapHop Maps","version":"0.2.0","description":"Every operation an agent's API key can call: register with POST /api/agents, then send its apiKey as a bearer token. /llms.txt is the guide to this API, with a curl command for each step. An agent's account is deleted, with every map it published, after 7 days without a request made with its key."},"servers":[{"url":"https://maps.snaphop.ai"}],"security":[{"apiKey":[]}],"paths":{"/api/agents":{"post":{"tags":["agents"],"security":[],"summary":"Register an autonomous agent, its own workspace and its one API key, returned once (ADR 0021)","description":"Needs no session, CSRF token, email address or password. The agent owns a new workspace; its key has READ_MAPS, EDIT_MAPS and PUBLISH_MAPS there and nothing else, so it cannot invite anyone. Counted per client address and per installation per day; an agent's workspace holds at most `limits.mapsPerWorkspace` maps, and its publications, rollbacks and withdrawals are counted per workspace. The operator may close registration. The key lives the installation's default key lifetime; the agent replaces it before then with POST /api/admin/v1/agent/key, and one whose key is lost or expired registers again. The account is ephemeral: after `limits.inactivityDays` without a request made with its key, it is deleted and every map it published taken down (ADR 0022).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"The agent's name, one line of plain text, shown in the workspace's Activity"},"workspaceName":{"type":"string","minLength":1,"maxLength":200,"description":"Defaults to the agent's name"}}},"example":{"name":"Your agent's name"}}}},"responses":{"201":{"description":"Registered. Nothing on the way keeps a copy (no-store).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRegistration"}}}},"400":{"description":"AGENT_NAME_INVALID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"AGENT_REGISTRATION_CLOSED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"TOO_MANY_REQUESTS from this address","or AGENT_REGISTRATIONS_EXHAUSTED for the day":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/v1/agent/key":{"post":{"tags":["agents"],"summary":"Issue the calling agent a new key of the same scopes; the key used stops once the new one is used (ADR 0021)","description":"Only an agent's own undelegated key may call it; a person, a service account and a session are refused. The key the request was made with keeps working until the first request made with the new key, which revokes it together with any other key issued in its place, so an agent whose answer was lost asks again with the old key. An agent has no password or reset link, so it calls this before its key's expiresAt.","responses":{"201":{"description":"Replaced; the key is shown once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentKey"}}}},"403":{"description":"AGENT_KEY_REQUIRED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/v1/account":{"get":{"tags":["account"],"summary":"The signed-in account, its full name, its active workspace, role, permissions and memberships","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"},"principal":{"type":"string"},"displayName":{"type":["string","null"],"maxLength":200,"description":"The full name the person gave themselves; null until they do, and always null for an API key. Plain text."},"workspaceId":{"type":"string","format":"uuid"},"workspaceName":{"type":"string"},"role":{"$ref":"#/components/schemas/Role"},"permissions":{"type":"array","items":{"$ref":"#/components/schemas/Permission"}},"workspaces":{"type":"array","items":{"type":"object","properties":{"workspaceId":{"type":"string"},"workspaceName":{"type":"string"},"role":{"type":"string"}}}}}}}}}}}},"/api/admin/v1/installation":{"get":{"tags":["maps"],"summary":"Where maps are delivered from, which basemap a publication would pin, which styles a map may choose, and whether publication is configured","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"environment":{"enum":["production","staging","development"]},"tileSource":{"enum":["openfreemap","self-hosted"]},"deliveryBaseUrl":{"type":"string","format":"uri"},"publicationEnabled":{"type":"boolean"},"runtimeVersion":{"type":"string","pattern":"^[0-9a-f]{16}$"},"basemap":{"oneOf":[{"$ref":"#/components/schemas/Basemap"},{"type":"null"}]},"styles":{"type":"array","description":"The styles a map may be published in, in this order, each with the address the editor's preview loads (ADR 0020). Empty on a self-hosted installation, whose basemap has one style.","items":{"type":"object","properties":{"id":{"$ref":"#/components/schemas/Style"},"name":{"type":"string","examples":["Positron"]},"style":{"type":"string","format":"uri","examples":["https://tiles.openfreemap.org/styles/positron"]}}}}}}}}}}}},"/api/admin/v1/activity":{"get":{"tags":["maps"],"summary":"The active workspace's 50 most recent audit events (READ_MAPS)","description":"`subject` and `actor` are the identifiers recorded. `description` names what was acted on -- the map and release, the member's full name or address, the invitee's address, the key, the service account or the workspace -- and falls back to `subject`. `by` is the full name of the person who acted, or their address when they have not set one, a machine account's display name, or `actor` when the account no longer exists or the action was the system's. Names are read when the trail is, so an event shows a person's current name. A full name is free text anyone may choose, even another member's address, so it is never shown alone: `byAddress` is the sign-in address of the person who acted and `subjectAddress` that of a person acted on, and each is null for a machine account (whose address is synthetic), a non-person subject, or the system.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"subject":{"type":"string"},"description":{"type":"string"},"subjectAddress":{"type":["string","null"]},"actor":{"type":"string"},"by":{"type":"string"},"byAddress":{"type":["string","null"]},"at":{"type":"string","format":"date-time"}}}}}}}}}},"/api/admin/v1/maps":{"get":{"tags":["maps"],"summary":"The workspace's maps (READ_MAPS)","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MapSummary"}}}}}}},"post":{"tags":["maps"],"summary":"Create a map, and with publish=true publish it in the same request (EDIT_MAPS, and PUBLISH_MAPS to publish)","description":"A definition without a view opens on one that shows all its markers, or at the basemap's own view when it has none, so a name alone starts a map where it always did (ADR 0021). With publish=true a refused publication does not undo the creation: the map is kept as a draft, `published` is null and `publicationError` says why. A caller whose key cannot publish is refused before anything is created.","parameters":[{"name":"publish","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewMap"},"example":{"name":"Coffee in Lisbon","style":"positron","markers":[{"position":[-9.1427,38.7107],"title":"A Brasileira","description":"Open since 1905"},{"position":[-9.1365,38.7139],"title":"Fabrica Coffee Roasters","color":"#b5452b"}]}}}},"responses":{"201":{"description":"Created, and published when asked and not refused","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/MapDetail"},{"type":"object","properties":{"publicationError":{"description":"Present with publish=true; null when the map was published, else the refusal's code, message and any fields","oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Error/properties/error"}]}}}]}}}},"403":{"$ref":"#/components/responses/Error"},"409":{"description":"MAP_LIMIT_REACHED in an agent's workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/Error"}}}},"/api/admin/v1/maps/{id}":{"parameters":[{"$ref":"#/components/parameters/MapId"}],"get":{"tags":["maps"],"summary":"A map, its draft and, once published, its addresses and embed codes (READ_MAPS)","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MapDetail"}}}},"404":{"$ref":"#/components/responses/Error"}}},"put":{"tags":["maps"],"summary":"Save the draft if nobody saved it since draftVersion (EDIT_MAPS)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["draftVersion","definition"],"properties":{"draftVersion":{"type":"integer","minimum":1,"description":"The draftVersion GET answered; a newer save answers 409 DRAFT_CHANGED"},"definition":{"$ref":"#/components/schemas/MapDefinition"}}},"example":{"draftVersion":1,"definition":{"name":"Coffee in Lisbon","style":"positron","view":{"center":[-9.1396,38.7123],"zoom":15},"markers":[{"position":[-9.1427,38.7107],"title":"A Brasileira","description":"Open since 1905"}]}}}}},"responses":{"200":{"description":"Saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MapDetail"}}}},"400":{"description":"INVALID_REQUEST without a draftVersion","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/Error"},"409":{"description":"DRAFT_CHANGED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"MAP_INVALID with every refused field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["maps"],"summary":"Withdraw the map; its stable addresses answer 404 and it leaves the workspace (PUBLISH_MAPS)","responses":{"204":{"description":"Withdrawn"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"502":{"$ref":"#/components/responses/Error"},"503":{"description":"PUBLICATION_NOT_CONFIGURED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/AgentThrottled"}}}},"/api/admin/v1/maps/{id}/publications":{"parameters":[{"$ref":"#/components/parameters/MapId"}],"post":{"tags":["maps"],"summary":"Publish the draft as the next release and make it live (PUBLISH_MAPS)","responses":{"201":{"description":"Published","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Release"}}}},"404":{"$ref":"#/components/responses/Error"},"409":{"description":"PUBLICATION_IN_PROGRESS or BASEMAP_UNAVAILABLE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"MAP_INVALID, for example outside a self-hosted basemap's coverage, or under `style` in another style than a self-hosted basemap's own","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"PUBLICATION_FAILED; the previous release is still live","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"PUBLICATION_NOT_CONFIGURED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/AgentThrottled"}}}},"/api/admin/v1/maps/{id}/releases":{"parameters":[{"$ref":"#/components/parameters/MapId"}],"get":{"tags":["maps"],"summary":"The map's releases, newest first (READ_MAPS)","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Release"}}}}},"404":{"$ref":"#/components/responses/Error"}}}},"/api/admin/v1/maps/{id}/releases/{number}/activation":{"parameters":[{"$ref":"#/components/parameters/MapId"},{"name":"number","in":"path","required":true,"schema":{"type":"integer","minimum":1}}],"post":{"tags":["maps"],"summary":"Make an earlier release live again, naming the release expected to be active (PUBLISH_MAPS)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["expectedActive"],"properties":{"expectedActive":{"type":"integer"}}}}}},"responses":{"200":{"description":"Active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Release"}}}},"400":{"description":"INVALID_REQUEST without an expectedActive","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/Error"},"409":{"description":"ACTIVE_RELEASE_CHANGED, RELEASE_UNAVAILABLE or PUBLICATION_IN_PROGRESS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"$ref":"#/components/responses/Error"},"503":{"description":"PUBLICATION_NOT_CONFIGURED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/AgentThrottled"}}}}},"components":{"securitySchemes":{"session":{"type":"apiKey","in":"cookie","name":"JSESSIONID","description":"Form login at POST /login; state-changing requests carry the header and token GET /api/csrf returns"},"apiKey":{"type":"http","scheme":"bearer","description":"A service account's key, or an agent's (POST /api/agents)"}},"parameters":{"MapId":{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-km-np-z2-9]{12}$"}}},"requestBodies":{"Email":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"}}}}}},"Token":{"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"}}}}}},"TokenAndPassword":{"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"password":{"type":"string"}}}}}}},"responses":{"Error":{"description":"A refusal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"AgentThrottled":{"description":"TOO_MANY_REQUESTS: an agent's publications, rollbacks and withdrawals are counted per workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Role":{"enum":["OWNER","ADMIN","EDITOR","VIEWER"]},"AssignableRole":{"enum":["ADMIN","EDITOR","VIEWER"],"description":"A role that can be given; OWNER is assigned only when a workspace is created"},"Permission":{"enum":["MANAGE_WORKSPACE","MANAGE_MEMBERS","READ_MAPS","EDIT_MAPS","PUBLISH_MAPS"]},"Position":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":2,"description":"[longitude, latitude] within Web Mercator"},"Bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"[west, south, east, north]"},"Style":{"enum":["liberty","bright","positron","dark","fiord"],"description":"One of OpenFreeMap's public styles, by identifier (ADR 0020). Only the server turns it into an address, on tiles.openfreemap.org."},"MapDefinition":{"type":"object","required":["name","view"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"style":{"oneOf":[{"$ref":"#/components/schemas/Style"},{"type":"null"}],"default":"liberty","description":"The style the map is drawn and published in. Absent or null is liberty, and the API writes a liberty definition without it, as every definition was written before maps chose a style. Any other value, an address included, is refused under `style` with MAP_INVALID. On a self-hosted installation only liberty, the basemap's own style, can be published."},"view":{"type":"object","required":["center","zoom"],"additionalProperties":false,"properties":{"center":{"$ref":"#/components/schemas/Position"},"zoom":{"type":"number","minimum":0,"maximum":22},"minZoom":{"type":"number","minimum":0,"maximum":22,"default":0},"maxZoom":{"type":"number","minimum":0,"maximum":22,"default":20},"bounds":{"oneOf":[{"$ref":"#/components/schemas/Bounds"},{"type":"null"}]}}},"controls":{"type":"object","additionalProperties":false,"properties":{"navigation":{"type":"boolean","default":true},"scale":{"type":"boolean","default":true},"cooperativeGestures":{"type":"boolean","default":false}}},"markers":{"type":"array","maxItems":500,"items":{"type":"object","required":["position","title"],"additionalProperties":false,"properties":{"position":{"$ref":"#/components/schemas/Position"},"title":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":1000,"description":"Plain text; line breaks are kept"},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","default":"#245b47"}}}}}},"NewMap":{"type":"object","description":"A MapDefinition whose view may be left out; the map then opens on a view that shows its markers","required":["name"],"additionalProperties":false,"properties":{"name":{"$ref":"#/components/schemas/MapDefinition/properties/name"},"style":{"$ref":"#/components/schemas/MapDefinition/properties/style"},"view":{"$ref":"#/components/schemas/MapDefinition/properties/view"},"controls":{"$ref":"#/components/schemas/MapDefinition/properties/controls"},"markers":{"$ref":"#/components/schemas/MapDefinition/properties/markers"}}},"AgentKey":{"type":"object","properties":{"apiKey":{"type":"string","description":"The secret, shown this once; send it as Authorization: Bearer"},"keyId":{"type":"string","format":"uuid"},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/Permission"}},"expiresAt":{"type":"string","format":"date-time"}}},"AgentRegistration":{"allOf":[{"$ref":"#/components/schemas/AgentKey"},{"type":"object","properties":{"agentId":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"limits":{"type":"object","properties":{"mapsPerWorkspace":{"type":"integer"},"markersPerMap":{"type":"integer"},"inactivityDays":{"type":"integer","description":"Days without a request after which the account and everything it published are deleted"}}}}}]},"MapSummary":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"activeRelease":{"type":["integer","null"]},"unpublishedChanges":{"type":"boolean"},"markers":{"type":"integer"}}},"MapDetail":{"allOf":[{"$ref":"#/components/schemas/MapSummary"},{"type":"object","properties":{"draftVersion":{"type":"integer"},"draft":{"$ref":"#/components/schemas/MapDefinition"},"published":{"oneOf":[{"type":"null"},{"type":"object","properties":{"release":{"type":"integer"},"publishedAt":{"type":"string","format":"date-time"},"page":{"type":"string","format":"uri"},"document":{"type":"string","format":"uri"},"embedScript":{"type":"string","description":"HTML to paste into a page: the runtime's module script and an element whose `data-snaphop-map` names the map's document. When the active release names a runtime other than this build's, the stylesheet link, element and inline script calling `SnapHop.mount()` that every runtime supports (ADR 0019)."},"embedFrame":{"type":"string"}}}]}}}]},"Release":{"type":"object","properties":{"number":{"type":"integer"},"state":{"enum":["staging","verified","active","superseded","failed","retired"]},"tileSource":{"enum":["openfreemap","self-hosted"]},"basemapRelease":{"type":["string","null"]},"style":{"$ref":"#/components/schemas/Style","description":"The style the release was published in; liberty for one published before maps chose a style. On a release whose tileSource is self-hosted it is always liberty, and means the basemap's own style, not OpenFreeMap's Liberty"},"runtimeVersion":{"type":"string"},"publishedAt":{"type":"string","format":"date-time"},"activatedAt":{"type":["string","null"],"format":"date-time"},"deactivatedAt":{"type":["string","null"],"format":"date-time"},"retiredAt":{"type":["string","null"],"format":"date-time"}}},"Basemap":{"type":"object","properties":{"source":{"enum":["openfreemap","self-hosted"]},"release":{"type":["string","null"]},"style":{"type":"string","format":"uri"},"coverage":{"oneOf":[{"$ref":"#/components/schemas/Bounds"},{"type":"null"}]},"sourceMinZoom":{"type":"integer"},"sourceMaxZoom":{"type":"integer"}}},"MapDocument":{"description":"snaphop-map/1: a published map on the delivery host at maps/{id}/map.json (not served by this API)","type":"object","properties":{"format":{"const":"snaphop-map/1"},"map":{"type":"string"},"release":{"type":"integer"},"publishedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"style":{"type":"string","format":"uri","description":"The address of the style the release was published in: on the public basemap, https://tiles.openfreemap.org/styles/{style} for the map's chosen Style; on a self-hosted basemap, its release's style.json. Unchanged in format by ADR 0020; no field names the Style identifier."},"basemap":{"type":"object","description":"The basemap the release pins, as Basemap without its style, which is the document's own","properties":{"source":{"enum":["openfreemap","self-hosted"]},"release":{"type":["string","null"]},"coverage":{"oneOf":[{"$ref":"#/components/schemas/Bounds"},{"type":"null"}]},"sourceMinZoom":{"type":"integer"},"sourceMaxZoom":{"type":"integer"}}},"view":{"$ref":"#/components/schemas/MapDefinition/properties/view"},"controls":{"$ref":"#/components/schemas/MapDefinition/properties/controls"},"markers":{"$ref":"#/components/schemas/MapDefinition/properties/markers"},"links":{"type":"object","properties":{"page":{"type":"string"},"document":{"type":"string"},"runtime":{"type":"string"}}}}},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","examples":["MAP_NOT_FOUND","MAP_INVALID","DRAFT_CHANGED","PUBLICATION_IN_PROGRESS","BASEMAP_UNAVAILABLE","RELEASE_UNAVAILABLE","ACTIVE_RELEASE_CHANGED","PUBLICATION_NOT_CONFIGURED","PUBLICATION_FAILED","FORBIDDEN","REQUEST_REFUSED","INVALID_REQUEST","TOO_MANY_REQUESTS","DATABASE_UNAVAILABLE","MAP_LIMIT_REACHED","AGENT_NAME_INVALID","AGENT_REGISTRATION_CLOSED","AGENT_REGISTRATIONS_EXHAUSTED","AGENT_KEY_REQUIRED"]},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}}}}}},"requestId":{"type":"string","description":"Absent from a REQUEST_REFUSED answered by the filter chain"}}}}}}