# Tuor Developer documentation for Tuor — store model traces and manage human review. This file contains the full Tuor documentation corpus as Markdown. --- # Welcome Source: https://docs.tuor.dev/ # Welcome to Tuor Welcome to the developer documentation for **Tuor** — a full-stack platform for storing model traces and managing human review. Tuor sits between your model and your reviewers. You log every model call as a *trace*; reviewers approve, reject, or correct the output in a purpose-built review workspace; and reviewed data flows back to you as webhook events or a streaming JSONL export — ready to feed dashboards, evals, or training pipelines. --- ## Documentation map **Docs** — start here - **[Getting Started](./getting-started.md)** — Create a project, get an API key, send your first trace. - **[Core Concepts](./core-concepts.md)** — Traces, outcomes, payload shapes, and result delivery. **Reference** — integration details - **[Integration API](./reference/api-reference.md)** — Send traces, fetch review results, tag cohorts, and export reviewed data. - **[Webhooks](./reference/webhooks.md)** — Receive real-time review verdicts with signature verification. **Guides** — integration patterns - **[Reviewer Blueprints](./guides/reviewer-blueprints.md)** — Structure payloads so reviewers can work fast and accurately. - **[Dynamic Prompts](./guides/dynamic-prompts.md)** — Advanced: fetch the active prompt at runtime and link traces to the prompt version that produced them. --- ## What does Tuor do, at a glance? ```mermaid flowchart LR A[Your App] -->|POST /v1/traces| B[Tuor] B --> C[Review Workspace] C -->|approve / reject / correct| B B -->|Outbound Webhook| A B -->|GET /v1/traces/export| D[Fine-Tuning / Eval Pipeline] ``` - **Ingest** every model call (input + output + config) with one HTTP request. - **Review** in a workspace designed for non-technical experts: project-scoped trace lists, correction editors, saved diffs, PDF viewers, and image viewers. - **Receive** signed webhooks the moment a verdict lands, or stream the entire reviewed corpus as JSONL. --- ## Conventions used in this documentation - All API examples use `https://api.tuor.dev` as the base URL. - All programmatic endpoints are versioned under `/v1/`. - The full documentation corpus is available as [llms-full.txt](llms-full.txt). If you spot a gap or inaccuracy in these docs, contact [Tuor support](mailto:support@tuor.dev). --- # Getting Started Source: https://docs.tuor.dev/getting-started/ # Getting Started This guide walks you from zero to a first trace appearing in the Web Console in about five minutes. > **Prerequisite**: a Tuor account with access to at least one organization. Sign up at [tuor.dev](https://tuor.dev). --- ## Create a project Projects are the unit of organization in Tuor. Every trace belongs to exactly one project, and a project's input/output types control how reviewers see that trace. In the Web Console: 1. Open **Settings -> Projects**. 2. Click **Create Project**. 3. Give it a name (1-100 characters). 4. Copy the generated project ID from the project's **ID** column (IDs look like `proj_aB3x…`). You will use this as `project_id` in every ingest request. New projects start with `text` input and `key_value` output. To change the reviewer experience, click **Edit** on the project and update: - **Input type** - controls how the review workspace renders the data your model receives: - `text` — prompts, messages, or any unstructured input. Rendered as a high-density, multi-line viewer. - `pdf` — base64-encoded PDF documents. Rendered as a scrollable PDF pane. - `image` — base64-encoded images. Rendered as an image viewer. - **Output type** - controls the correction editor: - `key_value` — flat dictionary, rendered as an editable spreadsheet-like form. - `text` — long-form text, rendered as an editable text panel with word/character counts. - `classification` — single-label prediction with alternatives, rendered as a label selector with custom-label entry. You can change these later by editing the project, but the project setting controls how all of its traces render. Only change types when existing traces already have payloads compatible with the new viewer/editor. --- ## Get an API key API keys authenticate programmatic traffic from your services to Tuor. They are scoped to the organization that issued them. 1. Open **Settings -> API Keys** in the Web Console. 2. Click **Create API Key** and give it a name (e.g. `production-ingest`, `staging`). 3. Copy the key shown on screen — it starts with `tuor_` and is shown **only once**. Store it in your secret manager. Keys never expire on their own. Revoke any key from the same panel; revoked keys reject all requests immediately. --- ## Send your first trace Replace `proj_abc123` with the project ID you copied from the Web Console. The simplest possible ingest: ```bash curl -X POST "https://api.tuor.dev/v1/traces/" \ -H "X-API-Key: $TUOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "project_id": "proj_abc123", "model_input": { "content": "What is the capital of France?" }, "model_output": { "answer": "Paris" }, "trace_config": { "model": "gpt-4o", "temperature": 0.0 } }' ``` A successful response returns a `TraceResponse` with the new trace's `id`, status (`pending`), and timestamps. The trace should appear immediately on the **Traces** page for the corresponding project in the Web Console. > **Tip**: putting your model's main prompt in `model_input.content` makes the review workspace render it as a readable, full-width text block. See the [Reviewer Blueprints](./guides/reviewer-blueprints.md) page for the rendering conventions per project type. --- ## Call Tuor from application code Call the API directly from your service code and keep the API key in your server-side secret manager. ### Python ```python import os import httpx TUOR_API_KEY = os.environ["TUOR_API_KEY"] trace = httpx.post( "https://api.tuor.dev/v1/traces/", headers={"X-API-Key": TUOR_API_KEY}, json={ "project_id": "proj_abc123", "model_input": {"content": "Compute net income"}, "model_output": {"amount": 250_000}, "trace_config": {"model": "gpt-4o", "internal_run_id": "run_01j..."}, }, timeout=10, ) trace.raise_for_status() ``` ### TypeScript ```ts const response = await fetch("https://api.tuor.dev/v1/traces/", { method: "POST", headers: { "X-API-Key": process.env.TUOR_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ project_id: "proj_abc123", model_input: { content: "Compute net income" }, model_output: { amount: 250_000 }, trace_config: { model: "gpt-4o", internal_run_id: "run_01j..." }, }), }); if (!response.ok) throw new Error(await response.text()); const trace = await response.json(); ``` --- ## Where next? - Get familiar with the **[trace data model and lifecycle](./core-concepts.md)** before wiring Tuor into production traffic. - The **[Integration API](./reference/api-reference.md)** documents trace ingest, fetching, tagging, and export. - If you want corrections to flow back into your system in real time, set up an **[outbound webhook](./reference/webhooks.md)**. --- # Core Concepts Source: https://docs.tuor.dev/core-concepts/ # Core Concepts This page covers the small set of concepts you need to integrate Tuor cleanly: traces, review outcomes, payload shapes, and how reviewed data gets back to your system. --- ## Trace A **trace** is one model call that can be reviewed by a human. It has three payloads you control: | Field | Required | Purpose | |---|---|---| | `model_input` | yes | What your model saw: prompt, document, image, ticket, transcript, etc. | | `model_output` | yes | What your model produced. | | `trace_config` | no | Metadata for traceback: model name, prompt version, run ID, customer/account ID, experiment ID. | Tuor adds review fields: | Field | Meaning | |---|---| | `status` | `pending`, `approved`, `rejected`, or `corrected`. | | `corrected_output` | Human-edited output, present after a correction. | | `final_output` | The output to trust downstream. It equals `model_output` when approved, `corrected_output` when corrected, and `null` when pending or rejected. | | `tags` | Optional labels for routing, filtering, and export cohorts. | Each JSON payload field is capped at **5 MB** and **10 nesting levels**. --- ## Review outcomes Every trace starts as `pending`. Reviewers then choose one of three outcomes: | Outcome | Meaning | `final_output` | |---|---|---| | `approved` | The model output is correct as-is. | `model_output` | | `corrected` | A reviewer fixed the output. | `corrected_output` | | `rejected` | The output is not usable. | `null` | For most integrations, this is the state machine that matters: send traces while they are `pending`, then consume `approved` and `corrected` traces through webhooks or export. --- ## Payload shapes Project input/output types control how reviewers see each trace. Choose types that match the data your pipeline already produces. **Inputs** | Project `input_type` | Put this in `model_input` | |---|---| | `text` | `{"content": "...", "system_prompt": "..."}` | | `pdf` | `{"pdf_base64": "...", "filename": "..."}` | | `image` | `{"image_base64": "...", "media_type": "image/png", "filename": "..."}` | **Outputs** | Project `output_type` | Put this in `model_output` | |---|---| | `key_value` | A flat object such as `{"vendor": "Acme", "total": 1250.45}`. | | `text` | `{"content": "long-form text..."}`. | | `classification` | `{"label": "billing_dispute", "alternatives": ["billing_dispute", "technical_support"]}`. | See [Reviewer Blueprints](./guides/reviewer-blueprints.md) for practical recipes. --- ## Getting results back There are three common patterns: | Pattern | Best for | |---|---| | `trace_config` IDs | Tying every Tuor trace back to your own run, user, ticket, or database row. | | Webhooks | Reacting as soon as a reviewer approves, rejects, or corrects a trace. | | JSONL export | Batch evals, dashboards, analytics, training-data jobs, or prompt-improvement workflows. | Use `trace_config` aggressively. For example, include `internal_run_id`, `prompt_version_id`, and `customer_id` when you create a trace. Tuor echoes those values in webhooks and exports, so downstream systems do not need a separate mapping table. --- # Integration API Source: https://docs.tuor.dev/reference/api-reference/ # Integration API Use the Tuor API from your backend service to send model traces and retrieve reviewed outputs. The Web Console handles organization setup, project creation, API key management, analytics, and reviewer workflows. All integration endpoints are versioned under `/v1`. Examples use `https://api.tuor.dev` as the base URL. --- ## Authentication Send your API key in the `X-API-Key` header: ```http X-API-Key: tuor_AbCdEf123... ``` API keys are created in the Web Console and are scoped to the organization that issued them. Store them server-side only; do not expose them in browser code. --- ## Limits and payload constraints The default ingest limit is **200 trace creates per minute**. Other integration endpoints have lower per-route limits and return `429 Too Many Requests` with `Retry-After` when exceeded. Every JSON payload field is independently validated: - **Maximum size**: 5 MB per field. - **Maximum nesting depth**: 10 levels. Oversize or over-nested payloads are rejected with `422 Unprocessable Entity`. --- ## Core endpoints | Method | Path | Use when | |---|---|---| | `POST` | `/v1/traces/` | Send a model input/output pair to Tuor for review. | | `GET` | `/v1/traces/{trace_id}` | Fetch one trace and its current review result. | | `GET` | `/v1/traces/` | List or filter traces by project, status, or tag. | | `GET` | `/v1/traces/export` | Stream reviewed traces as JSONL for evals, dashboards, or training pipelines. | | `GET` | `/v1/tags/` | List tags and copy IDs for trace tagging or export filters. | | `POST` | `/v1/traces/{trace_id}/tags` | Attach tags for routing, cohorting, or downstream filtering. | | `DELETE` | `/v1/traces/{trace_id}/tags/{tag_id}` | Remove a tag from a trace. | Webhook receiver setup is covered separately in [Webhooks](./webhooks.md). --- ## Send a trace ### `POST /v1/traces/` Creates a new trace with status `pending`. **Request body** | Field | Type | Required | Notes | |---|---|---|---| | `project_id` | string | yes | The project that owns this trace. Copy it from the project's **ID** column in the Web Console. | | `model_input` | object | yes | What your model saw. Shape it for the project input type. | | `model_output` | object | yes | What your model produced. Shape it for the project output type. | | `trace_config` | object | no | Metadata such as `model`, `temperature`, `internal_run_id`, or `prompt_version_id`. Echoed in webhooks and exports. | ```bash curl -X POST "https://api.tuor.dev/v1/traces/" \ -H "X-API-Key: $TUOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "project_id": "proj_abc123", "model_input": { "content": "Extract company name from this invoice." }, "model_output": { "company": "Acme Corp" }, "trace_config": { "model": "gpt-4o", "internal_run_id": "run_01j2abcde" } }' ``` **Response 201** returns a trace object. The most important fields for integrations are: | Field | Meaning | |---|---| | `id` | Store this if you want to fetch the trace later. | | `status` | Starts as `pending`; later becomes `approved`, `rejected`, or `corrected`. | | `model_input` / `model_output` | The original payloads you sent. | | `corrected_output` | Reviewer-edited output, present after a correction. | | `final_output` | The authoritative result: original output if approved, corrected output if corrected, `null` if rejected or still pending. | | `trace_config` | Your metadata, round-tripped unchanged. | | `tags` | Tags attached to the trace. | | `version` | Optimistic-lock version for advanced lifecycle writes. | --- ## Read review results ### `GET /v1/traces/{trace_id}` Fetch one trace by ID. Use this when your system wants to poll for the final review outcome. ```bash curl "https://api.tuor.dev/v1/traces/trace_abc123" \ -H "X-API-Key: $TUOR_API_KEY" ``` ### `GET /v1/traces/` List traces with pagination and filters. | Parameter | Type | Notes | |---|---|---| | `project_id` | string | Restrict to one project. | | `status` | `pending` \| `approved` \| `rejected` \| `corrected` | Filter by review outcome. | | `tag` | string | Restrict to traces carrying this tag ID. | | `skip` | int | Offset for pagination. Default `0`. | | `limit` | int | Page size. Default `50`, max `500`. | --- ## Export reviewed data ### `GET /v1/traces/export` Streams one JSON object per line. Use this for offline evals, dashboards, prompt analysis, or training-data jobs. | Parameter | Type | Notes | |---|---|---| | `project_id` | string | Restrict to one project. | | `status` | string | Commonly `approved` or `corrected` for training/eval datasets. | | `tag` | string | Restrict to traces carrying this tag ID. | | `from_date`, `to_date` | ISO 8601 | Filter by `created_at`. | | `exclude_fields` | comma-separated paths | Remove direct child keys such as `model_input.pdf_base64`. | ```bash curl "https://api.tuor.dev/v1/traces/export?status=corrected&exclude_fields=model_input.pdf_base64" \ -H "X-API-Key: $TUOR_API_KEY" ``` Each JSONL row includes: ```json { "model_input": { "...": "..." }, "model_output": { "...": "..." }, "corrected_output": { "...": "..." }, "final_output": { "...": "..." }, "trace_config": { "...": "..." }, "tags": ["critical", "q2-audit"], "events": [], "metadata": { "trace_id": "trace_abc123", "status": "corrected", "project_id": "proj_abc123", "created_at": "2026-05-20T10:00:00Z", "updated_at": "2026-05-20T14:19:55Z", "last_reviewed_at": "2026-05-20T14:19:55Z", "version": 2 } } ``` `metadata` is abbreviated above; it also carries provenance fields — `last_reviewed_by`, `created_by`, `org_id`, `updated_by`, and the `deleted_*`/`restored_*` audit columns. Treat it as an open object rather than coding against a fixed key set. The `events` array contains the audit trail for teams that need provenance. Most downstream jobs can rely on `status`, `final_output`, `trace_config`, and `tags`. --- ## Tags Tags are optional. Use them when you need to route review work or export a specific cohort. Create and manage tags in the Web Console under **Settings -> Tags**. Integration code needs tag IDs, not tag names. To discover IDs, call `GET /v1/tags/` and store the returned `id` for each tag you plan to apply. ```bash curl "https://api.tuor.dev/v1/tags/" \ -H "X-API-Key: $TUOR_API_KEY" ``` An abbreviated response includes each tag's `id`, `name`, `color`, and description: ```json [ { "id": "tag_critical", "name": "critical", "color": "#ef4444", "description": "High-priority reviews" } ] ``` ```bash curl -X POST "https://api.tuor.dev/v1/traces/trace_abc123/tags" \ -H "X-API-Key: $TUOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "tag_ids": ["tag_critical"] }' ``` A trace can have up to 10 tags. --- ## Advanced Most integrations do not need these endpoints on day one. | Area | Endpoints | Notes | |---|---|---| | Prompt versions | `GET /v1/projects/{project_id}/prompts`, `POST /v1/projects/{project_id}/prompts`, `POST /v1/projects/{project_id}/prompts/refine`, `POST /v1/projects/{project_id}/prompts/accept-refinement` | Use when your service fetches active prompts from Tuor at runtime. See [Dynamic Prompts](../guides/dynamic-prompts.md). | | Trace lifecycle automation | `POST /v1/traces/{trace_id}/review`, `POST /v1/traces/{trace_id}/reset`, `DELETE /v1/traces/{trace_id}`, `POST /v1/traces/{trace_id}/restore` | The Web Console is the normal review surface. These endpoints require `expected_version` to prevent conflicting writes. | | Project and tag administration | `/v1/projects/*`, `/v1/tags/*` | Usually handled in the Web Console. Useful only for provisioning automation. | --- ## Error responses Errors are returned as JSON: ```json { "detail": "Invalid or revoked API key" } ``` Common status codes: | Status | Meaning | |---|---| | `400` | Malformed request. | | `401` | Missing or invalid API key. | | `402` | Trace limit reached. | | `403` | API key is valid but does not have an organization context. | | `404` | Resource does not exist or is not visible to this organization. | | `409` | Optimistic-lock mismatch or forbidden lifecycle transition. | | `422` | Payload exceeds size/depth limits or fails schema validation. | | `429` | Rate limit exceeded. Respect `Retry-After`. | --- # Webhooks Source: https://docs.tuor.dev/reference/webhooks/ # Outbound Webhooks Webhooks let your systems react to reviewer verdicts in real time — for example, updating a record the instant a reviewer corrects a model output, or paging an on-call engineer when a critical trace is rejected. This page covers receiver setup, payload shape, signature verification, and delivery semantics. --- ## How webhooks work 1. You configure a webhook URL and secret on a project. 2. Whenever a reviewer submits a verdict in the review workspace — `approve`, `reject`, or `correct` — Tuor enqueues a background job. 3. The background task dispatches an HTTP `POST` request asynchronously to the configured URL with a JSON body and signature headers. 4. Delivery status is recorded in Tuor and can be inspected from the Web Console. ```mermaid sequenceDiagram participant Reviewer as Review Workspace participant API as Tuor API participant Q as Background Task participant App as Your Server Reviewer->>API: Submit review (approve/reject/correct) API->>API: Persist trace + event API->>Q: Enqueue webhook delivery Q->>App: POST (signed JSON) App-->>Q: 2xx OK Q->>API: Record delivery (success) ``` ### Outbound security Tuor only delivers to **publicly routable HTTPS hosts**. Loopback addresses (`localhost`, `127.0.0.1`, `::1`) and RFC-1918 private ranges (`10.0.0.0/8`, `192.168.0.0/16`, `172.16.0.0/12`) are rejected to prevent SSRF. URLs must use the `https://` scheme and may not include embedded credentials. --- ## Configure a webhook Configure the webhook URL and secret from the project's Webhook settings in the Web Console. For provisioning automation, the same fields are available on `PUT /v1/projects/{project_id}`: ```bash curl -X PUT "https://api.tuor.dev/v1/projects/proj_abc123" \ -H "X-API-Key: $TUOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_url": "https://hooks.example.com/tuor", "webhook_secret": "whsec_8f3...", "webhook_active": true }' ``` | Field | Notes | |---|---| | `webhook_url` | The HTTPS endpoint that will receive `POST` requests. Up to 2048 characters. | | `webhook_secret` | A shared secret used for HMAC-SHA256 signing. Up to 512 characters. | | `webhook_active` | Boolean. Set `false` to stop deliveries without losing the URL or secret. | You can also send a one-off test ping from the Web Console, or via `POST /v1/projects/{project_id}/test-webhook` when provisioning by API. --- ## Tying webhook events back to your system (the traceback loop) The cleanest way to map a webhook delivery to a row in your own database is to round-trip an identifier through `trace_config`. Every value you put in `trace_config` at ingest time is echoed back to you on the webhook payload under `trace.trace_config`. ### 1. Stamp your identifiers at ingest time ```json { "project_id": "proj_xyz", "model_input": { "content": "Compute net income" }, "model_output": { "amount": 250000 }, "trace_config": { "model": "gpt-4o", "internal_run_id": "run_01j2abcde", "user_session_id": "sess_999xxx", "prompt_version_id": "prm_abc123" } } ``` ### 2. Look up the row when the webhook arrives ```ts app.post("/tuor", async (req, res) => { const { trace } = req.body; const runId = trace.trace_config?.internal_run_id; await db.runs.update(runId, { verdict: req.body.event_type, final_output: req.body.final_output_after, }); res.sendStatus(200); }); ``` You never need to keep a separate mapping table from Tuor `trace_id` to your row — the trace itself carries the link. --- ## Payload structure Every webhook delivery is a JSON object with the following shape: ```json { "event_id": "evt_abc123xyz", "event_type": "review.corrected", "actor_type": "user", "actor_id": "user_2def3gh", "occurred_at": "2026-05-20T14:19:55Z", "status_before": "pending", "status_after": "corrected", "final_output_before": null, "final_output_after": { "company": "Google Inc." }, "corrected_output_before": null, "corrected_output_after": { "company": "Google Inc." }, "correction_diff": [ { "path": "company", "from": "Gogle Inc.", "to": "Google Inc." } ], "trace": { "id": "trace_555aaa", "model_input": { "content": "Extract company name" }, "model_output": { "company": "Gogle Inc." }, "trace_config": { "model": "gpt-4o", "internal_run_id": "run_01j2abcde", "user_session_id": "sess_999xxx", "prompt_version_id": "prm_abc123" } } } ``` `webhook.test` payloads are intentionally smaller: they include `event_id`, `event_type`, `action`, `occurred_at`, `project`, and `trace: null`. ### Field reference | Field | Description | |---|---| | `event_id` | ID of the underlying trace event. It is stable across manual retries, so use it for idempotent receiver-side processing. | | `event_type` | One of `review.approved`, `review.rejected`, `review.corrected`, `webhook.test`. | | `actor_type` | `user`, `api_key`, or `system`. | | `actor_id` | The user ID or API key ID that performed the action. | | `occurred_at` | ISO 8601 timestamp of the underlying event. | | `status_before` / `status_after` | Trace status before and after the review. | | `final_output_before` / `final_output_after` | The authoritative output, before and after. `null` is meaningful (e.g. after a `reject`). | | `corrected_output_before` / `corrected_output_after` | The stored correction, before and after. | | `correction_diff` | On `review.corrected` only: a list of field-level changes. `null` otherwise. | | `trace` | A compact snapshot of the trace: its `id`, `model_input`, `model_output`, and `trace_config`. | ### Event types | Event | Fires when | |---|---| | `review.approved` | A reviewer (or API caller) submits an approve action. | | `review.rejected` | A reviewer submits a reject action. | | `review.corrected` | A reviewer submits a correction. | | `webhook.test` | You hit `POST /v1/projects/{project_id}/test-webhook` to verify wiring. | > Only review outcomes produce webhook deliveries. Resets, deletes, restores, and tag changes are recorded in the trace event log but do not call your webhook. --- ## Request headers Every webhook request includes: | Header | Purpose | |---|---| | `Content-Type: application/json` | Body is always JSON. | | `X-Tuor-Event-Id` | Unique event identifier (matches the `event_id` in the body). | | `X-Tuor-Trace-Id` | The trace's ID. Empty string for `webhook.test` events. | | `X-Tuor-Signature` | HMAC-SHA256 signature of the raw request body, formatted `sha256=`. | --- ## Signature verification **Always** verify the signature before trusting a webhook body. Tuor signs the **raw request body bytes** with HMAC-SHA256 using the `webhook_secret` you configured on the project. Use a constant-time comparison; never compare hex strings with `==` in user code. ### Python ```python import hmac import hashlib def verify_tuor_signature(raw_body: bytes, header_value: str, secret: str) -> bool: if not header_value.startswith("sha256="): return False expected = hmac.new( secret.encode("utf-8"), raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(header_value.split("=", 1)[1], expected) ``` ### TypeScript ```ts import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyTuorSignature( rawBody: Buffer, headerValue: string, secret: string, ): boolean { if (!headerValue.startsWith("sha256=")) return false; const expected = createHmac("sha256", secret).update(rawBody).digest("hex"); const provided = headerValue.slice("sha256=".length); const a = Buffer.from(expected, "hex"); const b = Buffer.from(provided, "hex"); return a.length === b.length && timingSafeEqual(a, b); } ``` > **Important**: signatures are computed over the **raw bytes** of the request body, *before* any JSON parsing. Frameworks that re-serialize the body will produce a different digest. In Express, use `express.raw({ type: "application/json" })` on the webhook route; in FastAPI, read `await request.body()` before parsing. ### Complete raw-body handlers Mount the raw-body webhook route before any global JSON parser middleware. === "Express" ```ts import express from "express"; const app = express(); const webhookSecret = process.env.TUOR_WEBHOOK_SECRET!; app.post("/tuor", express.raw({ type: "application/json" }), (req, res) => { const signature = req.header("X-Tuor-Signature") ?? ""; if (!verifyTuorSignature(req.body, signature, webhookSecret)) { return res.sendStatus(401); } const event = JSON.parse(req.body.toString("utf8")); // Store event.event_id idempotently, then enqueue your own processing. return res.sendStatus(204); }); app.use(express.json()); ``` === "FastAPI" ```python import json import os from fastapi import FastAPI, Header, HTTPException, Request, Response app = FastAPI() webhook_secret = os.environ["TUOR_WEBHOOK_SECRET"] @app.post("/tuor") async def tuor_webhook( request: Request, x_tuor_signature: str = Header(default=""), ) -> Response: raw_body = await request.body() if not verify_tuor_signature(raw_body, x_tuor_signature, webhook_secret): raise HTTPException(status_code=401, detail="invalid signature") event = json.loads(raw_body) # Store event["event_id"] idempotently, then enqueue your own processing. return Response(status_code=204) ``` --- ## Delivery semantics - **Asynchronous**: review API calls return as soon as the trace is persisted; webhooks fire shortly after in a background task. - **No automatic retries**: a failed delivery is recorded with `status = "failed"` and is **not** retried automatically. Use the **Webhooks** page in the Web Console to re-attempt it manually. - **HTTP timeout**: 10 seconds per attempt. - **Considered successful**: any `2xx` response. Anything else is recorded as a failure. Delivery history and manual retry controls are available on the project's **Webhooks** page in the Web Console. --- ## Receiver checklist - [ ] Listen on a stable, publicly routable HTTPS URL. - [ ] Respond with `2xx` only **after** you've durably stored the event (or are sure you can drop it). - [ ] Verify `X-Tuor-Signature` on every request — constant-time compare. - [ ] Deduplicate on `event_id` in case you manually retry a delivery. - [ ] Process within 10 seconds; for heavy work, hand off to your own background queue and return immediately. - [ ] Handle `webhook.test` events gracefully — they use `trace: null` and identify the project being tested. --- # Reviewer Blueprints Source: https://docs.tuor.dev/guides/reviewer-blueprints/ # Reviewer Blueprints Aligning your reviewers is the single biggest lever for review throughput and accuracy. This page is a set of practical recipes for shaping `model_input` and `model_output` so that the Tuor review workspace renders intuitively — even for non-technical reviewers. Most of these recipes are about **putting your data in the keys the review workspace expects**. Get the keys right and the rendering is automatic. --- ## Reviewer context mapping Reviewers need to see *what your model saw* alongside *what your model produced*. The Tuor review workspace layout is driven by the project's `input_type`. ### Text input (`input_type: "text"`) Use a free-form text input field for any prompt-only model. The review workspace renders these keys, if present: | Key in `model_input` | What it does | |---|---| | `content` | Rendered as the prominent, full-width text block — the main reading panel. | | `system_prompt` | Rendered in a collapsible panel above the content, styled to indicate "instructions to the model". | ```json // POST /v1/traces/ — model_input { "system_prompt": "You are a financial assistant. Always return JSON.", "content": "Extract the net income from the attached statement..." } ``` Any additional keys are still shown (as a structured tree below the main panel) but are not given the prominent layout. ### Document input (`input_type: "pdf"`) Reviewers compare the extraction form against the original document side-by-side. Configure the project's `input_type` to `pdf` and provide: | Key in `model_input` | What it does | |---|---| | `pdf_base64` | Base64-encoded PDF bytes. Rendered in a scrollable PDF viewer. | | `filename` | Optional. Displayed in the viewer chrome. | ```json // POST /v1/traces/ — model_input { "pdf_base64": "JVBERi0xLjQKJ...", "filename": "acme-invoice-2026-05-15.pdf" } ``` Note the 5 MB per-field cap — for very large PDFs, downsample or paginate before ingest. ### Image input (`input_type: "image"`) For image-grounded tasks (receipts, screenshots, ID documents, charts): | Key in `model_input` | What it does | |---|---| | `image_base64` | Base64-encoded image bytes. Rendered in an image viewer. | | `media_type` | MIME type (e.g. `image/png`). Used as the `data:` URI type. | | `filename` | Optional. Displayed in the viewer chrome. | ```json // POST /v1/traces/ — model_input { "image_base64": "iVBORw0KGgoAAAANS...", "media_type": "image/png", "filename": "receipt-2026-05-15.png" } ``` --- ## Task-specific schema blueprints The shape of `model_output` determines which editor reviewers get. Match your output payload to the project's `output_type` and the editor is automatic. ### Structured extraction (`output_type: "key_value"`) Best for contracts, invoices, receipts, forms — anything where the model produces a flat dictionary of fields. The reviewer sees a clean spreadsheet-style table where they can edit values, add missing keys, or remove spurious ones. ```json // POST /v1/traces/ — model_output { "vendor_name": "Acme Corp", "invoice_date": "2026-05-15", "total_amount": 1250.45, "tax_id": "12-345678" } ``` > **Keep it flat.** Nested objects render as nested tables, which slow reviewers down. If your model produces nested data (e.g. line items), put the per-line-item details in a separate trace, or in `trace_config` as reference metadata. ### Multi-class classification (`output_type: "classification"`) Best for routing, triage, and labeling tasks. The reviewer sees the model's selected label prominently, plus the valid alternatives for quick correction. | Key in `model_output` | What it does | |---|---| | `label` | The model's predicted label. Pre-selected in the dropdown. | | `alternatives` | The full set of valid labels (including `label` itself). | ```json // POST /v1/traces/ — model_output { "label": "billing_dispute", "alternatives": [ "billing_dispute", "cancellation_request", "technical_support", "account_settings" ] } ``` `alternatives` must be a flat array of strings. If you don't have a closed set of labels, leave `alternatives` empty and reviewers can type a freeform value. ### Generative copy / summarization (`output_type: "text"`) Best for long-form text outputs: summaries, copywriting, translations, rewrites. The reviewer sees an editable text panel with live word and character counts and diff highlighting against the original. | Key in `model_output` | What it does | |---|---| | `content` | The long-form text. Rendered in a wide, vertically-scrolling editor. | ```json // POST /v1/traces/ — model_output { "content": "The customer requested a refund due to a defective software module. The refund was processed successfully under policy guideline A-12." } ``` --- ## Reviewer ergonomics A few patterns that keep reviewers fast: - **Always provide `model_input.content`** for text-heavy tasks. Avoid burying the prompt in a deeply-nested object. - **Stamp `model` and `prompt_version_id`** in `trace_config`. They're displayed inline in the reviewer's trace metadata strip and are echoed in webhooks for traceback. - **Keep `model_output` shapes stable per project**. Reviewers build muscle memory for where each field is. Shape drift across traces in the same project slows them down significantly. - **Use tags to specialize trace lists**. If you have heterogeneous traffic (e.g. PDFs for finance reviewers, contracts for legal), tag accordingly and have each reviewer filter to their domain. --- ## Putting it all together: a worked example Suppose you're building an invoice extraction pipeline. Each model call ingests a PDF, runs `gpt-4o`, and emits a flat dictionary of extracted fields. **Project config** | Setting | Value | |---|---| | `input_type` | `pdf` | | `output_type` | `key_value` | | `webhook_url` | `https://hooks.example.com/tuor/invoices` | | `webhook_active` | `true` | **Ingest call** ```bash curl -X POST "https://api.tuor.dev/v1/traces/" \ -H "X-API-Key: $TUOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "project_id": "proj_invoices", "model_input": { "pdf_base64": "JVBERi0xLjQK...", "filename": "acme-invoice-2026-05-15.pdf" }, "model_output": { "vendor_name": "Acme Corp", "invoice_date": "2026-05-15", "total_amount": 1250.45, "tax_id": "12-345678" }, "trace_config": { "model": "gpt-4o", "prompt_version_id": "prm_abc123", "internal_run_id": "run_01j2abcde" } }' ``` **Reviewer experience** - Left pane: the original PDF, scrollable. - Right pane: a four-row spreadsheet they can edit field-by-field. - Metadata: `gpt-4o`, prompt version `prm_abc123`, run ID `run_01j2abcde`. - Actions: approve, reject, or edit the output and submit the correction. **Webhook on correction** Your `/tuor/invoices` endpoint receives a signed `review.corrected` event; you read `trace.trace_config.internal_run_id` and update the corresponding row in your database with the corrected fields. That's the full loop. --- # Dynamic Prompts Source: https://docs.tuor.dev/guides/dynamic-prompts/ # Dynamic Prompts Dynamic Prompts is an advanced integration pattern. Tuor can store and version the system prompts your models use. Pulling the active prompt at runtime — instead of hard-coding it — means every prompt change automatically applies to new traces, and every trace is permanently linked to the exact prompt version that produced it. --- ## How it works 1. You store your prompt in Tuor and fetch the active version at runtime. 2. Your model runs with that prompt content. 3. You ingest the trace with the prompt version ID stamped in `trace_config`. 4. Tuor accumulates enough reviewed traces to suggest a refinement. 5. You review and accept the suggestion — it becomes the new active version. 6. Your service picks it up on the next fetch. ```mermaid sequenceDiagram participant App as Your App participant Tuor as Tuor API participant Model as Your Model App->>Tuor: GET /v1/projects/{id}/prompts Tuor-->>App: { active: { id, content, version } } App->>Model: Run with active.content Model-->>App: model_output App->>Tuor: POST /v1/traces/ (trace_config.prompt_version_id = active.id) ``` --- ## Fetch the active prompt ```bash curl "https://api.tuor.dev/v1/projects/proj_abc123/prompts" \ -H "X-API-Key: $TUOR_API_KEY" ``` **Response** ```json { "active": { "id": "prm_abc123", "project_id": "proj_abc123", "version": 3, "content": "You are a financial assistant. Extract the following fields...", "source": "manual", "is_active": true, "created_at": "2026-05-20T10:00:00Z", "created_by": "user_1abc" }, "history": [...] } ``` `active` is `null` if no prompt has been saved yet for the project. The objects above are abbreviated — each prompt also carries `org_id`, `parent_id`, `rationale`, and `lineage_event_ids`. --- ## Ingest traces linked to the prompt Stamp `prompt_version_id` in `trace_config` on every ingest call. Tuor echoes it back in webhook payloads and exports so you can trace any output back to the exact prompt that produced it. ```bash curl -X POST "https://api.tuor.dev/v1/traces/" \ -H "X-API-Key: $TUOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "project_id": "proj_abc123", "model_input": { "content": "Extract net income from the attached statement." }, "model_output": { "net_income": 250000 }, "trace_config": { "model": "gpt-4o", "prompt_version_id": "prm_abc123", "internal_run_id": "run_01j2abcde" } }' ``` --- ## Cache the active prompt The active prompt ID only changes when a new version is saved. Fetching it once at startup — or on a short TTL — is sufficient. There is no need to fetch it per-request. **Python** ```python import os import httpx TUOR_API_KEY = os.environ["TUOR_API_KEY"] PROJECT_ID = "proj_abc123" def fetch_active_prompt() -> dict: resp = httpx.get( f"https://api.tuor.dev/v1/projects/{PROJECT_ID}/prompts", headers={"X-API-Key": TUOR_API_KEY}, timeout=5, ) resp.raise_for_status() return resp.json()["active"] # Cache at startup prompt = fetch_active_prompt() def run_and_trace(user_input: str) -> dict: model_output = call_your_model(prompt["content"], user_input) httpx.post( "https://api.tuor.dev/v1/traces/", headers={"X-API-Key": TUOR_API_KEY, "Content-Type": "application/json"}, json={ "project_id": PROJECT_ID, "model_input": {"content": user_input}, "model_output": model_output, "trace_config": { "model": "gpt-4o", "prompt_version_id": prompt["id"], }, }, ) return model_output ``` **TypeScript** ```ts const TUOR_API_KEY = process.env.TUOR_API_KEY!; const PROJECT_ID = "proj_abc123"; async function fetchActivePrompt() { const res = await fetch( `https://api.tuor.dev/v1/projects/${PROJECT_ID}/prompts`, { headers: { "X-API-Key": TUOR_API_KEY } } ); const data = await res.json(); return data.active; } // Cache at startup const prompt = await fetchActivePrompt(); async function runAndTrace(userInput: string) { const modelOutput = await callYourModel(prompt.content, userInput); await fetch("https://api.tuor.dev/v1/traces/", { method: "POST", headers: { "X-API-Key": TUOR_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ project_id: PROJECT_ID, model_input: { content: userInput }, model_output: modelOutput, trace_config: { model: "gpt-4o", prompt_version_id: prompt.id, }, }), }); return modelOutput; } ``` --- ## Save a new prompt version When you want to update the prompt manually, `POST` a new version. It becomes active immediately and all subsequent fetches will return it. ```bash curl -X POST "https://api.tuor.dev/v1/projects/proj_abc123/prompts" \ -H "X-API-Key: $TUOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "content": "You are a financial assistant. Extract the following fields with strict ISO formatting..." }' ``` Prompt content is capped at **16 KB**. The previous active version is retained in history and linked traces are unaffected. --- ## Prompt refinement Once a project has accumulated at least 10 reviewed traces since the current active version, Tuor can suggest a revised prompt based on what reviewers actually corrected. See the [Integration API](../reference/api-reference.md#advanced) for the `/refine` and `/accept-refinement` endpoints.