HTTP API
The auto-generated HTTP API reference (SK-DOCS-003 slice d) is blocked on
apps/api emitting an OpenAPI schema. The contract this page will eventually
render is canonical in packages/sdk/src/index.ts — every
HTTP endpoint has a matching SDK method, by GLOBAL-001.
While slice d is blocked, use the SDK reference: each method (ask,
runSql, createDatabase, listKeys, …) documents the exact wire
shape that flows over the corresponding POST /v1/<resource> endpoint.
Endpoints (overview)
Section titled “Endpoints (overview)”POST /v1/ask— the canonical NL→SQL endpoint. SeeNlqClient.ask.POST /v1/run— raw-SQL escape hatch (GLOBAL-015). SeeNlqClient.runSql.POST /v1/databases— create a database from a plain-English brief. SeeNlqClient.createDatabase.GET /v1/databases— list databases. SeeNlqClient.listDatabases.POST /v1/keys,GET /v1/keys,DELETE /v1/keys/:id— API-key lifecycle.POST /v1/chat/messages— chat-style transcript endpoint.
Curl-shaped examples for /v1/ask (read + write + anonymous mode) live in
the curl tutorial.
Error codes
Section titled “Error codes”Every non-2xx response carries a JSON envelope
{ "error": { "code": <code>, "message", "action", "retryable", "params"? } }
(GLOBAL-012). An agent can read this table before calling to decide which codes are
retryable and which need a human. code is the discriminant the SDK exposes as
err.code; the same set is the ApiErrorCode union in
packages/sdk/src/index.ts, kept in lockstep by a build-time guard
(apps/docs/scripts/check-error-codes.ts).
| Code | HTTP | Meaning | Retryable? | Recovery action |
|---|---|---|---|---|
db_not_found |
404 | Pinned dbId doesn’t exist. |
No | Drop dbId to create from the goal, or fix the id. |
schema_unavailable |
422 | The DB’s schema fingerprint isn’t ready yet. | Yes | Retry shortly. |
db_misconfigured |
502 | The DB’s stored config is invalid. | No | Recreate the database; if it persists, contact support. |
db_unreachable |
502 | The backing engine is unreachable. | Yes | Retry with backoff. |
sql_rejected |
400 | The SQL failed the allow-list or the engine (body.reason). |
No | Rephrase the goal, or hand-write valid SQL via /v1/run. |
llm_failed |
502 | The model chain couldn’t produce SQL. | Yes | Retry; if persistent, simplify the goal. |
rate_limited |
429 | Too many requests (body.limit, body.count). |
Yes | Wait, honor Retry-After, then retry. |
unauthorized |
401 | Missing, invalid, or expired credential. | No | Sign in again, or mint a new key. |
auth_unavailable |
503 | The session store hit a transient blip while verifying the caller — not a signed-out session. | Yes | Retry shortly; you stay signed in. |
forbidden |
403 | A read-only key tried to write via /v1/run (SK-APIKEYS-003). |
No | Use a write-capable key. |
ambiguous_db |
409 | 2+ databases match; confidence below the floor. | No | Pick from body.candidate_dbs, resend with dbId. |
clarify_required |
409 | Pinned dbId but the goal looks like a create (body.pinned_db). |
No | Resend without dbId to create, or cancel. |
invalid_engine |
400 | engine isn’t in the allowed set. |
No | Use postgres or clickhouse. |
invalid_model |
400 | model isn’t in the preset set (body.allowed). |
No | Use auto, fast, or best. |
model_unavailable |
409 | model: "best" but the account has no frontier lane (no BYOLLM key, no paid plan). |
No | Add a provider key at body.link, or drop model. |
goal_required |
400 | goal missing or empty. |
No | Supply a non-empty goal. |
dbId_required |
400 | dbId required for this call. |
No | Supply dbId. |
sql_required |
400 | /v1/run called without sql. |
No | Supply sql. |
sql_too_long |
400 | sql exceeds the size cap. |
No | Shorten the query. |
db_required |
400 | /v1/run called without db. |
No | Supply db. |
invalid_json |
400 | Request body isn’t valid JSON. | No | Fix the JSON. |
invalid_body |
400 | Body failed validation (body.message). |
No | Fix the offending field. |
invalid_email |
400 | Email is malformed. | No | Fix the email. |
invalid_byollm_key |
400 | The BYOLLM credential is mis-shaped. | No | Fix provider / model / key. |
byollm_unavailable |
503 | The server can’t seal keys (KEK unset). | Yes | Retry; if persistent it’s an operator action. |
secret_unconfigured |
503 | The server is missing a required secret. | No | Operator action — not caller-fixable. |
wrong_preset |
409 | remember() target DB isn’t an agent_memory_v1 preset. |
No | Point at an agent-memory database, or create one. |
connect_requires_account |
403 | /v1/db/connect called without a signed-in session. |
No | Sign in, then reconnect. |
invalid_request |
400 | Malformed body, or a bad / non-HTTPS / egress-blocked connection URL (body.message). |
No | Fix the field named in body.message. |
introspection_failed |
502 | The connected database couldn’t be reached or read. | Yes | Check the host and credential, then retry. |
sealing_unconfigured |
503 | The server can’t seal secrets (KEK unset). | No | Operator action — not caller-fixable. |
schema_mismatch |
409 | The plan named a table this database doesn’t have (body.params). |
No | Ask about a table it has, or create a new database. |
write_constraint |
409 | An approved write hit a constraint (FK / unique / not-null / check); nothing was written (body.params). |
No | Fix the named field, then ask again. |
write_no_rows |
409 | An approved write matched no rows — previewed or committed (body.params.phase). |
No | Name the row or widen the filter, then ask again. |
invalid_value |
409 | A value didn’t fit its column (Postgres SQLSTATE class 22). | No | Restate with values that match the column types. |
goal_too_long |
400 | goal exceeds the length limit (body.params.maxLength). |
No | Shorten it to the essential question. |
invalid_scope |
400 | agentId / endUserId / threadId weren’t strings. |
No | Send strings, or omit the fields. |
byollm_requires_session |
400 | A per-request provider key needs a signed-in session. | No | Sign in, or save the key to your account. |
auth_required |
401 | The anonymous budget is exhausted (body.params.cap). |
No | Sign in — your prompt is saved. |
account_required |
403 | The call needs an account-scoped key, not a public embed key. | No | Mint an account key and retry. |
create_requires_session |
403 | Creating a database needs a signed-in session. | No | Sign in, then create it. |
challenge_required |
428 | A human check is required before running that. | No | Complete the check, then retry. |
key_not_found |
404 | No key of yours matched that id. | No | Check the key list, then retry. |
unconfigured |
503 | That feature isn’t configured on this deployment. | No | Operator action — retry later. |
internal_error |
500 | An unexpected server-side failure. | No | Retry once; if it persists, report it. |
invalid_bearer |
400 | The Authorization header wasn’t a usable bearer token. |
No | Send Authorization: Bearer <key>. |
invalid_token |
400 | The anonymous token isn’t valid. | No | Start a new session. |
token_taken |
409 | The anonymous session was already claimed by another account. | No | Carry on in this account. |
adopt_failed |
500 | nlqdb couldn’t move anonymous work into this account. | No | Retry once; if it persists, contact support. |
import_not_found |
404 | The import draft no longer exists. | No | Start the import again. |
import_busy |
409 | That import is already running. | Yes | Wait for it to finish, then check the result. |
unknown_pack |
400 | The import pack doesn’t exist (body.params.allowed). |
No | Use a supported pack. |
source_required |
400 | No source was given for the import. | No | Name the source, then retry. |
invalid_preset |
400 | The schema preset doesn’t exist (body.params.allowed). |
No | Use a supported preset. |
preset_disabled |
400 | That preset isn’t enabled on this deployment. | No | Create without a preset, or ask the operator to enable it. |
preset_engine_conflict |
400 | The preset doesn’t run on the requested engine. | No | Drop the engine override, or pick a compatible preset. |
provision_failed |
502 | nlqdb couldn’t finish creating the database. | Yes | Retry; nothing was left half-created. |
oauth_not_configured |
503 | This deployment can’t connect that provider by OAuth. | No | Connect with a connection URL instead. |
pick_expired |
410 | The connection choice expired before it was confirmed. | No | Start the connection again. |
unknown_error |
5xx | A 5xx with no parseable envelope. | Yes | Retry; if persistent, report it. |
non_json_response |
— | A proxy/CDN returned non-JSON (SDK sentinel). | Maybe | Retry; check the network path. |
network_error |
— | Transport failure, no response (SDK sentinel, httpStatus === 0). |
Yes | Retry with backoff. |
aborted |
— | The caller’s AbortSignal fired (SDK sentinel, httpStatus === 0). |
No | Re-issue the request if still wanted. |
The last four are SDK-only sentinels — the API never sends them; the SDK raises them
for transport states. Recoverable failures (transport errors and transient 5xx) are
retried automatically by @nlqdb/sdk before they ever surface (SK-SDK-008).
Envelope fields beyond code
Section titled “Envelope fields beyond code”Some codes carry extra fields (under error.params, and mirrored top-level on
err.body by the SDK) an agent should branch on:
candidate_dbs— onambiguous_db:[{ id, slug }]to render a picker.pinned_db— onclarify_required: the DB that was pinned when the goal looked like a create.requires_confirm+diff— on a write preview (dryRun/ destructive op): apply only after the human approves the diff (GLOBAL-023).limit+count— onrate_limited: the window cap and how many you’ve used.link— onmodel_unavailable: deep-link to add a BYOLLM key or plan.