API and agentsAPI reference
Query
Read-only SQL against tables.
4 operations on 2 paths.
/api/query
POSTRun read-only SQL
Sign-in Session cookie or Bearer tokenMCP tool run_sql
At most 16,777,216 wire bytes; a larger body is refused with request_body_too_large before JSON parsing. Only one read-only SELECT over named datasets is accepted. File readers, file-relation shorthand and engine path or definition discovery are refused before binding with sql_rejected, including nested SQL and computed targets. File-referencing PROJ coordinate definitions are also refused before native projection use, including computed or per-row values; EPSG identifiers and inline definitions without files remain supported. Unknown tables receive the same sql_error whether hidden or nonexistent, without catalogue suggestions. Engine errors do not expose filesystem paths or internal statements; input positions are retained when available. Existing saved SQL is retained unchanged; affected saved and published queries fail. Named datasets resolve only within the caller's reach: a plain name, and its catalog-qualified forms, is the caller's own dataset; another person's dataset the caller reaches through a project is <sql name>.<dataset>, and the caller's own SQL name qualifies their own. Both hold in query()/query_table() expressions and nested SQL. Each evaluation uses fresh connection-local views on one shared hardened engine. Returns a capped preview plus the exact total row count. Geometry cells in the preview are WKT text by default; pass geometry: "geojson" to receive GeoJSON geometry objects instead. The complete UTF-8 JSON answer is limited to 33,554,432 bytes (32 MiB), and each non-geometry cell to 2,097,152 bytes (2 MiB), including JSON escaping. Equality is admitted. Native GEOMETRY columns, including CRS-qualified types, count toward the result limit only. Geometry-looking text and explicit WKT/GeoJSON text remain non-geometry. An oversized answer is refused in full with HTTP 422 query_result_too_large, without partial rows. These fixed limits do not change row limits, engine budgets or exports. A failing statement answers with the engine's message and, when the location maps to the submitted text, a 1-based line and column into the submitted SQL.
Request body
At most 16,777,216 bytes (16 MiB); a larger body is refused with request_body_too_large.
application/json
object
sqlstringrequiredOnly one read-only SELECT over named datasets is accepted. File readers, file-relation shorthand and engine path or definition discovery are refused before binding with sql_rejected, including nested SQL and computed targets. File-referencing PROJ coordinate definitions are also refused before native projection use, including computed or per-row values; EPSG identifiers and inline definitions without files remain supported. Unknown tables receive the same sql_error whether hidden or nonexistent, without catalogue suggestions. Engine errors do not expose filesystem paths or internal statements; input positions are retained when available. Existing saved SQL is retained unchanged; affected saved and published queries fail.
geometrystringHow geometry columns travel in the preview. Default wkt.
One of
wkt,geojsonwrittenInWrittenInEvaluate the SQL where it was written: in a map's SQL home, as its layers are, or in a workflow's context with its declared outputs as the caller runs them, always within the caller's reach. A name that binds nothing there, when the home is someone else's, is refused naming it, as a layer's would be. Omitted or console, the caller's own console.
Responses
200
Success.
application/json400401403413422503default
Error with a machine-readable code and human message.
application/json405
method_not_allowed: the method is unsupported for this path.
application/jsonAllowHeaderExactly the documented methods for this path.
stringalways
OPTIONS, POST
Examples
Query the three largest cities
Needs a token and the dataset world_cities, which the upload makes.
curl
curl -s http://localhost:3000/api/query \ -H "Authorization: Bearer <YOUR_TOKEN>" \ -H "Content-Type: application/json" \ -d '{"sql": "SELECT name, country, population FROM world_cities ORDER BY population DESC LIMIT 3"}'JavaScript
const response = await fetch("http://localhost:3000/api/query", { method: "POST", headers: { Authorization: "Bearer <YOUR_TOKEN>", "Content-Type": "application/json", }, body: JSON.stringify({ sql: "SELECT name, country, population FROM world_cities ORDER BY population DESC LIMIT 3", }),});console.log(JSON.stringify(await response.json(), null, 2));Answers 200 application/json
{ "columns": [ { "name": "name", "type": "VARCHAR" }, { "name": "country", "type": "VARCHAR" }, { "name": "population", "type": "BIGINT" } ], "rowCount": 3, "preview": [ { "name": "Tokyo", "country": "Japan", "population": "35676000" }, { "name": "New York", "country": "United States of America", "population": "19040000" }, { "name": "Mexico City", "country": "Mexico", "population": "19028000" } ], "previewTruncated": false, "runtimeMs": 4}Query a dataset that does not exist
Needs a token. No dataset need exist: the answer is the error.
curl
curl -s http://localhost:3000/api/query \ -H "Authorization: Bearer <YOUR_TOKEN>" \ -H "Content-Type: application/json" \ -d '{"sql": "SELECT * FROM no_such_dataset"}'JavaScript
const response = await fetch("http://localhost:3000/api/query", { method: "POST", headers: { Authorization: "Bearer <YOUR_TOKEN>", "Content-Type": "application/json", }, body: JSON.stringify({ sql: "SELECT * FROM no_such_dataset" }),});console.log(JSON.stringify(await response.json(), null, 2));Answers 400 application/json
{ "error": { "code": "sql_error", "message": "The query refers to an unknown table.", "position": { "line": 1, "column": 15 } }}Query one value over 2 MiB
Needs a token. No dataset need exist: the one value is over the size a value may have in an answer, so no row is returned.
curl
curl -s http://localhost:3000/api/query \ -H "Authorization: Bearer <YOUR_TOKEN>" \ -H "Content-Type: application/json" \ -d '{"sql": "SELECT repeat('\''x'\'', 3000000) AS value"}'JavaScript
const response = await fetch("http://localhost:3000/api/query", { method: "POST", headers: { Authorization: "Bearer <YOUR_TOKEN>", "Content-Type": "application/json", }, body: JSON.stringify({ sql: "SELECT repeat('x', 3000000) AS value" }),});console.log(JSON.stringify(await response.json(), null, 2));Answers 422 application/json
{ "error": { "code": "query_result_too_large", "message": "The result is too large to return: more than 32 MB in total, or one non-geometry value over 2 MB. Select fewer rows or columns, or shorten the value." }}Also on this path: OPTIONS under System.
/api/query/export
POSTDownload a query's full result
Sign-in Session cookie or Bearer tokenMCP tool export_sql_result
At most 16,777,216 wire bytes; a larger body is refused with request_body_too_large before JSON parsing. Private execution retains the initiating account through completion. Retained images and download receipts belong only to that account, including for administrators, even after the source or output is deleted. A copied result identifier has the same refusal as an unknown identifier and cannot consume or release the owner's result. Account activity is checked before returning bytes or retaining a new result; deactivation freezes access until reactivation. Reach is asked again where the work commits. A prepared layer download is refused on the claim when its layer, on a map the claimant does not own, or a dataset it drew that someone else owns has left the claimant's reach since the ticket, with the same answer as an expired ticket. A server render and a look check ask at every phase whether the caller still reaches the map and are refused as absent otherwise. A project export checks every selected member against the caller's reach as the archive completes and fails with export_failed naming the first one withdrawn rather than delivering a thinner archive. Results already issued (a query download, a retained look image, a project archive, an assistant answer) remain the initiator's for their retention lifetime after reach is withdrawn, as a copy they hold; they grant no new access to the source. Every row the SQL produces — not the capped preview the query operation returns — as a file: csv, geoparquet (plain parquet when the result has no geometry), or geojson, which needs a geometry column and is refused with a coded reason otherwise. The first geometry column is the feature geometry; every other geometry column travels as WKT. Runs on the hardened read-only instance under the universal query bound — timeout, memory budget, and cancellation when the caller departs. The response is an attachment named <name>.<ext>; through the agent tool it arrives as a base64 attachment envelope.
Request body
At most 16,777,216 bytes (16 MiB); a larger body is refused with request_body_too_large.
application/json
object
sqlstringrequiredOnly one read-only SELECT over named datasets is accepted. File readers, file-relation shorthand and engine path or definition discovery are refused before binding with sql_rejected, including nested SQL and computed targets. File-referencing PROJ coordinate definitions are also refused before native projection use, including computed or per-row values; EPSG identifiers and inline definitions without files remain supported. Unknown tables receive the same sql_error whether hidden or nonexistent, without catalogue suggestions. Engine errors do not expose filesystem paths or internal statements; input positions are retained when available. Existing saved SQL is retained unchanged; affected saved and published queries fail.
formatstringrequiredOne of
geoparquet,geojson,csvnamestringDownload name without extension. Default query_result.
preparebooleanThe page's road: write the file and answer a single-use ticket instead of the bytes; redeem it with GET within a minute. Leave unset to receive the attachment directly.
Responses
200
The file, as an attachment — or, with prepare, a JSON ticket {ticket, key, format}.
application/jsonobject
ticketstringrequiredkeystringrequiredformatstringrequiredOne of
geoparquet,geojson,csv
application/octet-streamstringformat
binary400
Unknown format, GeoJSON without geometry, invalid SQL, or the bounded lane's coded errors.
401403413503default
Error with a machine-readable code and human message.
application/json405
method_not_allowed: the method is unsupported for this path.
application/jsonAllowHeaderExactly the documented methods for this path.
stringalways
GET, HEAD, OPTIONS, POST
Example
Download a query's result as CSV
Needs a token and the dataset world_cities. The answer is the file largest_cities.csv, every row of the result.
curl
curl -s http://localhost:3000/api/query/export \ -H "Authorization: Bearer <YOUR_TOKEN>" \ -H "Content-Type: application/json" \ -d '{"sql": "SELECT name, country, population FROM world_cities ORDER BY population DESC LIMIT 3", "format": "csv", "name": "largest_cities"}'JavaScript
const response = await fetch("http://localhost:3000/api/query/export", { method: "POST", headers: { Authorization: "Bearer <YOUR_TOKEN>", "Content-Type": "application/json", }, body: JSON.stringify({ sql: "SELECT name, country, population FROM world_cities ORDER BY population DESC LIMIT 3", format: "csv", name: "largest_cities", }),});console.log(await response.text());Answers 200 application/octet-stream
name,country,populationTokyo,Japan,35676000New York,United States of America,19040000Mexico City,Mexico,19028000GETRedeem a prepared query-result download
Sign-in Session cookie or Bearer token
Private execution retains the initiating account through completion. Retained images and download receipts belong only to that account, including for administrators, even after the source or output is deleted. A copied result identifier has the same refusal as an unknown identifier and cannot consume or release the owner's result. Account activity is checked before returning bytes or retaining a new result; deactivation freezes access until reactivation. Reach is asked again where the work commits. A prepared layer download is refused on the claim when its layer, on a map the claimant does not own, or a dataset it drew that someone else owns has left the claimant's reach since the ticket, with the same answer as an expired ticket. A server render and a look check ask at every phase whether the caller still reaches the map and are refused as absent otherwise. A project export checks every selected member against the caller's reach as the archive completes and fails with export_failed naming the first one withdrawn rather than delivering a thinner archive. Results already issued (a query download, a retained look image, a project archive, an assistant answer) remain the initiator's for their retention lifetime after reach is withdrawn, as a copy they hold; they grant no new access to the source. The second half of the page's download road: a ticket from a prepare call streams its file once as a native attachment, bound to the SQL key and format it was prepared for. Tickets expire after a minute; a redeemed, expired or mismatched ticket answers 410. Not an agent tool — agents call the POST directly.
Parameters
ticketQueryrequiredThe ticket the prepare call answered.
string
keyQueryrequiredThe key the prepare call answered.
string
formatQueryrequiredThe format the ticket was prepared for.
string
One of
geoparquet,geojson,csv
Responses
200
The file, as an attachment.
application/octet-streamstringformat
binary401403503default
Error with a machine-readable code and human message.
application/json405
method_not_allowed: the method is unsupported for this path.
application/jsonAllowHeaderExactly the documented methods for this path.
stringalways
GET, HEAD, OPTIONS, POST
410
The ticket is gone.
HEADRead response headers
Sign-in Session cookie or Bearer tokenAnswers 200, 401, 403, 405, 410, 503, defaultAllow
GET, HEAD, OPTIONS, POSTAutomatic HEAD uses GET with no response body.
Also on this path: OPTIONS under System.
7439201b