Data API

Typed-table CRUD service with dynamic row schemas, full-text search, bulk operations, CSV import/export, and real-time SSE change streams.

✅ Self-service ready (2026-06-10)

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:

  1. an explicit X-Project-Id header (must belong to your org);
  2. the project the API key is bound to (mint with {"project_id": "…"} — the key dies with the project);
  3. 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.

MethodPathDescription
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.

MethodPathDescription
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.

MethodPathDescription
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.

MethodPathDescription
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);
});
Column Types

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.