Data Tables
Tables store structured data per website. Each table has a schema with typed fields (string, text, number, boolean, datetime). Rows are created, updated, and deleted via MCP tools or REST API.
Tables are accessible in three ways:
- MCP tools —
list_tables,create_table,update_table,delete_table,list_table_rows,create_table_row,update_table_row,delete_table_row - Liquid templating —
site.tables.{slug}in page HTML - Public JSON API —
GET /t/{table_id}on the website’s own domain (no auth required)
Managing Tables
Create a Table
create_table(website, name: "Products", display_column_key: "name", fields_schema: { fields: [
{ name: "name", type: "string" },
{ name: "price", type: "number" },
{ name: "description", type: "text" }
]})
name— Human-readable nameslug— URL-safe identifier (auto-generated from name if omitted)display_column_key— Field used for row slug generation (required)fields_schema—{ fields: [{ name, type }] }- Types:
string,text,number,boolean,datetime
- Types:
Update a Table
update_table(table_id, name: "New Name")
Only name and display_column_key can be updated. Field schema changes require creating a new table.
List Tables
list_tables(website) # all tables
list_tables(website, table_id: "...") # single table
Delete a Table
delete_table(table_id)
Requires confirmation. Permanently removes the table and all rows.
Managing Rows
Create a Row
create_table_row(table_id, data: { name: "Widget", price: 29.99 })
data— Key-value pairs matching the table’s field names- Row
slugis auto-generated from thedisplay_column_keyfield
List Rows
list_table_rows(table_id) # all rows, paginated
list_table_rows(table_id, row_id: "...") # single row
list_table_rows(table_id, offset: 20, limit: 10)
Update a Row
update_table_row(row_id, data: { price: 19.99 })
Only provided fields change.
Delete a Row
delete_table_row(row_id)
Requires confirmation.
Liquid Templating
Tables are accessible via site.tables.{slug}:
{% for row in site.tables.products %}
<li>{{ row.name }} — ${{ row.price }}</li>
{% endfor %}
Filtering
{% for row in site.tables.products | where: "category", "electronics" %}
{{ row.name }}
{% endfor %}
Multiple filters chain together:
{% assign filtered = site.tables.products | where: "category", "electronics" | where: "in_stock", "true" %}
Ordering
{% for row in site.tables.products | sort_by: "price", "asc" %}
{{ row.name }}: ${{ row.price }}
{% endfor %}
Pagination
{% paginate site.tables.products by 10 order_by: "created_at" order_dir: "desc" %}
{% for row in site.tables.products %}
<article>{{ row.name }}</article>
{% endfor %}
{% if paginate.next %}<a href="{{ paginate.next.url }}">Next</a>{% endif %}
{% endpaginate %}
Pagination parameters:
by— Rows per page (default 20, max 100)order_by— Field name orcreated_atorder_dir—ascordescwhere— Filter expression ("field:value")
Row Properties
Every row exposes:
row.id— UUIDrow.slug— Auto-generated slugrow.created_at— Timestamprow.updated_at— Timestamprow.url— URL to the row’s template page (if configured)- Any field from the table schema:
row.name,row.price, etc.
Template Pages
Associate a page with a table to create dynamic row URLs. Path must contain :slug; additional :field_name placeholders (e.g. :lang in /blog/:lang/:slug) map to string/text fields in the table schema.
create_page(website, path: "/products/:slug", html: "...", table_slug: "products")
create_page(website, path: "/blog/:lang/:slug", html: "...", table_slug: "blog")
Each row renders at /products/{row-slug}. Template HTML has access to the row variable:
<article>
<h1>{{ row.name }}</h1>
<p>${{ row.price }}</p>
<div>{{ row.description }}</div>
</article>
Public JSON API
Every table serves a public JSON endpoint at GET /t/{table_id} on the website’s own domain. No authentication required. Always-on for all tables.
Endpoint
GET https://{subdomain}.pageweave.site/t/{table_id}
GET https://{custom-domain}/t/{table_id}
{table_id}— Table UUID (not slug). Get it vialist_tablesor the REST API.
Query Parameters
| Parameter | Description | Default |
|---|---|---|
page |
Page number | 1 |
per |
Rows per page (max 100) | 20 |
sort |
Sort field. Prefix with - for descending |
created_at desc |
search |
Full-text search across string/text fields | — |
filter[field] |
Exact match on field | — |
filter[field_neq] |
Not equal | — |
filter[field_gt] |
Greater than (numeric fields) | — |
filter[field_gte] |
Greater than or equal | — |
filter[field_lt] |
Less than (numeric fields) | — |
filter[field_lte] |
Less than or equal | — |
filter[field_cont] |
Contains (case-insensitive) | — |
filter[field_start] |
Starts with (case-insensitive) | — |
filter[field_end] |
Ends with (case-insensitive) | — |
Examples
Basic listing:
curl "https://my-site.pageweave.site/t/abc123-def456"
Pagination:
curl "https://my-site.pageweave.site/t/abc123-def456?page=2&per=10"
Sorting:
# Ascending by name
curl "https://my-site.pageweave.site/t/abc123-def456?sort=name"
# Descending by price
curl "https://my-site.pageweave.site/t/abc123-def456?sort=-price"
Filtering:
# Exact match
curl "https://my-site.pageweave.site/t/abc123-def456?filter[category]=electronics"
# Numeric range
curl "https://my-site.pageweave.site/t/abc123-def456?filter[price_gte]=10&filter[price_lte]=50"
# Contains (case-insensitive)
curl "https://my-site.pageweave.site/t/abc123-def456?filter[name_cont]=widget"
# Starts with
curl "https://my-site.pageweave.site/t/abc123-def456?filter[name_start]=A"
Search:
curl "https://my-site.pageweave.site/t/abc123-def456?search=blue widget"
Combined:
curl "https://my-site.pageweave.site/t/abc123-def456?page=1&per=20&sort=-price&filter[category]=electronics&filter[price_gte]=10"
Response Shape
{
"rows": [
{
"id": "uuid-1",
"slug": "widget-a",
"data": { "name": "Widget A", "price": 29.99, "category": "electronics" },
"created_at": "2026-06-01T12:00:00Z"
}
],
"meta": {
"current_page": 1,
"next_page": 2,
"prev_page": null,
"total_pages": 5,
"total_count": 47
}
}
JavaScript Example
const res = await fetch("https://my-site.pageweave.site/t/abc123-def456?filter[category]=electronics&sort=-price");
const { rows, meta } = await res.json();
rows.forEach(row => {
console.log(`${row.data.name}: $${row.data.price}`);
});
CORS
The API sets Access-Control-Allow-Origin only when the request Origin matches the website’s own origins:
- The pageweave.site subdomain URL
- Any configured custom domain
External origins receive no CORS headers.
Rate Limiting
60 GET requests per minute per IP for paths starting with /t/. Returns 429 Too Many Requests when exceeded.
Error Responses
| Status | Meaning |
|---|---|
| 404 | Table not found (invalid UUID or table doesn’t exist) |
| 410 | Website is suspended |