Upload

Direct-to-storage upload, and why bytes never reach the application server.

Note Before you start. A finished Compiled World is open to read in full today, with its evidence attached. Compiling your own sources is set up with us rather than enabled by a plan purchase. Everything on this page is the contract the capability endpoint serves once intake is arranged; it is not a request TAVONEL will accept from you today.

Why bytes never reach our server

Uploads are direct. The capability endpoint returns a short-lived URL to object storage; you PUT the bytes there. The application server sees the request for permission and the receipt afterwards, and never the document.

Requesting a capability and listing documents

Note requestedBytes is the field the per-source byte ceiling acts on. Above 5 MB the answer is 413 SOURCE_EXCEEDS_PROCESSING_CEILING, carrying maxBytes, maxPages and a sentence you can show a person — before any byte is stored, because admitting the file would only move the refusal somewhere you cannot see it. A free evaluation has its own lower bound and answers TRIAL_FILE_TOO_LARGE with its own maxBytes.

POST/uploads/capabilityScope documents:intake

Returns a short-lived browser/agent-direct R2 PUT URL. Document bytes never pass through the application server. `requestedBytes` is checked against the deployment's per-source ceiling here, before any byte is stored: above it the answer is 413 SOURCE_EXCEEDS_PROCESSING_CEILING carrying `maxBytes`, `maxPages` and a sentence you can show a customer. Send `x-tavonel-source-idempotency-key` (a sha256 hex digest of the source event) to make the document id deterministic, so a retried intake converges on one document rather than two.

curl -sS -X POST https://tavonel.com/api/v1/uploads/capability \
  -H "Authorization: Bearer $TAVONEL_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "originalFilename": "<originalFilename>", "declaredMimeType": "application/pdf", "requestedBytes": 1, "estimatedPages": 1 }'
Request body
{
  "originalFilename": "<originalFilename>",
  "declaredMimeType": "application/pdf",
  "requestedBytes": 1,
  "estimatedPages": 1
}
StatusResponse
200A qualified direct upload capability and the immutable document id the bytes will be registered under. PUT the bytes to url with the headers in headers and nothing else; the URL is the credential and it is short-lived.
400UNQUALIFIED_INPUT — A required field was missing or was not of the declared type. UNQUALIFIED_MIME — The declared MIME type is not in the capability manifest. FILENAME_MIME_MISMATCH — The extension and the declared MIME type disagree. FILE_NAME_INVALID — The filename carried a path separator or a character the store refuses. SOURCE_IDEMPOTENCY_KEY_INVALID — x-tavonel-source-idempotency-key was present and was not 64 hex characters.
401AUTH_REQUIRED — No credential was presented, or the bearer token is not a key TAVONEL issued. This is what an unauthenticated request to any scoped route returns. API_KEY_INVALID — The bearer token was well-formed but did not match a stored key. API_KEY_EXPIRED — The key matched and its expiry has passed. API_KEY_REVOKED — The key matched and was revoked. A rotation revokes the key it replaces.
402STUDIO_SUBSCRIPTION_REQUIRED — The operation needs the higher plan tier — activation, rollback and retrieval-index rebuild are the three. TRIAL_ARCHIVE_NOT_INCLUDED — ZIP upload is not included in the free evaluation. TRIAL_FILE_LIMIT_EXCEEDED — The free evaluation's file count is spent. GPU_CREDITS_REQUIRED — The workspace has no processing balance left to reserve against.
409INTAKE_IDEMPOTENCY_CONFLICT — The same source idempotency key was already used for different bytes. COMPUTE_IDEMPOTENCY_CONFLICT — The processing reservation ledger already holds a different reservation under this request's idempotency key.
413SOURCE_EXCEEDS_PROCESSING_CEILING — requestedBytes is above what every processor in the chain can read. The body carries maxBytes, maxPages and a limit sentence. This is the refusal the 5 MB per-source ceiling produces. TRIAL_FILE_TOO_LARGE — Above the free-evaluation per-file bound, which is lower than the deployment ceiling.
429API_RATE_LIMITED — The key has used its per-minute allowance for this scope. The window is a fixed clock minute per key and scope; the allowances are on /docs/billing-and-limits. INTAKE_RATE_LIMITED — Too many upload capabilities were requested in the window. INTAKE_DAILY_QUOTA_EXCEEDED — The workspace's daily intake quota is spent.
503SIGNER_NOT_CONFIGURED — The upload URL signer is not configured, so no capability can be issued. INTAKE_DISABLED — Intake is closed today. API_RATE_LIMIT_UNAVAILABLE — The allowance could not be read, so the request was refused rather than run unbounded.
GET/documentsScope documents:read

The workspace's immutable document inventory with each document's processing state and version key. Uploading the same file twice produces two documents that share one content digest; nothing merges them. This operation takes no paging parameters — the inventory is returned whole.

curl -sS -X GET https://tavonel.com/api/v1/documents \
  -H "Authorization: Bearer $TAVONEL_API_KEY"
StatusResponse
200{ code: OK, workspaceId, documents }. A document whose source could not be resolved is present with its refusal rather than omitted.
401AUTH_REQUIRED — No credential was presented, or the bearer token is not a key TAVONEL issued. This is what an unauthenticated request to any scoped route returns. API_KEY_INVALID — The bearer token was well-formed but did not match a stored key.
403API_SCOPE_REQUIRED — The key authenticated and does not carry the scope this operation requires. The request was refused rather than answered with less. PILOT_ACCESS_REQUIRED — The credential is valid and the workspace it names is not admitted to TAVONEL. CONNECTOR_SOURCE_ACCESS_DENIED — The provider refused the stored credential. AUTHORIZATION_CHANGED_RETRY — The caller's authorization changed while the request was in flight, so the request was abandoned rather than finished under a permission that may no longer hold.
429API_RATE_LIMITED — The key has used its per-minute allowance for this scope. The window is a fixed clock minute per key and scope; the allowances are on /docs/billing-and-limits.
503CONNECTOR_SOURCE_ACCESS_UNAVAILABLE — The provider could not be reached. READ_FAILED — A backing store could not be read. API_RATE_LIMIT_UNAVAILABLE — The allowance could not be read, so the request was refused rather than run unbounded.

The PUT itself

The capability response carries url, method, headers and expiresAt. Send exactly the headers it lists and no others: the URL is signed over that header set, so an extra header, a different content-type or a content-length that does not match the bytes makes the signature invalid and object storage refuses the PUT. Do not send your API key to this URL — the capability is the credential, and the storage host has no use for a TAVONEL key.

# URL, TYPE and SIZE are capability.url, capability.headers and requestedBytes.
curl -sS -X PUT "$URL" \
  -H "content-type: $TYPE" \
  -H "content-length: $SIZE" \
  --data-binary @manual.pdf
# 200 with an empty body. A 403 naming an expired request means the capability
# window closed: ask for a new one rather than retrying this URL.

How long the URL lives, and what happens when it does not

Note expiresAt in the capability response is the authority, and it is an absolute instant rather than a duration — read it, do not assume a number. The window is deliberately short: a signed URL is a credential that travels, and a long-lived one is a long-lived credential. After it passes, object storage refuses the PUT with its own expiry error and nothing was written; request a fresh capability for the same file. A document id issued for a capability that was never used carries no bytes, is never compiled, and needs no cleaning up.

The codes these endpoints return

Note 402 on the capability call is a plan or balance refusal — STUDIO_SUBSCRIPTION_REQUIRED, GPU_CREDITS_REQUIRED, or one of the TRIAL_ codes on a free evaluation. 429 is INTAKE_RATE_LIMITED (honour Retry-After: 60), INTAKE_DAILY_QUOTA_EXCEEDED (Retry-After: 3600), or API_RATE_LIMITED for the per-minute scope allowance. Every code named here is in the Errors catalogue with what to do about it.

API version 2026-09-02.1 · reviewed 11 September 2026

Something here out of date or wrong? Report an issue with this page.