The REST API, API clients, and webhooks
The Developers page manages workspace-scoped API clients for the REST API (/api/v1) and outbound webhooks that push record changes to your systems. Clients exchange an id + secret for a short-lived access token. Full endpoint reference with curl examples lives at /Developers/Docs.
Create an API client
Only workspace Admins can create clients. On Developers, name the client, choose when it expires (30 / 90 / 365 days or never), and pick its scopes — read (every GET: tables, records, members, audit), write (create, update, delete, and restore records), and schema (create tables and fields, and manage webhooks) — then press Create client.
You get a public Client ID (rfc_…, safe to share) and a client secret (rfsk_…) shown once — copy the secret immediately, because RowFold stores only a hash of it and can never display it again. The client is your durable credential; it never rides on API calls. Instead, exchange it for a short-lived access token:
curl -X POST https://YOUR-HOST/api/v1/auth/token -H "Content-Type: application/json" -d '{"clientId":"rfc_…","clientSecret":"rfsk_…"}'
That returns an accessToken (a signed JWT) with expiresIn seconds — about an hour. Send it as Authorization: Bearer <accessToken> (an X-Api-Key header works too) on every other call, and exchange again when it expires. A client acts on its one workspace with admin-grade access, so treat the secret like a password — keep it server-side and never ship it in client code. Revoke a client from the Developers page at any time; it then mints no new tokens, and any it already issued stop working within the hour.
Call the API
Every request goes to /api/v1 over HTTPS, and every response is JSON. After exchanging your client for an access token, confirm it with GET /api/v1/meta, which echoes the workspace it opens, its scopes, and its expiry. A first real call — list your tables and their field keys — looks like this:
curl https://YOUR-HOST/api/v1/tables -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Conventions hold everywhere: JSON is camelCase in and out, timestamps are UTC ISO-8601 (their field names end in Utc), and record values are keyed by field key — the stable slug listed by GET /tables, with Relation values as arrays of record ids and People values as arrays of person ids. A People write names workspace members by person id or by email address and stores the id; a name, or anyone who isn't a member, is refused with a 400. Errors always use one envelope, { "error": { "code", "message", "details" } }, with a matching status: 400 validation (details names the fields), 401 a missing, invalid, or expired access token, 402 the workspace's trial has ended, 403 the token lacks the required scope, 404 not found in this workspace, and 429 rate limited. Each access token may make 300 requests per minute (a sliding window); over that returns 429 with a Retry-After header. The token-exchange endpoint has its own tighter per-IP limit — cache your access token for its lifetime instead of exchanging on every call.
API writes behave exactly like edits made in the app: they run through the same pipeline, land in the audit log as API · your-client-name, and fire your automations and webhooks. Deletes are soft — DELETE moves a record to the Trash, and restore brings it back.
Open the interactive API reference
Open the API reference at /Developers/Docs — the complete, per-endpoint reference and a live console. Enter a client ID and secret to authorize (it exchanges them for an access token in-browser) and every endpoint gains a Try it runner that calls your own workspace, next to copy-ready samples in cURL, JavaScript, and Python. Path fields like tableId autocomplete from your workspace once you authorize; read-only calls are marked safe to run and writes are flagged because they act on real data. It is the canonical place for the exact parameters, request body, and response of every endpoint — start there whenever this overview isn't specific enough.
What you can call
The API reaches everything in a workspace. Endpoints, with the scope each needs:
• Meta — GET /meta (any scope): the workspace a token opens, its scopes, and expiry.
• Members — GET /members (read): every member's person id, name, and role — the ids People fields store. It lists no email addresses, deliberately: a People write accepts a member's address instead.
• Tables — GET /tables (read) lists every table with its fields and field keys; POST /tables (schema) creates a table and optional starting fields; PATCH /tables/{tableId} (schema) renames one (its slug and field keys stay stable).
• Fields — POST /tables/{tableId}/fields (schema) adds a field; PATCH /fields/{fieldId} (schema) renames a field or edits a Select/MultiSelect's options. A field's key never changes; Relation fields need a relatedTableId, and computed Lookup and Rollup fields can't be created through the API.
• Records — GET /tables/{tableId}/records (read) lists them with paging, sorting, search, and filters; POST /tables/{tableId}/records (write) creates one or a batch; GET /records/{id} (read) retrieves a record plus its inbound and outbound links; PATCH /records/{id} (write) merges changes (send only what changes; null clears a field); DELETE /records/{id} (write) soft-deletes to the Trash; POST /records/{id}/restore (write) brings it back.
• Audit — GET /audit (read): the workspace audit log, newest first, including changes made through the API.
• Webhooks — GET /webhooks (schema) lists subscriptions with their secret and health; POST /webhooks (schema) creates one; PATCH /webhooks/{id} (schema) edits the URL, events, table scope, or pauses it with "enabled": false; POST /webhooks/{id}/rotate-secret (schema) issues a fresh signing secret; DELETE /webhooks/{id} (schema) removes it.
List, filter, and batch records
Listing records is the workhorse read. GET /tables/{tableId}/records takes pageSize (1–200, default 50) and offset; each response carries total and the next offset (null at the end). Add q to search the display name, and sort to order by createdAt, updatedAt, or displayName (prefix - for descending; the default is -updatedAt).
The filter parameter is a URL-encoded JSON array of {field, op, value} conditions, combined with match=all (default) or match=any. Operators: eq, neq, contains, not_contains, empty, not_empty, gt, gte, lt, lte; target the record name with the special field __displayName. Field-value filters scan up to 5,000 rows per request, and the response sets scannedCap when it hits that boundary.
Creating supports batches: send one record object, or { "records": [ … ] } for up to 50 at once. Pass an Idempotency-Key header and RowFold replays the first response for 24 hours if the same key and body arrive again — so a retried create never duplicates a record.
Sending dates through the API
Two different kinds of time come through the API, and mixing them up is the most common integration bug.
A record date is plain wall-clock text with no zone attached: YYYY-MM-DD, or YYYY-MM-DDTHH:mm when the field carries a time. Each field’s includeTime flag in GET /api/v1/tables tells you which shape to send. The wall these clocks are on is your workspace’s time zone — a shift at 9:00 AM is 9:00 AM at the site, whoever reads it.
A timestamp RowFold generates — createdAtUtc, updatedAtUtc, postedAt — is a real instant in UTC, in full ISO 8601. Convert those for display; never convert a record date.
Writes are read tolerantly and then normalised. Send 2026-05-18 10:00:00 and it is stored — and returned — as 2026-05-18T10:00. So what you read back may not be byte-identical to what you sent: compare dates by parsing them, never by string equality.
Two things are refused with a 400 naming the field, rather than stored:
• Text that isn’t a date. Storing it would leave a column no date filter can ever match, and you’d find out weeks later.
• A date carrying a zone — 2026-05-18T09:00Z, 2026-05-18T09:00+01:00. An offset turns the value into an instant, and turning an instant back into wall-clock time is a guess RowFold won’t make on your behalf. Convert to the workspace’s local time and send the bare form.
Parse tolerantly on the way out, too: turning Include a time off in the app never strips times already stored, so a date-only column can still hand you a timed value from before the change.
Receive webhook events
A webhook subscription POSTs each matching record change to your URL. Subscribe to any of record.created, record.updated, record.deleted, and record.restored, for all tables or a single one. Each delivery carries an X-RowFold-Event header, a unique X-RowFold-Delivery id, and an X-RowFold-Signature; update payloads include the record's previous values so your receiver can diff.
Verify every delivery. The signature header reads t=<timestamp>,v1=<hex>. Recompute an HMAC-SHA256 over timestamp + "." + rawBody using the hook's secret, compare it to v1 in constant time, and reject timestamps older than five minutes to stop replays — that proves the POST really came from RowFold. Answer with any 2xx within 10 seconds.
Deliveries run on a durable job queue that survives restarts and make up to 3 attempts — immediately, after ~30 seconds, then after ~5 minutes. A 4xx answer (other than 408/429) means your endpoint refused the payload and is not retried; a hook that fails 20 times in a row pauses itself. Every attempt — HTTP status, duration, error, and whether a retry is coming — is recorded under Recent webhook deliveries on the Developers page and kept for 30 days. Rotate the signing secret there (or with POST /webhooks/{id}/rotate-secret) and update your receiver before the next event fires; re-enabling a paused hook clears its failure counter.
Tips
• Confirm any token with <code>GET /api/v1/meta</code> — it echoes the workspace, scopes, and expiry without changing anything.
• Call <code>GET /api/v1/tables</code> first: the field <strong>keys</strong> it returns are exactly what you put in record <code>values</code> and in <code>filter</code> conditions.
• Enter a client ID and secret into the live console at <a href="/Developers/Docs">/Developers/Docs</a> and copy the generated cURL, JavaScript, or Python for any endpoint.
• Send an <code>Idempotency-Key</code> on record creates so a network retry replays the first response instead of creating a duplicate.
Good to know
• A client secret is shown once and never again — if you lose it, revoke the client and create a new one rather than hunting for it.
• A client secret carries admin-grade access to its whole workspace with no per-record checks, so keep it server-side and treat it like a password.
• Lookup and Rollup fields are computed: the API rejects writing them and can't create them either — wire those in the field editor.
• Field-value filters scan at most 5,000 rows per request; when the response sets <code>scannedCap</code>, narrow the filter or page through instead of trusting one call to see everything.
• A webhook that answers with a 4xx isn't retried, and 20 straight failures pause it — watch <strong>Recent webhook deliveries</strong> and press <strong>Enable</strong> after fixing your endpoint.
Who can do what
• Only workspace Admins can create or revoke API clients and create or manage webhooks — both live on the Admin-only Developers page.
• An API client is scoped to exactly one workspace and carries admin-grade access within it; it never reaches another workspace's data.
• Each endpoint checks the token's scope — <strong>read</strong> for GETs, <strong>write</strong> for record changes, <strong>schema</strong> for tables, fields, and webhooks — and a missing scope returns <code>403</code>.
• The reference at <code>/Developers/Docs</code> is readable by any signed-in user, but every live call it makes still needs a valid token with the right scope.