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.
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}.
| Method | Path | Body / 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}.
| Method | Path | Body / 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
| Method | Path | Body / 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}.
| Method | Path | Body / 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
| Method | Path | Body / 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/....
| Method | Path | Body / 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}
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.
| Method | Path | Body / 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 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.