Docs index
Quickstart
From an API key to a verified evidence-bound answer, with the one step no key can take.
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. So steps 1 to 5 below are the contract you will call once intake is arranged with us, not a request TAVONEL will accept from you today. 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. Any other caller is refused with STUDIO_SUBSCRIPTION_REQUIRED, and an evaluation trial with SUBSCRIPTION_REQUIRED; branch on those two codes. Team is arranged with us rather than bought at a checkout. That is the plan gate, and it is not the only one: the deployment-wide intake gate at the top of this page stops steps 1 to 4 on every plan until intake is arranged, so a Developer key passing the plan check still will not compile your files here today.
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
fingerprint=$(curl -fsS https://tavonel.com/api/export/trust | jq -r .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.