Upload API
Base URL: https://upload.opteryx.app
Overview
Agree the column types before any data is sent, then upload files against that agreement and commit them as one snapshot.
Upload flow
An upload is an agreement, not a transfer. You send a sample of each file and the service answers with what the data will become; you read that, accept it, and only then does any data move. Everything that can refuse an upload — a type that will not cast, two files that disagree, a column the dataset does not declare, a permission you do not have — is decided while the cost is a few megabytes rather than the whole export.
-
1
POST /v2/contracts -
2
201 ·
contract_id,state, resolvedmode,plan[],issues[],values -
-
PATCH /v2/contracts/{contract_id} - 200 · the whole contract, re-planned
-
PUT /v2/contracts/{contract_id}/accept -
200 ·
state: accepted
Only inference proposes. A declared schema, or an existing dataset's, arrives accepted -
-
-
POST /v2/contracts/{contract_id}/data -
200 ·
written[]— what the file turned out to be - 409 · a value that cannot be stored as the column it was promised to, with its row
-
-
3
POST /v2/contracts/{contract_id}/commit -
4
200 ·
snapshot,rows_written
- Negotiating uploads nothing.
POST /v2/contractscarries a sample per file and no data — a prefix for text, the footer for parquet, which is where its schema lives. So agreeing costs a few megabytes whatever the files weigh, and a caller who is going to be refused finds out now rather than after four gigabytes. Every file is sampled, not just the first: one contract covers all of them, so two files that disagree are caught here. - There is no default schema source.
schema.modeisdeclared(you name every column),infer(read them from the data and show me first),dataset(use the types the dataset already declares) orauto.autois not a fourth source — it resolves to one of the three from the destination before a contract exists, and the response says which. A dataset that declares its columns has nothing to infer; one that does not exist has nothing to read. - An inferred schema is proposed, not agreed. It comes back
state: proposedand refuses writes untilPUT .../accept, so nothing is catalogued that nobody looked at. Accepting echoes theschema_fingerprintyou were shown, so a proposal that moved between being read and being accepted is refused rather than confirmed blind. Adeclaredordatasetschema arrivesaccepted— you already said what you meant, in the request. plan[]says what happens to every column before it happens:keep,retag(relabelled, no value changes),widen(nothing is lost),cast(values are rewritten),ignored,undeclaredorunsupported.valuescarries one real sampled value per column, which is what makes a mistyped column obvious at a glance.issues[]carries the service's own severity —blockingstops the upload,warningdoes not.- A value that will not cast is a 409 on the write that carried it, naming the column, the row and the value — not a failure at commit after everything has been sent. The file is not staged, and nothing else is affected.
- Nothing is visible until commit. Files are written under a prefix the catalog has never named, so an upload that is abandoned, expires or fails is unreachable rather than mess somebody has to clean up.
DELETE /v2/contracts/{contract_id}gives up;expires_atsays how long you have. - Commit is idempotent on an
Idempotency-Keyheader: a retry after a lost response returns the original snapshot instead of writing a second one. - A contract is checked against the catalog before every write. If the target's definition moved after the contract was agreed, the contract goes
staleand is refused — rows already written were cast to a definition that no longer exists. Nothing was published, so the cost is work rather than a dataset somebody has read. Re-negotiating against the new definition is the fix.
Endpoints
| Service | Docs |
|---|---|
Negotiate a Contractpost/v2/contracts |
View |
Read a Contractget/v2/contracts/{contract_id} |
View |
Amend a Proposalpatch/v2/contracts/{contract_id} |
View |
Abandon a Contractdelete/v2/contracts/{contract_id} |
View |
Accept a Proposalput/v2/contracts/{contract_id}/accept |
View |
Commit a Contractpost/v2/contracts/{contract_id}/commit |
View |
Write a Filepost/v2/contracts/{contract_id}/data |
View |
Negotiate a Contract
Request: post/v2/contracts
Tags: contracts
Negotiate. Carries schema and samples; no upload happens here.
multipart/form-data with a contract part and one sample part per file,
because a contract covers every file and two files that disagree have to be
caught before anything is sent.
Header Parameters
- authorization
string | null[header; optional]
Responses
- 201 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://upload.opteryx.app/v2/contracts
Read a Contract
Request: get/v2/contracts/{contract_id}
Tags: contracts
The whole contract: plan, issues, every write, running totals.
One document read. Nothing is listed, downloaded or re-parsed to answer it,
which is what /inspect does today for every call.
Path Parameters
- contract_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://upload.opteryx.app/v2/contracts/{contract_id}
Amend a Proposal
Request: patch/v2/contracts/{contract_id}
Tags: contracts
Retype or decline columns. Returns the whole re-planned contract.
Path Parameters
- contract_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
patch https://upload.opteryx.app/v2/contracts/{contract_id}
Abandon a Contract
Request: delete/v2/contracts/{contract_id}
Tags: contracts
Give up. Nothing written was ever reachable, so nothing has to be undone.
Path Parameters
- contract_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 204 — Successful Response
- 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
delete https://upload.opteryx.app/v2/contracts/{contract_id}
Accept a Proposal
Request: put/v2/contracts/{contract_id}/accept
Tags: contracts
Confirm a proposed schema, echoing the fingerprint you were shown.
Path Parameters
- contract_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
put https://upload.opteryx.app/v2/contracts/{contract_id}/accept
Commit a Contract
Request: post/v2/contracts/{contract_id}/commit
Tags: contracts
Path Parameters
- contract_id
string[path; required]
Header Parameters
- idempotency-key
string | null[header; optional] - authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://upload.opteryx.app/v2/contracts/{contract_id}/commit
Write a File
Request: post/v2/contracts/{contract_id}/data
Tags: contracts
Stream one file in. Answered with what it turned out to be, or refused.
Path Parameters
- contract_id
string[path; required]
Header Parameters
- x-file-name
string | null[header; optional] - content-type
string | null[header; optional] - authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)