DocsPush API

Push API

The push API is how a script, a scraper or a scheduled job sends data to one of your organization’s datasets, without a conversation. It’s checked and added exactly as rows sent from your assistant are: the same rules, the same load modes, the same answers.

Before you start

  • The dataset must exist, with an approved structure. Set it up through your assistant first (Quickstart). The push API adds data; it never changes a structure.
  • You need a push key. An owner or admin creates one on the dashboard’s API keys page. Name it after the script that will use it. The key is shown once, so copy it then. A push key can only add data to your organization’s datasets: it can’t read anything, and it doesn’t work for connecting an assistant. A revoked key stops working on its next request.

The address

https://api.ontyx.io/v1

Every request is a POST and sends the key as a bearer token:

Authorization: Bearer ontyx_live_ak_<keyId>_<secret>
EndpointWhat it does
POST /v1/datasets/{name}/loadsThe request body is the file, up to 10 MB. It’s checked and added before the response comes back.
POST /v1/datasets/{name}/uploadsFor a file up to 100 MB: returns an address to upload it to.
POST /v1/uploads/{id}/finishChecks the uploaded file and adds it.

Requests are limited to 10 a second across the API, with bursts of up to 20.

Options

Each option goes in the query string. For uploads they can also go in a small JSON body, such as {"format": "csv", "mode": "append"}.

OptionValues
formatcsv (with a header row), json (one list of records), jsonl (one record per line) or parquet. For loads it can come from the Content-Type header instead: text/csv, application/json, application/x-ndjson or application/vnd.apache.parquet.
modereplace, append or upsert (needs a dataset with something that identifies each row). Leave it out to use the dataset’s own setting. See Load modes.
as_ofWhen the data is true of the world, as a date and time with its offset, such as 2026-10-30T06:00:00Z. Leave it out to use now.

Send files uncompressed.

Examples

export ONTYX_PUSH_KEY='ontyx_live_ak_<keyId>_<secret>'   # your push key
BASE=https://api.ontyx.io/v1
 
# A CSV export, added or updated by order number
curl --fail-with-body -X POST "$BASE/datasets/orders/loads?mode=upsert" \
  -H "Authorization: Bearer $ONTYX_PUSH_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @orders.csv
 
# JSON Lines from a scraper, with the time of the run as the as-of date
curl --fail-with-body -X POST "$BASE/datasets/orders/loads?format=jsonl&as_of=2026-10-30T06:00:00Z" \
  -H "Authorization: Bearer $ONTYX_PUSH_KEY" \
  --data-binary @orders.jsonl
 
# A file over 10 MB: ask for an upload address, upload the file, then finish
curl --fail-with-body -s -X POST "$BASE/datasets/orders/uploads?format=csv&mode=append" \
  -H "Authorization: Bearer $ONTYX_PUSH_KEY" > upload.json
#   upload.json has upload_id, put (url, method, headers) and upload_command:
#   a ready curl line that PUTs your file with exactly the headers it needs.
#   Run it with your file's path in place of ./your-file, then:
curl --fail-with-body -X POST "$BASE/uploads/$(jq -r .upload_id upload.json)/finish" \
  -H "Authorization: Bearer $ONTYX_PUSH_KEY"

The upload address works for about 30 minutes. The file must be uploaded with exactly the headers in put.headers, or the upload is refused.

What a load answers

A load that lands answers 200 with what it did. This is the JSON Lines example above, sent to a dataset that already had three orders (checks shortened):

{
  "dataset": "orders",
  "data_version": 2,
  "mode": "upsert",
  "rows_loaded": 3,
  "rows_added": 1,
  "rows_updated": 1,
  "rows_already_there": 1,
  "total_rows": 4,
  "as_of": "2026-10-30T06:00:00.000Z",
  "checks": [
    { "check": "key", "status": "passed", "columns": ["order_id"], "rows_checked": 4, "message": "Each of the 4 rows of \"orders\" has its own \"order_id\"; this load's rows were matched against the ones already there." },
    { "check": "freshness", "status": "stale", "as_of": "2026-10-30T06:00:00.000Z", "expected_cadence": "daily", "message": "Out of date: the data is as of 1 day ago (2026-10-30), and new data is expected every day. The rows were loaded; \"orders\" shows as out of date until newer data arrives." }
  ],
  "message": "Loaded 3 rows into \"orders\" (each load updates rows that already exist and adds new ones): 1 updated a row that was already there, 1 was new and added and 1 was already there exactly the same, so it was skipped. It now has 4 rows. …"
}
FieldMeaning
data_versionThe data version this load made. When every row was already there exactly the same, no new version is made and this is the version that stays in force.
modeHow the rows were added.
rows_loadedRows the request carried.
rows_addedRows that were new.
rows_updatedRows that replaced a row already there (upsert only).
rows_already_thereRows that were already there exactly the same, and were skipped (append or upsert, on a dataset with something that identifies each row).
identical_rows_combinedRows repeated exactly within the request, kept once (only when there were some).
total_rowsRows in the dataset after the load.
as_ofThe as-of date recorded for this version.
fields_without_a_columnFields in the records that have no column yet, kept in the original record (only when the dataset keeps it).
checksEverything the load verified, each with a check name, a status and a message: key, required, not_null, unique, accepted_values, relationship and freshness. A relationship that doesn’t hold is reported here; it never stops a load.
messageAll of the above in plain words, with anything that needs attention first.

finish answers the same, plus file_size.

When data doesn’t fit

A load is checked against the approved structure before anything is written, and a load that breaks a rule is refused whole: nothing is written and no version is made. The answer says why, in the same sentences your assistant would get:

{
  "error": {
    "code": "ROWS_REFUSED",
    "message": "Nothing was loaded: Row 2: column \"ordered_on\" needs a value but is blank. Column \"status\" only allows \"open\", \"shipped\" or \"cancelled\", but 1 row has a value that isn't on that list: \"pending\" (1 row). … 1 of 2 rows have a problem; nothing was loaded.",
    "problems": [
      "Row 2: column \"ordered_on\" needs a value but is blank.",
      "Column \"status\" only allows \"open\", \"shipped\" or \"cancelled\", but 1 row has a value that isn't on that list: \"pending\" (1 row). …",
      "1 of 2 rows have a problem; nothing was loaded."
    ],
    "failed_checks": [
      { "check": "required", "columns": ["ordered_on"], "message": "Row 2: column \"ordered_on\" needs a value but is blank." },
      { "check": "accepted_values", "columns": ["status"], "message": "Column \"status\" only allows \"open\", \"shipped\" or \"cancelled\", …" }
    ]
  }
}

problems lists up to 20 problems, each naming the row and column. failed_checks names each check the load didn’t pass. Fix the data and send it again. If the data is right and the structure should change (a new column, a new allowed value), ask your assistant to propose the change; once an owner or admin approves it, the same file loads.

The dashboard’s Datasets page shows how each dataset’s last load went, including a refused one and why.

Errors

Errors are JSON: { "error": { "code", "message" } }, plus problems and failed_checks on a refused load.

StatusCodeWhen
400INVALID_REQUESTA mode or as_of that isn’t valid, or upsert on a dataset with nothing that identifies each row.
401UNAUTHENTICATEDThe key is missing, unknown or revoked.
403FORBIDDENThe organization’s data is being deleted.
404NOT_FOUNDNo dataset by that name in the key’s organization, an unknown upload, or an address that doesn’t exist.
405METHOD_NOT_ALLOWEDA method other than POST.
409CONFLICTThe dataset has no approved structure yet, is marked for deletion, or is built from other datasets (load those instead); the uploaded file hasn’t arrived yet; or the structure changed while the load ran.
413TOO_LARGEA loads body over 10 MB. Use an upload instead.
415UNSUPPORTED_FORMATThe format wasn’t given, isn’t one of the four, or the body is compressed.
422ROWS_REFUSEDThe data doesn’t fit the structure (above). Nothing was written.
429Over the request limit. Wait and try again.
500INTERNALSomething went wrong on Ontyx’s side and nothing was loaded. Try again in a minute.

A request turned away before it reaches Ontyx’s own checks (no valid key, or too many requests) gets a short {"message": "…"} answer instead of this shape.

Timing

A load is checked and added before the response comes back, and a request can take at most 30 seconds. A load that waits behind a large one can run out of time and still land. Before re-sending a replace after a timeout, check the dataset (ask your assistant, or look at the dashboard), so you know whether the first one landed. On a dataset with something that identifies each row, re-sending an append or upsert of the same rows is safe: rows already there exactly the same are skipped.