Skip to main content

GET /api/v1/schemas

List all schemas with their fields. Supports cursor-based pagination.

Query Parameters

ParameterTypeDefaultDescription
limitinteger20Max results per page
cursorstringCursor for pagination

Response

{
  "data": [
    {
      "id": "schema_001",
      "tenantId": "tenant_001",
      "name": "customers",
      "displayName": "Customers",
      "description": "Core customer entity",
      "tableName": "ds_customers",
      "entityType": "customer",
      "schemaType": "customer",
      "status": "active",
      "linkedSchemaId": null,
      "joinMapping": null,
      "summaryColumns": null,
      "autoEnrich": false,
      "fields": [
        {
          "id": "field_001",
          "schemaId": "schema_001",
          "name": "email",
          "displayName": "Email",
          "dataType": "varchar",
          "length": 255,
          "precision": null,
          "scale": null,
          "isNullable": false,
          "isPrimaryKey": false,
          "isUnique": true,
          "isPredictor": false,
          "defaultValue": null,
          "description": "",
          "ordinal": 1
        }
      ],
      "createdAt": "2026-01-10T08:00:00.000Z",
      "updatedAt": "2026-01-10T08:00:00.000Z"
    }
  ],
  "pagination": {
    "total": 5,
    "hasMore": false,
    "limit": 50,
    "cursor": null
  }
}

POST /api/v1/schemas

Create a new schema. This creates both a metadata record and a real PostgreSQL table via DDL.
Creating a schema executes CREATE TABLE against the database. If the DDL fails, the metadata record is automatically rolled back.

Request Body

FieldTypeRequiredDescription
namestringYesSchema name (auto-sanitized to lowercase alphanumeric + underscores)
displayNamestringYesHuman-readable name
descriptionstringNoDescription
entityTypestringNoEntity type: "customer", "account", "custom". Default: "custom"
schemaTypestringNoSchema type: "customer", "transaction", "event", etc. Default: "customer"
linkedSchemaIdstringNoID of a customer-type schema to link to (must be in the same tenant)
joinMappingobjectNoJoin configuration for linking to the parent schema
summaryColumnsarrayNoColumn names to display in summary views. Default: []
autoEnrichbooleanNoWhether to auto-enrich from this schema during decision flows. Default: false
customPrimaryKeyobjectNoSugar for declaring a natural primary key column. Shape: { "name": "customer_id", "dataType": "varchar" }. When set, the auto id BIGSERIAL column is skipped and the named column is promoted to a fields[0] entry with isPrimaryKey: true. Equivalent to passing it explicitly in fields[] — pick whichever is cleaner.
fieldsarrayNoInitial field definitions (see field object below)

Field Object

FieldTypeDefaultDescription
namestringColumn name
displayNamestringnameDisplay label
dataTypestring"varchar"PostgreSQL data type
lengthintegernullLength for varchar/char types
precisionintegernullPrecision for decimal types
scaleintegernullScale for decimal types
isNullablebooleantrueWhether the column allows NULL
isPrimaryKeybooleanfalsePrimary key constraint
isUniquebooleanfalseUnique constraint
defaultValuestringnullDefault value expression
descriptionstring""Field description

Example

curl -X POST https://playground.kaireonai.com/api/v1/schemas \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: my-tenant" \
  -d '{
    "name": "customers",
    "displayName": "Customers",
    "entityType": "customer",
    "fields": [
      { "name": "email", "dataType": "varchar", "length": 255, "isUnique": true },
      { "name": "loan_amount", "dataType": "decimal", "precision": 12, "scale": 2 },
      { "name": "tenure_months", "dataType": "integer" }
    ]
  }'

Example with customPrimaryKey sugar

curl -X POST https://playground.kaireonai.com/api/v1/schemas \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: my-tenant" \
  -d '{
    "name": "accounts",
    "displayName": "Accounts",
    "entityType": "account",
    "schemaType": "collection",
    "customPrimaryKey": { "name": "account_id", "dataType": "varchar" }
  }'
This is equivalent to passing fields: [{ "name": "account_id", "dataType": "varchar", "isPrimaryKey": true, "isNullable": false, "isUnique": true }] directly — pick whichever is more ergonomic for your use case. When set, the table is not given the default id BIGSERIAL auto-key. Response: 201 Created

PUT /api/v1/schemas

Update an existing schema’s metadata. Only provided fields are changed. This does not modify the underlying PostgreSQL table structure — use the field endpoints for DDL changes.

Request Body

FieldTypeRequiredDescription
idstringYesSchema ID to update
displayNamestringNoUpdated display name
descriptionstringNoUpdated description
entityTypestringNoUpdated entity type
schemaTypestringNoUpdated schema type
linkedSchemaIdstringNoUpdated linked schema ID (must reference a customer-type schema in the same tenant)
joinMappingobjectNoUpdated join configuration
summaryColumnsarrayNoUpdated summary columns
autoEnrichbooleanNoUpdated auto-enrich flag
Response: 200 OK with the updated schema object including fields.

Error Codes

CodeReason
400Missing id, or linkedSchemaId references an invalid schema
404Schema not found

DELETE /api/v1/schemas

Delete a schema and drop its backing PostgreSQL table.

Query Parameters

ParameterTypeRequiredDescription
idstringYesSchema ID to delete
This drops the underlying database table. All data in the table is permanently lost.
Response: 204 No Content

GET /api/v1/schemas/fields

List the fields (columns) for a single schema. Used by the Schema Joins editor and any other UI surface that needs a column list without pulling the full schema object.

Query Parameters

ParameterTypeRequiredDescription
schemaIdstringYesSchema ID whose fields you want
Response: 200 OK with an array of field objects, sorted by ordinal.
[
  {
    "id": "...",
    "schemaId": "...",
    "name": "customer_id",
    "displayName": "customer_id",
    "dataType": "varchar",
    "length": 64,
    "isNullable": false,
    "isPrimaryKey": true,
    "ordinal": 1
  }
]
Errors:
  • 400 Bad RequestschemaId query parameter missing
  • 404 Not Found — schema does not exist in caller’s tenant

POST /api/v1/schemas/fields

Add a column to an existing schema. Executes ALTER TABLE ADD COLUMN.

Request Body

FieldTypeRequiredDescription
schemaIdstringYesParent schema ID
namestringYesColumn name
dataTypestringYesPostgreSQL data type
displayNamestringNoDisplay label
lengthintegerNoLength for varchar/char
precisionintegerNoPrecision for decimal
scaleintegerNoScale for decimal
isNullablebooleanNoDefault: true
isPrimaryKeybooleanNoDefault: false
isUniquebooleanNoDefault: false
isPredictorbooleanNoMark column as ML predictor. Default: false
defaultValuestringNoDefault value expression
descriptionstringNoField description
upsertbooleanNoWhen true, allows overwriting metadata on an existing field with the same name. Used by the CSV re-upload + predictor-toggle codepaths. Default: false (returns 409 on duplicate name).
Response: 201 Created with the field object on success, 200 OK when upsert: true matched an existing row.

Duplicate-name handling

Without upsert: true, the endpoint returns 409 Conflict if a field with the same name already exists on this schema:
{
  "title": "Conflict",
  "detail": "A field named \"first_name\" already exists on this schema. Use PUT to alter it, DELETE to remove it, or pass upsert=true to overwrite metadata."
}
This protects the metadata-vs-PG invariant — re-POSTing a field name without opting in won’t silently overwrite length/nullable/predictor flags while the real PG column stays unchanged.

PUT /api/v1/schemas/fields

Alter an existing column. Executes ALTER TABLE RENAME COLUMN and/or ALTER COLUMN TYPE / SET NOT NULL / DROP NOT NULL against the live table.

Request Body

FieldTypeRequiredDescription
fieldIdstringYesThe SchemaField.id to alter
newNamestringNoNew column name (snake_case). Triggers ALTER TABLE RENAME COLUMN when different from current.
dataTypestringNoNew PostgreSQL data type
lengthintegerNoNew length
precisionintegerNoNew precision
scaleintegerNoNew scale
isNullablebooleanNoWhen provided, toggles SET NULL / DROP NULL
displayNamestringNoDisplay label
descriptionstringNoField description
Response: 200 OK with the updated field object.

Error Codes

StatusMeaning
400Invalid newName format, missing fieldId, or unsupported dataType
404Field not found in this tenant
409newName collides with another field on the same schema
422Primary-key columns can’t be altered here (use the PrimaryKey panel), or the requested type cast would fail on existing data (e.g. varchar → integer with non-numeric rows)
Type changes run a USING cast on existing data. Narrowing casts that lose precision return 422 — the operation is refused, not silently truncated.

DELETE /api/v1/schemas/fields

Remove a column from a schema. Executes ALTER TABLE DROP COLUMN.

Query Parameters

ParameterTypeRequiredDescription
fieldIdstringYesField ID to delete
This drops the column from the underlying table. Data in this column is permanently lost.
Response: 204 No Content

Roles

EndpointAllowed Roles
GET /schemasadmin, editor, viewer
POST /schemasadmin, editor
PUT /schemasadmin, editor
DELETE /schemasadmin, editor
GET /schemas/fieldsadmin, editor, viewer
POST /schemas/fieldsadmin, editor
PUT /schemas/fieldsadmin, editor
DELETE /schemas/fieldsadmin, editor
See also: Data Platform | Computed Values