Kaps API

The route reference for kaps: create and save kaps, manage versions, triggers, tokens and audience, and read and write a kap's database, file store and secrets. For the guide, see Kaps.

Authentication

Base URL: https://{org}.kapable.ai/v1/orgs/{org_id}. Authenticate with X-Session-Token: kses_…, Authorization: Bearer kses_…, or a platform session cookie. Every change (anything that is not a GET) requires X-Kapable-Client: <name> (1–128 bytes). Cookie-authenticated changes also require Origin: https://<actual-host>. Send Content-Type: application/json; bodies are capped at 2 MiB. Errors come back as {error:{code,message,details}} with a human message.

Kaps, files and versions

All paths below are relative to /v1/orgs/{org_id}.

MethodPathBody / result
POST /kaps {name,main_file,files:[{path,content}],audience?:"members"} → 201 {kap,version,address,bypass_token}. The token is shown once.
GET /kaps {kaps:[...]} — the active list.
GET /kaps/{id} {kap,files,version,triggers} for the current snapshot.
PUT /kaps/{id}/files {base_version_id,main_file?,files:[{path,content}]} → {version}. A save replaces the whole file set; a stale base gets 409 VERSION_CONFLICT.
GET /kaps/{id}/versions {versions:[...]}, newest sequence first, complete snapshots.
GET /kaps/{id}/versions/{version_id} {version}.
POST /kaps/{id}/versions/{version_id}/restore {base_version_id} → a new {version} with restored_from. Restoring never deletes earlier versions.
GET /kaps/{id}/diff?from={uuid}&to={uuid} {from,to,main_file:{before,after},files:[{path,kind,before,after}]}.
GET /kaps/{id}/history {events:[...]} in event sequence order, including deletion and recovery.
DELETE /kaps/{id} {kap} with deleted_at and recover_until. Deletion stops the runtime and enters recovery.
GET /recovery {kaps:[...]} — deleted kaps, including expired deadlines.
POST /kaps/{id}/recover No body → {kap} restored with the same versions, files and token states.

Example: create a kap

curl -X POST https://acme.kapable.ai/v1/orgs/$ORG_ID/kaps \
  -H "X-Session-Token: kses_..." \
  -H "X-Kapable-Client: my-script" \
  -H "Content-Type: application/json" \
  -d '{"name":"hello","main_file":"main.ts","files":[{"path":"main.ts","content":"export default (request: Request): Response => new Response('"'"'Hello'"'"')"}]}'

The create JSON can contain any files you like; the new kap is born with an enabled request trigger and one machine token named default.

Machine tokens and endpoint secrets

Paths relative to /v1/orgs/{org_id}.

MethodPathBody / result
GET /kaps/{id}/bypass-tokens {tokens:[{id,name,created_at,revoked_at}]} — metadata only; the bearer value is never listed.
POST /kaps/{id}/bypass-tokens {name} → 201 {bypass_token:{...,token}}, shown once.
DELETE /kaps/{id}/bypass-tokens/{token_id} Revoke durably → {bypass_token} metadata.
GET /kaps/{id}/endpoint-secrets Live and revoked secrets, the address template and the scheme. The address carries a {secret} placeholder, never a value.
POST /kaps/{id}/endpoint-secrets {name} → 201 with the secret under shown_once and nowhere else.
DELETE /kaps/{id}/endpoint-secrets/{secret_id} Revoke; both doors refuse it on the next request.

Machines call a kap with Authorization: Bearer kbt_… (a bypass token). Outside webhook senders that cannot hold a token use an endpoint secret: either the secret address https://{kap}.{org}.kapable.run/__kaps/e/{secret} or a signed request with X-Kaps-Signature: t=<unix seconds>,v1=<lowercase hex HMAC-SHA256>, where the signed message is kaps-endpoint-v1\nMETHOD\nTARGET\nt\nbody (method, path with query, timestamp, then the exact body bytes). Requests more than 300 seconds from the service clock are refused as stale.

Audience

MethodPathBody / result
GET /v1/orgs/{org}/kaps/{kap}/audience {policy:{audience,invited,revision},events:[...]} — audience history only.
PUT /v1/orgs/{org}/kaps/{kap}/audience Full policy replacement with its displayed revision, e.g. {"audience":"invited","invited":["person@corp.example","@corp.example"],"base_revision":0} → {policy,changed:true}. Widening needs a member; going public additionally needs confirm_public:true. A stale write gets 409 AUDIENCE_CONFLICT.

Triggers

The request trigger's switch is part of the core table (paths relative to /v1/orgs/{org_id}); schedule and inbox triggers and their runs live under /v1/orgs/{org}/kaps/{kap}.

MethodPathBody / result
GET /kaps/{id}/triggers {triggers:[...]}.
PUT /kaps/{id}/triggers/{trigger_id} {kind:"request",file_path:<main>,enabled} → {trigger}; pausing and resuming are attributed.
GET /triggers Trigger metadata, the kap's mail address, and deployment limits.
POST /triggers {kind:"schedule",file_path,interval_seconds} or {kind:"schedule",file_path,cron} → 201 {trigger}.
POST /triggers {kind:"inbox",file_path} → 201 {trigger}.
POST /triggers/{id}/pause or /resume No body; attributed change.
DELETE /triggers/{id} No body; removes the attachment, keeps run receipts.
POST /triggers/{id}/run-now No body; {run}, including a missed or failed reason.
GET /trigger-runs?before={run_id} Up to 100 newest rows with has_more and next_cursor.
GET /trigger-runs/{id} The run plus its retained parsed payload, or an explicit expiry notice.
POST /trigger-runs/{id}/acknowledge No body; attributed acknowledgement of a failure.

Schedule cron has five numeric UTC fields with lists, ranges and positive steps; the default shortest cadence is 15 minutes. A kap's inbox address is {kap}@{org}.kapable.email; incoming messages are capped at 30 MiB and rate-limited per kap per hour.

Runs, logs, and switching a kap off

MethodPathBody / result
GET /v1/orgs/{org}/kaps/{kap}/runs?limit={n}&cursor={c}&failed_since={t} Newest first; runs, next_cursor, has_more, status, message. 20 by default, 100 at most; failed_since is an RFC 3339 instant narrowing to failed runs. Every row carries a plain-words outcome (outcome, what_to_do) and the raw detail behind it.
GET /v1/kaps/{id}/log?after=<opaque-cursor> Up to 200 retained console/trace rows with lines, next_cursor, has_more, status, and a human message.
POST /v1/orgs/{org}/kaps/{kap}/disable {"reason":"..."} (required) — the kap serves nothing, runs no schedule and accepts no email; every refusal names who switched it off, when, and the reason. Switch it back on with POST .../enable.
GET /v1/orgs/{org}/usage What the organisation consumed, per kap, per day, for the last 30 days. A measure, not a ceiling: "enforced": false.

Per-kap database, files and secrets

Every kap gets its own PostgreSQL schema, file store and secret set. The same routes accept two bases: /v1/orgs/{org_id}/kaps/{kap_id}, or /v1/kaps/{kap_id} (the organisation comes from the authenticated member). The second base also accepts the kap's kst1_ store capability token, but only for /db/... and /store/....

MethodPathBody / result
POST /db/query {sql,params?:[]} → {rows:[],row_count}. One PostgreSQL statement.
GET /db/tables {tables:[{name,columns:[{name,type,nullable,default}]}]}.
GET /db/tables/{table}/rows {rows:[],row_count} — the complete result, or a named refusal.
POST /db/tables/{table}/rows An object of column values → 201 {rows:[inserted],row_count:1}. Defaults apply to omitted columns.
GET /db/tables/{table}/rows/{key} One row, selected by the table's single-column primary key.
PUT /db/tables/{table}/rows/{key} An object of changed columns; explicit null stays null.
DELETE /db/tables/{table}/rows/{key} No body → the deleted row; an absent key gives 404.
GET /db/export A PostgreSQL SQL dump, application/sql, attachment store.sql.
GET /store/files {files:[{key,size,last_modified,etag}]}; all pages are followed for you.
PUT /store/files/object?key={encoded_key} Raw body and Content-Type → {stored:true,key}.
GET /store/files/object?key={encoded_key} Raw bytes, preserved Content-Type, attachment disposition.
DELETE /store/files/object?key={encoded_key} No body → {deleted:true,key}.
GET /store/files/export A tar stream, attachment files.tar.
GET /secrets {secrets:[{name,group_id,group_name}],refresh_pending} — names only, never values.
PUT /secrets/{NAME} {value:string} → {name,set:true,refresh_pending:false}. The value is never returned.
DELETE /secrets/{NAME} No body → {name,deleted:true,refresh_pending:false}.
GET /secret-groups The attached {groups:[{id,name}]}.
PUT /secret-groups/{group_id} No body → {attached:true} after the current-version refresh.
DELETE /secret-groups/{group_id} No body → {detached:true} after the refresh.
GET /export/manifest {format,kap,versions,secret_names,store:{sql,files}} with links to the separate store downloads.

Example: query a kap's database

curl -X POST https://acme.kapable.ai/v1/kaps/$KAP_ID/db/query \
  -H "X-Session-Token: kses_..." \
  -H "X-Kapable-Client: my-script" \
  -H "Content-Type: application/json" \
  -d '{"sql":"SELECT id, body FROM notes WHERE id = $1","params":[1]}'
# → {"rows":[{"id":1,"body":"hello"}],"row_count":1}
SQL notes

Parameters support null, bool, int2/4/8, float4/8, text/varchar/name, UUID and JSON/JSONB, matched to the prepared statement's expected type. Cast anything else through text, e.g. $1::text::numeric. Results additionally support date/timestamp and bytea (base64url). Duplicate result column names are refused; use aliases. Tables without a single-column primary key need /db/query for row changes.

Organisation secret groups

A secret group is a set of secrets shared with every kap attached to it. Base: /v1/orgs/{org_id}/secret-groups.

MethodPathBody / result
GET /v1/orgs/{org_id}/secret-groups {groups:[{id,name}]}.
POST /v1/orgs/{org_id}/secret-groups {name} → 201 {group:{id,name}}; a group can exist before the first kap.
GET /{group_id}/secrets Names and their group origin only.
PUT /{group_id}/secrets/{NAME} {value} → name/set receipt; refreshes every attached active kap.
DELETE /{group_id}/secrets/{NAME} No body → name/deleted receipt; refreshes every attached active kap.
GET /{group_id}/history Attributed metadata events; no values.
Secret names

Secret names are uppercase ASCII environment identifiers starting with a letter, at most 128 bytes. Runtime names (PATH, HOME, DENO_*, NODE_* and friends) and KAP_* are reserved. Effective names must be unique across the kap and every attached group; a conflict is refused with 409 SECRET_NAME_CONFLICT and the whole change is left unapplied.

Next

Kaps guide · Authentication