JustPaid Workflows
API Reference

Tables API

Create, manage, and read rows in user-managed data tables

Tables API

Data tables are tenant-scoped, schema-flexible tables you can use to persist workflow state, build small lookups, or maintain queues that humans resolve out-of-band. The same tables that show up under Data in the Studio (route /data) and that the Data Table block operates on are exposed via this REST API.

Every endpoint requires the standard x-api-key header described in Authentication. Reads require the workflows:read scope; writes require workflows:write.

Every table is owned by exactly one company. The API never returns rows belonging to a different company than the one the API key is scoped to.

Resource Shape

A table:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Processed Invoices",
  "description": "Deduplication table for Stripe invoice events",
  "columns": [
    { "id": "stripeInvoiceId", "name": "stripeInvoiceId", "type": "text", "required": true },
    { "id": "amountCents", "name": "amountCents", "type": "number" }
  ],
  "schema": {
    "columns": [
      { "id": "stripeInvoiceId", "name": "stripeInvoiceId", "type": "text", "required": true },
      { "id": "amountCents", "name": "amountCents", "type": "number" }
    ]
  },
  "metadata": {},
  "maxRows": 10000,
  "rowCount": 47,
  "createdAt": "2024-01-15T10:00:00.000Z",
  "updatedAt": "2024-01-20T08:12:00.000Z"
}

A row:

{
  "id": "660e8400-e29b-41d4-a716-446655440001",
  "data": {
    "stripeInvoiceId": "in_1234567890",
    "amountCents": 9900
  },
  "position": null,
  "createdAt": "2024-01-15T10:30:00.000Z",
  "updatedAt": "2024-01-15T10:30:00.000Z"
}

Column Types

TypeDescription
textFree-form string
numberNumeric value
booleanTrue or false
dateISO-8601 date or datetime string
selectOne value from an enumerated set (provide options: string[])
urlURL (string)
emailEmail address (string)
jsonArbitrary nested JSON value

Columns are advisory — the underlying storage is JSONB and any keys you write in data are preserved, even if they're not declared in columns. The Studio uses columns to render forms and CSV importers; workflows that write rows via the API can include extra fields without redefining the schema.

List Tables

GET /api/v1/tables

Returns every table the API key's company owns, ordered by createdAt descending.

Example

curl -X GET "http://localhost:3000/api/v1/tables" \
  -H "x-api-key: YOUR_API_KEY"

Response

{
  "success": true,
  "data": [
    { "id": "...", "name": "Processed Invoices", "...": "..." },
    { "id": "...", "name": "Pending Approvals", "...": "..." }
  ]
}

Create Table

POST /api/v1/tables

Request Body

FieldTypeRequiredDescription
namestringYesDisplay name (1-255 chars).
descriptionstringNoFree-form description.
columnsobject[]NoColumn definitions (see Column Types).
schemaobjectNoFree-form schema metadata. When columns is provided it is merged into schema.columns.
metadataobjectNoFree-form metadata (used by the Studio for view preferences).
maxRowsnumberNoRow cap (1 - 100,000, default 10,000).

Example

curl -X POST "http://localhost:3000/api/v1/tables" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Processed Invoices",
    "description": "Deduplicate Stripe events",
    "columns": [
      { "name": "stripeInvoiceId", "type": "text", "required": true },
      { "name": "amountCents", "type": "number" }
    ],
    "maxRows": 25000
  }'

Response (201 Created)

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Processed Invoices",
    "columns": [
      { "id": "stripeInvoiceId", "name": "stripeInvoiceId", "type": "text", "required": true },
      { "id": "amountCents", "name": "amountCents", "type": "number" }
    ],
    "maxRows": 25000,
    "rowCount": 0,
    "createdAt": "2024-01-15T10:00:00.000Z",
    "updatedAt": "2024-01-15T10:00:00.000Z"
  }
}

Each column without an explicit id is assigned one matching its name so it can be referenced stably even if the display name later changes.

Get Table

GET /api/v1/tables/:id

Returns a single table including its current rowCount and columns.

Returns 404 NOT_FOUND if the table does not exist or belongs to another company.

Update Table

PATCH /api/v1/tables/:id

Patch one or more table fields. Any field omitted from the body is left unchanged.

Request Body

FieldTypeDescription
namestringRename the table.
descriptionstringReplace description.
columnsobject[]Replace schema.columns entirely.
schemaobjectReplace the full schema JSONB.
metadataobjectReplace metadata JSONB.
maxRowsnumberAdjust the row cap (1 - 100,000).

columns is a full replacement of schema.columns. To add a column, send the existing columns plus the new one — partial updates are not supported.

Delete Table

DELETE /api/v1/tables/:id

Deletes the table and cascades to every row it contains.

{ "success": true, "data": { "deleted": true } }

List Rows

GET /api/v1/tables/:id/rows

Query Parameters

ParameterTypeDefaultDescription
limitnumber501 - 100 rows per page.
offsetnumber0Offset within the result set.

Rows are returned in position order (when set) then createdAt ascending.

Response

{
  "success": true,
  "data": [
    {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "data": { "stripeInvoiceId": "in_1234567890", "amountCents": 9900 },
      "position": null,
      "createdAt": "2024-01-15T10:30:00.000Z",
      "updatedAt": "2024-01-15T10:30:00.000Z"
    }
  ]
}

Create Row

POST /api/v1/tables/:id/rows

Request Body

FieldTypeRequiredDescription
dataobjectYesRow payload. Keys beyond declared columns are preserved.

Returns 201 Created with the new row, or 400 BAD_REQUEST with "Table has reached its maximum row limit of N" when the table is full.

Example

curl -X POST "http://localhost:3000/api/v1/tables/$TABLE_ID/rows" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "data": { "stripeInvoiceId": "in_1234567890", "amountCents": 9900 } }'

Update Row

PATCH /api/v1/tables/:id/rows/:rowId

Replaces the entire data field with the value in the request body. To merge fields, fetch the row first and POST the merged object back.

Request Body

FieldTypeRequiredDescription
dataobjectYesNew row payload. Replaces the existing data JSONB.

Returns 404 NOT_FOUND if the row does not belong to the given table or company.

Delete Row

DELETE /api/v1/tables/:id/rows/:rowId

Deletes one row and decrements rowCount on the table.

{ "success": true, "data": { "deleted": true } }

Error Responses

HTTPCodeTrigger
400BAD_REQUESTValidation failure or row-limit reached
401UNAUTHORIZEDMissing or invalid x-api-key
403FORBIDDENAPI key lacks workflows:read / workflows:write
404NOT_FOUNDTable or row does not exist for this company
429RATE_LIMITEDOver the read (1000/min) or write (100/min) bucket

Audit Logging

POST, PATCH, and DELETE on /api/v1/tables/:id emit audit events with actions table.created, table.updated, and table.deleted. Row writes do not emit per-row audit events — query /api/v1/audit-logs for table-level events only.

Use From Workflows

Workflows shouldn't usually call this API directly — use the Data Table block instead. The block dispatches to the same storage through the workflow runtime, which handles tenant scoping, validation, and row-count bookkeeping inside a single transaction.

On this page