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
| Endpoint | Auth | Purpose |
|---|---|---|
POST /api/auth/sign-in/username | public, rate-limited | Body { username, password }. Creates a session row and sets the cookie; 429 + Retry-After when throttled. |
POST /api/auth/sign-out | session | Deletes the session row and clears the cookie. |
GET /api/auth/me | session | Returns { 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
| Endpoint | Purpose |
|---|---|
GET /api/pipelines | { pipelines: [{ id, name }] }, the pipelines the caller can see. |
POST /api/pipelines | Body { 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/import | Multipart 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.
| Endpoint | Purpose |
|---|---|
PATCH /api/account | Body { 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.
| Endpoint | Purpose |
|---|---|
GET /api/users | Accounts, with role and whether each came from the environment. |
POST /api/users | Body { 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
| Endpoint | Purpose |
|---|---|
GET /api/settings | Workspace UI settings (workspace name, starred pipelines). |
PUT /api/settings | Admin. 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/lake | Body { 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
| Endpoint | Purpose |
|---|---|
GET /api/p/[pipeline]/config | The live config, with its version in X-Karet-Config-Version. |
PUT /api/p/[pipeline]/config | Save 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/history | Versions 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]/revert | Write that version forward as a new one. |
GET /api/p/[pipeline]/role | The caller's effective role on this pipeline, which a membership or ownership may differ from their instance role. Presentation only. |
GET /api/p/[pipeline]/members | admin here. { visibility, owner, members, accounts }. |
PUT /api/p/[pipeline]/members | admin 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]/validate | Forward to the worker's /config/validate. |
GET /api/p/[pipeline]/dashboards | List published dashboards and drafts. |
POST /api/p/[pipeline]/dashboards | Create 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]/validate | Advisory 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]/publish | Validate a draft (schema, bindings, SQL) and publish it. |
POST /api/p/[pipeline]/dashboards/[name]/data | Run all panel queries with filter params; returns per-panel results. |
GET /api/p/[pipeline]/tables | Per-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]/rows | One 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]/restore | Publish that version's file list as a new version. 404 if it is not retained. |
POST /api/p/[pipeline]/query | Body { 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]/search | Search 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/fields | The 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]/queries | List saved queries ({ queries: SavedQuery[] }). |
POST /api/p/[pipeline]/queries | Body { 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]/jobs | Job 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]/jobs | Trigger 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/events | Server-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]/export | Stream 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:
{ "error": "<machine_code>", "message": "Human-readable detail" }Common codes:
| Code | Meaning |
|---|---|
unauthorized | Missing or invalid session cookie or token. |
bucket_not_found | An S3 bucket doesn't exist. Most-common cause: an S3_BUCKET_* mistype. |
s3_error | Catch-all for everything else from the S3 SDK. |
pipeline_config_not_found | 404. The pipeline has no published config. |
dashboard_not_found | The slug exists but no dashboard at the given name. |
invalid_slug | The pipeline id failed sanitization. |
already_exists | 409. An account with that username exists. |
not_found | Also returned instead of 403 for a members-only pipeline the caller is not on, so its existence is not leaked. |
stale_config | 412. The config moved on since the editor loaded it. |
owner_access_is_permanent | 422. The owner cannot be removed from the member list or set below admin; transfer the pipeline instead. |
bootstrap_admin | 422. That account comes from the environment and is restored on restart. |
self_role_change, self_delete | 422. An admin cannot demote or delete the account they are signed in as. |
not_owner | 403. Only the owner or an instance admin may transfer a pipeline. |
query_error | 400. SQL failed. A table the pipeline configures but has never run reads "has no data yet. Run the pipeline to load it." |