Skip to main content

Managed Tables API

Endpoints for managing company-owned PostgreSQL tables. Each company has a dedicated managed database, provisioned on demand. Supports manual table creation, file upload (CSV/Excel/ZIP) with schema inference, and row-level CRUD.

Difference vs. Data API:

  • Data API = consumes saved visualizations as a public endpoint (read-only via API key).
  • Managed Tables = the user storage layer (CRUD via console JWT).

Visualizations published via the Data API can read from managed tables, but the two modules are independent.

All endpoints require the standard JWT auth (Authorization: Bearer <token>) and are prefixed with /v1/managed-tables.

Provisioning

Provision the managed database

POST/v1/managed-tables/provision

Creates a dedicated PostgreSQL database for the company (idempotent — re-calling returns the existing connection).

Required Permission: managed-tables:create

Response: 200 OK

{
"provisioned": true,
"connection_id": "507f1f77bcf86cd799439011"
}

Provisioning status

GET/v1/managed-tables/status

Required Permission: managed-tables:list

Response: 200 OK

{
"provisioned": true,
"connection_id": "507f1f77bcf86cd799439011"
}

provisioned: false indicates that POST /provision has not run yet.


Tables CRUD

List tables

GET/v1/managed-tables

Required Permission: managed-tables:list

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number
per_pageinteger20Items per page (max 100)
searchstring-Filter by display_name (case-insensitive)

Response: 200 OK

{
"total": 12,
"quantity": 12,
"records": [
{
"_id": "507f1f77bcf86cd799439011",
"company_id": "507f1f77bcf86cd799439012",
"connection_id": "507f1f77bcf86cd799439013",
"display_name": "vendas_mensais",
"columns": [
{ "name": "produto", "display_name": "Product", "type": "text", "nullable": false },
{ "name": "valor", "display_name": "Value", "type": "decimal", "nullable": true }
],
"source": "manual",
"status": "active",
"row_count": 1500,
"created_by_user_id": "507f1f77bcf86cd799439014",
"created_at": "2026-01-15T10:30:00.000Z",
"updated_at": "2026-01-15T10:30:00.000Z"
}
]
}

Get table

GET/v1/managed-tables/:id

Required Permission: managed-tables:list

Response: 200 OK — same shape as one item from the list response.

Errors: 404 { "error": "Tabela não encontrada" }


Create table (manual)

POST/v1/managed-tables

Required Permission: managed-tables:create

Prerequisite: database provisioned (POST /provision). Otherwise returns 400 Base de dados não provisionada. Ative "Meus Dados" primeiro.

Body:

FieldTypeRequiredDescription
display_namestringYesTable name. Must match ^[a-z_][a-z0-9_]*$ (snake_case, no leading digit).
columnsarrayYesColumn list (>=1).
columns[].namestringYesPG name (snake_case).
columns[].display_namestringYesFriendly name.
columns[].typeenumYestext | integer | decimal | boolean | date | datetime.
columns[].nullablebooleanYesWhether NULL is allowed.
{
"display_name": "vendas_mensais",
"columns": [
{ "name": "produto", "display_name": "Product", "type": "text", "nullable": false },
{ "name": "valor", "display_name": "Value", "type": "decimal", "nullable": true }
]
}

Response: 201 Created — same shape as GET /:id.

Errors:

HTTPCodeDescription
400MANAGED_TABLE_DUPLICATE_NAMEActive name already exists
400-Database not provisioned
422-Invalid schema (display_name or columns)

Update table

PUT/v1/managed-tables/:id

Required Permission: managed-tables:update

Body (optional fields):

{
"display_name": "novo_nome",
"columns": [...],
"status": "active"
}

Response: 200 OK


Delete table

DELETE/v1/managed-tables/:id

Required Permission: managed-tables:delete

Soft-delete: the table disappears from listings. Restoration is not exposed by the API.

Response: 204 No Content


File upload

3-step flow:

  1. Upload → creates a job in queued status. Worker processes the file and infers the schema (status analisandowaiting_confirm).
  2. Confirm → user reviews the inferred schema and confirms; job goes to insertingdone.
  3. Polling → frontend tracks state via GET /upload-jobs/:jobId.

Accepts .csv, .xlsx, .xls, .zip. Up to 10 files per upload, 100MB total.

Upload (create new table)

POST/v1/managed-tables/upload

Required Permission: managed-tables:upload

Content-Type: multipart/form-data

Form fields:

FieldTypeDescription
filesfile[]1-10 files (repeated field).

Response: 201 Created

{
"job_id": "507f1f77bcf86cd799439011",
"status": "queued"
}

Upload (append to existing table)

POST/v1/managed-tables/:id/upload

Required Permission: managed-tables:upload

Content-Type: multipart/form-data

Form fields:

FieldTypeDefaultDescription
filesfile[]-1-10 files.
modestringappendappend (adds rows) or replace (truncate + insert).

Response: same shape as the new-table upload.


List active jobs

GET/v1/managed-tables/upload-jobs/active

Required Permission: managed-tables:list

Returns jobs that are not yet terminal (queued, analisando, waiting_confirm, inserting).

Response: 200 OK

{
"records": [
{
"_id": "...",
"company_id": "...",
"user_table_id": null,
"files": [
{ "original_name": "vendas.csv", "storage_path": "...", "size_bytes": 12345, "mime_type": "text/csv" }
],
"mode": "create",
"status": "waiting_confirm",
"inferred_schema": {
"suggested_table_name": "vendas",
"columns": [{ "name": "produto", "display_name": "Product", "type": "text", "nullable": false }],
"sample_rows": [{ "produto": "X" }],
"total_row_count_estimate": 1500
},
"confirmed_schema": null,
"progress": null,
"error": null,
"created_at": "2026-01-15T10:30:00.000Z",
"updated_at": "2026-01-15T10:30:00.000Z"
}
]
}

Get job

GET/v1/managed-tables/upload-jobs/:jobId

Required Permission: managed-tables:list

Response: 200 OK — same shape as the active list item.

Possible statuses: queued, analisando, waiting_confirm, inserting, done, failed.

Errors: 404 { "error": "Job não encontrado" }


Confirm job schema

POST/v1/managed-tables/upload-jobs/:jobId/confirm

Required Permission: managed-tables:upload

Applies the user-confirmed schema and enqueues the inserting phase.

Body:

{
"display_name": "vendas",
"columns": [
{ "name": "produto", "display_name": "Product", "type": "text", "nullable": false }
]
}

For an upload into an existing table (user_table_id set on the job), display_name reuses the legacy name and is not validated against the regex.

Response: 200 OK

{ "status": "inserting" }

Errors:

HTTPCodeDescription
400-Job is not in waiting_confirm
400MANAGED_TABLE_DUPLICATE_NAMEName already exists (only on creation)
422-Invalid display_name (regex), invalid column (name/type)

Data CRUD

List rows

GET/v1/managed-tables/:id/data

Required Permission: managed-tables:data-read

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number
per_pageinteger50Items per page
sort_bystring-field ASC or -field DESC

Response: 200 OK

{
"total": 1500,
"quantity": 50,
"records": [
{ "id": "uuid", "produto": "X", "valor": 12.5 }
]
}

Insert rows

POST/v1/managed-tables/:id/data

Required Permission: managed-tables:data-write

Body:

{
"rows": [
{ "produto": "X", "valor": 12.5 },
{ "produto": "Y", "valor": 30 }
]
}

Response: 201 Created

{ "inserted": 2 }

Errors: 422 if rows is missing or empty.


Update row

PUT/v1/managed-tables/:id/data/:rowId

Required Permission: managed-tables:data-write

Body: object with the fields to update.

Response: 200 OK — updated row.

Errors: 404 { "error": "Linha não encontrada" }


Delete row

DELETE/v1/managed-tables/:id/data/:rowId

Required Permission: managed-tables:data-write

Response: 204 No Content

Errors: 404 { "error": "Linha não encontrada" }


Column types

TypePostgreSQL DDLDescription
textTEXTFree-form string
integerBIGINT64-bit integer
decimalNUMERICArbitrary-precision number
booleanBOOLEANtrue/false
dateDATEDate (no time)
datetimeTIMESTAMPTZTimestamp with timezone

Limits and constraints

  • Upload: max 100MB total, max 10 files per call.
  • Table / column name: regex ^[a-z_][a-z0-9_]*$.
  • Table display_name is unique per company among active tables.
  • DELETE is a soft-delete: the table disappears from listings. Restoration is not exposed by the API.

Error codes

HTTPCodeDescription
400MANAGED_TABLE_DUPLICATE_NAMEdisplay_name conflict in the company
400-Database not provisioned / job not in waiting_confirm
401-Missing or invalid JWT
403-Missing IAM permission (managed-tables:*)
404-Table, row, or job does not exist
422-Invalid schema (identifier regex / column type)
500-Internal error