API reference
This page is the lookup summary. For the end-to-end walkthrough with examples, see Send data via the API. The interactive endpoint-by-endpoint API reference is served at /docs/api on your Ronja domain. For AI agents, the machine-readable entry point is a lean index at /llms.txt (also at /docs/api/llms.txt and /.well-known/llms.txt); it links to the compact machine-readable list of every endpoint (with the scope each needs) at /docs/api/endpoints.md, alongside the full OpenAPI schema at /docs/api/openapi.json.
API tokens
Section titled “API tokens”Tokens are created by an Admin on the API Tokens page (page title “Tokens”): click Create New.
| Create field | Details |
|---|---|
| Name | A label, e.g. “CI pipeline” |
| Role | Every token is bound to a role (default User); the role bounds everything the token can do. An admin cannot mint a token at or above their own role level |
| Access | Limited to scopes (recommended default — grant at least one scope) or Full access (unrestricted within its role; a warning is shown) |
- The token secret is shown exactly once at creation (“Copy this token now — you won’t be able to see it again.”). It cannot be re-displayed — only revoked.
- Send it as a bearer token:
Authorization: Bearer <token>(theBearerprefix is optional). - Tokens are bound to your organization and can be revoked at any time from the token list or detail page.
Token scopes
Section titled “Token scopes”Scopes narrow a token within its role — they never widen it. Each scope is granted at None, Read, or Write. A scoped token is denied on anything outside its grants.
| Scope | Covers |
|---|---|
| Data | Tables, files, tags, connectors & search |
| Structure | Features & access groups |
| Analytics | Apps, notes & knowledge |
| Automation | Workflows, schedules & scripts |
| Agents | Saved Agents (create, edit, run), chat & MCP |
| Secrets | Secrets & OAuth connections |
| Admin | Members, billing, governance & settings |
A token can later be restricted to scopes, or widened back to full access (confirm-gated), from its detail page.
Personal access tokens
Section titled “Personal access tokens”A personal access token (PAT) is a token that acts as you — it uses your live permissions rather than a role fixed at creation. If your role changes, the token’s access changes with it; if you lose access, the token stops working. This makes a PAT the simplest way to call the API as yourself, for example to build resources owned by you.
Because a PAT is personal, you create and manage it on your own Account page: open Account, go to the Access tokens tab, and click Create token.
| Create field | Details |
|---|---|
| Name | A label, e.g. “My laptop” |
| Access | Limited to scopes (recommended default — grant at least one scope) or Full access (a warning is shown). A full-access PAT carries your full live role |
| Expires | Optional — the token stops authenticating after this date |
A PAT has no Role picker — it has no role of its own; it inherits yours. Whether it is limited to scopes or full access, a PAT never exceeds what you can already do, and its access follows your live role — so it auto-limits if you’re demoted or offboarded. You can restrict a PAT to scopes, or widen it back to full access (confirm-gated), from its detail page.
- The secret is shown exactly once at creation, cannot be re-displayed, and can be revoked any time from the Access tokens tab.
- Send it as a bearer token:
Authorization: Bearer <token>(theBearerprefix is optional). - Everything a PAT does is audited as you, with the token noted as the mechanism (“via PAT <name>”) — so your activity stays complete whether you used the app or a PAT.
Creating a PAT is Admin-only for now; the token always acts as the Admin who created it. An Admin can also see and revoke any personal access token across the organization from the API Tokens page (page title “Tokens”), where each PAT is marked with a PAT badge and its owner.
Ronja API credential
Section titled “Ronja API credential”A Ronja API credential is a scoped API token sealed inside a secret, pointed back at your own Ronja organization, so Ronja’s own agents and workflows can call your Ronja API for operations the built-in tools don’t cover. An Admin can create one either with the endpoint below or simply by asking Ronja in a chat (“create a Ronja API credential”), which mints it after an approval step. See Let Ronja call your API for the walkthrough.
| Field | Details |
|---|---|
| Endpoint | POST /api/v2/secret/ronja-api — Admin only |
name |
A label for the secret |
featureID |
Required — the feature the credential lives in. A private feature keeps it personal; a shared feature exposes it to that feature’s members |
scopeGrants |
Optional map of scope → read/write (same scopes as a token). Omitted → the read-oriented default (Data, Structure, Analytics at Read; Agents and Automation excluded) |
role |
Optional — the minted token’s role (default Admin, so the credential can reach role-gated read endpoints such as listing every table in the organization; the read-only scope default still bounds writes). Pass User for a narrower credential. Capped to the caller’s own role |
expiresInDays |
Optional — the credential’s lifetime. Omitted → 90 days; a larger number lives longer. After it expires the credential stops authenticating and must be re-created |
The response is the created secret’s metadata. The token is never returned — it is sealed in the secret and can’t be read back. An agent or workflow references the secret and Ronja injects the Authorization header automatically.
A Ronja API credential can’t create another Ronja API credential, and each organization can hold only a limited number at once — delete an unused one before creating another if you reach the limit.
Reachability caveat. A scoped credential (like any scoped token) reaches only routes that carry a scope annotation; anything outside its grants is denied. The interactive reference at /docs/api lists the set of public endpoints, which is not identical to the scope-reachable set — some listed endpoints aren’t reachable by a scoped credential, and some reachable ones aren’t listed. Treat /docs/api as a guide, not a guarantee, and grant scopes to match the routes you actually call.
Creating a secret
Section titled “Creating a secret”Store an API key or credential from the API with POST /api/v2/secret, then read it from a Workflow. The value is write-only — it goes to secure storage and is never returned by any endpoint (reads expose metadata only). Requires a token with Secrets: Write (a User, or Data Admin when the secret is bound to a shared Feature).
| Field | Notes |
|---|---|
name |
A label for the secret |
secretType |
api_key (default), oauth, client_credentials, database, or url_scope |
credentials |
Required — JSON object of field → value, e.g. {"token":"sk-..."} |
agentFields |
The field names a Workflow is allowed to read, e.g. ["token"]. Must list every field a Workflow will read — a field absent from it is not readable at run time and the run fails with an error like Field '<name>' not available on secret |
allowedURLs |
Egress allowlist, e.g. ["*.example.com"]. Required unless secretType is database |
featureID |
Optional — binds the secret to a Feature. A private Feature must be one you own |
To rotate or replace a secret’s value later, send the new credentials with PUT /api/v2/secret/{secretID}; that also runs an automatic health check and returns its result.
A Workflow reads a stored field with the two-argument secret marker in its source — {{ secret('secret-...', 'token') }}, where the second argument is a field name from credentials that is also listed in agentFields.
File push API
Section titled “File push API”Push data files into a manual-integration table — Integration tables fed by files instead of a connector. Requires a token whose role has Admin rights and (if scoped) Data: Write; listing files only needs read access.
Format: Parquet only. Filenames must match [a-zA-Z0-9._-]+.parquet; convert CSV to Parquet client-side first.
| Step | Endpoint | Notes |
|---|---|---|
| 1. Create the table (once) | POST /api/v2/feature/model with {"name":"...","featureID":"...","kind":"manual_integration"} |
featureID goes in the body, not the path. Returns the table ID (table-…), also visible in the table’s URL |
| 2. Upload a file | PUT /api/v2/feature/model/{tableID}/file/{filename} |
Raw body (not multipart), max 1 GiB per file. Re-uploading the same filename replaces it; different filenames accumulate and build together |
| 3. List files | GET /api/v2/feature/model/{tableID}/files |
Paginated (200 per page, nextCursor) |
| 4. Delete a file | DELETE /api/v2/feature/model/{tableID}/file/{filename} |
404 if the file doesn’t exist |
| 5. Build | POST /api/v2/feature/model/{tableID}/build |
Required — uploads alone are not queryable. Async: poll GET /api/v2/feature/model/{tableID} and branch on buildVerdict (see below) |
The table’s schema is auto-detected from the Parquet files.
Polling caveat. status alone is not a success signal. A table that already holds data from an earlier build is restored to ready after a FAILED rebuild, so it stays queryable and serves its last good data — the failure surfaces as a populated lastBuildError ({message, type, occurredAt}) on the same response. A table with no previous data ends at status: "build_failed" — and so does one whose rebuild can never succeed (an input table was deleted), even though its last good data is still queryable. See Table types.
Branch on buildVerdict, not on status. The same response carries buildVerdict, which already combines the two: building and pending mean keep polling, ok means it worked, ok_partial means the table is queryable but the build skipped unreadable source files, failed_stale is the trap above (the run failed and the table is still serving its previous data, with status reading ready), failed means the run did not produce a result — not that the table is empty — and invalidated means something upstream changed and this table is due a rebuild.
A failed_stale with no lastBuildError beside it is narrower than it looks: Ronja could not read the failure record for that table, so the last run’s outcome is unknown rather than known-bad. It is reported that way deliberately — a verdict that claimed success on a record nobody could read is the one mistake an automated publish would act on.
failed is usually a first build with nothing to fall back on, but it is also where a table sits when every rebuild is bound to fail the same way — an input table was deleted, say. Such a table keeps its last good data and stays queryable while reading failed, so treat the verdict as a statement about the last run, never about whether there is data to read. lastBuildError stays the field that tells you why.
Each upload is validated as a readable Parquet file before it is accepted: the footer must parse, there must be at least one column, and the data pages must decode. A file that fails any of these is rejected with a 400 and discarded, so a bad file can never be stored. An empty file (valid Parquet, zero rows) is accepted — it is treated as “no data this window” and skipped at build time, contributing nothing to the table.
Row push API
Section titled “Row push API”Push rows into a Dynamic table (Table types), for data your own app or scheduled script produces — as JSON, or as a Parquet file when the volume warrants it. Data in, queryable table out, with no build step: when the call returns, the rows are there.
Requires a token with write access to the table’s Feature and (if scoped) Data: Write.
| Step | Endpoint | Notes |
|---|---|---|
| 1. Create the table (once) | POST /api/v2/feature/model with {"name":"...","featureID":"...","kind":"dynamic"} |
Requires Admin. Returns the table ID (table-…) |
| 2. Write rows | POST /api/v2/feature/model/{tableID}/rows with {"rows":[{...}],"writeMode":"append"} |
Synchronous. Max 50,000 rows and 8 MiB of JSON per request. Returns {tableID, rowsWritten, writeMode, partitionCount, warnings} |
| 2b. Or upload Parquet | POST /api/v2/feature/model/{tableID}/rows/parquet?writeMode=append, the file as the raw request body |
Same write, bulk envelope. Max 1 GiB, no row limit. Returns {tableID, bytesReceived, writeMode, partitionCount, warnings} |
| 3. Query | POST /api/v2/duckdb/query with {"sql":"SELECT * FROM {{ ref('table-…') }}"} |
No build required |
| Undo — empty the table | POST /api/v2/feature/model/{tableID}/clear with {"reason":"..."} |
Same access as writing rows. Contents are snapshotted first |
| Undo — restore a snapshot | GET /api/v2/feature/model/{tableID}/dynamic-versions, then POST /api/v2/feature/model/{tableID}/dynamic-versions/{versionID}/restore |
Same access as writing rows. Snapshots expire after 7 days |
writeMode is append (the default) or replace, and is strict — "REPLACE" or any other value is a 400 rather than a guess. On the Parquet endpoint it is a query parameter rather than a body field, since the body is the file.
The Parquet upload must be the raw request body (curl --data-binary @data.parquet), never a form upload (curl -F): a multipart envelope is not a Parquet file and is rejected with a 400. Every upload is validated before anything is written — the footer must parse, there must be at least one column, and the data pages must decode — so a bad file is rejected and the table is untouched. Apart from the envelope and the size cap, the Parquet endpoint behaves exactly like the row push: same access, same writeMode semantics, same partition ceiling, same drift warnings, same Automation triggering, same snapshots.
append is at-least-once. A retry after a timeout can write the same batch twice. For a recurring refresh, send the whole current dataset with "writeMode":"replace" — that is the idempotent shape.
Appends accumulate. Each append adds a data file that is never merged away, and both writes and queries slow down as they pile up, so an append past a ceiling is refused with a 400 naming the two fixes. Watch partitionCount on each response; when it climbs, switch to replace or clear the table.
Schema evolution is lenient — new columns are added. A column whose type diverges from the existing table still writes, and warnings explains the drift.
Every successful write counts as an update to the table, so any Automation watching it fires, exactly as an in-app write would.
One limit worth planning around. Writing rows — and clearing or restoring, which need exactly the same access — requires write access to the table’s Feature: for a personal Feature that means its owner, or an Admin. Any other colleague who can write the table inside Ronja still gets a 403 over the API. Move the table to a shared Feature if a second person needs to push to it.
Error responses
Section titled “Error responses”Bodies are always {"error": "<message>"}, on every endpoint. A missing or invalid token is always 401 Unauthorized.
The two data-push APIs above have separate error vocabularies — including two different meanings for 403 — so they are listed separately.
File push API errors
Section titled “File push API errors”| Status | Message | Cause |
|---|---|---|
| 403 | forbidden |
Role below Admin, or a scoped token without the Data: Write grant |
| 400 | invalid filename: must match [a-zA-Z0-9._-]+.parquet |
Bad filename |
| 400 | model is not a manual_integration table |
Wrong table kind |
| 400 | no rows: ... |
Nonexistent table ID (returned as 400, not 404) |
| 400 | uploaded file is not a valid parquet file (corrupt or missing footer): re-export the file and upload again |
Not a Parquet file, or a corrupt/truncated footer; the upload is discarded |
| 400 | uploaded parquet file has no columns |
The file declares an empty schema; the upload is discarded |
| 400 | uploaded parquet file is unreadable (…): re-export the file and upload again |
The footer parses but the data pages fail to decode; the upload is discarded |
| 404 | file not found |
DELETE on a missing file |
| 413 | file too large: max 1073741824 bytes |
Over 1 GiB — split the file client-side |
Row push API errors
Section titled “Row push API errors”Covers /rows, /rows/parquet, /clear, and the version-restore endpoint.
| Status | Message | Cause |
|---|---|---|
| 403 | forbidden |
No write access to the table’s Feature — you are neither the owner of a personal Feature nor an Admin, or you are not an Admin on a shared one — or a scoped token without the Data: Write grant. Any role below User is refused outright |
| 404 | not found |
Nonexistent, or deleted, table ID |
| 400 | table … is a … table, not a dynamic one |
The table is not a Dynamic table |
| 400 | table … is a session-scoped scratch table and is not writable over HTTP … |
The table belongs to a chat session rather than a Feature; save it to a Feature first |
| 400 | no rows provided — \rows` must be a non-empty array of objects` |
Empty rows |
| 400 | too many rows: … exceeds the limit of 50000 per request |
Split the batch |
| 400 | invalid writeMode "…": must be "append" (default) or "replace" |
Unrecognized writeMode (case-sensitive) |
| 400 | table already has … committed partitions (limit …) |
Append ceiling — re-push with replace, or clear the table |
| 409 | table is busy with another write — retry shortly |
Another writer holds the table’s lock; nothing was written |
| 413 | request body too large: limit is 8388608 bytes |
/rows body over 8 MiB — split the batch, or send Parquet |
| 400 | request body is not a valid Parquet file (corrupt or missing footer). Send the file as the RAW request body … |
Parquet upload that is not a Parquet file — most often a curl -F form upload; the upload is discarded |
| 400 | uploaded Parquet file has no columns |
Parquet upload declaring an empty schema; the upload is discarded |
| 400 | uploaded Parquet file is unreadable: re-export the file and upload again |
Parquet upload whose footer parses but whose data pages fail to decode; the upload is discarded |
| 413 | parquet file too large: max 1073741824 bytes |
Parquet upload over 1 GiB — split the file client-side |