Skip to content

API examples

The QGIS Publish Service does not implement login. Authenticate with Central first, then supply its session cookie to the same platform host. The examples assume:

ORIGIN=https://app.example.com
BASE=/qgis-publish-service
COOKIE=acugis_session=<session-value-issued-by-Central>
PROJECT=city-map

Do not copy a cookie into source control. Central may also support bearer credentials, but cookie authentication is shown because the QGIS service resolves Central sessions directly.

curl

List only projects the session can view:

curl -fsS \
  -H 'Accept: application/json' \
  -H "Cookie: $COOKIE" \
  "$ORIGIN$BASE/projects"

Publish a QGIS package, capture its ID, then add metadata:

UPLOAD_JSON=$(
  curl -fsS \
    -H 'Accept: application/json' \
    -H "Cookie: $COOKIE" \
    -F 'file=@project.qgz;type=application/octet-stream' \
    "$ORIGIN$BASE/projects/upload"
)

# Set PROJECT from the "id" property using your JSON tool.
curl -fsS -X PUT \
  -H 'Content-Type: application/json' \
  -H "Cookie: $COOKIE" \
  --data '{"title":"City map","description":"Published from the API","tags":["city","demo"]}' \
  "$ORIGIN$BASE/api/projects/$PROJECT/metadata"

Inspect layers and WMS capabilities:

curl -fsS -H "Cookie: $COOKIE" \
  "$ORIGIN$BASE/api/projects/$PROJECT/layers"

curl -fsS -H "Cookie: $COOKIE" \
  "$ORIGIN$BASE/qgis/$PROJECT?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities"

Update and restore:

curl -fsS -H "Cookie: $COOKIE" \
  -F 'file=@project-v2.zip;type=application/zip' \
  "$ORIGIN$BASE/projects/$PROJECT/update"

curl -fsS -H "Cookie: $COOKIE" \
  "$ORIGIN$BASE/projects/$PROJECT/versions"

curl -fsS -X POST -H "Cookie: $COOKIE" \
  "$ORIGIN$BASE/projects/$PROJECT/versions/1/restore"

Create and update a PostGIS feature (field names must be writable in the published QGIS form):

curl -fsS -X POST \
  -H 'Content-Type: application/json' -H "Cookie: $COOKIE" \
  --data '{"layer":"assets","attributes":{"name":"Pump 12","status":"active"},"geometry":{"type":"Point","coordinates":[-73.99,40.73]}}' \
  "$ORIGIN$BASE/api/projects/$PROJECT/features/create"

curl -fsS -X POST \
  -H 'Content-Type: application/json' -H "Cookie: $COOKIE" \
  --data '{"layer":"assets","feature_id":12,"attributes":{"status":"inactive"}}' \
  "$ORIGIN$BASE/api/projects/$PROJECT/features/update"

Seed cached tiles and poll the returned job_id:

curl -fsS -X POST \
  -H 'Content-Type: application/json' -H "Cookie: $COOKIE" \
  --data '{"min_zoom":0,"max_zoom":8,"bbox":[-8238310,4968191,-8230000,4977000]}' \
  "$ORIGIN$BASE/projects/$PROJECT/seed"

# JOB is the job_id from the previous response.
curl -fsS "$ORIGIN$BASE/cache/api/seed/status?id=$JOB"

JavaScript

Browser code automatically sends the Central cookie only when credentials: "include" is set and same-origin/cookie policy permits it. It must not set a Cookie header directly.

const origin = "https://app.example.com";
const base = "/qgis-publish-service";

async function api(path, options = {}) {
  const response = await fetch(origin + base + path, {
    credentials: "include",
    headers: { Accept: "application/json", ...(options.headers || {}) },
    ...options,
  });
  if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
  if (response.status === 204) return null;
  return response.json();
}

const projects = await api("/projects");
const project = await api(`/projects/${encodeURIComponent(projects[0].id)}`);
const layers = await api(`/api/projects/${encodeURIComponent(project.id)}/layers`);
console.log(project, layers);

Publish and set metadata:

const form = new FormData();
form.append("file", fileInput.files[0]);
const created = await api("/projects/upload", { method: "POST", body: form });

const metadata = await api(`/api/projects/${encodeURIComponent(created.id)}/metadata`, {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    title: "City map",
    description: "Published from JavaScript",
    tags: ["city"],
  }),
});
console.log(metadata);

Create a browser map. map_config is validated by the server and its detailed schema is dynamic to the Map Builder implementation:

const browserMap = await api("/api/browser-maps", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    title: "Operations overview",
    description: "",
    visibility: "restricted",
    map_config: { version: 1, layers: [] },
  }),
});

Python

Supply a session cookie value obtained from Central:

from pathlib import Path
import requests

ORIGIN = "https://app.example.com"
BASE = "/qgis-publish-service"
PROJECT = "city-map"

s = requests.Session()
s.cookies.set("acugis_session", "<session-value-issued-by-Central>")
s.headers["Accept"] = "application/json"

r = s.get(f"{ORIGIN}{BASE}/projects", timeout=60)
r.raise_for_status()
print(r.json())

with Path("project.qgz").open("rb") as handle:
    r = s.post(
        f"{ORIGIN}{BASE}/projects/upload",
        files={"file": ("project.qgz", handle, "application/octet-stream")},
        timeout=(30, 600),
    )
r.raise_for_status()
project_id = r.json()["id"]

r = s.put(
    f"{ORIGIN}{BASE}/api/projects/{project_id}/metadata",
    json={"title": "City map", "description": "Published from Python", "tags": []},
    timeout=60,
)
r.raise_for_status()
print(r.json())

Run an analysis:

r = s.get(
    f"{ORIGIN}{BASE}/api/projects/{PROJECT}/outliers-data",
    params={
        "layer": "assets",
        "field": "pressure",
        "label_field": "name",
        "threshold": 2,
        "scope": "layer",
    },
    timeout=60,
)
r.raise_for_status()
for row in r.json()["rows"]:
    if row["classification"] != "Normal":
        print(row)

Go

The cookie jar can be populated from a Central login performed elsewhere. This example uses a supplied cookie and publishes a file.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "net/http/cookiejar"
    "net/url"
    "os"
)

func main() {
    origin := "https://app.example.com"
    base := "/qgis-publish-service"
    u, _ := url.Parse(origin)
    jar, _ := cookiejar.New(nil)
    jar.SetCookies(u, []*http.Cookie{{Name: "acugis_session", Value: "<session-value-issued-by-Central>"}})
    client := &http.Client{Jar: jar}

    resp, err := client.Get(origin + base + "/projects")
    if err != nil { panic(err) }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK { panic(resp.Status) }
    io.Copy(os.Stdout, resp.Body)

    var body bytes.Buffer
    mw := multipart.NewWriter(&body)
    part, _ := mw.CreateFormFile("file", "project.qgz")
    f, _ := os.Open("project.qgz")
    io.Copy(part, f)
    f.Close()
    mw.Close()

    req, _ := http.NewRequest(http.MethodPost, origin+base+"/projects/upload", &body)
    req.Header.Set("Content-Type", mw.FormDataContentType())
    req.Header.Set("Accept", "application/json")
    resp, err = client.Do(req)
    if err != nil { panic(err) }
    defer resp.Body.Close()
    if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(resp.Status) }
    var created map[string]any
    json.NewDecoder(resp.Body).Decode(&created)
    fmt.Println(created["id"])
}

Update render mode:

payload := bytes.NewBufferString(`{"render_mode":"cache"}`)
req, _ := http.NewRequest(http.MethodPost, origin+base+"/projects/city-map/render-mode", payload)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)

PHP

Use cURL with a supplied Central cookie. The first request lists projects; the second publishes a package and stores metadata.

<?php
$origin = 'https://app.example.com';
$base = '/qgis-publish-service';
$cookie = 'acugis_session=<session-value-issued-by-Central>';

function request($method, $url, $cookie, $body = null, $headers = []) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => array_merge(['Accept: application/json', 'Cookie: ' . $cookie], $headers),
    ]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    $raw = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    if ($raw === false || $status < 200 || $status >= 300) {
        throw new RuntimeException("HTTP $status: " . ($raw ?: curl_error($ch)));
    }
    curl_close($ch);
    return $raw === '' ? null : json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
}

$projects = request('GET', $origin . $base . '/projects', $cookie);
print_r($projects);

$upload = request(
    'POST',
    $origin . $base . '/projects/upload',
    $cookie,
    ['file' => new CURLFile('project.qgz', 'application/octet-stream', 'project.qgz')]
);
$id = rawurlencode($upload['id']);

$metadata = request(
    'PUT',
    "$origin$base/api/projects/$id/metadata",
    $cookie,
    json_encode(['title' => 'City map', 'description' => 'Published from PHP', 'tags' => []]),
    ['Content-Type: application/json']
);
print_r($metadata);

Download the current project without trying to decode it as JSON:

$ch = curl_init("$origin$base/projects/" . rawurlencode($id) . "/download");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Cookie: ' . $cookie],
    CURLOPT_FILE => fopen('runtime.zip', 'wb'),
    CURLOPT_FOLLOWLOCATION => true,
]);
curl_exec($ch);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) !== 200) {
    throw new RuntimeException('Download failed');
}
curl_close($ch);

Notes

  • Always send Accept: application/json to GET /projects; an HTML accept header selects the browser redirect.
  • WMS/WFS, downloads, attachments, tiles, and previews may not be JSON.
  • Quick-import, ingest action options, report definitions, and browser-map map_config contain payloads owned by adjacent packages/services. Their dynamic fields should be discovered from the corresponding UI/source rather than guessed.
  • {BASE}/cache/ and {BASE}/routing/ are direct external-service proxies. The cache proxy itself has no Central ACL, even though project-management wrappers do.