Skip to content
DezfilDocsv0.1.0

How the API works

Call Dezfil's HTTP API from a script: the address, signing in, editing with revisions, who may do what, paging, errors and PostGIS connection strings.

Send every call to your instance’s address, under /api, with your API token:

Terminal
curl -s http://localhost:3000/api/account -H "Authorization: Bearer <YOUR_TOKEN>"
  • http://localhost:3000 is the address you open Dezfil at. Behind your own domain, use that address, such as https://maps.example.com.
  • /api/account is the operation: it reads the account the token belongs to.
  • Authorization: Bearer carries the token. Make a token shows how to get one.
Output
{"account":{"id":"<ACCOUNT_ID>","name":"<YOUR_NAME>","email":"<YOUR_EMAIL>","sqlName":"<YOUR_SQL_NAME>",...}}

Paths carry no version number. Request bodies are JSON, except a file upload, which is a multipart/form-data form.

Answers are JSON, except the files you download, such as exports, tiles and map pictures, and the assistant’s reply, which streams as lines of JSON. A 204 answer, such as a deleted map’s, has no body.

The operation reference lists every operation with its parameters, body and answers. It is built from openapi.json, an OpenAPI 3.1 description you can give to a client generator or an agent.

Your instance serves the same description at /api/openapi.json, and shows it as a reference at /developer. Both need you signed in.

HeaderWhen to send it
Authorization: Bearer <YOUR_TOKEN>On every call that needs sign-in.
Content-Type: application/jsonWith every JSON body.
X-Edit-RevisionOn a change to a map or workflow: the editRevision you last read. See Edit with revisions.
X-Change-GroupOptional, on map and layer edits that should undo as one step. Any value of 1 to 64 letters, digits, _ or -; send the same value with each edit.
X-Dezfil-AccountOptional: the account ID you expect. A credential of another account answers 401 authentication_required.
WayForHow
API tokenScripts, programs, agents and MCP clientsSend it in the Authorization header on every call.
Session cookieThe app’s own pages in your browserThe cookie the app sets when you sign in. It lasts as long as Stay signed in for says, counted from your last visit.

Both act as your account, with its rights. A change sent with the session cookie must carry an Origin header with the instance’s own address, as the app’s pages do, or it answers 403 forbidden.

A token works from any script or program that reaches the instance. A web page on another site can’t call the API, since the instance allows no cross-origin requests.

A few calls need no sign-in: GET /api/health; GET /api/auth/entry, which reads what the sign-in screen shows; signing in, creating the first account and redeeming a sign-in link; and a published map’s calls under /api/published/<PUBLISH_TOKEN>.

Security and rules lists Stay signed in for and the token rules below, with their defaults.

Make your first token in the app: in Settings › API & MCP, enter a Token name under API / MCP tokens and select Create token. The API quickstart walks you through it, and shows how to make one from a script. With one token, you can make more over the API:

Terminal
curl -s http://localhost:3000/api/account/tokens \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"name": "Nightly import"}'
Output
{"id":"<TOKEN_ID>","name":"Nightly import","createdAt":"2026-10-20T09:00:00.794Z","lastUsedAt":null,"revokedAt":null,"expiresAt":null,"secret":"<NEW_TOKEN>"}
  • secret is the new token. It is in this answer only, so store it somewhere safe.
  • expiresAt is when the token stops working, or null if it never does. An admin sets how long new tokens last with New tokens expire after, in Settings › Security.

GET /api/account/tokens lists your tokens without their secrets. DELETE /api/account/tokens/<TOKEN_ID> revokes one, and its next call answers 401 authentication_required.

An expired token answers 401 token_expired. If an admin lets only admins create tokens, with Who may create tokens, creating one answers 403 rule_refused.

Every map and workflow carries an editRevision. A change sends the revision it last read in X-Edit-Revision, and each accepted change answers with the next one. Create a map, then change it twice:

curl

Terminal
curl -s http://localhost:3000/api/maps \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"name": "Field notes"}'
Output
{"id":"<MAP_ID>","name":"Field notes",...,"editRevision":"<MAP_REVISION>","lastChangedAt":"2026-10-20T09:00:01.507Z","lastChangedBy":{"name":"<YOUR_NAME>","sqlName":"<YOUR_SQL_NAME>"},...}

Rename the map, sending the revision of that answer:

Terminal
curl -s -X PATCH http://localhost:3000/api/maps/<MAP_ID> \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-H "X-Edit-Revision: <MAP_REVISION>" \
-d '{"name": "Survey map"}'
Output
{"id":"<MAP_ID>","name":"Survey map",...,"editRevision":"<RENAME_REVISION>",...}

Change its basemap, sending the revision the rename gave:

Terminal
curl -s -X PATCH http://localhost:3000/api/maps/<MAP_ID> \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-H "X-Edit-Revision: <RENAME_REVISION>" \
-d '{"basemap": "light"}'
Output
{"id":"<MAP_ID>","name":"Survey map",...,"basemap":"light",...,"editRevision":"<BASEMAP_REVISION>",...}

You should see a map named Survey map on your Maps page. Delete it there when you are done.

For a map that already exists, read it first with GET /api/maps/<MAP_ID>. Its answer carries the editRevision the same way.

  • Send the revision on every change to a map or workflow, its layers and steps included. The reference marks each operation that takes X-Edit-Revision.
  • Make one change at a time: wait for its answer, then send the next change with the revision it gave.
  • A change without a revision, or with a malformed one, answers 400 invalid_request and changes nothing.
  • 409 stale_revision means someone changed the item after you read it: “This item has changed. Reload it before trying again.” Your change was not applied.
  • After stale_revision, read the item again and redo your change on what you find. Never resend the old change with the new revision.
  • Moving only a map’s view, viewport alone, needs no revision.

Map and workflow answers carry lastChangedAt and lastChangedBy, the account behind the last accepted change. lastChangedBy takes one of three forms:

  • { name, sqlName }: the account that made the change.
  • { deleted: true }: a deleted account. A change’s author can be a deleted account, and then no name is kept.
  • null: the author is not known.

An undo step’s author and a workflow run’s runner take the same three forms.

A token reaches what its account reaches: your own work, and everything filed in the projects shared with you. A project’s role names how you stand to it.

You areYou readYou change
The owner of a map, workflow or datasetItEverything: you delete it, transfer a map or workflow, and publish a map
An editor of a projectEverything filed in the projectIts maps and workflows; you run its workflows and file your own work in it
A viewer of a projectEverything filed in the projectNothing; you may duplicate a map or workflow into your own space
The owner of a projectEverything filed in the projectYou rename, share, transfer and dissolve the project, and change its maps and workflows
An adminOnly what your own account reachesAccounts, under /api/users, and the instance’s settings, under /api/instance
  • A dataset changes, refreshes and is deleted only by its owner, whatever the role. Connections are never shared.
  • Something you don’t reach answers 404 not_found, exactly as if it did not exist. A change you may not make to something you reach answers 403 forbidden, and nothing changes.
  • In SQL and in dataset reads, another person’s dataset is <sql name>.<dataset>, such as ana_diaz.stations. Every dataset list gives each one’s qualifiedName, the name to send.

The API calls a workflow a recipe: workflows live under /api/recipes. Share and publish shows how to share a project in the app.

Most lists answer everything you reach, in one answer. Add ?project=<PROJECT_ID> to keep one project’s items, or ?project=none to keep your own items that are in no project.

OperationHow it pages
GET /api/catalogue/tableslimit: datasets per page, 1 to 32, default 32. after: the nextAfter of the page before. columns: columns per dataset, 0 to 64, default 64. columnOffset: the column to continue from. It takes project too.
GET /api/operations/runslimit: how many runs, newest first; 100 by default, at most 1,000.
GET /api/maps, GET /api/tables, GET /api/recipes and every other listNo paging. project narrows the first three.

The catalogue gives each dataset’s columns without reading its rows. With two datasets, the quickstart’s world_cities and the world_cities_2 that uploading it again makes, the first page of one:

Terminal
curl -s "http://localhost:3000/api/catalogue/tables?limit=1" -H "Authorization: Bearer <YOUR_TOKEN>"
Output
{"tables":[{"id":"<DATASET_ID>","name":"world_cities","qualifiedName":"world_cities",...,"columnCount":6,"nextColumnOffset":null}],"total":2,"nextAfter":"world_cities"}

Your answer lists your own datasets.

  • total counts every dataset that matches, on all pages.
  • nextAfter is the last name on this page. Send it as after=world_cities for the next page. It is null on the last page.
  • nextColumnOffset is set when a dataset has more columns than the page. Ask again with names=<NAME> and that columnOffset.

A query answers a preview of its rows with the exact row count. Limits gives the preview’s size and every other limit.

A refused call answers an error status and a JSON body with a code:

Terminal
curl -s -w "\n%{http_code}\n" http://localhost:3000/api/maps/no-such-map -H "Authorization: Bearer <YOUR_TOKEN>"
Output
{"error":{"code":"not_found","message":"The requested resource was not found."}}
404
  • -w "\n%{http_code}\n" prints the status after the body.
  • Branch on code, not on the message or the status.

Errors says what an error answer holds and what to do about each common code. The error catalogue lists every code, and the operation reference the statuses each operation answers.

POST /api/connections/postgis saves a PostGIS connection, and PATCH /api/connections/postgis/<CONNECTION_ID> changes one. Both take a url: a Postgres URL, such as postgres://[email protected]:5432/gis, or a key-value string, such as host=db.example.com dbname=gis user=ana.

  • Dezfil reads the string once and never stores or returns it. It keeps the host, port, database, user name, password and SSL choice, and leaves out every other parameter.
  • A field you send on its own, such as password, wins over the same part of the string.
  • sslmode=require requires SSL. prefer, disable and allow don’t. Without one, a new connection requires SSL and a change keeps the saved choice.
  • verify-ca, verify-full, an unknown mode, or two different modes are refused.

Some parameters ask for a protection Dezfil can’t give. A string that gives one of them a value other than those listed here is refused, before Dezfil contacts the database:

ParameterValues Dezfil accepts and leaves out
channel_binding, gssencmodeprefer, disable
sslcertmodeallow
sslsni1
sslcompression, gssdelegation0, 1
sslnegotiationpostgres
requiressl0
sslfalse
options, service, min_protocol_version, ssl_min_protocol_version, ssl_max_protocol_versionnone
sslrootcert, sslcrl, sslcrldir, sslcert, sslkey, sslpasswordnone
require_auth, krbsrvname, gsslibnone
scram_client_key, scram_server_keynone
oauth_issuer, oauth_client_id, oauth_client_secret, oauth_scopenone
  • An empty value counts as left out. Names match in any case, and every time a name appears counts, so a later value can’t hide a refused one.
  • The answer is 400 invalid_request: “This connection string uses the unsupported protection parameter channel_binding; Dezfil supports connection fields and SSL required or not required.” The first refused parameter’s name stands in place of channel_binding.
  • A refused string changes nothing. A saved connection isn’t checked again, since its string was never kept.
Dezfil · Apache-2.0docs from 7439201b