Skip to content

Send data via the API

  1. Open API Tokens in the admin sidebar (under People & Access).
  2. 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.
  3. 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.
  4. Click Create token and copy the secret from the Token created modal.

The Token created modal showing the one-time secret with a Copy button “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.

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:

Terminal window
python -c "import pandas as pd; pd.read_csv('sales.csv').to_parquet('sales.parquet')"
  1. Create the table once — featureID, the feature that will own the table, is a required body field alongside name. 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>"}'
  2. 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.

  3. 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"
  4. 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-a1b2c3d4e5 and branch on buildVerdict — 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 — so status alone would report success on a build that never landed. lastBuildError on 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.

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.

  1. 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>"}'
  2. Write rows. writeMode is append (the default) or replace:

    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}
  3. 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.

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:

Terminal window
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.

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):

Terminal window
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"}
}'
  • scopeGrants is 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.
  • role is 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-only scopeGrants default above still bounds what it can write. Pass User for a narrower credential; the role is capped to your own.
  • expiresInDays is 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.