Send data via the API
Create an API token
Section titled “Create an API token”- Open API Tokens in the admin sidebar (under People & Access).
- Click Create New. In the Create token modal, name the token (e.g.
CI pipeline) and pick a Role — the role bounds everything the token can do. - Keep the access setting on Limited to scopes (the default) and grant only what your integration needs: each scope (Data, Structure, Analytics, Automation, Agents, Secrets, Admin) can be None, Read, or Write. Full access skips scope limits entirely and shows a red warning — prefer Limited.
- Click Create token and copy the secret from the Token created modal.
“Copy this token now — you won’t be able to see it again.”
Send the token on every request in the Authorization header. The interactive API reference is served at /docs/api on your Ronja domain; a summary is in the API reference.
Push data files
Section titled “Push data files”The file push API feeds a manual-integration table — a table whose data you push from your own systems instead of syncing through a connection. It accepts Parquet files only, sent as a raw request body (never multipart), up to 1 GiB per file. Convert CSV first, for example:
python -c "import pandas as pd; pd.read_csv('sales.csv').to_parquet('sales.parquet')"-
Create the table once —
featureID, the feature that will own the table, is a required body field alongsidename. The response contains the table ID,table-…, which also appears in the table’s URL in the app:Terminal window curl -X POST "$RONJA_BASE_URL/api/v2/feature/model" \-H "Authorization: $RONJA_API_TOKEN" \-d '{"name":"sales_pushes","kind":"manual_integration","featureID":"<your feature ID>"}' -
Upload a file:
Terminal window curl -X PUT \"$RONJA_BASE_URL/api/v2/feature/model/table-a1b2c3d4e5/file/sales_2026.parquet" \-H "Authorization: $RONJA_API_TOKEN" \--data-binary @./sales_2026.parquet# -> 200 {"filename":"sales_2026.parquet","size":48230}Re-uploading the same filename replaces the previous file; different filenames accumulate and are built together.
-
List or delete files as needed:
Terminal window curl "$RONJA_BASE_URL/api/v2/feature/model/table-a1b2c3d4e5/files" \-H "Authorization: $RONJA_API_TOKEN"curl -X DELETE \"$RONJA_BASE_URL/api/v2/feature/model/table-a1b2c3d4e5/file/old.parquet" \-H "Authorization: $RONJA_API_TOKEN" -
Build to make the data queryable — uploading alone is not enough:
Terminal window curl -X POST "$RONJA_BASE_URL/api/v2/feature/model/table-a1b2c3d4e5/build" \-H "Authorization: $RONJA_API_TOKEN"The build runs asynchronously; poll
GET /api/v2/feature/model/table-a1b2c3d4e5and branch onbuildVerdict— the one field that already accounts for the trap below. Keep polling while it reads"building"or"pending";"ok"means the build landed,"failed"and"failed_stale"mean it did not, and"ok_partial"means it landed but skipped source files it could not read — the table is queryable, with less data in it than you uploaded. (Those are five of seven values; the full list is in the API reference.) That trap is why the field exists: a table that already held data is put back to"ready"after a failed rebuild — it keeps serving its last good data — sostatusalone would report success on a build that never landed.lastBuildErroron the same response tells you why it failed. The schema is detected automatically from the Parquet files.
$RONJA_BASE_URL is your organization’s Ronja URL. Tokens are bound to your organization — a token from one organization can never read or write another’s tables.
Push rows
Section titled “Push rows”A Dynamic table holds records your own app or scheduled script produces. Send them as JSON rows, or — for bulk loads — as a Parquet file. There is no build step: when the write returns, the rows are queryable.
-
Create the table once (Admin only):
Terminal window curl -X POST "$RONJA_BASE_URL/api/v2/feature/model" \-H "Authorization: $RONJA_API_TOKEN" \-d '{"name":"order_events","kind":"dynamic","featureID":"<your feature ID>"}' -
Write rows.
writeModeisappend(the default) orreplace:Terminal window curl -X POST \"$RONJA_BASE_URL/api/v2/feature/model/table-a1b2c3d4e5/rows" \-H "Authorization: $RONJA_API_TOKEN" \-d '{"rows":[{"sku":"A1","qty":3},{"sku":"B2","qty":7}],"writeMode":"append"}'# -> 200 {"tableID":"table-a1b2c3d4e5","rowsWritten":2,"writeMode":"append","partitionCount":1} -
Read it back — no build needed:
Terminal window curl -X POST "$RONJA_BASE_URL/api/v2/duckdb/query" \-H "Authorization: $RONJA_API_TOKEN" \-d '{"sql":"SELECT * FROM {{ ref('"'"'table-a1b2c3d4e5'"'"') }}"}'
Column names and types are taken from the JSON values. New columns are added automatically on a later write; a column whose type stops matching still writes, and the response’s warnings explains what changed.
Each write counts as an update to the table, so any Automation watching it fires — the same as an edit made inside Ronja.
Sending a lot of data at once
Section titled “Sending a lot of data at once”If you already have a Parquet file — or the 50,000-row / 8 MiB JSON limits are in your way — send the file itself to the same table. JSON is a bulky envelope for tabular data, so the same rows take a small fraction of the bytes, and the limit rises to 1 GiB per request with no row cap:
curl -X POST \ "$RONJA_BASE_URL/api/v2/feature/model/table-a1b2c3d4e5/rows/parquet?writeMode=append" \ -H "Authorization: $RONJA_API_TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary @data.parquet# -> 200 {"tableID":"table-a1b2c3d4e5","bytesReceived":48219,"writeMode":"append","partitionCount":2}writeMode is a query parameter here (there is no JSON body to carry it), and it works exactly as it does for rows: append (the default) or replace. The file is checked before anything is written — a body that is not a readable Parquet file is rejected and nothing lands. Everything else is identical to writing rows: same access, same automations, same snapshots, same partition ceiling.
To empty a dynamic table, POST /api/v2/feature/model/{tableID}/clear with an optional {"reason":"..."}. The contents are snapshotted first: list snapshots with GET .../dynamic-versions and put one back with POST .../dynamic-versions/{versionID}/restore. Snapshots expire after 7 days. Clearing and restoring take the same access as writing rows — whoever can push rows to a table can also empty it and put it back.
Clearing and restoring also count as updates to the table, so any Automation watching it fires — which means an Automation can run against a table you have just emptied. Pause it first, or make sure whatever it does copes with zero rows.
Let Ronja call your API
Section titled “Let Ronja call your API”A Ronja API credential lets Ronja’s own agents and workflows call your Ronja API directly, for operations the built-in tools don’t already cover (general resource reads and writes, and so on). It is a scoped API token sealed inside a secret — the same kind of stored credential you’d use for any external service, but pointed back at your own Ronja organization.
The easiest way is to ask Ronja in a chat: as an Admin, tell Ronja to “create a Ronja API credential” and it mints one for you, pausing for your approval before it lands. The credential defaults to a safe read-oriented scope and to your working feature, and the token is sealed in the secret — never shown. If you are not an Admin, ask an Admin to create it (they can do it right in a chat).
You can also create one directly through the API. featureID is required — the credential lives in a feature, and putting it in your private feature keeps it personal to you (a shared feature would let that feature’s members use it too):
curl -X POST "$RONJA_BASE_URL/api/v2/secret/ronja-api" \ -H "Authorization: $RONJA_API_TOKEN" \ -d '{ "name": "Ronja self-call", "featureID": "<your private feature ID>", "scopeGrants": {"data": "read", "structure": "read"} }'scopeGrantsis optional. Omit it and the credential defaults to a read-oriented grant (Data, Structure, and Analytics at Read), which deliberately excludes the Agents and Automation scopes so a defaulted credential can’t start agent sessions or fire automations. Grant only what the work needs — the same scopes as any API token, each at Read or Write.roleis optional and defaults to Admin, so the credential can reach role-gated read endpoints — such as listing every table in the organization — that a User-role token would be denied. Role and scope are independent, so the read-onlyscopeGrantsdefault above still bounds what it can write. Pass User for a narrower credential; the role is capped to your own.expiresInDaysis optional. Omit it and the credential expires 90 days after it is created; pass a larger number for a longer-lived credential (for example, one a recurring automation depends on). Once it expires it stops authenticating and must be re-created — so if a scheduled workflow relies on it, set a lifetime that outlasts the schedule.- The response is the created secret’s metadata. The token itself is never returned — it is sealed in the secret and cannot be read back by anyone, including you. If you need a different scope or role, create a new credential.
A Ronja API credential can’t be used to create another Ronja API credential, and each organization can hold a limited number of them at once — delete an unused credential (delete its secret) before creating a new one if you hit the limit.
The credential’s underlying token appears in your API Tokens list marked Managed by secret, with a link back to the secret. You can’t delete it from there — deleting the secret revokes the token. On the secret’s own page a Ronja API token badge marks it as a self-call credential, and Admins get a View token link to its token.
Once the credential exists, an agent or workflow simply references the secret; Ronja injects the Authorization header automatically, so the token never appears in a prompt or a workflow’s code. Because the credential is scoped, it can only reach the routes those scopes allow — see API reference for the reachability caveat.