DataTables
DataTables are user-created relational tables — clients, inventory, invoices, anything. Custom fields on tasks can reference datatable rows as live data sources. The API exposes full CRUD on rows and read-only access on schemas.
Creating new datatables is UI-only — schema design has implications (formulas, autonumbers, indexes) that need a UI flow.
GET /datatables
Lists all datatables in the workspace.
{
"datatables": [
{ "id": "...", "name": "Customers", "row_count": 1234 },
{ "id": "...", "name": "Invoices", "row_count": 567 }
]
}
GET /datatables/{tableId}/schema
Returns column definitions, including read-only ones (formulas, autonumbers, relations to other tables).
{
"columns": [
{ "id": "col_abc...", "name": "Status", "type": "dropdown",
"options": [{ "id": "...", "label": "Active", "color": "#10b981" }] },
{ "id": "col_def...", "name": "Revenue", "type": "currency",
"currency_code": "USD" },
{ "id": "col_ghi...", "name": "Customer ID", "type": "autonumber",
"readonly": true, "format": "CUST-{NNNNN}" },
{ "id": "col_jkl...", "name": "Total Tax", "type": "formula",
"readonly": true, "formula": "Revenue * 0.08" }
]
}
The readonly flag is true for columns the API can't write — timestamps, autonumbers, formulas, cross-table relations. They appear in row payloads but POST/PATCH ignores attempted writes.
GET /datatables/{tableId}/rows
Lists rows with pagination, filtering, and sorting.
Query parameters:
page(default1),limitorpageSize(default50, max500)sort=Col:asc,Col2:desc— multi-sort by column name, direction defaults toascselect=Col1,Col2— projection by column name<ColumnName>=<value>— equality filter on that column (one per query string param)
# 50 most recently created customers in EU
curl -H "Authorization: Bearer kdt_..." \
'https://api.konduit.work/v1/datatables/<id>/rows?Region=EU&sort=Created:desc&limit=50'
Response:
{
"rows": [
{ "id": "...", "col_abc": "Active", "col_def": 12500, ... }
],
"total": 8421,
"page": 1,
"pageSize": 50
}
Row keys are column IDs (from the schema). Use the schema endpoint to map IDs to friendly names.
POST /datatables/{tableId}/query
For richer filters than the GET endpoint supports, POST a structured query body. Supported operators:
| Type | Operators |
|---|---|
| Equality | eq, neq |
| Text | contains, not_contains, starts_with, ends_with |
| Range | gt, gte, lt, lte, between |
| Membership | in, not_in |
| Null | is_empty, is_not_empty |
| Boolean | is_true, is_false |
POST /datatables/{tableId}/rows
Insert one row.
curl -H "Authorization: Bearer kdt_..." \
-H "Content-Type: application/json" \
-d '{ "data": { "col_abc": "Active", "col_def": 12500 } }' \
https://api.konduit.work/v1/datatables/<id>/rows
Response:
{ "id": "<new row id>", "data": { ... } }
POST /datatables/{tableId}/rows/bulk
Insert up to bulkRowsPerRequest rows in one call.
curl -H "Authorization: Bearer kdt_..." \
-H "Content-Type: application/json" \
-d '{ "rows": [
{ "col_abc": "Active", "col_def": 12500 },
{ "col_abc": "Churned", "col_def": 3200 },
{ "col_abc": "Lead", "col_def": 8400 }
] }' \
https://api.konduit.work/v1/datatables/<id>/rows/bulk
Response:
{ "success": true, "count": 3, "total": 3 }
Per-minute rate-limited (bulkInsertPerMinute). Validates each row against the schema; rows with type mismatches or invalid dropdown values fail individually with errors in the response.
Single-row operations
GET /datatables/{tableId}/rows/{rowId}
Returns one row with all columns (including read-only).
PATCH /datatables/{tableId}/rows/{rowId}
Partial update. Body:
{ "data": { "col_abc": "Active", "col_def": 13000 } }
Read-only columns are silently ignored. Returns the updated row.
DELETE /datatables/{tableId}/rows/{rowId}
Permanent. No soft-delete recovery in v1. The row is removed immediately and cannot be restored.
Bulk UPDATE / DELETE by filter
Not exposed in v1. Use the in-app SQL Console for filter-based mass updates. Public API users must iterate single-row PATCH/DELETE for now.
Async bulk insert (jobs)
For larger imports (up to 50,000 rows in one request), submit an async job. The server queues the work, processes it in the background, and exposes status via a polling endpoint. Use this for migrations from other tools, periodic ETL syncs, or any case where the synchronous /rows/bulk ceiling (250 rows/request) is too tight.
POST /datatables/{tableId}/jobs/insert
Submit a bulk-insert job.
curl -H "Authorization: Bearer kdt_..." \
-H "Content-Type: application/json" \
-d '{"rows": [
{ "col_abc": "Active", "col_def": 12500 },
... up to 50,000 rows ...
]}' \
https://api.konduit.work/v1/datatables/<tableId>/jobs/insert
Response (202 Accepted):
{
"job_id": "a1b2c3d4e5f6...",
"status": "queued",
"rows_submitted": 9876
}
Plan limits:
| Free | Pro | Business | |
|---|---|---|---|
| Async bulk insert | ❌ | ✅ | ✅ |
| Max rows per job | 0 | 50,000 | 50,000 |
| Concurrent jobs per workspace | 0 | 1 | 5 |
Errors:
403 limitReached— async bulk not on your plan, OR table is locked413 limitReached— submission exceedsbulkRowsPerJob429 limitReached resource:concurrentBulkJobs— already at the concurrent-job cap (Retry-Afterheader included)
GET /jobs/{jobId}
Poll a job's status. The job's workspace is checked against your API key's workspace — a key for workspace A can never read jobs from workspace B (returns 404).
curl -H "Authorization: Bearer kdt_..." \
https://api.konduit.work/v1/jobs/<jobId>
While running:
{
"id": "a1b2c3d4e5f6...",
"type": "api_bulk_insert",
"status": "processing",
"resource_id": "<tableId>",
"created_at": 1735689600,
"updated_at": 1735689610,
"completed_at": null,
"error_message": null,
"metadata": {
"table_name": "Customers",
"submitted": 9876,
"row_count": 4500,
"progress": 0.45,
"total": 9876,
"errors": 0
}
}
When complete:
{
"status": "done",
"completed_at": 1735689640,
"metadata": {
"row_count": 9870,
"progress": 1,
"total": 9876,
"errors": 1
},
"error_message": "Chunk 18: invalid value for col_def"
}
Status values: queued, processing, done, failed. error_message is populated when chunks fail (per-chunk errors are collected and joined; up to 3 are surfaced).
Polling pattern
Don't poll faster than once every 500ms — the server doesn't update progress more often than that. A 30k-row job typically completes in 5-15 seconds; very large jobs (50k rows) take 30-60 seconds.
async function waitForJob(jobId, apiKey) {
while (true) {
const res = await fetch(`https://api.konduit.work/v1/jobs/${jobId}`, {
headers: { "Authorization": `Bearer ${apiKey}` },
});
const job = await res.json();
if (job.status === "done" || job.status === "failed") return job;
await new Promise(r => setTimeout(r, 500));
}
}
Synchronous vs async — when to use which
| Need | Use |
|---|---|
| Sync ≤ 250 rows | POST /rows/bulk — immediate response |
| 250 < N ≤ 50,000 rows | POST /jobs/insert — async, single submission |
| > 50,000 rows | Multiple jobs, or use the in-app CSV import |
Attachments
POST /datatables/{tableId}/rows/{rowId}/attachments?col=<colId>
Upload a file to an attachment-typed column on a row. Same multipart shape as task attachments. Optional ?col=<colId> query param picks which attachment column receives the file (defaults to the first attachment column on the table).
curl -H "Authorization: Bearer kdt_..." \
-F "file=@./invoice.pdf" \
"https://api.konduit.work/v1/datatables/<tableId>/rows/<rowId>/attachments?col=col_files"
Response:
{
"attachment": {
"key": "dt/<tableId>/row/<rowId>/<colId>/invoice.pdf",
"filename": "invoice.pdf",
"size": 845321,
"contentType": "application/pdf",
"uploadedAt": 1735689600
},
"row": { "id": "...", "version": 4, "data": { ... } }
}
The attachment metadata is stored inside the row's column data (not a separate table) so it appears in subsequent GET /datatables/{id}/rows/{rowId} responses under <colId>.
DELETE /datatables/{tableId}/rows/{rowId}/attachments/{key}
Remove an attachment. {key} is the URL-encoded storage key from the attachment record. The route enforces that the key starts with dt/{tableId}/row/{rowId}/... — keys outside that scope return 400.