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, andadminon service keyqgis_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'sviewgrant. - 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 (normally403); an authorization-store failure returns500.
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;200JSON,400invalid ID,404project/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 completeProjectReportsFile; replaces saved report configuration and returns{project_id,reports}. The report schema is dynamic tointernal/reports; invalid config is400.GET {BASE}/api/projects/{id}/metadata— project metadata object;404if none.PUT {BASE}/api/projects/{id}/metadata— project metadata JSON;?create=1|truemakes it create-only (201, or409if present), otherwise upsert (200). Metadata service absent:503.DELETE {BASE}/api/projects/{id}/metadata—204;404if absent.GET {BASE}/api/projects/{id}/relation-reference— required querylayer(form layer),field(FK widget); optionalsearch,value(resolve one FK),limit(default 50, max 200),filter_ctxandparent(JSON filter contexts). Returns an array of{value,label}options. RelationReference configuration is extracted dynamically from QGIS. Missing/invalid widget context is400, unknown layer/project404, PostGIS/WFS failure502.GET {BASE}/api/projects/{id}/lookup— required querylayer,fields(comma-separated safe column names); optional positivelimit(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 is400; both backends failing is502.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 containauto_layer_legends,resync(defaults true),popupCharts, andlayer_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/projectedit. Returns{success:true,project,removed}. Errors:404project,503qcarta URL unavailable,502purge 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,403non-writable field,409read-only,501subset SQL,502database.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?};404when no matching feature.POST {BASE}/api/projects/{id}/attachments— multipartfile; optionallayer,feature_id, and accepted-but-currently-unusedfield. 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...}andHEAD— stream a project-relative attachment. Auth: projectview. Rejects traversal;404for 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— serviceview; returns pg_service definitions. Note: definitions can include connection secrets; this is deliberately gated.POST {BASE}/pgservice— serviceadmin; JSON pgservice object (includingname,host,port,dbname,user,password,sslmode); upserts and echoes it.POST {BASE}/pgservice/test— serviceedit; JSON{host,port?,dbname,user?,password?,sslmode?}(port defaults 5432; supported sslmode values normalize todisable|require|verify-ca|verify-full). Returns{success:true}or, for a connection failure,200 {success:false,error}; malformed/missing required input is400.DELETE {BASE}/pgservice/{name}— serviceadmin;{ok:true}.GET {BASE}/basemaps— no route-level service gate; returns basemap array, or503without registry.POST {BASE}/basemaps— serviceadmin; complete basemap JSON; upserts and returns saved object.DELETE {BASE}/basemaps/{id}— serviceadmin;{ok:true}.
Browser maps¶
GET {BASE}/api/browser-maps— serviceview; returns only maps with browser-mapview; list omits largemap_config.POST {BASE}/api/browser-maps— authenticated user, serviceedit; body{title,description,map_config,visibility}as defined bybrowsermap.CreateInput; validates all referenced GeoVault datasets. Returns created map201.GET {BASE}/api/browser-maps/{id}— browser-mapview(Public/owner/admin included); returns full map including config.PUT {BASE}/api/browser-maps/{id}— serviceeditplus browser-mapedit(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— serviceedit, authenticated user, source mapview; returns copy201.POST {BASE}/api/browser-maps/{id}/thumbnail— service/browser-mapedit; multipartfile, max 5 MiB, PNG/JPEG/WebP; returns map/thumbnail result.GET {BASE}/api/browser-maps/datasets/{datasetId}/resolve— serviceview, 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; serviceview, 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;201session 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— multipartfile, or JSON{source_type,source_name?,metadata?}; JSON types:postgis,geovault,url,s3,esri. Returns source201.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 errors422.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 action422. Option payloads depend on source/action and are intentionally treated as dynamic.POST {BASE}/api/ingest/postgis/connect— serviceedit; 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— serviceview; JSON{connection_id,pg_service_name}. Returns connection fields plusschemasand spatialtables(schema,name,geometry_column,geometry_type,srid,feature_count,extent?,crs?). Some discovery failures intentionally return200withsuccess:false.
Import and integration APIs¶
POST {BASE}/api/geosync-publish— authenticated user/serviceedit; multipartfile(zip/qgs/qgz), requiredgeosync_project_id,host,dbname,schema; optionalpublisher_project_id,port(default 5432),username,password,sslmode. Returns{success:true,publisher_project_id,viewer_url,pg_service_name}; errors are{success:false,error}with400/401/422/500.POST {BASE}/api/quick-import— serviceedit; 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 upstream502.GET|POST {BASE}/api/quick-import/postgis-service/{path...}— external GeoVault/api/postgis-service/...proxy; GET serviceview, POST serviceedit; 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 serviceview; query/body/status preserved.POST {BASE}/api/operational/import— serviceedit. JSON or multipart withsource_typedelegates to quick import. Legacy multipart withoutsource_type:filemust be.gpkg; optionaldisplay_name,slug,auto_publish,rewrite_project_id; authenticated user/runtime registry required. Returns dynamicruntimedataset.ImportResult.GET {BASE}/api/operational/datasets— serviceview;{datasets:[...]}.GET {BASE}/api/operational/datasets/{id}— serviceview;{dataset,tables,relations}.POST {BASE}/api/operational/datasets/{id}/rewrite-project— serviceeditat route level, but intentionally disabled: always410JSON 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/wmsand legacyGET /mproxy/service— WMS/QGIS proxy and cache. Standard WMS query parameters, especiallyMAP,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 (notablyMAPand layer/style context) remain in query.400malformed/out-of-range tile,502/500generation/cache failure./api/wmts— placeholder; every method returns501 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}. Validation400.GET /api/seed/status?id={job_id}— returnsSeedJob(id,total,completed,statusand any current error fields);400missing ID,404unknown job.POST /api/prewarm— JSON{map}; returns{ok,map,project_key,get_capabilities_ms,get_project_settings_ms};400invalid,502upstream.GET /api/healthandGET /health— plainOK. 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— requiresAuthorization: Bearer <QCARTA_BEARER_TOKEN>(qcarta token, not Central session). Plain-text body is scope, defaultall; response{success:true,removed,scope}.401token,500missing server token/purge failure.
Browser/static and redirect registrations¶
These are registered routes but are not REST APIs:
GET {BASE}/publish-service-bootstrap.jsandGET {BASE}/ui/publish-service-bootstrap.js— public JavaScript exposing the base path.GET {BASE}/maps/view/{id}—302to{BASE}/ui/maps/view/?id=....GET {BASE}/seed.html—302to{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/andbasemaps/assets require serviceadmin; other administrative UI assets require serviceview.- Base root redirects: service root to
/projects, platform root to/ui/workspace.htmldepending onBASE_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.