BetaappTown is live but still in beta: things may change while we finish it.
appTown Docs Open appTown

Developers

REST API

The REST API reads, creates and updates records in your apps. Every request acts as the user who owns the API key, with exactly their permissions: the same tables, rows and fields they can see and edit in the app.

The API does not create or configure apps, tables, fields, views or automations, and it does not delete records. Use the app editor for that. It only sees the published version of an app (not the draft), and the real data (never the editor's test data).

A key can be limited when you create it (Settings → API keys): to one app, and/or to read only (lists and reads; writes answer 403 forbidden).

#Basics

  • Base URL: https://apptown.app/api/v1
  • Authentication: Authorization: Bearer <api key> on every request. Cookies are ignored.
  • Requests and responses are JSON (Content-Type: application/json), UTF-8. Request bodies are limited to 1 MB.
  • IDs are UUIDs. Times are RFC 3339 in UTC, for example 2026-11-03T14:05:00Z.
  • Call the API from a server or script, never from a browser page. Browsers block cross-site calls to it.

#Check your key

GET /api/v1/me

{
  "id": "0192f0c4-…",
  "email": "[email protected]",
  "name": "Your Name",
  "timezone": "Europe/London",
  "api_key_id": "0192f0d1-…"
}

#Records

{app} is the app's ID (in the app's address: /a/{app}). {table} is the table's ID or its API name: its name in lowercase with _ between words (Hourly rates → hourly_rates). Field keys are the fields' API names, made the same way (a repeated name gets _2). Renaming a table or field changes its API name; IDs never change.

Method Path Does
GET /apps/{app}/tables/{table}/records list records
GET /apps/{app}/tables/{table}/records/{record} get one record
POST /apps/{app}/tables/{table}/records create a record
PUT /apps/{app}/tables/{table}/records/{record} update a record

#A record

{
  "id": "0192f0e2-…",
  "table": "orders",
  "version": 3,
  "created_at": "2026-11-03T14:05:00Z",
  "updated_at": "2026-11-03T15:20:11Z",
  "fields": {
    "customer": "Ada Lovelace",
    "total": 120.5,
    "status": "paid"
  }
}

Each field type has a fixed JSON form: numbers as numbers, dates as "2026-11-03", date and times as RFC 3339, select options by label ("paid" or the label as written), links as the linked record's ID (a list for "many"), people as their email, prices as {"amount": "12.50", "currency": "USD"}, files as [{"id", "name", "size", "mime"}]. Calculated fields (formulas, created at, record ID …) are included but can't be set.

#List records

GET /apps/{app}/tables/{table}/records?limit=50&cursor=…&filter=…&sort=…

  • limit: 1–100, default 50.
  • cursor: the next_cursor from the previous page. Leave it out for the first page.
  • filter (optional): an appTown expression with field names, for example AND([Status] = "Paid", [Total] > 100).
  • sort (optional): field API names separated by commas, plus created_at and updated_at. Put - before a name to sort that field descending, for example -created_at,customer.
{ "records": [ … ], "next_cursor": "eyJpZCI6…" }

next_cursor is null on the last page. You only ever get rows your row-level security lets you see.

#Create a record

POST /apps/{app}/tables/{table}/records with {"fields": { … }}. The response is 201 Created with the new record. Fields you leave out get their default values.

Send an Idempotency-Key header, such as a UUID you generate, so a retried request can't create the record twice. A retry with the same key within 24 hours returns the first response.

#Update a record

PUT /apps/{app}/tables/{table}/records/{record} with {"fields": { … }}.

  • Only the fields you send change. Fields you leave out keep their values. To clear a field, send null.
  • To be safe against overwriting someone else's change, send If-Match: "<version>" with the version you read. If the record changed since, you get 412 precondition_failed.
  • The response is the updated record. Every record response has an ETag header with its version.

Creates and updates made through the API work exactly like edits in the app: they're validated the same way, kept in the record's history (marked as made by your API key), shown live to people who have the app open, and they can trigger automations.

#Errors

Every error has the same shape:

{
  "error": {
    "code": "validation_failed",
    "message": "Please fix the listed fields.",
    "fields": { "total": "Must be a number." }
  }
}

fields appears only on validation_failed.

Status code Means
400 bad_request malformed JSON or parameters
401 unauthorized missing, invalid, expired or revoked API key
403 forbidden you can see this but aren't allowed to do that
404 not_found doesn't exist, or you have no access to it
409 conflict a request with the same Idempotency-Key is still running
412 precondition_failed the record changed since the version in If-Match
413 — body over 1 MB
422 validation_failed field values failed validation (fields lists them)
429 rate_limited too many requests; wait for Retry-After seconds
500 internal our fault; the message includes a request ID for support

#Limits

  • 10 requests per second per key, with bursts up to 50. There is also a limit per IP address.
  • Every response carries an X-Request-Id header. Include it when you contact support.