Skip to content

HTTP API

Every route is under https://api.persql.com/api/v1, takes and answers JSON, and needs a developer key as a bearer token: Authorization: Bearer persql_... (X-Api-Key: persql_... works too). These are the routes the console itself uses; there is no second API.

A failure answers a status and { "error": "<sentence>" }:

Status Meaning
400 bad request The body did not fit the route.
401 sign in first No key, or a revoked one.
402 The free week or the subscription has ended; the sentence says which. Reading stays open; writing does not.
404 not found Not yours, or gone.
409 A limit: too many databases, too many saved queries, too many notes.
422 the statement failed SQLite refused the SQL; rejection.message carries its own words.
429 The assistant’s hourly cap; Retry-After says when.

Times are milliseconds since the epoch. A cell is a string, a number, a boolean, null, or bytes as a base64 string.

GET /databases — your databases: id, name, tableCount, sizeBytes, createdAt, updatedAt.

POST /databases { name } — makes one; answers 201 with the database. A name is up to 80 characters.

GET /databases/:id — the database with its tables, each { name, rowCount }.

PATCH /databases/:id { name } — renames it.

DELETE /databases/:id — deletes it with its backups, jobs and notes; 204.

GET /databases/:id/schematables (with columns: name, type, pk, notNull, defaultValue, and the CREATE sql), indexes, foreignKeys, views.

GET /databases/:id/tables/:table?offset=0&limit=100 — a page of a table’s rows as { columns, rows }; limit is at most 500. For millions of rows, page by rowid in your own SQL instead.

POST /databases/:id/query { sql, params? } — runs one statement, up to 200000 characters, with ? placeholders bound from params in order. Answers { columns, rows, rowsRead, rowsWritten, changes, lastInsertRowid, truncated, wrote, ms }. A result is capped at 1000 rows and says truncated: true past that; page with LIMIT/OFFSET or a WHERE rowid > ?. A statement that writes counts as a change for the plan’s door.

A file goes to storage in parts, then an import job reads it. Any size.

POST /databases/:id/uploads { name, type } — begins an upload; answers 201 with { id, name, type, bytes, partBytes }. Send the file in parts of exactly partBytes bytes, the last one smaller.

PUT /databases/:id/uploads/:uploadId/parts/:n — one part, raw bytes as the body with Content-Length set, n from 1; answers { partNumber, etag }.

POST /databases/:id/uploads/:uploadId/complete { parts: [{ partNumber, etag }] } — closes the upload; answers the upload with its bytes.

POST /databases/:id/import { uploads: [id] } — starts an import job over up to 200 uploads: CSV, TSV, Excel, a SQL dump, a SQLite file, or a zip of any. Answers 202 with the job. The job’s result lists tables made (name, rows, from) and files skipped with a reason.

POST /databases/:id/export { kind, table? }kind is sql, sqlite or csv; csv takes a table and the others take none. Answers 202 with the job; when it is done, result has fileName and bytes.

GET /databases/:id/jobs — the database’s jobs, newest first.

GET /jobs/:jobId — one job: kind (import, export, snapshot, restore), status (queued, running, done, failed), progress, result, error. Poll it every second or so.

GET /jobs/:jobId/download — the finished export’s bytes, with Content-Disposition.

GET /databases/:id/snapshots — backups, newest first: id, label, auto, kind (bookmark, a point-in-time mark SQLite keeps for thirty days; dump, a SQL file), status, bytes, expiresAt, createdAt.

POST /databases/:id/snapshots { label? } — takes a dump; answers 201 with { snapshot, job }. The dump is ready when the job is done.

POST /databases/:id/snapshots/:snapshotId/restore — restores it, after taking a backup of now; answers { before, job }, where job is null when the restore was a bookmark and happened at once.

GET /databases/:id/snapshots/:snapshotId/download — a ready dump’s SQL.

DELETE /databases/:id/snapshots/:snapshotId204.

GET /databases/:id/queries, POST /databases/:id/queries { name, sql }, PATCH /databases/:id/queries/:queryId { name?, sql? }, DELETE /databases/:id/queries/:queryId — the SQL view’s saved queries.

Notes are what the person and the assistant know about a table or a column: about is a table name or table.column, text says what it means.

GET /databases/:id/notes, POST /databases/:id/notes { about, text }, PATCH /notes/:noteId { about?, text? }, DELETE /notes/:noteId.

The assistant is the console’s conversation; these routes drive it and are listed for completeness.

GET /databases/:id/runs and DELETE /databases/:id/runs — the thread and its clearing. GET /databases/:id/proposals — every change the assistant proposed. POST /databases/:id/run/stream { instruction?, after?, runId?, attach? } — starts or rejoins a run as a stream of events. POST /databases/:id/run/cancel — stops it. POST /proposals/:proposalId/apply and POST /proposals/:proposalId/discard — the person’s decision on a proposal.

GET /usage — this month’s runs, queries and tokens. GET /plan — the trial, the subscription, and what is on offer. GET /agents — the agents you allowed in through the MCP door for hosted clients: client id, name, when. DELETE /agents/:clientId — removes one: its tokens stop at once and it has to ask again. The subscription and store routes — POST /subscription/checkout, POST /subscription/confirm, POST /subscription/cancel, POST /subscription/resume, POST /subscription/portal, POST /purchases/apple, POST /purchases/google — are the console’s and the phone’s; a key does not need them.