Quickstart
Docs index
From an API key to an evidence-bound answer, with the one step no key can take.
Try it, with no key
Three unauthenticated reads, run from this page against TAVONEL. No signup, no credential, nothing that spends. Where a response sends a Content-Digest header, this page hashes the bytes it received and says whether they match.
What TAVONEL can read
GET /api/v1/capabilitiesThe same list the upload route validates against. A format absent from it is refused at upload rather than accepted and dropped.
What is open right now
GET /api/statusThe deployment's own state, including whether compiling your own files is open here.
The shape of an answer
GET /reproducibility/sample-worldA deterministic product fixture, unsigned and labelled as such in its own disclosure field. Build against the shape; do not judge extraction from it.
The same checks from a terminal
# Every readable format, with its tier and its stated limitations. No key.
curl -fsS https://tavonel.com/api/v1/capabilities | jq '{schemaVersion, entries: (.entries | length), contentSha256}'
# The pin a caller keeps: drop contentSha256 -- it is the last key -- and re-serialize.
curl -fsS https://tavonel.com/api/v1/capabilities \
| jq -S 'del(.contentSha256)' --indent 0 | tr -d '\n' | sha256sum
# The contract, and the two paths it deliberately does not have.
curl -fsS https://tavonel.com/api/openapi | jq '[.paths | keys[] | select(test("promote|rollback"))]'
# []
# Who signs an export here, or the refusal that says nobody does.
curl -fsS https://tavonel.com/api/export/trust | jq '{activeKeyId, keys: [.keys[] | {keyId, status, publicKeySpkiSha256}]}'curl -fsSO https://tavonel.com/developer/tavonel-public-sample.py
python tavonel-public-sample.py --base-url https://tavonel.com
# sample: 3 objects, 2 evidence records, ... bytes
# digest: sha-256=:...:
# ev-01 -> src_v_01 page 4 bbox [118, 214, 886, 374]
# ev-02 -> src_v_01 page 4 bbox [118, 214, 886, 374]
# PUBLIC SAMPLE OKBoth are from Integration recipes, which says what each one proves.
The same shape as a person reads it, a claim with its evidence and the page region it came from: Explore a Compiled World.
Transcript
- The API reference runs unauthenticated reads from the page: no signup, no credential, nothing that spends.
- What TAVONEL can read: the same list the upload route validates against.
- The deployment's own state, including that compiling your own files is not open here.
- A fixed product sample that shows the shape of an answer. Build against the shape; do not judge extraction from it.
- The response carries a Content-Digest header.
- Now a terminal. This screen replays a captured session: the commands that ran and the output they printed, unedited.
- The hash computed from the bytes received equals the Content-Digest the server stated.
- The published Python recipe is downloaded and matched against its hash in the channel manifest.
- The recipe recomputes the digest and prints each piece of evidence with the page and region it points at.
- The sample labels itself a product sample, not customer proof.
- The CLI file is matched against the channel manifest the same way.
- The CLI's status command runs without a key. It reports reading the public World as open, and compiling your own documents as closed.
- A finished Compiled World is open to read in full today. Compiling your own sources is set up with us.
Note Before you start. Signing in creates your workspace, and uploading and compiling your own sources open for it once its owner accepts the processing terms. Your workspace shows whether it is open. Once it is open, the Developer plan can be bought online. Team and Enterprise are arranged with us rather than enabled by a plan purchase. Steps 1 to 5 below are the calls that do it once your workspace is open. Steps 6 and 7 read a World that already exists, and the completed public Compiled World is readable in full right now — including from the unauthenticated reads the API reference at /api will run for you.
Every request is tenant-scoped by the key it carries. There is no account switch and no impersonation header: a key belongs to one workspace and reaches nothing else.
The seven steps
- Ask for an upload capability. The response is a short-lived direct URL; document bytes never pass through the application server.
- PUT the file to that URL with the same content type you declared.
- Start a compile with the document ids you want in the World. It answers 202 with a job id, not a World.
- Poll GET /api/compile-jobs/{jobId} until state is ready, review_required, failed or cancelled. A settled job carries the collectionId the candidate was written to.
- A person activates the World. This step is not in the script and not in the API: activation is a browser-session action by a human in the workspace, and no API key can activate or roll back a World.
- Ask the active World a question, and read which retrieval runtime answered it.
- Download the signed package and verify it offline with the published verifiers.
Why step 5 stops a script
Note Step 5 is the one that stops a script, and it stops for two separate reasons. Activation is human-only by design — a candidate is not organizational truth until a person says so, and no API key of any plan has a promote or rollback path to call. Separately, the activation surface is plan-gated: it runs on the Developer plan held by the workspace owner, or on the Team plan under its usual workspace roles, so steps 1-4 and 6-7 are what a Developer key is scoped for, and step 5 is open to you as well when you own the workspace. A free evaluation reaches it once, as the owner, for its one World. Any other caller is refused with STUDIO_SUBSCRIPTION_REQUIRED, an evaluation asking for a second World with TRIAL_WORLD_LIMIT_REACHED, and a workspace with neither a plan nor a live evaluation with SUBSCRIPTION_REQUIRED. Branch on those three codes. Team is arranged with us rather than bought at a checkout. That is the plan gate, and it is not the only one. Steps 1 to 4 also need your workspace to be open for processing, which it is once its owner accepts the processing terms, and the workspace shows whether it is. From then on a Developer key that passes the plan check compiles your files.
Compile a document set
# Requires curl and jq. TAVONEL_API_KEY holds a key scoped documents:intake + collections:compile + collections:read.
capability=$(curl -fsS https://tavonel.com/api/v1/uploads/capability \
-H "Authorization: Bearer $TAVONEL_API_KEY" -H "content-type: application/json" \
-d '{"originalFilename":"manual.pdf","declaredMimeType":"application/pdf","requestedBytes":184320}')
document_id=$(printf '%s' "$capability" | jq -r .documentId)
# 2. The bytes go straight to storage. Content-Type must match declaredMimeType.
curl -fsS -X PUT "$(printf '%s' "$capability" | jq -r .uploadUrl)" \
-H "content-type: application/pdf" --data-binary @manual.pdf
# 3. 202 Accepted, with a job id. The compile continues if this shell exits.
job_id=$(curl -fsS https://tavonel.com/api/compile-jobs \
-H "Authorization: Bearer $TAVONEL_API_KEY" -H "content-type: application/json" \
-d "{\"documentIds\":[\"$document_id\"]}" | jq -r .jobId)
# 4. Poll until it settles. Nothing here is a World yet.
while :; do
job=$(curl -fsS "https://tavonel.com/api/compile-jobs/$job_id" -H "Authorization: Bearer $TAVONEL_API_KEY")
state=$(printf '%s' "$job" | jq -r .job.state)
echo "state=$state"
case "$state" in ready|review_required|failed|cancelled) break ;; esac
sleep 5
done
collection_id=$(printf '%s' "$job" | jq -r .job.collectionId)
echo "candidate: $collection_id — a person activates it in the workspace before step 6"import json, os, time, urllib.request
BASE = "https://tavonel.com"
KEY = os.environ["TAVONEL_API_KEY"]
def call(method, path, body=None, headers=None):
data = json.dumps(body).encode() if body is not None else None
request = urllib.request.Request(BASE + path, data=data, method=method)
request.add_header("authorization", f"Bearer {KEY}")
if data is not None:
request.add_header("content-type", "application/json")
for name, value in (headers or {}).items():
request.add_header(name, value)
with urllib.request.urlopen(request) as response:
return json.loads(response.read() or b"{}")
capability = call("POST", "/api/v1/uploads/capability", {
"originalFilename": "manual.pdf",
"declaredMimeType": "application/pdf",
"requestedBytes": os.path.getsize("manual.pdf"),
})
# The PUT is unauthenticated: the capability URL is the credential, and it is short-lived.
with open("manual.pdf", "rb") as handle:
put = urllib.request.Request(capability["uploadUrl"], data=handle.read(), method="PUT")
put.add_header("content-type", "application/pdf")
urllib.request.urlopen(put).read()
accepted = call("POST", "/api/compile-jobs", {"documentIds": [capability["documentId"]]})
while True:
job = call("GET", f"/api/compile-jobs/{accepted['jobId']}")["job"]
print("state=", job["state"])
if job["state"] in {"ready", "review_required", "failed", "cancelled"}:
break
time.sleep(5)
print("candidate:", job["collectionId"], "— a person activates it before step 6")import { readFile, stat } from "node:fs/promises";
const BASE = "https://tavonel.com";
const KEY = process.env.TAVONEL_API_KEY!;
async function call<T>(method: string, path: string, body?: unknown): Promise<T> {
const response = await fetch(BASE + path, {
method,
headers: { authorization: `Bearer ${KEY}`, ...(body ? { "content-type": "application/json" } : {}) },
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) throw new Error(`${method} ${path} -> ${response.status} ${await response.text()}`);
return response.json() as Promise<T>;
}
const capability = await call<{ documentId: string; uploadUrl: string }>(
"POST", "/api/v1/uploads/capability",
{ originalFilename: "manual.pdf", declaredMimeType: "application/pdf", requestedBytes: (await stat("manual.pdf")).size },
);
await fetch(capability.uploadUrl, {
method: "PUT",
headers: { "content-type": "application/pdf" },
body: await readFile("manual.pdf"),
});
const accepted = await call<{ jobId: string }>("POST", "/api/compile-jobs", { documentIds: [capability.documentId] });
const settled = new Set(["ready", "review_required", "failed", "cancelled"]);
let job: { state: string; collectionId: string | null };
do {
({ job } = await call<{ job: typeof job }>("GET", `/api/compile-jobs/${accepted.jobId}`));
console.log("state=", job.state);
if (!settled.has(job.state)) await new Promise((done) => setTimeout(done, 5_000));
} while (!settled.has(job.state));
console.log("candidate:", job.collectionId, "— a person activates it before step 6");Ask a question and download the package
# 6. Ask the active World. `retrievalPath` names which runtime answered:
# compiled-retrieval-v1, or excerpt-concatenation-fallback when the active World
# has no compiled retrieval run. /search has no fallback and answers 409 in that case.
# The fallback path returns citations; the compiled path returns a contextPacket.
curl -fsS "https://tavonel.com/api/v1/collections/$collection_id/ask" \
-H "Authorization: Bearer $TAVONEL_API_KEY" -H "content-type: application/json" \
-d '{"question":"What is the documented retention period?"}' \
| jq '{code, retrievalPath, retrievalNotice, citations, contextPacket}'
# 7. Export, then verify without us. The fingerprint comes from the trust endpoint,
# not from the archive -- an archive cannot vouch for its own key.
curl -fsS "https://tavonel.com/api/v1/collections/$collection_id/download" \
-H "Authorization: Bearer $TAVONEL_API_KEY" -o world.zip
# The trust record is v2: the active key's fingerprint is under keys[], not at the top.
fingerprint=$(curl -fsS https://tavonel.com/api/export/trust | jq -r '.activeKeyId as $id | .keys[] | select(.keyId == $id) | .publicKeySpkiSha256')
node tavonel-verify-export.mjs --archive world.zip --trusted-fingerprint "$fingerprint"
node tavonel-verify-package.mjs --package world.zip --require-signatureanswer = call("POST", f"/api/v1/collections/{job['collectionId']}/ask",
{"question": "What is the documented retention period?"})
print(answer["code"], answer["retrievalPath"])
for citation in answer.get("citations", []):
print(citation["sourceVersionId"], citation["pageNumber1"], citation["bbox1000"])
# The download is bytes, not JSON, so it does not go through call().
download = urllib.request.Request(f"{BASE}/api/v1/collections/{job['collectionId']}/download")
download.add_header("authorization", f"Bearer {KEY}")
with urllib.request.urlopen(download) as response, open("world.zip", "wb") as out:
out.write(response.read())
# Verification is the two published Node verifiers; there is no Python port of them.
# See the CLI page for the download-and-pin commands.import { writeFile } from "node:fs/promises";
const answer = await call<{ code: string; retrievalPath: string; retrievalNotice?: string }>(
"POST", `/api/v1/collections/${job.collectionId}/ask`,
{ question: "What is the documented retention period?" },
);
console.log(answer.code, answer.retrievalPath, answer.retrievalNotice ?? "");
const archive = await fetch(`${BASE}/api/v1/collections/${job.collectionId}/download`, {
headers: { authorization: `Bearer ${KEY}` },
});
await writeFile("world.zip", Buffer.from(await archive.arrayBuffer()));
// Then run the two published verifiers; see the CLI page.What /ask answers from
Note /ask answers only from the World a person has approved. A review_required candidate stays readable and exportable until then, and it is never treated as authoritative: there is no parameter that points /ask at a candidate.
API version 2026-09-02.1 · reviewed 11 September 2026
Something here out of date or wrong? Report an issue with this page.