REST Endpoints
Every HTTP endpoint faz serve exposes — paths, request shapes, response envelopes, status codes.
faz serve runs a FastAPI app on 127.0.0.1:8787. Every endpoint is under the /v1 prefix. The contract is identical to the MCP tools — same Pydantic schemas, same envelopes — so picking REST over MCP is just a transport choice.
For setting up REST as your client transport, see REST API integration.
Endpoint summary
| Method | Path | Purpose |
|---|---|---|
GET | /v1/health | Liveness probe. |
GET | /v1/databases | Paginated database list with discovered schemas. |
GET | /v1/databases/{db}/tables/{table} | One table's schema in detail. |
POST | /v1/query/simple | Single-database query through the safety pipeline. |
POST | /v1/query | Multi-step federated query. |
GET | /v1/results/{request_id} | Paginated retrieval of a stored federated result. |
GET /v1/health
Liveness probe. Returns instantly without pinging databases.
curl http://localhost:8787/v1/health{
"status": "ok",
"version": "0.1.0",
"databases_connected": 2
}databases_connected is the count of databases that connected successfully at startup. Use it as a smoke test, not as definitive health for any single connection — for that, hit /v1/databases and check the reachable field per entry.
GET /v1/databases
Paginated list of every configured database — including ones that failed to connect at startup, which appear with reachable: false and an error message. Each entry includes discovered tables and column metadata.
| Query param | Type | Default | Notes |
|---|---|---|---|
page | integer | 1 | 1-indexed page number. |
size | integer | 4 | Databases per page. Max 100. Keeps responses bounded. |
curl 'http://localhost:8787/v1/databases?page=1&size=10'{
"databases": [
{
"name": "<database>",
"type": "postgresql",
"database": "<db-name>",
"reachable": true,
"tables": [
{
"name": "<table>",
"fields": [
{
"name": "<column>",
"data_type": "integer",
"nullable": false,
"is_primary": true,
"is_foreign_key": false,
"description": "",
"distribution": null
}
],
"row_count_estimate": 8421,
"description": ""
}
],
"error": null
}
],
"pagination": {
"page": 1,
"size": 10,
"total_databases": 2,
"total_pages": 1,
"has_more": false
}
}Loop until pagination.has_more is false. Status code is always 200 — connector failures are reported in-line, not as HTTP errors.
GET /v1/databases/{db}/tables/{table}
One table's schema in full detail. Same shape as a single entry in /v1/databases.tables[], returned at the top level.
curl http://localhost:8787/v1/databases/<database>/tables/<table>{
"name": "<table>",
"fields": [
{
"name": "<column>",
"data_type": "integer",
"nullable": false,
"is_primary": true,
"is_foreign_key": false,
"description": "",
"distribution": {
"distinct_count": 8421,
"null_fraction": 0.0,
"sample_values": [1, 2, 3, 4, 5],
"min_value": 1,
"max_value": 8421
}
}
],
"row_count_estimate": 8421,
"description": ""
}Returns 404 if the database isn't configured or the table isn't found in the connector's discovered schema.
POST /v1/query/simple
Run one query through the safety pipeline. The primary endpoint for single-database operations.
curl -X POST http://localhost:8787/v1/query/simple \
-H 'Content-Type: application/json' \
-d '{
"database": "<database>",
"table": "<table>",
"language": "sql",
"query": "SELECT * FROM <table> LIMIT 5"
}'| Field | Type | Required | Notes |
|---|---|---|---|
database | string | yes | Connector name from faz.yaml. |
table | string | yes | Declared target table. |
language | string | null | no | sql, mql, cypher, es_dsl, vector, dynamo, couchdb. Inferred from connector when omitted. |
query | string | yes | Native query body. Format depends on language — see MCP tools for per-language formats. |
Success — 200
{
"request_id": "req_a1b2c3d4e5f6",
"status": "ok",
"data": {
"columns": ["<column-1>", "<column-2>"],
"rows": [
{ "<column-1>": "<value>", "<column-2>": "<value>" }
],
"row_count": 1
},
"safety": {
"stages_passed": ["PROMPT_GUARD", "RBAC_GATE", "AST_CHECKER", "INJECTION_ANALYSER", "GUARDRAILS"],
"warnings": []
},
"metadata": {
"database": "<database>",
"execution_time_ms": 23.4,
"transport": "rest/local",
"steps": [],
"merge_latency_ms": null
}
}Blocked — 403
{
"request_id": "req_a1b2c3d4e5f6",
"status": "blocked",
"error": {
"stage": "RBAC_GATE",
"reason": "Access denied: <database>.<sensitive-table> requires permission for SELECT; policy grants none",
"suggestion": null
}
}The status code differentiates: 200 for safe execution (with success or warnings), 403 when the safety pipeline rejected the query, 400 for malformed requests, 404 for unknown database, 500 for downstream connector failures during execution.
POST /v1/query
Multi-step federated query. Each step runs against its declared connector; results land in DuckDB tables s0, s1, ... and the optional merge SQL joins them.
curl -X POST http://localhost:8787/v1/query \
-H 'Content-Type: application/json' \
-d '{
"steps": [
{
"step_id": "s0",
"database": "<database-1>",
"table": "<table-1>",
"query": "SELECT <link-column>, <other-column> FROM <table-1> WHERE <condition>"
},
{
"step_id": "s1",
"database": "<database-2>",
"table": "<collection>",
"language": "mql",
"query": "{\"operation\": \"find\", \"filter\": {}}",
"depends_on": ["s0"],
"link_from": "<link-column>",
"link_to": "<foreign-key>"
}
],
"merge": "SELECT s1.<column>, s0.<other-column> FROM s0 JOIN s1 ON s0.<link-column> = s1.<foreign-key>"
}'Request body and per-step fields are documented on MCP tools › query. The conceptual model is on Federated queries.
The response envelope is the same QueryResponseSuccess / QueryResponseBlocked shape as /v1/query/simple, but metadata.steps is populated with per-step latencies.
If any step touches a denied table under DenyPolicy.PARTIAL (the default), it's silently stripped and the merge runs without it — metadata.steps will be missing the stripped entry but the request returns 200.
If the request needs to return more rows than the inline response carries (over the row cap), the result is stored in a scratchpad keyed by request_id. Retrieve subsequent pages via GET /v1/results/{request_id}.
GET /v1/results/{request_id}
Paginated retrieval of a stored federated result.
| Path param | Type | Notes |
|---|---|---|
request_id | string | The request_id from a previous POST /v1/query response. |
| Query param | Type | Default | Notes |
|---|---|---|---|
page | integer | 1 | 1-indexed page number. |
size | integer | 100 | Rows per page. Max 10,000. |
curl 'http://localhost:8787/v1/results/req_a1b2c3d4e5f6?page=2&size=500'{
"request_id": "req_a1b2c3d4e5f6",
"page": 2,
"size": 500,
"columns": ["<column-1>", "<column-2>", "<column-3>"],
"rows": [ { "<column-1>": "<value>", "<column-2>": "<value>", "<column-3>": "<value>" } ],
"has_more": true
}Returns 404 if the request_id has expired or never existed. Scratchpads are short-lived; pull pages soon after the initial query.
Status code reference
| Code | When |
|---|---|
200 | Successful query, allowed by the safety pipeline. |
400 | Malformed request body. Pydantic validation errors, missing required fields, or invalid IR (cycle in depends_on, duplicate step_id, unknown reference). |
403 | Safety pipeline blocked the query. Body includes { status: "blocked", error: { stage, reason, suggestion } }. |
404 | Database not in faz.yaml, table not in the connector's discovered schema, or request_id expired. |
500 | Connector failure during execution (database unreachable mid-query, malformed merge SQL). The request passed safety but failed downstream. |
Authentication
There isn't any. faz binds 127.0.0.1 by default. Anything off-loopback prints a warning at startup; if you're exposing faz remotely, put a reverse proxy with auth in front (nginx, Cloudflare Tunnel, Tailscale). See REST API integration for the deployment pattern.
Related
- MCP tools — same contract over stdio.
- REST API integration — deploying
faz servefor clients. - Federated queries — the multi-step model.
- How faz protects you — what runs on every
/v1/query*call.