HTTP API reference
Reference · HTTP API
@askdb/http-api is a thin HTTP wrapper around the same ask() pipeline as the library. Two endpoints, JSON over HTTP, designed to sit behind your own gateway.
Server
Section titled “Server”import { createAskDbHttpServer } from "@askdb/http-api";
const server = createAskDbHttpServer({ port: 3000, host: "127.0.0.1", maxBodyBytes: 1024 * 1024,});
await server.listen();createAskDbHttpServer() takes an AskDbHttpServerOptions object. Every option is optional.
| Option | Default | Description |
|---|---|---|
port | httpApi.listen.port, else 3000 | Port to listen on. |
host | httpApi.listen.host, else 127.0.0.1 | Bind address. Bind to 0.0.0.0 only behind a gateway. |
schemaPath | host.schemaJson, then host.schemaPath | Server-default schema artifact path. |
maxBodyBytes | 1048576 (1 MiB) | Request body size limit. Larger requests return payload_too_large. |
createAskDbHttpServer() returns an object with these members:
| Member | Description |
|---|---|
host, port | The bind address and port the server uses, after the defaults above. |
server | The underlying Node http.Server, for hosts that manage its lifecycle themselves. |
listen() | Starts listening. Returns a Promise<void> that resolves once the server listens, and rejects on a listen error such as EADDRINUSE. |
close() | Stops the server. Returns a Promise<void> that resolves once it has closed. |
Config loading
Section titled “Config loading”The server reads its schema and AI provider settings from askdb.config.ts via @askdb/config — see Configuration for the resolution rules. It gets them when you call createAskDbHttpServer():
- If your process already called
bootstrapAskDbEnv(), the server uses that config and loads nothing. - Otherwise the server calls
bootstrapAskDbEnv()itself, which loads.envandaskdb.config.*from the current working directory. If it finds no config, or an existing.envcan’t be read,createAskDbHttpServer()throws.
To load the config from another directory, call bootstrapAskDbEnv({ cwd: "/path/to/project" }) before you create the server.
Server config (httpApi)
Section titled “Server config (httpApi)”These keys in askdb.config.ts control request handling. They apply to both createAskDbHttpServer() and the askdb-http binary.
| Key | Default | Description |
|---|---|---|
httpApi.listen.port / httpApi.listen.host | 3000 / 127.0.0.1 | Listen address when no port / host option or CLI flag is given. |
httpApi.allowSchemaOverride | false | Accept a per-request schemaJson override. Must be a boolean; any other value (such as "false") fails config loading. When false, a request that sends schemaJson gets 403 schema_override_disabled. An override is a schema artifact the server parses and puts into the prompt through your model key, so enable it only for tests or trusted callers, on @askdb/http-api 1.0.0-beta.48 or later (GHSA-qqx9-qpw2-wcqg). |
httpApi.requestTimeoutMs | 60000 | Abort the model call after this many milliseconds. A timed-out request returns 502 sql_generation_error. Must be a positive integer no larger than 2147483647 (the Node timer maximum, about 24.8 days); any other value is a config error when the server loads its config. |
modes.omitSensitiveFromPrompt | false | When true, sensitive identifiers are left out of every prompt. A request’s omitSensitiveFromPrompt: false can’t turn this off. |
modes.askdbMode | schema_only | The loosest mode a request gets. A request that sends neither mode nor x-askdb-mode runs this mode. A request can tighten it to schema_only; one that asks for bounded_results while this is schema_only runs schema_only, with no error. |
export default defineConfig({ // ... httpApi: { listen: { port: 3000, host: "127.0.0.1" }, allowSchemaOverride: false, requestTimeoutMs: 30_000, },});Standalone binary
Section titled “Standalone binary”The package also ships an askdb-http binary, so you can run the service without writing any server code:
npx askdb-http --schema-path ./my-app.schema --port 3000 --host 127.0.0.1| Flag | Description |
|---|---|
--schema-path <path> | Server-default schema artifact path. |
--port <n> | Port to listen on. |
--host <host> | Bind address. |
-h, --help | Print the usage and these flags to stdout, then exit with code 0. It doesn’t load the config or start the server. |
Every flag is optional. Each setting comes from its flag, then askdb.config.ts, then the default:
- Schema:
--schema-path, thenhost.schemaJson, thenhost.schemaPath. There’s no default: with none,POST /askanswers500 generation_not_configured, unless the request sends its ownschemaJson(accepted whenhttpApi.allowSchemaOverrideistrue). - Port and host:
--portand--host, thenhttpApi.listen.portandhttpApi.listen.host, then3000and127.0.0.1.
askdb-http loads one .env file before the config: the first of these that exists.
- The
.envin the working directory. - Only when you run it from AskDB’s own repository: a
.envin theapps/http-apifolder. - Only there too: the
.envat the repository root.
An installed askdb-http reads only the working directory’s .env, and it never reads a .env inside node_modules. createAskDbHttpServer() reads only the working directory’s .env too (see Config loading).
askdb-http prints an error to stderr and exits with code 1 when:
- A flag is wrong: an unknown flag, a value flag (
--schema-path,--port,--host) with no value or with a value that starts with--, or a--portthat isn’t a whole number from 1 to 65535.askdb-httpprints the error, such asaskdb-http: Unknown option: --verboseoraskdb-http: Invalid --port: 70000, then the help text. It checks every flag before it reads.envor the config. It also checks them before it acts on--help, so a wrong flag beside--helpexits with code1too. - It can’t load its config: no
askdb.config.*found, or an invalid value such as an out-of-rangehttpApi.requestTimeoutMs. The error is one line. - It can’t bind: for example, the port is already in use. The error is one line.
Endpoints
Section titled “Endpoints”GET /health
Section titled “GET /health”Liveness probe.
curl http://localhost:3000/health{ "ok": true }POST /ask
Section titled “POST /ask”Generate SQL from a question.
Request (AskHttpRequest)
{ "question": "How many customers signed up last week?", "mode": "schema_only", "explain": false, "omitSensitiveFromPrompt": false, "schemaJson": "{\"version\":2, \"schemaId\":\"...\", \"tables\":[...]}"}| Field | Required | Default | Description |
|---|---|---|---|
question | yes | — | The natural-language question. |
mode | no | modes.askdbMode, else schema_only | One of schema_only, bounded_results. Also accepted as the x-askdb-mode header (the body wins). It can only tighten the server’s modes.askdbMode: bounded_results runs schema_only unless the server sets modes.askdbMode: "bounded_results". An unrecognized value returns 400 bad_request. |
explain | no | false | Return metadata on the read-only checks. Must be a JSON boolean: any other value, such as "false", returns 400 bad_request. |
omitSensitiveFromPrompt | no | modes.omitSensitiveFromPrompt | true omits sensitive columns from the prompt for this request. It can only tighten the server setting. When the server sets modes.omitSensitiveFromPrompt: true, sending false has no effect. Must be a JSON boolean: any other value, such as "true" or 1, returns 400 bad_request. |
schemaJson | no | — | Inline schema artifact that replaces the server schema for this request. Rejected with 403 schema_override_disabled unless the server sets httpApi.allowSchemaOverride: true. The server generates and checks SQL for the artifact’s own provider, which must be a built-in dialect id; the config’s dialect applies to the server’s own schema only. Preferred: set host.schemaPath in askdb.config.ts server-side. |
An optional field sent as JSON null counts as absent, as does an empty or whitespace-only schemaJson. A null body mode falls back to the x-askdb-mode header, then to config.
Success response (200, AskHttpSuccessResponse)
{ "ok": true, "correlationId": "8f14e45f-ceea-4e7a-9f2b-1c3d5e7a9b0c", "sql": "SELECT count(*) FROM customers WHERE created_at >= …", "usage": { "promptTokens": 1250, "completionTokens": 45, "totalTokens": 1295 }, "sensitiveGuardrail": { "passed": true, "references": [] }}| Field | Type | Notes |
|---|---|---|
sql | string | The generated, checked SQL. |
explain | object, only when requested | Metadata on the read-only checks: which ran. Present only when the request sent explain: true. |
usage | object | null | Token counts for the generation call: promptTokens, completionTokens, totalTokens (each number | null). null when the provider does not report usage. |
sensitiveGuardrail | object, only on some responses | Present only when the schema marks at least one table or column sensitive. Shows which sensitive identifiers the returned sql references: passed (boolean), references ({ table, schema?, column, matchKind }[]), and unresolvedScope when the table scope couldn’t be resolved. passed: false is a warning. The SQL is still returned, so check it before you execute. See validateSensitiveReferences. |
Error response (4xx / 5xx, AskHttpErrorResponse)
{ "ok": false, "correlationId": "8f14e45f-ceea-4e7a-9f2b-1c3d5e7a9b0c", "error": { "code": "sql_validation_error", "message": "SQL must start with SELECT or WITH (got 'delete').", "rule": "SQL_NOT_SELECT_OR_WITH" }}Error codes
Section titled “Error codes”| Code | HTTP | When |
|---|---|---|
not_found | 404 | Any route other than GET /health and POST /ask, including GET /ask. |
bad_request | 400 | Malformed JSON, missing question, invalid mode, non-boolean omitSensitiveFromPrompt or explain, non-string schemaJson, a retired execution field, or a request schemaJson that records no provider or one with no AskDB dialect. |
payload_too_large | 413 | Body exceeds maxBodyBytes. |
schema_override_disabled | 403 | The request sent schemaJson but httpApi.allowSchemaOverride is not true. |
schema_parse_error | 400 | A request schemaJson couldn’t be loaded or parsed. The server’s own schema gets 500 generation_not_configured instead. |
sql_validation_error | 400 | Model output failed the SQL checks (must start with SELECT/WITH, a single statement, no comments, no unterminated strings or quoted identifiers, no write or DDL keywords, no locking reads, no blocked side-effecting functions). Includes rule, the check’s SqlValidationRuleCode, such as SQL_NOT_SELECT_OR_WITH. |
guardrail_violation | 422 | Reserved for the tenant or sensitive check rejecting the generated SQL, with rule. The server doesn’t return it today: it runs the sensitive-identifier check in warn mode (see sensitiveGuardrail), and it can’t pass a tenant scope, so a schema with a tenant policy fails earlier with 500 tenant_scope_not_configured. |
generation_not_configured | 500 | The server can’t generate SQL for its configured schema, for one of four reasons: no model provider is configured; no schema is configured; the schema can’t be loaded, such as a host.schemaPath that doesn’t exist; or the schema’s provider has no AskDB dialect and dialect isn’t set in askdb.config.*. The server never falls back to PostgreSQL. The message is generic and names no path. The log line’s errName names the cause: ModelNotConfiguredError, SchemaNotConfiguredError, SchemaLoadError or DialectNotSupportedError, with the full error. |
tenant_scope_not_configured | 500 | The schema has a tenant policy (tenant-policy.md), and the server can’t set a tenantScope, so ask() refuses it before any model call. No SQL is returned. To serve a tenant-scoped schema, call ask() with a tenantScope in your own server; server-side scoping for this server is planned in #343. |
sql_generation_error | 502 | The model call failed (provider error, or it hit httpApi.requestTimeoutMs). The message is generic. The provider’s own error text is logged server-side and never returned. |
internal_error | 500 | Anything else. The message is generic. The full error is logged server-side under the response’s correlationId. |
The server picks the status code from the error’s type, never from its message text. Every non-2xx response from POST /ask is logged as an askdb.run.error event with its status and code, under the same correlationId. A request rejected before ask() runs (malformed or oversized body, invalid field, schema_override_disabled) is logged with the client-facing message, or the body parse error. A failure inside ask() is logged with the error’s name, message, and cause, plus the stack for 5xx. A 404 not_found for an unknown route isn’t logged. A last-resort 500 (an error that escapes the request handler) is written to stderr with the correlation ID, not to the structured log.
Correlation IDs
Section titled “Correlation IDs”Every response except GET /health includes a correlationId. The same ID is emitted in the server’s structured log events, so you can join HTTP requests to generated SQL and any errors.
To carry a correlation ID supplied by your gateway, send the x-correlation-id header on the request. The server trims the value and uses it; when the header is missing or blank, it generates a UUID.
curl -X POST http://localhost:3000/ask \ -H "content-type: application/json" \ -H "x-correlation-id: req_abc123" \ -d '{"question":"How many users?"}'@askdb/http-api exports four types, for a host or a client written in TypeScript:
import type { AskDbHttpServerOptions, AskHttpErrorResponse, AskHttpRequest, AskHttpSuccessResponse,} from "@askdb/http-api";| Type | Describes |
|---|---|
AskDbHttpServerOptions | The options of createAskDbHttpServer(): see Server. |
AskHttpRequest | The POST /ask body: see POST /ask. Its fields have no null member. The server still reads a JSON null as absent. |
AskHttpSuccessResponse | The 200 body, with ok: true. explain is typed unknown. |
AskHttpErrorResponse | The error body, with ok: false. error.code is a union of the codes in Error codes. error.rule is an optional string. |
What this server doesn’t do
Section titled “What this server doesn’t do”- No auth. Put your gateway in front.
- No TLS termination. Use your ingress.
- No rate limiting. Same.
- No database connection. It returns SQL; execution is your caller’s responsibility.
See Deploy as HTTP service for the production deployment pattern.
Read next
Section titled “Read next”© 2026 Yahya Gilany

