Skip to content

Web HTTP API ​

The web service (karet) hosts both the UI and the JSON API the UI talks to. The /api/* routes exist to back the browser UI and are not a stable public surface. They take a session cookie, or the worker token as a bearer token for machine callers.

Every /api/* route enforces auth except the explicitly-public ones below.

Auth ​

EndpointAuthPurpose
POST /api/auth/sign-in/usernamepublic, rate-limitedBody { username, password }. Creates a session row and sets the cookie; 429 + Retry-After when throttled.
POST /api/auth/sign-outsessionDeletes the session row and clears the cookie.
GET /api/auth/mesessionReturns { authenticated, user: { username, role, service } }. The instance role.

/api/auth/* is better-auth; the routes above are the ones the UI uses.

The admin credential is provisioned via KARET_ADMIN_PASSWORD_HASH; there is no setup or password-change endpoint. See Authentication.

Pipelines ​

EndpointPurpose
GET /api/pipelines{ pipelines: [{ id, name }] }, the pipelines the caller can see.
POST /api/pipelinesBody { name, template }. Provisions a new pipeline from a template and returns { ok, pipeline, name }, where pipeline is the generated id (p-<time>-<random>).
POST /api/pipelines/importMultipart upload of a .zip exported from another instance.
DELETE /api/pipelines/[slug]Delete the pipeline: its registry row, config versions and job rows, and every object under pipelines/<slug>/ in the pipelines and warehouse buckets. Lake files are kept, since other pipelines may read them.
PATCH /api/pipelines/[slug]Body { name }. Changes the display name; the id never changes.

Your own account ​

Any signed-in role. The username comes from the session, so this route only ever addresses the caller.

EndpointPurpose
PATCH /api/accountBody { displayName } or { currentPassword, newPassword }. A blank display name clears it. A password change needs the current one: 403 wrong_password if it does not match, and it ends every session.

Other people's accounts ​

Admin only, including the reads.

EndpointPurpose
GET /api/usersAccounts, with role and whether each came from the environment.
POST /api/usersBody { username, password, role }. Creates an account. 422 for a username outside 3 to 32 letters, numbers, underscores or dots, a password under 8 characters, or an unknown role; 409 if the name is taken.
GET /api/users/[username]What deleting the account would cost: { ownedPipelines }.
PATCH /api/users/[username]Body { role } or { password }. Either change ends that account's sessions. 422 for a password under 8 characters.
DELETE /api/users/[username]Deletes the account.

PATCH and DELETE refuse the bootstrap admin (bootstrap_admin), since the environment sets its role and password on every start. A role change and a deletion also refuse the caller's own account (self_role_change, self_delete); a password reset does not, and signs the caller out. All 422.

Workspace ​

EndpointPurpose
GET /api/settingsWorkspace UI settings (workspace name, starred pipelines).
PUT /api/settingsAdmin. Replaces the settings document, input sanitized. The workspace name is an instance setting, not a personal one.
GET /api/lake?prefix=One level of the lake bucket: folders and files.
PUT /api/lake?key=Upload one file (validated key, 100 MB cap). With auto-runs set up, the upload can trigger a debounced run.
POST /api/lakeBody { from, to }. Move an object (copy then delete).
DELETE /api/lake?key=Delete an object.
GET /api/lake/object?key=Download an object as an attachment.

Per-pipeline ​

EndpointPurpose
GET /api/p/[pipeline]/configThe live config, with its version in X-Karet-Config-Version.
PUT /api/p/[pipeline]/configSave a new version. Send back the version you loaded in X-Karet-Config-Version; a save against a stale version is refused with 412 stale_config.
GET /api/p/[pipeline]/config/historyVersions newest first, with author and note.
GET /api/p/[pipeline]/config/history/[version]One version, with its diff against the live config.
POST /api/p/[pipeline]/config/history/[version]/revertWrite that version forward as a new one.
GET /api/p/[pipeline]/roleThe caller's effective role on this pipeline, which a membership or ownership may differ from their instance role. Presentation only.
GET /api/p/[pipeline]/membersadmin here. { visibility, owner, members, accounts }.
PUT /api/p/[pipeline]/membersadmin here. One of { visibility }, { username, role } or { owner }. Answers with the resulting { visibility, owner, members }.
DELETE /api/p/[pipeline]/members?username=admin here. Revoke a grant.
POST /api/p/[pipeline]/validateForward to the worker's /config/validate.
GET /api/p/[pipeline]/dashboardsList published dashboards and drafts.
POST /api/p/[pipeline]/dashboardsCreate a draft from the v2 YAML template.
GET /api/p/[pipeline]/dashboards/[name]Fetch a dashboard's YAML (?draft=1 for the draft).
PUT /api/p/[pipeline]/dashboards/[name]Save YAML. Published saves run the full gate; ?draft=1 saves without validation.
POST /api/p/[pipeline]/dashboards/[name]/validateAdvisory full-gate validation (editor live feedback); always 200 with the verdict.
DELETE /api/p/[pipeline]/dashboards/[name]Delete draft and published objects.
POST /api/p/[pipeline]/dashboards/[name]/publishValidate a draft (schema, bindings, SQL) and publish it.
POST /api/p/[pipeline]/dashboards/[name]/dataRun all panel queries with filter params; returns per-panel results.
GET /api/p/[pipeline]/tablesPer-table metadata: name, schema, partition and dedup keys, file count, live version, row count (rowCount, null when it cannot be read) and when the live version was published (publishedAt). version: 0 means no run has published the table yet, so it is not queryable.
GET /api/p/[pipeline]/tables/[table]/rowsOne page of the table: { columns, rows, total, offset, limit }, where columns carries DuckDB types and total counts the rows matching the filter. ?where= is a SQL condition, ?sort= a column with ?dir=asc or desc (nulls last), ?offset= and ?limit= (default 100, at most 500). ?version= reads a retained version instead of the live one.
GET /api/p/[pipeline]/tables/[table]/versions{ versions }: the retained table versions, newest first, with file count, bytes and which is live.
POST /api/p/[pipeline]/tables/[table]/versions/[version]/restorePublish that version's file list as a new version. 404 if it is not retained.
POST /api/p/[pipeline]/queryBody { sql, versions?, limit? }. Runs SQL against the pipeline's warehouse tables, each exposed as a DuckDB relation. versions maps a table to a retained version to read instead of the live one. limit (at most 10,000) caps the rows returned. Returns { columns, rows, truncated }, where truncated says whether there were more rows than limit.
GET /api/p/[pipeline]/searchSearch one table; see Search. ?table= (required) and ?q=, the search; ?range= (15m, 1h, 4h, 24h, 7d, 30d, all; default all) or ?from= and ?to= in epoch seconds; ?timeField= a column, or none, to override the detected time column; ?offset= and ?limit= (default 50, at most 200). Returns { columns, timeField, total, offset, limit, events, range, histogram, latest }: events are { time, row }, newest first; histogram is { start, interval, buckets, counts } in seconds, or null without a time column; latest is the time of the newest match outside an empty range. A search that cannot run is 400 with error: "invalid_search" and a message.
GET /api/p/[pipeline]/search/fieldsThe search parameters plus ?field=. Returns { field, total, present, distinct, values }: the matching events, how many have the field, an approximate count of its distinct values, and its 10 most common values as { value, count }.
GET /api/p/[pipeline]/queriesList saved queries ({ queries: SavedQuery[] }).
POST /api/p/[pipeline]/queriesBody { name, sql }. Save a query under a unique name. 409 if the name is taken.
GET /api/p/[pipeline]/queries/[name]Fetch a single saved query.
DELETE /api/p/[pipeline]/queries/[name]Delete a saved query.
GET /api/p/[pipeline]/jobsJob history from Postgres merged with live queue state from Valkey. Active jobs carry a progress object (stage, file/mapping counters). Statuses: queued, running, completed, failed.
POST /api/p/[pipeline]/jobsTrigger a manual run. Enqueues onto the job stream and returns the initial queued record immediately; a worker claims and executes it. Runs are never clean: the route ignores a clean parameter.
GET /api/p/[pipeline]/jobs/eventsServer-sent events: the id of each job whose live state changed. The Jobs page uses it to refresh. 503 too_many_streams past 8 open streams per web process.
GET /api/p/[pipeline]/exportStream a .zip of the pipeline: its live config as pipeline.json, plus every object under the slug in the pipelines bucket (dashboards, saved queries). Lake files and warehouse tables are not included.

Webhooks ​

The S3-event receiver lives on the worker (POST /events/s3), not the web service. See Auto-runs and the Worker API.

Auth shape ​

The middleware (middleware.ts) checks only that a session cookie or the service token is present. Every route then resolves the caller against the pipeline it addresses, because a membership or ownership can change the answer and the edge cannot read the database. See per-pipeline access.

Only /api/auth/* bypasses the middleware; everything else requires a valid session cookie or the service token.

Error shape ​

Most routes return a JSON error body on 4xx/5xx:

json
{ "error": "<machine_code>", "message": "Human-readable detail" }

Common codes:

CodeMeaning
unauthorizedMissing or invalid session cookie or token.
bucket_not_foundAn S3 bucket doesn't exist. Most-common cause: an S3_BUCKET_* mistype.
s3_errorCatch-all for everything else from the S3 SDK.
pipeline_config_not_found404. The pipeline has no published config.
dashboard_not_foundThe slug exists but no dashboard at the given name.
invalid_slugThe pipeline id failed sanitization.
already_exists409. An account with that username exists.
not_foundAlso returned instead of 403 for a members-only pipeline the caller is not on, so its existence is not leaked.
stale_config412. The config moved on since the editor loaded it.
owner_access_is_permanent422. The owner cannot be removed from the member list or set below admin; transfer the pipeline instead.
bootstrap_admin422. That account comes from the environment and is restored on restart.
self_role_change, self_delete422. An admin cannot demote or delete the account they are signed in as.
not_owner403. Only the owner or an instance admin may transfer a pipeline.
query_error400. SQL failed. A table the pipeline configures but has never run reads "has no data yet. Run the pipeline to load it."

MIT licensed