Skip to content

QGIS Publish Service API

This reference is derived from cmd/server/main.go, internal/api, and qcarta-tiles. Let BASE be the configured BASE_PATH (normally /qgis-publish-service). IDs in braces are path parameters.

Authentication and authorization

Central authentication accepts the Central session cookie (normally acugis_session) and, where the shared auth layer supports it, bearer credentials. This service does not register a login endpoint. Obtain a session from Central and send the cookie to this service.

When Central auth is not configured, ACL helpers generally allow access and management is open. When it is configured:

  • Service permissions are view, edit, and admin on service key qgis_publish. The route middleware and some handlers both enforce this; administrators bypass checks.
  • Project permissions are independently checked on resource type qgis_project, key {id}. Anonymous access can succeed through the Public group's view grant.
  • Browser maps use their own resource ACL. Owners and administrators may edit; public maps can be viewed anonymously.
  • Ingest sessions are private to their creating user; platform administrators may access another user's session.
  • A failed session check normally returns 401; a failed ACL returns the shared permission-denied response (normally 403); an authorization-store failure returns 500.

Unless stated otherwise, successful JSON responses are 200. Invalid JSON/parameters are 400, missing resources 404, wrong methods 405, unavailable configured stores 503, and unexpected storage/upstream failures 500/502.

Discovery, identity, and expression API

GET {BASE}/api/discovery

Purpose: list visible published projects in portal-discovery form. Auth: no service gate; per-project view filtering (including Public for anonymous callers). Parameters: none. Response: {service:"qgis-publish-service",resource_type:"map",items:[{id,title,description,url,thumbnail?,category:"Maps"}]}. Errors: 500 scan/ACL failure.

GET {BASE}/api/auth/context

Purpose: viewer expression identity. Auth: optional session; no service/project gate. Response: {authenticated,auth_enabled,user} where user is null or {id,username,email?,roles,groups,attributes}; Cache-Control: no-store. Roles are derived from admin/group/service permission data. An unauthenticated request is still 200.

POST {BASE}/api/qgis/evaluate-expression

Purpose: evaluate the server-supported QGIS-expression subset. Auth: if projectId is present, project view; otherwise no resource gate. Body: {projectId?,layer?,expression,feature?,context?:{user?:{username,roles,groups,attributes}}}; projectId may instead be a query parameter. Response is the dynamic expression result (ok, value and/or error as produced by internal/expr), always JSON/no-store. Empty expression returns a successful null value. Errors: 400 malformed JSON; ACL errors.

POST {BASE}/api/resource-sync

Purpose: reconcile on-disk projects into Central resources. Auth: service admin. Body: none. Response: reconciliation counts (registered, already_exists, errors; exact key casing follows the handler). Errors: auth/store failures.

Projects and versions

GET {BASE}/projects

Purpose: list visible projects. Auth: per-project view filtering; no service gate for JSON. Send Accept: application/json; Accept: text/html (without JSON) instead performs a 302 redirect to {BASE}/projects/ and requires service view. Response: array of project objects including id, name, qgs_path, render/config fields and visibility fields. Errors: 500.

GET {BASE}/projects/{id}

Purpose: project manifest/configuration. Auth: project view. Response: project object; the handler may first repopulate layer config from QGIS. Errors: 400 unsafe ID, 404 missing project.

POST {BASE}/projects/upload

Purpose: create a published project. Auth: authenticated user, service edit; creates project resource/default grants. Body: multipart field file, extension .qgs, .qgz, or .zip, up to configured maximum (default 512 MiB). Response: generated project identity/config including id; payload is source-dependent and should be treated as dynamic. Errors: 400 multipart/type, 401, 422 package/publish failure, 500 storage/manifest/resource registration.

POST {BASE}/projects/{id}/update

Purpose: snapshot and replace a project bundle, then purge/prewarm runtime cache. Auth: service edit plus project edit. Body: multipart file (.qgs, .qgz, .zip). Response: {id,qgs_relative,project_name,snapshot_version,snapshot_at,map_param_hint}. Errors: 400, 404, 422, 500.

DELETE {BASE}/projects/{id}

Purpose: delete a project directory. Auth: service edit plus handler-level project admin (stronger than most project writes). Response: 200 plain text Project deleted. Errors: 400 missing ID, 403 unsafe path/ACL, 404 missing project, 500 filesystem failure. It does not delete a Central resource row in this handler.

GET {BASE}/projects/{id}/download

Purpose: download current runtime bundle. Auth: project view. Response: ZIP attachment {id}-runtime.zip. Errors: 400, 404, 500 before streaming.

GET {BASE}/projects/{id}/versions

Purpose: list snapshots. Auth: project view. Response: {project_id,versions:[{version,created_at,size_bytes}]}. Errors: 400, 500.

GET {BASE}/projects/{id}/versions/{version}/download

Purpose: download a snapshot. Auth: project view. {version} is a positive integer. Response: ZIP attachment {id}-v{version}.zip. Errors: 400, 404.

POST {BASE}/projects/{id}/versions/{version}/restore

Purpose: snapshot current content, restore a version, and reload runtime cache. Auth: service edit plus project edit. Body: none. Response: {success:true,project_id,restored_version,pre_restore_snapshot}. Errors: 400, 404, 500.

POST {BASE}/projects/generate

Purpose: generate a raster-backed QGIS project. Auth: authenticated user and service edit; registers a project resource. Body: either JSON {raster_path,name,crs?} or multipart file (.tif/.tiff), name, optional crs; default max 512 MiB. Response: {id,project_path,map_param_hint}. Errors: 400, 401, 422, 500.

Project configuration and descriptive APIs

All read endpoints below require project view; writes require service edit and project edit.

  • GET {BASE}/api/projects/{id}/forms — extracted form definitions keyed by layer; 200 JSON, 400 invalid ID, 404 project/forms unavailable.
  • GET {BASE}/api/projects/{id}/layers{project_id,layers:[...]} with QGIS-derived layer metadata; fields vary with layer/provider.
  • GET {BASE}/api/projects/{id}/reports{project_id,reports:[...]}.
  • PUT {BASE}/api/projects/{id}/reports — body is the complete ProjectReportsFile; replaces saved report configuration and returns {project_id,reports}. The report schema is dynamic to internal/reports; invalid config is 400.
  • GET {BASE}/api/projects/{id}/metadata — project metadata object; 404 if none.
  • PUT {BASE}/api/projects/{id}/metadata — project metadata JSON; ?create=1|true makes it create-only (201, or 409 if present), otherwise upsert (200). Metadata service absent: 503.
  • DELETE {BASE}/api/projects/{id}/metadata204; 404 if absent.
  • GET {BASE}/api/projects/{id}/relation-reference — required query layer (form layer), field (FK widget); optional search, value (resolve one FK), limit (default 50, max 200), filter_ctx and parent (JSON filter contexts). Returns an array of {value,label} options. RelationReference configuration is extracted dynamically from QGIS. Missing/invalid widget context is 400, unknown layer/project 404, PostGIS/WFS failure 502.
  • GET {BASE}/api/projects/{id}/lookup — required query layer, fields (comma-separated safe column names); optional positive limit (default 5,000, max 10,000). Returns an array of objects containing the requested fields, ordered by the final field. It tries PostGIS then WFS. Invalid input is 400; both backends failing is 502.
  • POST {BASE}/projects/{id}/render-mode — body {render_mode:"direct"|"cache"|"xyz"}; response {render_mode} (normalization occurs server-side).
  • POST {BASE}/projects/{id}/legend-settings — body may contain auto_layer_legends, resync (defaults true), popupCharts, and layer_title_fields; returns updated project/config data.
  • POST {BASE}/projects/{id}/basemap-settings — body {basemap_ids:[...],initial_basemap}; returns updated settings.
  • POST {BASE}/api/projects/{id}/runtime/reload — purges qcarta cache for the project; route auth is service/project edit. Returns {success:true,project,removed}. Errors: 404 project, 503 qcarta URL unavailable, 502 purge failure. (Update/restore separately invoke purge plus prewarm.)

Analysis APIs

All require project view and operate on PostGIS-backed layers.

GET {BASE}/api/projects/{id}/pivot-data

Query: required layer, row_field, value_field; optional col_field, aggregation (COUNT|SUM|AVG|MIN|MAX, default SUM), scope=layer|selection, and selection as a JSON array when selection-scoped. Response: {row_field,col_field?,value_field,aggregation,row_labels,col_labels,cells}. Errors: 400 identifiers/options, 502 PostGIS/source/subset failure.

GET {BASE}/api/projects/{id}/outliers-data

Query: required layer, field; optional label_field, positive threshold (default 2), scope, selection JSON array. Response: {field,label_field?,threshold,mean,std_dev,rows:[{feature_id,feature_label,value,z_score,classification}]}; at most 5,000 source rows. Errors: 400, 502.

GET {BASE}/api/projects/{id}/hotspots-data

Query: same as outliers; threshold defaults to 1.5. Requires geometry. Response: {field,label_field?,threshold,rows:[{feature_id,feature_label,value,neighbor_avg,score,classification,neighbor_count?}]} based on intersecting neighbors, up to 5,000 rows. Errors: 400, 502.

Feature editing and attachments

All writes require service edit and project edit. They only support writable PostGIS layers represented in extracted forms; subset-SQL writes return 501.

  • POST {BASE}/api/projects/{id}/features/create — body {layer,attributes,geometry?} where geometry is GeoJSON geometry. Returns {success,feature?,feature_id?,error?}. Validates form fields, required values, geometry, column types, read-only state. Typical errors: 400, 403 non-writable field, 409 read-only, 501 subset SQL, 502 database.
  • POST {BASE}/api/projects/{id}/features/update — body {layer,feature_id,attributes}. Returns {success,feature?,error?}. It merges/validates required form values and updates by the layer primary key; same error classes as create.
  • POST {BASE}/api/projects/{id}/features/delete — body {layer,feature_id}. Returns {success,error?}; 404 when no matching feature.
  • POST {BASE}/api/projects/{id}/attachments — multipart file; optional layer, feature_id, and accepted-but-currently-unused field. Maximum 25 MiB; allowed extensions: jpg/jpeg/png/gif/webp/pdf/txt/doc/docx/zip. Response {success:true,path:"DCIM/..."}.
  • GET {BASE}/project-data/{id}/{path...} and HEAD — stream a project-relative attachment. Auth: project view. Rejects traversal; 404 for absent/directory targets.

Relations

GET {BASE}/api/feature-relations

GET {BASE}/api/feature-relations.php

Equivalent compatibility routes. Auth: project view, determined by required query project, layer (parent layer), field (parent join field), and value; optional debug=1. Response is {relations:[{name,layer,records:[{properties,label?}],_debug?}]}. It tries PostGIS and WFS paths and preserves typed/UUID join semantics. Errors: missing/invalid context 400, project 404, ACL, or project parse 500; a valid request with no matching layer/relation returns an empty relations array. The .php suffix is only an alias; no PHP is executed.

PostgreSQL services and basemaps

  • GET {BASE}/pgservice — service view; returns pg_service definitions. Note: definitions can include connection secrets; this is deliberately gated.
  • POST {BASE}/pgservice — service admin; JSON pgservice object (including name, host, port, dbname, user, password, sslmode); upserts and echoes it.
  • POST {BASE}/pgservice/test — service edit; JSON {host,port?,dbname,user?,password?,sslmode?} (port defaults 5432; supported sslmode values normalize to disable|require|verify-ca|verify-full). Returns {success:true} or, for a connection failure, 200 {success:false,error}; malformed/missing required input is 400.
  • DELETE {BASE}/pgservice/{name} — service admin; {ok:true}.
  • GET {BASE}/basemaps — no route-level service gate; returns basemap array, or 503 without registry.
  • POST {BASE}/basemaps — service admin; complete basemap JSON; upserts and returns saved object.
  • DELETE {BASE}/basemaps/{id} — service admin; {ok:true}.

Browser maps

  • GET {BASE}/api/browser-maps — service view; returns only maps with browser-map view; list omits large map_config.
  • POST {BASE}/api/browser-maps — authenticated user, service edit; body {title,description,map_config,visibility} as defined by browsermap.CreateInput; validates all referenced GeoVault datasets. Returns created map 201.
  • GET {BASE}/api/browser-maps/{id} — browser-map view (Public/owner/admin included); returns full map including config.
  • PUT {BASE}/api/browser-maps/{id} — service edit plus browser-map edit (owner fallback); partial {title?,description?,map_config?,visibility?}; returns updated map.
  • DELETE {BASE}/api/browser-maps/{id} — same edit ACL; returns {ok:true}.
  • POST {BASE}/api/browser-maps/{id}/duplicate — service edit, authenticated user, source map view; returns copy 201.
  • POST {BASE}/api/browser-maps/{id}/thumbnail — service/browser-map edit; multipart file, max 5 MiB, PNG/JPEG/WebP; returns map/thumbnail result.
  • GET {BASE}/api/browser-maps/datasets/{datasetId}/resolve — service view, plus GeoVault's dataset ACL through forwarded caller credentials. Returns {datasetId,title,type,geometryType?,fileType?,datasourceType?,latestVersion?,bbox?,crs?,renderMode,clientStyleable,endpoints,error?}.
  • GET {BASE}/api/map-builder/geovault/{path...} — external JSON proxy to an allowlisted set of GeoVault browse endpoints; service view, caller cookie/bearer forwarded, query preserved. Upstream status/body pass through. Not a native REST resource.

Ingest sessions

Every session route requires an authenticated session owner (or admin). Route middleware additionally requires service view for reads and edit for writes.

  • GET {BASE}/api/ingest/sessions — latest 20 sessions for current user; {sessions:[...]}.
  • POST {BASE}/api/ingest/sessions — creates empty session; 201 session object.
  • GET {BASE}/api/ingest/sessions/{id} — session object.
  • DELETE {BASE}/api/ingest/sessions/{id} — removes files/session; {ok:true}.
  • POST {BASE}/api/ingest/sessions/{id}/sources — multipart file, or JSON {source_type,source_name?,metadata?}; JSON types: postgis, geovault, url, s3, esri. Returns source 201.
  • GET {BASE}/api/ingest/sessions/{id}/sources/{sourceId} — source object.
  • PATCH {BASE}/api/ingest/sessions/{id}/sources/{sourceId} — partial {source_name?,status?,metadata?}; returns updated source.
  • DELETE {BASE}/api/ingest/sessions/{id}/sources/{sourceId}{ok:true}.
  • GET {BASE}/api/ingest/sessions/{id}/sources/{sourceId}/file — temporary file attachment.
  • GET {BASE}/api/ingest/sessions/{id}/sources/{sourceId}/preview?layer=... — GeoJSON/extent preview; GeoVault status can pass through; conversion errors 422.
  • POST {BASE}/api/ingest/sessions/{id}/sources/{sourceId}/actions/{action} — optional dynamic JSON options. Implemented actions: store_library, import_postgis, publish_project, publish_raster, import_url, import_s3. Returns {ok:true,action,result,source}; unknown/failed action 422. Option payloads depend on source/action and are intentionally treated as dynamic.
  • POST {BASE}/api/ingest/postgis/connect — service edit; JSON {connection_name,host,port,database,username,password,sslmode}. Tests and writes pg_service (and may mirror GeoVault). Returns {success,error?,pg_service_name?,connection_id?,connection_name?,host?,port?,database?,username?,sslmode?}.
  • POST {BASE}/api/ingest/postgis/discover — service view; JSON {connection_id,pg_service_name}. Returns connection fields plus schemas and spatial tables (schema,name,geometry_column,geometry_type,srid,feature_count,extent?,crs?). Some discovery failures intentionally return 200 with success:false.

Import and integration APIs

  • POST {BASE}/api/geosync-publish — authenticated user/service edit; multipart file (zip/qgs/qgz), required geosync_project_id, host, dbname, schema; optional publisher_project_id, port (default 5432), username, password, sslmode. Returns {success:true,publisher_project_id,viewer_url,pg_service_name}; errors are {success:false,error} with 400/401/422/500.
  • POST {BASE}/api/quick-import — service edit; transparently forwards JSON or multipart to GeoVault /api/quick-import, persists returned pg_service, then returns {success,source_type,database_id,tables,schema,pg_service_name,pg_service}. Input is GeoVault-defined/dynamic. Upstream errors/status pass through; malformed upstream 502.
  • GET|POST {BASE}/api/quick-import/postgis-service/{path...} — external GeoVault /api/postgis-service/... proxy; GET service view, POST service edit; query/body/status preserved.
  • GET|POST {BASE}/api/quick-import/geovault/{path...} — external GeoVault /api/... proxy; route middleware assigns view/edit by method, while the handler also enforces service view; query/body/status preserved.
  • POST {BASE}/api/operational/import — service edit. JSON or multipart with source_type delegates to quick import. Legacy multipart without source_type: file must be .gpkg; optional display_name, slug, auto_publish, rewrite_project_id; authenticated user/runtime registry required. Returns dynamic runtimedataset.ImportResult.
  • GET {BASE}/api/operational/datasets — service view; {datasets:[...]}.
  • GET {BASE}/api/operational/datasets/{id} — service view; {dataset,tables,relations}.
  • POST {BASE}/api/operational/datasets/{id}/rewrite-project — service edit at route level, but intentionally disabled: always 410 JSON explaining that datasource rewrite must occur in QGIS Desktop.

QGIS, cache, routing, and seeding

GET|HEAD {BASE}/qgis/{projectId}

External-protocol proxy to QGIS Server, not a JSON REST resource. Auth: project view. Forwards WMS/WFS query parameters, injects the project's server-side MAP, preserves query semantics, and filters unsafe client MAP values. Response media/status are from QGIS Server. Typical errors: 400 invalid project/request, 404, ACL, 502.

POST {BASE}/projects/{id}/seed

Native wrapper around qcarta POST /api/seed. Auth: service/project edit. Body {min_zoom,max_zoom,bbox:[minx,miny,maxx,maxy]} in EPSG:3857; server injects project qgs_path. Response/status pass through (normally {job_id}); bad input 400, missing project 404, qcarta failure 502.

{BASE}/cache/{path...}

Unauthenticated reverse-proxy prefix; strips {BASE}/cache and forwards every method/query/body to qcarta-tiles. This is not one JSON endpoint and does not apply QGIS service/project ACLs. Public exposure must be controlled by deployment/network policy.

{BASE}/routing/{path...}

Unauthenticated reverse-proxy prefix to OSRM, stripping {BASE}/routing; supports OSRM's own methods, paths, query, payloads, statuses, and errors. Example: /route/v1/driving/.... This is an external protocol, not native REST.

Bundled qcarta-tiles API

These routes exist on the qcarta process and are also reachable under {BASE}/cache/... because the prefix is stripped. Except purge, qcarta itself implements no authentication.

  • GET /api/wms and legacy GET /mproxy/service — WMS/QGIS proxy and cache. Standard WMS query parameters, especially MAP, SERVICE, REQUEST, VERSION, and operation-specific fields. Response/status/content type come from QGIS; cache headers may be added. 405, 502, or upstream status on failure.
  • GET /api/tiles/{z}/{x}/{y}.png — XYZ PNG; WMS parameters (notably MAP and layer/style context) remain in query. 400 malformed/out-of-range tile, 502/500 generation/cache failure.
  • /api/wmts — placeholder; every method returns 501 WMTS not yet implemented.
  • POST /api/seed — JSON {map,min_zoom,max_zoom,bbox} (EPSG:3857), max zoom 30 and bounded tile count; returns {job_id}. Validation 400.
  • GET /api/seed/status?id={job_id} — returns SeedJob (id,total,completed,status and any current error fields); 400 missing ID, 404 unknown job.
  • POST /api/prewarm — JSON {map}; returns {ok,map,project_key,get_capabilities_ms,get_project_settings_ms}; 400 invalid, 502 upstream.
  • GET /api/health and GET /health — plain OK. Handlers do not method-restrict these aliases.
  • GET /api/metrics — JSON {cache_hits,cache_misses,cache_bypass}; handler does not method-restrict.
  • POST /admin/cache/purge — requires Authorization: Bearer <QCARTA_BEARER_TOKEN> (qcarta token, not Central session). Plain-text body is scope, default all; response {success:true,removed,scope}. 401 token, 500 missing server token/purge failure.

Browser/static and redirect registrations

These are registered routes but are not REST APIs:

  • GET {BASE}/publish-service-bootstrap.js and GET {BASE}/ui/publish-service-bootstrap.js — public JavaScript exposing the base path.
  • GET {BASE}/maps/view/{id}302 to {BASE}/ui/maps/view/?id=....
  • GET {BASE}/seed.html302 to {BASE}/ui/seed.html, preserving query.
  • {BASE}/shared/ui/ — shared static UI.
  • {BASE}/ui/ and the base catch-all — static files. Runtime viewer assets are public; pgservice/ and basemaps/ assets require service admin; other administrative UI assets require service view.
  • Base root redirects: service root to /projects, platform root to /ui/workspace.html depending on BASE_PATH.

Coverage

This document covers all 91 lexical mux.Handle/mux.HandleFunc registrations in cmd/server/main.go (including registrations in mutually exclusive BASE_PATH branches) and all 11 route registrations in bundled qcarta-tiles: 102 source registrations total. Method variants registered separately, such as GET and HEAD, are counted separately; wildcard proxy prefixes are counted once each. A running process installs only the branch matching its base-path configuration.