Navigation

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 (default 1), limit or pageSize (default 50, max 500)
  • sort=Col:asc,Col2:desc — multi-sort by column name, direction defaults to asc
  • select=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 locked
  • 413 limitReached — submission exceeds bulkRowsPerJob
  • 429 limitReached resource:concurrentBulkJobs — already at the concurrent-job cap (Retry-After header 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.

Next