
# Records Overview

Every table you declare automatically exposes a complete REST API for its records. There is no resolver to write and no endpoint to register — the moment a table exists in your config, its `records` collection is reachable under `/api/tables/:tableId/records`. This page documents the shared API surface: the URL layout, the response envelope, authorship metadata, and the cross-cutting rules (authentication, permissions, formatting) that every records endpoint obeys.

Records are the rows of a [table](tables-overview). Tables define the columns (fields); records hold the values. The Records API is the canonical read/write path for that data — pages render it, forms write to it, automations react to it, and external integrations consume it.

## Endpoint surface

`:tableId` accepts either the numeric table `id` or the table `name`/slug. All endpoints accept and return JSON.

| Method & Path                                                              | Description                                                             |
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `POST /api/tables/:tableId/records`                                        | Create a single record                                                  |
| `GET /api/tables/:tableId/records`                                         | List records (pagination, sort, filter, field-select, group, aggregate) |
| `GET /api/tables/:tableId/records/:recordId`                               | Read a single record by ID                                              |
| `PATCH /api/tables/:tableId/records/:recordId`                             | Update a single record (partial)                                        |
| `DELETE /api/tables/:tableId/records/:recordId`                            | Soft delete a record (`?permanent=true` to hard delete)                 |
| `POST /api/tables/:tableId/records/upsert`                                 | Create-or-update by match fields                                        |
| `POST /api/tables/:tableId/records/batch`                                  | Batch create                                                            |
| `PATCH /api/tables/:tableId/records/batch`                                 | Batch update                                                            |
| `DELETE /api/tables/:tableId/records/batch`                                | Batch (soft) delete                                                     |
| `POST /api/tables/:tableId/records/:recordId/restore`                      | Restore a soft-deleted record                                           |
| `POST /api/tables/:tableId/records/batch/restore`                          | Batch restore                                                           |
| `GET /api/tables/:tableId/records/:recordId/history`                       | Record change history                                                   |
| `GET\|POST\|PATCH\|DELETE /api/tables/:tableId/records/:recordId/comments` | Record comments                                                         |
| `GET /api/tables/:tableId/trash`                                           | Browse soft-deleted records (trash view)                                |
| `GET /api/tables/:tableSlug/subscribe`                                     | Real-time WebSocket subscription                                        |

Detailed behavior is split across focused pages: [CRUD & upsert](records-crud), [filtering, sorting & pagination](records-filtering-sorting), [batch operations](records-batch), [history & comments](record-history), [soft delete & trash](records-soft-delete), [real-time subscriptions](records-realtime), and [import/export](records-import-export).

## The `fields` envelope

Record write bodies use the canonical Airtable-style envelope: a `fields` object mapping field names to values.

```json
POST /api/tables/contacts/records
{
  "fields": {
    "email": "john@example.com",
    "first_name": "John",
    "last_name": "Doe"
  }
}
```

A successful create returns `201 Created` with the stored record, including its generated `id` and authorship metadata:

```json
{
  "id": "42",
  "fields": {
    "email": "john@example.com",
    "first_name": "John",
    "last_name": "Doe"
  },
  "createdBy": "user-uuid-123",
  "createdAt": "2025-01-15T10:30:00Z",
  "updatedAt": "2025-01-15T10:30:00Z"
}
```

:::callout
**Flat bodies are accepted too.** A body without a `fields` key (e.g. `{ "email": "john@example.com" }`) is automatically wrapped into the canonical `{ "fields": { ... } }` shape. The envelope form is preferred and is the only shape accepted by batch and upsert endpoints.
:::

## List response envelope

`GET .../records` returns a paginated envelope, never a bare array:

```json
{
  "records": [{ "id": "1", "fields": {} }],
  "pagination": { "total": 128, "limit": 20, "offset": 0 }
}
```

`records` is the page of results; `pagination` carries the `total` matching-row count plus the `limit`/`offset` that produced this page. See [Filtering, Sorting & Pagination](records-filtering-sorting) for the full query-parameter grammar.

## Authorship metadata

The API automatically stamps and exposes authorship on every write. These fields are server-controlled and **read-only** — values supplied in a request body are ignored, so the audit trail cannot be forged.

| Field                     | Set when                     | Cleared/updated          |
| ------------------------- | ---------------------------- | ------------------------ |
| `createdBy` / `createdAt` | Record is created            | Never changes            |
| `updatedBy` / `updatedAt` | Record is created or updated | Re-stamped on each write |
| `deletedBy` / `deletedAt` | Record is soft-deleted       | Cleared on restore       |

`createdBy`, `updatedBy`, and `deletedBy` resolve to the authenticated user's ID. They are surfaced through dedicated [user-audit field types](user-audit-fields) (`created-by`, `updated-by`, `deleted-by`) and the `updated-at` system field auto-bumps the timestamp on every write — enabling the [optimistic-locking](records-crud) contract.

## Cross-cutting rules

Every records endpoint enforces the same guarantees:

| Concern                     | Behavior                                                                                                                         |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Authentication**          | Endpoints require a session. Unauthenticated requests return `401`.                                                              |
| **Table existence**         | An unknown `:tableId` returns `404` — never a 403, to avoid enumeration.                                                         |
| **RBAC**                    | Create/read/update/delete are gated per [table permission](table-permissions) and the role's [RBAC](auth-roles-rbac) grants.     |
| **Field-level permissions** | Responses omit fields the caller cannot read; writes to fields the caller cannot write are rejected.                             |
| **Validation**              | Field values are validated against the field type; missing required fields and constraint violations return `400`.               |
| **Anti-enumeration**        | Unauthorized access to an existing record returns `404`, not `403`, so an attacker cannot distinguish "forbidden" from "absent". |

:::callout
**404 over 403 is intentional.** To prevent record enumeration, the API returns `404 Not Found` for records the caller may not access — identical to the response for records that do not exist. See the [authentication overview](auth-overview).
:::

## Display vs raw formatting

By default, field values are returned raw (the stored value). Pass `?format=display` to receive human-readable, locale- and timezone-aware values (formatted currency, dates, durations, attachment display names). The raw form is preferred for programmatic clients; the display form is preferred for direct rendering. See [Record formatting](records-crud) for the controls.

## Related pages

- [CRUD & Upsert](records-crud) — create, read, update, delete, upsert, formatting
- [Filtering, Sorting & Pagination](records-filtering-sorting) — query parameters
- [Batch Operations](records-batch) — bulk create/update/delete
- [Record History & Comments](record-history) — change log and collaboration
- [Soft Delete & Restore](records-soft-delete) — trash and recovery
- [Real-Time Subscriptions](records-realtime) — WebSocket/SSE/poll
- [Import & Export](records-import-export) — CSV/JSON and clipboard
- [API Reference](api-reference) — the generated endpoint catalog
