Data API
Typed-table CRUD service with dynamic row schemas, full-text search, bulk operations, CSV import/export, and real-time SSE change streams.
The Data API is fully reachable with a plain sk_live_ API key —
no extra setup. Every org gets a default project (a private
Postgres schema) at signup, and data requests resolve it automatically.
API keys are scope-gated: read covers
GET/list/SSE, write covers row mutations and table DDL.
Quickstart
Four calls from zero to live data — no headers beyond the key:
# 1. Sign up (returns a session token; org_slug is required)
curl -X POST https://api.kapable.ai/v1/auth/signup \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"...","org_name":"Acme","org_slug":"acme"}'
# 2. Mint an API key (secret is shown ONCE)
curl -X POST https://api.kapable.ai/v1/auth/api-keys \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H 'Content-Type: application/json' -d '{"name":"my-key"}'
# 3. Create a table (default project resolved server-side)
curl -X POST https://api.kapable.ai/v1/tables \
-H "x-api-key: $SK_LIVE_KEY" -H 'Content-Type: application/json' \
-d '{"name":"todos","columns":[{"name":"title","col_type":"text"},{"name":"done","col_type":"boolean"}]}'
# 4. CRUD rows
curl -X POST https://api.kapable.ai/v1/todos -H "x-api-key: $SK_LIVE_KEY" \
-H 'Content-Type: application/json' -d '{"title":"first","done":false}'
curl https://api.kapable.ai/v1/todos -H "x-api-key: $SK_LIVE_KEY"
Or with the SDK (@kapable/sdk ≥ 0.3.0):
const client = new KapableClient({ baseUrl: 'https://api.kapable.ai', apiKey: 'sk_live_…' });
await client.data.createTable({ name: 'todos', columns: [{ name: 'title', col_type: 'text' }] });
await client.data.createRows('todos', { title: 'first' });
const rows = await client.data.listRows('todos');
Errors use the standard wrapper
{"error":{"code":"…","message":"…"}}. The ones you'll meet first:
403 FORBIDDEN (key lacks the write scope),
404 NOT_FOUND (querying a table before creating it), and
400 for reserved path names (tables,
projects, _meta, … can't be table names).
Projects & request resolution
A project is a private Postgres schema your tables live in.
Signup seeds a default project (slug default). Each data request
resolves its project in this order:
- an explicit
X-Project-Idheader (must belong to your org); - the project the API key is bound to (mint with
{"project_id": "…"}— the key dies with the project); - your org's default project.
Discover project ids with GET /v1/projects (works with keys and
sessions; SDK: client.projects.list()). Create/delete projects from
an org-member session (client.projects.create({name}) /
client.projects.delete(id) — delete requires ?confirm=true
and drops the schema irreversibly).
Tables
Tables define the schema for your data. Each table has typed columns that are enforced on write. Schemas can be migrated (add/drop columns) without downtime.
| Method | Path | Description |
|---|---|---|
| GET | /v1/tables | List all tables for the org |
| POST | /v1/tables | Create a new table with column definitions |
| DELETE | /v1/tables/{table} | Drop a table and all its data |
| POST | /v1/tables/{table}/migrate | Migrate schema (add/drop columns) |
SDK Examples
// Create a table. Field is `col_type` (not `type`); the live API expects
// LOWERCASE values: text, integer, big_int, float, boolean, timestamp, uuid, json.
// (The SDK's published ColumnType union is PascalCase and is being corrected —
// until then, pass the lowercase string.)
const table = await client.data.createTable({
name: 'contacts',
columns: [
{ name: 'email', col_type: 'text', nullable: false, unique: true },
{ name: 'name', col_type: 'text' },
{ name: 'age', col_type: 'integer' },
{ name: 'active', col_type: 'boolean', default_value: true },
],
});
// List tables
const { data: tables } = await client.data.listTables();
// Add a column
await client.data.migrateTable('contacts', {
add: [{ name: 'company', type: 'text' }],
});
// Drop a table
await client.data.dropTable('contacts');
// Rust SDK
use kapable_sdk::data::types::*;
let table = client.data().create_table(&CreateTableRequest {
name: "contacts".into(),
columns: vec![
ColumnDef { name: "email".into(), col_type: "text".into(), required: Some(true), default: None },
ColumnDef { name: "name".into(), col_type: "text".into(), required: None, default: None },
ColumnDef { name: "age".into(), col_type: "integer".into(), required: None, default: None },
],
}).await?;
let tables = client.data().list_tables().await?;
client.data().migrate_table("contacts", &MigrateTableRequest {
add: Some(vec![ColumnDef { name: "company".into(), col_type: "text".into(), required: None, default: None }]),
drop: None,
}).await?;
client.data().drop_table("contacts").await?;
Row CRUD
Rows are dynamic JSON objects whose shape matches the table schema. The Data service validates column types on write and returns errors for type mismatches.
| Method | Path | Description |
|---|---|---|
| GET | /v1/{table} | List rows with pagination and filters |
| POST | /v1/{table} | Create one or more rows |
| GET | /v1/{table}/{id} | Get a single row by UUID |
| PUT | /v1/{table}/{id} | Replace a row (full update) |
| PATCH | /v1/{table}/{id} | Patch a row (partial update) |
| DELETE | /v1/{table}/{id} | Delete a row |
| POST | /v1/{table}/search | Full-text search across all text columns |
SDK Examples
// Create a single row
const row = await client.data.createRows('contacts', {
email: 'alice@example.com',
name: 'Alice',
age: 30,
});
// Create multiple rows (batch)
const rows = await client.data.createRows('contacts', [
{ email: 'bob@example.com', name: 'Bob', age: 25 },
{ email: 'carol@example.com', name: 'Carol', age: 35 },
]);
// List with pagination
const { data, total } = await client.data.listRows('contacts', {
limit: 20,
offset: 0,
sort: 'name',
order: 'asc',
});
// Get a single row
const contact = await client.data.getRow('contacts', rowId);
// Patch a row (partial update)
await client.data.patchRow('contacts', rowId, {
name: 'Alice Updated',
});
// Full-text search
const results = await client.data.searchRows('contacts', {
query: 'alice',
limit: 10,
});
// Auto-paginate
for await (const row of client.data.paginateRows('contacts')) {
console.log(row.email, row.name);
}
// Rust SDK
use serde_json::json;
let row = client.data().create_rows("contacts", &json!({
"email": "alice@example.com",
"name": "Alice",
"age": 30
})).await?;
let result = client.data().list_rows("contacts", &DataListParams {
limit: Some(20),
offset: Some(0),
sort: Some("name".into()),
order: Some("asc".into()),
..Default::default()
}).await?;
let contact = client.data().get_row("contacts", row_id).await?;
client.data().patch_row("contacts", row_id, &json!({
"name": "Alice Updated"
})).await?;
let search = client.data().search_rows("contacts", &SearchRowsRequest {
query: "alice".into(),
limit: Some(10),
..Default::default()
}).await?;
Bulk Operations
Bulk endpoints accept arrays of IDs or objects and operate in a single database transaction for consistency.
| Method | Path | Description |
|---|---|---|
| POST | /v1/{table}/bulk/update | Update multiple rows by ID |
| POST | /v1/{table}/bulk/delete | Delete multiple rows by ID |
| POST | /v1/{table}/import | Import rows from CSV |
| POST | /v1/{table}/export | Export all rows as JSON (max 10,000) |
SDK Examples
// Bulk update
await client.data.bulkUpdate('contacts', {
rows: [
{ id: 'uuid-1', data: { active: false } },
{ id: 'uuid-2', data: { active: false } },
],
});
// Bulk delete
await client.data.bulkDelete('contacts', {
ids: ['uuid-1', 'uuid-2', 'uuid-3'],
});
// CSV import
const csv = `email,name,age
dave@example.com,Dave,40
eve@example.com,Eve,28`;
const imported = await client.data.importCsv('contacts', csv);
// Export all rows as JSON
const allRows = await client.data.exportData('contacts');
// Rust SDK
client.data().bulk_update("contacts", &BulkUpdateRequest {
rows: vec![
BulkUpdateRow { id: uuid1, data: json!({"active": false}) },
BulkUpdateRow { id: uuid2, data: json!({"active": false}) },
],
}).await?;
client.data().bulk_delete("contacts", &BulkDeleteRequest {
ids: vec![uuid1, uuid2, uuid3],
}).await?;
SSE Change Streams
Subscribe to real-time change events via Server-Sent Events. The stream emits insert, update, and delete events as they happen.
| Method | Path | Description |
|---|---|---|
| GET | /v1/sse | SSE stream for table change notifications (query: tables=a,b) |
Usage
// Subscribe to changes on the "contacts" table
const es = new EventSource(
'https://api.kapable.ai/v1/sse?tables=contacts',
// Add auth header via polyfill or proxy
);
es.addEventListener('insert', (e) => {
const row = JSON.parse(e.data);
console.log('New contact:', row);
});
es.addEventListener('update', (e) => {
const row = JSON.parse(e.data);
console.log('Updated:', row.id);
});
es.addEventListener('delete', (e) => {
const { id } = JSON.parse(e.data);
console.log('Deleted:', id);
});
Supported col_type values (lowercase, exactly as the API validates):
text, integer, big_int, float,
boolean, timestamp, uuid, json.
Per-column flags: nullable, default_value,
unique, indexed.