# Create API key Source: https://developers.lighton.ai/api-reference/api-keys/create-api-key /api-reference/api-console.yaml post /api/v3/keys Create a new API key for the authenticated user. **The full key value is returned only once** in the creation response and cannot be retrieved again afterwards. `expires_at` is required: - A future datetime expires the key at that time. - `null` creates a key that never expires. **Workspace scoping (optional):** include `scopes` — a list of `{workspace_id, permission}` entries — to restrict the key to those workspaces. Each entry's `permission` is one of `viewer`, `editor`, or `owner`, and cannot exceed the role you currently hold on that workspace. Different scopes on the same key can carry different permissions. # List API keys Source: https://developers.lighton.ai/api-reference/api-keys/list-api-keys /api-reference/api-console.yaml get /api/v3/keys List all API keys belonging to the authenticated user. Each entry exposes its workspace scope via `scopes`. Every scope entry carries a `scope_type`: - `workspace` — the key is explicitly scoped to that workspace, with its own per-workspace permission (`viewer`, `editor`, or `owner`). - `global` — the key has no explicit scope rows and reaches every workspace you can access; `scopes` then lists those workspaces with your own current role on each (the effective access ceiling). A global key never mixes the two types. # Retrieve API key Source: https://developers.lighton.ai/api-reference/api-keys/retrieve-api-key /api-reference/api-console.yaml get /api/v3/keys/{id} Retrieve a single API key belonging to the authenticated user, including its workspace scope. # Revoke API key Source: https://developers.lighton.ai/api-reference/api-keys/revoke-api-key /api-reference/api-console.yaml delete /api/v3/keys/{id} Revoke an API key. Revoked keys can no longer be used to authenticate requests. # Update API key Source: https://developers.lighton.ai/api-reference/api-keys/update-api-key /api-reference/api-console.yaml patch /api/v3/keys/{id} Update an existing API key. Supports renaming and replacing the workspace scope: - Pass `scopes` with a non-empty list of `{workspace_id, permission}` entries to replace the full scope set. Existing scope rows not in the new list are dropped. - Pass `scopes: []` to unscope the key entirely. - The permission ceiling on each scoped workspace is re-validated against your current role there. # Ask a question over your documents Source: https://developers.lighton.ai/api-reference/ask/ask-a-question-over-your-documents /api-reference/api-console.yaml post /api/v3/ask Retrieval-augmented generation: searches your indexed corpus, then generates an LLM answer grounded in the retrieved passages. **Modes:** - `stream=false` (default): returns a single JSON response with `results` and `answer`. - `stream=true`: returns Server-Sent Events — `event: sources` (retrieved chunks), `event: token` (answer tokens), `event: done` (stream complete), or `event: error` (generation failure). **Model:** omit `model` to use the default model configured for your organization. To name one explicitly, pass any technical name from `GET /api/v3/instance/models` — that endpoint lists exactly what this endpoint accepts: the models your deployment offers publicly, your organization's default, and any model your organization registered itself (`custom-{company_id}-{uuid}`). Any other value is rejected. **Structured output:** pass `response_format` with a JSON Schema object to constrain the LLM answer to valid JSON matching your schema. The schema must have `type: "object"` and `properties`. When set, the `answer` field contains a JSON string conforming to the schema. Works with both sync and streaming modes. Omit for free-text answers (default). **Relevance scoring:** relevance scoring always runs in `scoring_and_filtering` mode — candidates are scored for relevance and only those above the quality threshold are used as context. `score` equals the relevance score (`scores.relevance`, 0–1). Results are returned in descending order of `score`. If the scoring model is temporarily unavailable, `score` falls back to the combined retrieval score (higher is better, no fixed upper bound) and `scores.relevance` is null. **Scoping:** same rules as `/api/v3/search` — use `workspace_id` and/or `tag_id` to narrow results, or `file_id` to target specific files. `file_id` cannot be combined with `workspace_id` or `tag_id`. **Facet filtering:** use `content_type` and `attribute` to narrow results by facet metadata. Content type uses colon-separated paths (e.g. `legal:contract:nda`). **Repeated `attribute` entries are ANDed; values inside one entry are ORed with `|` (pipe, recommended).** Example: `attribute=fiscal_year:2024|2025&attribute=status:active` → (fiscal_year 2024 OR 2025) AND (status active). Supports operators (`>`, `>=`, `<`, `<=`), prefix (`name:prefix*`), smart dates, and content-type scoping. If a retrieval or scoring signal is temporarily unavailable, results are still returned — in retrieval order when the degraded signal is the reranker — and each result item includes a `warnings` array. Each warning has a `code` matching the degraded `scores` key (`relevance`, `multivector`, `vision`) and a `reason` classifying the failure: `model_not_found`, `embed_failed`, `timeout`, `service_error`, or `unknown`. A degraded signal lowers ranking quality and leaves its `scores` entry null. The `warnings` key is absent from result items when all pipeline steps succeed. Billing: 1 search-with-generation credit per request. # Create budget alert Source: https://developers.lighton.ai/api-reference/budget/create-budget-alert /api-reference/api-console.yaml post /api/v3/billing/budget/alerts Add an alert threshold to the company's budget. Requires CompanyAdmin role. # Create or update company budget Source: https://developers.lighton.ai/api-reference/budget/create-or-update-company-budget /api-reference/api-console.yaml put /api/v3/billing/budget Set or update the company's monthly spend budget. Requires CompanyAdmin role. Creates the budget on first call; updates it on subsequent calls. # Delete budget alert Source: https://developers.lighton.ai/api-reference/budget/delete-budget-alert /api-reference/api-console.yaml delete /api/v3/billing/budget/alerts/{id} Delete an alert threshold. Requires CompanyAdmin role. # Delete company budget Source: https://developers.lighton.ai/api-reference/budget/delete-company-budget /api-reference/api-console.yaml delete /api/v3/billing/budget Delete the company's budget and all associated alerts. Requires CompanyAdmin role. # Get company budget Source: https://developers.lighton.ai/api-reference/budget/get-company-budget /api-reference/api-console.yaml get /api/v3/billing/budget Return the company's monthly budget configuration, current spend, and alert thresholds. # List budget alerts Source: https://developers.lighton.ai/api-reference/budget/list-budget-alerts /api-reference/api-console.yaml get /api/v3/billing/budget/alerts Return all alert thresholds for the company's budget. # Toggle all budget alerts Source: https://developers.lighton.ai/api-reference/budget/toggle-all-budget-alerts /api-reference/api-console.yaml patch /api/v3/billing/budget/alerts Enable or disable all alert thresholds for the company's budget in a single operation. Requires CompanyAdmin role. # Update budget alert Source: https://developers.lighton.ai/api-reference/budget/update-budget-alert /api-reference/api-console.yaml patch /api/v3/billing/budget/alerts/{id} Update an existing alert threshold. Requires CompanyAdmin role. # Extract structured data from a document Source: https://developers.lighton.ai/api-reference/extract/extract-structured-data-from-a-document /api-reference/api-console.yaml post /api/v3/extract Pull specific fields from a document into a typed schema. Accepts exactly one of: a **file upload** (multipart/form-data), a **document URL** (JSON body), or a **`file_id`** referencing an already-ingested document (from `POST /v3/files`), plus a **JSON Schema** (the `schema` field) describing what to extract. ### Sync mode (default) Blocks until extraction completes and returns **200** with the full result. ```bash curl -X POST https://api.lighton.ai/api/v3/extract \ -H 'Authorization: Bearer $TOKEN' \ -F file=@invoice.pdf \ -F 'schema={"type":"object","properties":{"invoice_number":{"type":"string"}}}' ``` ### Async mode (`options.async = true`) Returns **202** immediately with an `ext_` job id. Poll `GET /api/v3/extract/{id}` with that same id until `status` is `completed` or `failed`. ```bash curl -X POST https://api.lighton.ai/api/v3/extract \ -H 'Authorization: Bearer $TOKEN' \ -H 'Content-Type: application/json' \ -d '{"document": "https://example.com/report.pdf", "schema": {"type": "object", "properties": {"title": {"type": "string"}}}, "options": {"async": true}}' ``` For multipart uploads, pass `options` as a JSON-encoded form field: `-F 'options={"async":true}'`. **Supported file types:** `.pdf`, `.png`, `.jpg`, `.jpeg`, `.pptx`, `.ppt`, `.odp`, `.docx`, `.odt`, `.doc`, `.html` **Sync limits:** 20 MB file size, 15 pages. **Async limits:** 100 MB file size, 1000 pages. # Get the status and result of an async extract job Source: https://developers.lighton.ai/api-reference/extract/get-the-status-and-result-of-an-async-extract-job /api-reference/api-console.yaml get /api/v3/extract/{job_id} Poll for the status and result of an async extract job submitted via `POST /api/v3/extract` with `options.async=true`. Returns the same envelope shape as the synchronous extract endpoint once `status` is `completed`. ```bash curl https://api.lighton.ai/api/v3/extract/ext_0196e4b2a3c14d5e \ -H 'Authorization: Bearer $TOKEN' ``` **Pagination:** when completed, `result.data` is paginated with a fixed page size of 15. Use the `page` query param (1-based) to navigate; `result.pagination` reports `total_items`, `total_pages`, `has_next`, `has_prev`. **Recommended polling cadence:** 1s for the first 10s, then 5s, capped at 30s. Stop polling once `status` is in `{completed, failed}`. # Batch classify and set attribute values Source: https://developers.lighton.ai/api-reference/facets/batch-classify-and-set-attribute-values /api-reference/api-console.yaml post /api/v3/files/{file_id}/facets/batch Execute multiple file facet actions in a single request. All actions are **validated upfront** before any execution begins. If any action has invalid fields, the entire batch is rejected with a 422 response and no actions are executed. On **domain errors** (e.g., unknown content type, sibling conflict), the batch fails fast at the failing action. The error response includes an `"index"` field (0-based) indicating which action caused the failure. Actions before the failing index are committed; their results are not returned. All verbs are idempotent — it is safe to re-send the entire batch after fixing the error. **Request:** `{"actions": [, , ...]}` Each action object follows the same schema as the single-action `POST /api/v3/files/{file_id}/facets` endpoint. Maximum 50 actions per batch. **Response:** `{"results": [{"status": , "data": }, ...]}` Results are in the same order as the input actions. The `data` key is `null` for 204 actions (unclassify, clear_value). See `POST /api/v3/files/{file_id}/facets` for available actions and their fields. Requires edit access to the file. # Batch define content types and attributes Source: https://developers.lighton.ai/api-reference/facets/batch-define-content-types-and-attributes /api-reference/api-console.yaml post /api/v3/content-types/batch Execute multiple content-type actions in a single request. All actions are **validated upfront** before any execution begins. If any action has invalid fields, the entire batch is rejected with a 422 response and no actions are executed. On **domain errors** (e.g., content type not found, permission denied), the batch fails fast at the failing action. The error response includes an `"index"` field (0-based) indicating which action caused the failure. Actions before the failing index are committed; their results are not returned. All verbs are idempotent — it is safe to re-send the entire batch after fixing the error. **Request:** `{"actions": [, , ...]}` Each action object follows the same schema as the single-action `POST /api/v3/content-types` endpoint. Maximum 50 actions per batch. **Response:** `{"results": [{"status": , "data": }, ...]}` Results are in the same order as the input actions. The `data` key is `null` for 204 actions (deletes). See `POST /api/v3/content-types` for available actions and their fields. # Classify file and set attribute values Source: https://developers.lighton.ai/api-reference/facets/classify-file-and-set-attribute-values /api-reference/api-console.yaml post /api/v3/files/{file_id}/facets Apply content-type classifications to a file and set attribute values (metadata) on it. This is how you tag a document with structured metadata from your content-type schema. **Typical workflow:** 1. **Classify** the file: `{"action": "classify", "content_type_path": "legal:contract:nda"}` 2. **Set attribute values**: `{"action": "set_value", "content_type_path": "legal:contract:nda", "attribute_name": "jurisdiction", "value": ["FR", "DE"]}` 3. **Read back** with `GET /api/v3/files/{file_id}/facets` A file can be classified under **multiple content types**. Just call `classify` for each one. Removing a classification (`unclassify`) cascades: all attribute values under that content type are removed too. **Actions:** - `classify`: assign a content type to the file (idempotent) - `unclassify`: remove a content type and all its attribute values - `set_value`: set or update an attribute value (the content type must be classified first) - `clear_value`: remove an attribute value **Value types for `set_value`:** - `text` / `rich-text` → string - `number` → number or numeric string - `date` → date string, normalized to `YYYY-MM-DD` - `boolean` → `true` / `false` - `select` → one string from `choices` - `multi-select` → array of strings from `choices` To clear a value, use `clear_value` (not `set_value` with `null`). **Prerequisites:** Content types must be set up first. See `GET /api/v3/content-types/templates` (browse templates) and `POST /api/v3/content-types` (adopt or create). Requires edit access to the file. # Define content types and attributes Source: https://developers.lighton.ai/api-reference/facets/define-content-types-and-attributes /api-reference/api-console.yaml post /api/v3/content-types Create, modify, and organise your company's content-type trees and their attributes. Every action is idempotent, safe to re-run. **Two ways to build your schema:** 1. **Adopt from starter templates**: import ready-made trees from the seed catalog (`GET /api/v3/content-types/templates`). Once adopted, they're yours to customize. 2. **Build from scratch**: create your own trees, nodes, and attributes entirely with `define_content_type` and `define_attribute`. You can mix both: adopt `legal` from the seeds, then add a custom `compliance` tree alongside it. Each tree is independent. **Actions:** - `adopt`: import trees from the seed catalog (starter templates) - `define_content_type`: create or update a tree node (root or child) - `undefine_content_type`: delete a node and its entire subtree - `define_attribute`: add or update a metadata field on a node - `undefine_attribute`: remove a metadata field **Building a tree from scratch, example flow:** 1. Create a root: ```json {"action": "define_content_type", "code": "compliance", "label": "Compliance"} ``` 2. Add children: ```json {"action": "define_content_type", "parent_path": "compliance", "code": "audit-report", "label": "Audit Report"} ``` 3. Add attributes: ```json {"action": "define_attribute", "content_type_path": "compliance", "name": "owner", "attribute_type": "text"} ``` 4. Classify files via `POST /api/v3/files/{id}/facets`: ```json {"action": "classify", "content_type_path": "compliance:audit-report"} ``` **Attributes** are metadata fields you define on each node. Types: `text`, `number`, `date`, `boolean`, `select`, `multi-select`, `rich-text`. `select` and `multi-select` require `choices`. Attributes inherit down the tree: define `jurisdiction` on `legal` and it's available on all children. **Description best practices:** The `description` field on attributes feeds into the scope inference endpoint (`POST /api/v3/content-types/scope`), where it helps the LLM generate accurate search filters. Use keywords that signal the attribute's nature: - **Person names**: include "name", "examiner", "inventor", or "author" → enables `*value*` wildcard syntax hints - **Codes/identifiers**: include "code", "classification", or "identifier" → enables exact-match hints (don't guess from topic keywords) **Multi-classification:** A single document can be classified under multiple content types from different trees (e.g., both `legal:contract:nda` and `compliance:audit-report`). Available seed roots for `adopt`: finance, healthcare, legal, manufacturing, tech Requires admin-level access to modify the company's schema. # List content type templates Source: https://developers.lighton.ai/api-reference/facets/list-content-type-templates /api-reference/api-console.yaml get /api/v3/content-types/templates Browse the platform's **starter-kit templates** for content types. These are ready-made taxonomies (legal, finance, healthcare, tech, manufacturing) you can adopt to quickly organise your documents, no manual setup required. **Getting started:** 1. Browse templates here to see what's available 2. Adopt the ones you need: `POST /api/v3/content-types {"action": "adopt", "paths": ["legal", "finance"]}` 3. Once adopted, they become **your company's own** content types, fully editable 4. Customize: rename nodes, add children, define new attributes, or delete what you don't need 5. Create entirely new content types from scratch with `define_content_type` All write operations happen on `POST /api/v3/content-types`. This endpoint is read-only: it shows the catalog of available templates. **After adoption**, manage your company's content types (adopted + custom) via `GET /api/v3/content-types` and `POST /api/v3/content-types`. Classify files with `POST /api/v3/files/{id}/facets`. **Query modes:** - No params → full detail for all templates (children + attributes) - `?path=legal` → detail for one template - `?path=legal,finance` → detail for multiple (comma-separated) - `?path=legal:contract:nda` → detail for a nested node - `?depth=0` → root info only (code, label, description, no children/attributes) - `?depth=1` → roots + direct children only - `?include_attributes=false`: omit attribute definitions for a lighter response **Path separator:** `:` (colon). Example: `legal:contract:nda` **Attribute inheritance:** Attributes defined at a parent node are inherited by all its children. A document classified as `legal:contract:nda` gets attributes from `legal`, `legal:contract`, and `legal:contract:nda`. # List content types Source: https://developers.lighton.ai/api-reference/facets/list-content-types /api-reference/api-console.yaml get /api/v3/content-types List your classification trees: the content types you've adopted from the starter templates or created from scratch. This is where you see what's available to classify your documents with. **Content types are tree-based.** Each tree is an independent classification hierarchy (e.g., `legal` → `contract` → `nda`). You can have multiple trees side by side (`legal`, `finance`, `compliance`), and a single document can be classified under **multiple trees** simultaneously. For instance, a contract can be both `legal:contract:nda` and `finance:investment:term-sheet`. Each node in the tree can carry **custom attributes** (metadata fields like `jurisdiction`, `effective_date`, `contract_value`). Attributes are inherited down the tree: a document at `legal:contract:nda` gets attributes from all three levels. **New here?** Start by browsing available templates at `GET /api/v3/content-types/templates`, then adopt the ones you need via `POST /api/v3/content-types {"action": "adopt"}`. **Query params:** - `?query=5G antennas`: filter to content types relevant to a search query (uses a first-pass retrieval). Without this param, the full catalog is returned. - `?path=legal`: filter to a specific subtree - `?path=legal,finance`: multiple subtrees (comma-separated) - `?depth=0`: roots only. `?depth=N`: N levels of children. - `?include_attributes=true` (default): include attribute definitions per node - `?include_attributes=false`: omit attribute definitions for a lighter response `query` and `path` can be combined: returns the intersection (only matched content types under the requested paths). Returns an empty list if no content types have been set up yet. # List file classifications and attribute values Source: https://developers.lighton.ai/api-reference/facets/list-file-classifications-and-attribute-values /api-reference/api-console.yaml get /api/v3/files/{file_id}/facets See what content types a file is classified under and what attribute values have been set on it. A file can be classified under **multiple content types** (e.g., both `legal:contract:nda` and `compliance:audit-report`). Each classification comes with its own set of **attribute values**, the metadata fields defined in your content-type schema. **Response structure:** Each content type entry includes: - `path`: the content type (e.g., `legal:contract:nda`) - `label`: human-readable name - `labels`: breadcrumb from root to leaf (`["Legal", "Contract", "NDA"]`) Each attribute value includes: - `name` / `label`: identifier and display name - `value`: the current value (shape depends on type) - `type`: the attribute type (`text`, `number`, `date`, `boolean`, `select`, `multi-select`) - `choices`: available options for `select` / `multi-select` - `required`: whether the attribute is required by the schema The response includes `can_edit`, which indicates whether you have permission to modify this file's classifications and attribute values. **To modify:** use `POST /api/v3/files/{file_id}/facets` to classify, set attribute values, or remove classifications. **To set up content types first:** see `GET /api/v3/content-types/templates` (browse starter templates) and `POST /api/v3/content-types` (adopt or create). # Resolve search scope Source: https://developers.lighton.ai/api-reference/facets/resolve-search-scope /api-reference/api-console.yaml post /api/v3/content-types/scope Resolves content type and attribute filters from a natural-language query. Call this **before** `/search`, `/ask`, or `/files` to narrow results by domain. Pass the inferred `content_type` and `attribute` directly to those endpoints. Returns ranked content types grouped by root schema, each with a relevance score and the attribute definitions available for filtering. ### Scores and decision signal Each content type in the response includes a `score` (relevance to your query). Use scores to decide how to scope your search — higher means stronger match. `has_signal` is a convenience shortcut: it's `true` when the top score meets a default confidence threshold. For custom logic, use `score` directly and apply your own threshold via the `threshold` request parameter. ### Three modes **1. Prompt mode** (default) — returns `prompt_context`, a self-contained LLM-ready text block. Feed it to any LLM alongside the user query to infer `content_type` and `attribute` filters. The prompt includes ranked content types, attribute definitions with filter syntax, inference rules, date ranges, and few-shot examples. **2. Completion mode** — pass `model` (technical name) and the API calls the LLM for you. Returns `scope_completion` with parsed, normalized filters: - Label-to-name mapping (e.g. "Filing Date" → `filing_date`) - Syntax validation against the attribute schema - Structured JSON output via guided decoding - `warnings` for any normalization applied or issues detected - If the LLM call fails, `scope_completion` is still returned with `warnings` explaining the failure — the rest of the response remains usable. **3. Catalog + completion mode** — set `relevance_scoring: "none"` with a `query` and `model` to get the full content type catalog AND an LLM-inferred `scope_completion`. Useful when you want the LLM to choose from ALL content types without retrieval pre-filtering. `max_results` and `threshold` are ignored. ### Response fields | Field | Description | |-------|-------------| | `score` | Relevance score. Higher = better match. Comparable across requests. | | `max_score` | Highest score in a root group. Compare roots without iterating. | | `chunk_count` | Retrieval chunks matching this CT. More chunks = broader evidence. | | `doc_count` | Total corpus documents classified under this CT. | | `prompt_context` | LLM-ready prompt text. Pass to your LLM as-is. | | `prompt_version` | Fingerprint (`t:.d:`) for eval reproducibility. | | `scope_completion` | Parsed LLM output (only when `model` is provided). | ### Schema context mode Omit `query` to get the full content type catalog — useful for system prompts, tool descriptions, or schema exploration. `groups` contains all content types with their attributes (`score=0`, `chunk_count=0` since there is no query to rank against). `prompt_context` contains the same catalog as LLM-ready text. ### Integration notes - Content type paths use `:` as separator (e.g. `patent:electricity:h04`). - Attribute filter syntax is documented in `prompt_context` per attribute type. Date ranges use `>=` / `<=` operators (e.g. `filing_date:>=2023-01-01`). - Writing meaningful attribute descriptions (especially for person-name and code/identifier fields) improves the quality of `prompt_context` hints. # Add tags to a file Source: https://developers.lighton.ai/api-reference/files/add-tags-to-a-file /api-reference/api-console.yaml post /api/v3/files/{id}/tags Add one or more tags to a file without affecting existing tags. **Behavior:** - Adds new tags to the document while preserving existing ones - New tags are marked as manually assigned (`auto_assigned=False`) - Duplicate tags are ignored (no error if tag already exists on document) **Validation:** - Returns 400 if tag IDs are invalid or don't belong to user's company - Returns 403 if user doesn't have permission to edit the document - Returns 404 if document doesn't exist # Delete a file Source: https://developers.lighton.ai/api-reference/files/delete-a-file /api-reference/api-console.yaml delete /api/v3/files/{id} Permanently delete a file and all associated data. **Requirements:** - Delete permission on the document - Workspace must allow manual document management (not sync-only) **Public datasets:** deleting a public-dataset file requires the instance-admin `MANAGE_PUBLIC_DATASET` permission. A workspace-scoped API key with read access (company opted in via `allow_access_to_public_datasets`) can see the file but gets 403, not 404, when attempting to delete it — public datasets are read-only. Returns 204 (No Content) on success. # Delete multiple files in a single request Source: https://developers.lighton.ai/api-reference/files/delete-multiple-files-in-a-single-request /api-reference/api-console.yaml post /api/v3/files/bulk-delete Permanently delete multiple files in a single request. **Authorization (applied to every file in the request):** - Visibility: file must be accessible to the user → 404 if any are not found - Delete permission: user must have the right to delete each file → 403 if any are denied - Workspace type: workspace must not be sync-managed → 400 if any are synced **Public datasets:** instance admins (`MANAGE_PUBLIC_DATASET`) can bulk-delete public-dataset files. A workspace-scoped API key with read-only access to a public dataset can see the file (so it counts toward visibility) but gets 403 on the delete-permission check — public datasets are read-only for opted-in companies. Returns 204 (No Content) on success. # Download a document file Source: https://developers.lighton.ai/api-reference/files/download-a-document-file /api-reference/api-console.yaml get /api/v3/files/{id}/download Download a document file. Returns the original file by default. Use the purpose parameter to request a specific version (e.g. rendered_pdf for frontend viewers). # Get document thumbnail Source: https://developers.lighton.ai/api-reference/files/get-document-thumbnail /api-reference/api-console.yaml get /api/v3/files/{id}/thumbnail Returns the 256x256 WebP thumbnail for a document. Thumbnails are generated asynchronously after ingestion completes: poll the `thumbnail.status` field on the file resource and request this endpoint once it reads `READY`. # List files accessible to the authenticated user Source: https://developers.lighton.ai/api-reference/files/list-files-accessible-to-the-authenticated-user /api-reference/api-console.yaml get /api/v3/files Retrieve a paginated list of files (documents) accessible to the authenticated user. Results are ordered by upload date (newest first) by default. When using the `search` parameter, results are ordered by relevance. **Public datasets:** instance admins (holding `MANAGE_PUBLIC_DATASET`) always see public-dataset files. A company that has opted in (`allow_access_to_public_datasets`) with a workspace-scoped API key granting a public workspace also sees its files — filter with `workspace_id` to scope the list to it. # Remove a tag from a file Source: https://developers.lighton.ai/api-reference/files/remove-a-tag-from-a-file /api-reference/api-console.yaml delete /api/v3/files/{id}/tags/{tag_id} Remove a specific tag from a file. **Behavior:** - Removes the specified tag from the document - Works for both manually assigned and auto-assigned tags - Idempotent: returns 204 even if tag was not on the document **Validation:** - Returns 403 if user doesn't have permission to edit the document - Returns 404 if document doesn't exist - Returns 404 if tag doesn't exist or doesn't belong to user's company # Retrieve a single file by ID Source: https://developers.lighton.ai/api-reference/files/retrieve-a-single-file-by-id /api-reference/api-console.yaml get /api/v3/files/{id} Retrieve detailed information for a single file by its ID. Only files that the authenticated user is authorized to access will be returned. Returns 404 if the file does not exist or the user does not have access. Query Parameters: - include_content: Set to 'true' to include the full text content of the document (default: false) The response includes comprehensive document details including: - Basic metadata (id, filename, title, extension, dates, page count) - Full text content of the document (only when include_content=true) - Processing status (status, status_vision, status_detail if failed) - Associated tags and workspace information - File size (if available) - Parser/ingestion pipeline (if available, after parsing starts) - Signature (TLSH hash for duplicate detection) - Facet content types and nested attribute values (full expanded with attribute definitions) **Public datasets:** reachable by instance admins (`MANAGE_PUBLIC_DATASET`) unconditionally, and by a company that has opted in (`allow_access_to_public_datasets`) with a workspace-scoped API key granting the file's public workspace. # Update file metadata or replace its file Source: https://developers.lighton.ai/api-reference/files/update-file-metadata-or-replace-its-file /api-reference/api-console.yaml patch /api/v3/files/{id} Update mutable fields of a file (document). **Updatable fields:** - `title`: Update the document title - `tags`: Replace ALL tags for the document (both manual and auto-assigned) - `external_metadata`: Create or update external source metadata - `file`: Replace the original file and re-ingest the document **Tag replacement behavior:** - Providing a tags array replaces ALL existing tags (manual and auto-assigned) - To remove all tags, send `[0]` (sentinel value for multipart format) - Omitting `tags` field leaves tags unchanged - New tags are marked as manually assigned (`auto_assigned=False`) **External metadata behavior:** - When creating for the first time, `external_id` is required - When updating existing metadata, `external_id` is optional (existing value is preserved) - Fields in `additional_metadata` are merged (not replaced) with existing values - `external_id` cannot be changed on a document imported from a datasource: the sync matches its documents on it, so a rename would detach the document from its source. `doc_type` and `additional_metadata` stay editable **File replacement behavior:** - The document keeps its `id`, `title`, manually assigned tags, content types, attribute values and external metadata. Only the file and everything derived from it (text, chunks, embeddings, auto-assigned tags, summaries) are rebuilt. - Any supported file type is accepted, including one that differs from the current file — the processing statuses are re-derived from the new file. - Replacing the file additionally requires an owner or editor role in the document's workspace, and is not available in workspaces configured for synced documents. - The storage quota is checked on the size difference, not the full new size. - `file` can be combined with `title`, `tags` and `external_metadata` in the same request: the metadata changes apply immediately, the file is queued. - The response reflects the document as it still is: `filename`, `extension`, `size`, `total_pages`, `status`, `signature`, `summaries` and `content` describe the previous file until re-ingestion begins. The document keeps serving its previous content — search, extracts and download stay consistent — and leaves the search index only once processing actually starts. - `pending_reprocess` is set to `update` on the response and on subsequent reads, and is the only signal that the queued file has not been processed yet: `status` still reports the previous run's outcome (typically `Embedded`) until the pipeline picks the document up. Poll until `pending_reprocess` is null **and** `status` is terminal — treating a terminal `status` alone as completion reports success against the previous file. - Sending a new file while one is already queued replaces the queued file, and a file replacement supersedes any other reprocessing already queued for the document: the most recent request is always the one that goes live. - Once re-ingestion starts, a document whose new file needs rendering has no rendered PDF until conversion completes. `download?purpose=rendered_pdf` serves the original during that window, as it does for any file still being converted, so a viewer can receive a non-PDF body. Gate the viewer on `status` rather than on the download succeeding. - The request is applied in full or not at all: tags are validated before the file is queued, so an invalid tag id rejects the whole request without staging anything. **Public datasets:** editing a public-dataset file requires the instance-admin `MANAGE_PUBLIC_DATASET` permission. A workspace-scoped API key with read access (company opted in via `allow_access_to_public_datasets`) can see a public-dataset file but is refused permission to edit it — public datasets are read-only. # Upload a file Source: https://developers.lighton.ai/api-reference/files/upload-a-file /api-reference/api-console.yaml post /api/v3/files Upload a file to a workspace. Files are added to an upload session and queued for asynchronous processing. To track progress, retrieve the file details using the GET endpoints to check the current status. **Idempotent upload:** When `external_metadata.external_id` is provided and a manually-uploaded document with the same external ID already exists in the target workspace, the existing document is returned with `200 OK` instead of creating a duplicate. This makes bulk re-runs safe without requiring a pre-check. Datasource-imported documents are not affected. **Accepted file formats:** `csv`, `doc`, `docx`, `htm`, `html`, `jpeg`, `jpg`, `json`, `md`, `odp`, `odt`, `pdf`, `png`, `ppt`, `pptx`, `txt`, `xhtml`, `xls`, `xlsx`, `yaml`, `yml` **Customization Options:** - `title`: Customize the document title (defaults to filename without extension) - `filename`: Override the uploaded filename - `parser`: Specify a custom ingestion pipeline instead of using the default # API Reference Source: https://developers.lighton.ai/api-reference/introduction Complete schema for the LightOn REST API. All endpoints are under `/api/v3/` and require a bearer token. See [Authentication](/authentication) for setup. The reference pages in this section are generated from the OpenAPI specification. Every parameter, response shape, and error code is documented exactly as the server implements it. ## Intelligence `POST /api/v3/ask`: retrieval-augmented generation: a grounded LLM answer plus its sources, sync or streamed. [Tutorial](/tutorials/ask) `POST /api/v3/search`: hybrid vector + lexical retrieval with optional reranking. [Tutorial](/tutorials/search) Upload, list, fetch, update, and delete documents in your workspaces. [Tutorial](/tutorials/files) Define classification trees, classify files, set attribute values, and read structured metadata. [Tutorial](/tutorials/facets/overview) Manage company-wide document tags used to scope search and organise content. Create and manage workspaces, the access-controlled containers your documents live in. ## Document processing `POST /api/v3/parse`: convert documents to clean Markdown, synchronously or as an async job. [Tutorial](/tutorials/parse) Pull typed fields from documents using a JSON Schema you provide. [Tutorial](/tutorials/extract) ## Administration Provision and revoke API keys used to authenticate against the API. # Get the status and result of an async parse job Source: https://developers.lighton.ai/api-reference/parse/get-the-status-and-result-of-an-async-parse-job /api-reference/api-console.yaml get /api/v3/parse/{id} Poll for the status and result of an async parse job submitted via `POST /api/v3/parse` with `options.async=true`. Returns the same envelope shape as the synchronous parse endpoint once `status` is `completed`. ```bash curl https://api.lighton.ai/api/v3/parse/parse_Kg \ -H 'Authorization: Bearer $TOKEN' ``` **Recommended polling cadence:** 1s for the first 10s, then 5s, capped at 30s. Stop polling once `status` is in `{completed, failed}`. # Parse a document to Markdown Source: https://developers.lighton.ai/api-reference/parse/parse-a-document-to-markdown /api-reference/api-console.yaml post /api/v3/parse Convert a document into readable, structured Markdown content. Accepts either a **file upload** (multipart/form-data) or a **document URL** (JSON body). ### Sync mode (default) Blocks until parsing completes and returns **200** with the full markdown result. ```bash curl -X POST https://api.lighton.ai/api/v3/parse \ -H 'Authorization: Bearer $TOKEN' \ -F file=@invoice.pdf ``` ### Async mode (`options.async = true`) Returns **202** immediately with a `parse_` job id. Poll `GET /api/v3/parse/{id}` with that same id until `status` is `completed` or `failed`. ```bash curl -X POST https://api.lighton.ai/api/v3/parse \ -H 'Authorization: Bearer $TOKEN' \ -H 'Content-Type: application/json' \ -d '{"document": "https://example.com/report.pdf", "options": {"async": true}}' ``` For multipart uploads, pass `options` as a JSON-encoded form field: `-F 'options={"async":true}'`. **Supported file types:** `.pdf`, `.png`, `.jpg`, `.jpeg`, `.pptx`, `.ppt`, `.odp`, `.docx`, `.odt`, `.doc`, `.html` **Sync limits:** 20 MB file size, 15 pages. **Async limits:** 100 MB file size, 1000 pages. # Render a document as PDF Source: https://developers.lighton.ai/api-reference/parse/render-a-document-as-pdf /api-reference/api-console.yaml post /api/v3/preview Convert a document into a PDF suitable for inline preview. Accepts a **file upload** (multipart/form-data), a **document URL**, or a **file_id** referencing a document already ingested in the platform (JSON body). The conversion is synchronous — the PDF bytes are returned directly. **Supported file types:** `.pdf`, `.png`, `.jpg`, `.jpeg`, `.pptx`, `.ppt`, `.odp`, `.docx`, `.odt`, `.doc`, `.html`, `.xhtml` **Size limit:** 20 MB. # Search document chunks Source: https://developers.lighton.ai/api-reference/search/search-document-chunks /api-reference/api-console.yaml post /api/v3/search Embedding → hybrid vector search → optional reranking, returning ranked chunks with provenance. No LLM generation is performed. Billing: 1 retrieval credit per request. **Relevance scoring (`relevance_scoring`):** controls the relevance scoring stage. - `scoring_and_filtering` (default): Score candidates for relevance and only return those above the quality threshold. - `scoring_only`: Score every candidate for relevance but return them all, even low-scoring ones. Useful for building your own filtering logic. - `none`: Skip the relevance scoring step and return all candidates unfiltered. Fastest option, useful when you handle scoring yourself. Omit `relevance_scoring` for the default; send `none` to skip scoring. `skip_rerank` is deprecated — `true` maps to `none`, `false` to `scoring_and_filtering`. **Result ordering:** results are returned in descending order of `score`. With `scoring_and_filtering` or `scoring_only`, `score` equals the relevance score (`scores.relevance`, 0–1). With `none`, `score` is the combined retrieval score (higher is better, no fixed upper bound). If a retrieval or scoring signal is temporarily unavailable, results are still returned — in retrieval order when the degraded signal is the scoring model — and a `warnings` array is included. Each warning has a `code` matching the degraded `scores` key (`relevance`, `multivector`, `vision`) and a `reason` classifying the failure: `model_not_found`, `embed_failed`, `timeout`, `service_error`, or `unknown`. A degraded signal lowers ranking quality and leaves its `scores` entry null; a null `scores` entry with no matching warning simply means the signal does not apply to that chunk. The `warnings` key is absent when all pipeline steps succeed. **Scoping:** use `workspace_id` and/or `tag_id` to narrow results, or `file_id` to target specific files. `file_id` cannot be combined with `workspace_id` or `tag_id`. Filters that resolve to no authorized resources are rejected. When no filters are provided, search runs across all documents authorized for the API key. **Facet filtering:** use `content_type` and `attribute` to narrow results by facet metadata. Content type uses colon-separated paths (e.g. `legal:contract:nda`). **Repeated `attribute` entries are ANDed; values inside one entry are ORed with `|` (pipe, recommended).** Example: `attribute=fiscal_year:2024|2025&attribute=status:active` → (fiscal_year 2024 OR 2025) AND (status active). Supports operators (`>`, `>=`, `<`, `<=`), prefix (`name:prefix*`), smart dates, and content-type scoping. **Modes:** - `text` (default): hybrid text search - `vision`: VLM-embedded page image search **Images:** set `include_image=true` to receive a base64-encoded page image with each result. In text mode the image is fetched from the VisionChunk covering the chunk's start page (empty string if no vision index exists for that page). **Bounding boxes (PDF only):** set `include_bboxes=true` to append a `bboxes` array to each result, giving the merged rectangles of the chunk's text on the source PDF (raw PDF points, top-left origin with y extending downward) so you can overlay highlights without re-locating the chunk. One rectangle per logical group; a chunk spanning two pages produces at least one rectangle per page. Available for PDF documents in text mode only — returns an empty list for non-PDF, vision-mode, or pre-v2.2.1 chunks. When `include_bboxes=false` (default) the `bboxes` key is omitted. # Create a new tag for the company Source: https://developers.lighton.ai/api-reference/tags/create-a-new-tag-for-the-company /api-reference/api-console.yaml post /api/v3/tags Create a new tag for the authenticated user's company. The auto_assign flag determines if the tag can be automatically assigned by the system (True) or only manually (False). Requires tag creation permission. # Delete a company tag Source: https://developers.lighton.ai/api-reference/tags/delete-a-company-tag /api-reference/api-console.yaml delete /api/v3/tags/{id} Delete a company tag. This will also remove all Document-Tag associations with this tag. Requires tag deletion permission. # List all tags for the authenticated user's company Source: https://developers.lighton.ai/api-reference/tags/list-all-tags-for-the-authenticated-users-company /api-reference/api-console.yaml get /api/v3/tags Retrieve a list of tags for the authenticated user's company. Results are ordered by creation date (newest first). # API Errors Source: https://developers.lighton.ai/api-reference/v3-error-codes Error codes, response format, and troubleshooting for the LightOn API. # API Errors When an API request fails, all LightOn API returns a JSON error response with a consistent structure. Every error includes a machine-readable `error` code your application can use for programmatic handling, and a human-readable `detail` message for debugging. ## Error Response Format All `/api/v3/` error responses share this envelope: ```json theme={null} { "id": null, "code": 400, "error": "bad_request", "detail": "Unknown content type path: 'fake:path'", "doc_url": "https://developers.lighton.ai/errors#bad_request" } ``` | Field | Type | Description | | - | - | - | | `id` | `string \| null` | Resource or job identifier, when one exists at error time. `null` for most errors. | | `code` | `integer` | HTTP status code (mirrors the response status). | | `error` | `string` | Machine-readable error code. Use this for programmatic error handling. | | `detail` | `string` | Human-readable explanation. May change between versions — do not parse. | | `doc_url` | `string` | Link to the relevant section of this page. | | `index` | `integer` (optional) | 0-based position of the failing action in a batch request. Only present on errors from dedicated `/batch` endpoints. | ### Validation errors (422) Requests that fail field-level validation return **422 Unprocessable Entity** with an additional `fields` key mapping each invalid field to its errors: ```json theme={null} { "id": null, "code": 422, "error": "validation_error", "detail": "One or more fields failed validation.", "doc_url": "https://developers.lighton.ai/errors#validation_error", "fields": { "name": [ {"error": "required", "detail": "Field required"} ], "attribute_type": [ {"error": "invalid_choice", "detail": "Input should be 'text', 'number', 'date', 'boolean', 'select' or 'multi-select'"} ] } } ``` Each entry in `fields` is an array of `{error, detail}` objects. A single field can have multiple errors. The `error` value is a [field error code](#field-error-codes) your application can map to form field states. ### Batch error responses Dedicated batch endpoints (e.g., `POST /api/v3/content-types/batch`, `POST /api/v3/files/{id}/facets/batch`) execute multiple actions in one request. When a batch request fails, the error response includes an `index` field — the 0-based position of the action that caused the failure: ```json theme={null} { "id": null, "code": 400, "error": "content_type_unknown", "detail": "Unknown content type path: 'fake:path'.", "doc_url": "https://developers.lighton.ai/errors#content_type_unknown", "index": 2 } ``` All error types (400, 403, 404, 422) include `index` when triggered inside a batch. Single-action requests never include this field. **Replay safety:** actions before the failing index are committed but their results are not returned. All action verbs are idempotent — re-send the entire batch after fixing the failing action to complete it. *** ## HTTP Status Codes | Status | Meaning | Typical action | | - | - | - | | **400** | Business rule violated — the request is structurally valid but the operation is not allowed | Read `error` and `detail` to understand the constraint. Fix the request logic. | | **401** | Not authenticated — credentials are missing or expired | Refresh your API key or session token. | | **403** | Forbidden — authenticated but lacking the required permission | Check your role and permissions for this resource. | | **404** | Not found — the resource doesn't exist or isn't accessible to you | Verify the resource ID or path. | | **409** | Conflict — a resource with the same identifier already exists | Use a different name, or fetch the existing resource. | | **422** | Validation error — one or more request fields are invalid | Read `fields` to identify which fields to fix. | | **429** | Rate limited — too many requests | Wait and retry after the indicated period. | | **500** | Server error — something unexpected went wrong | Retry later. If persistent, contact support. | | **502** | Upstream error — a dependent service (AI model, external provider) failed | Retry later. The `error` code indicates which service. | | **503** | Service unavailable — a required service is not deployed or reachable | Retry later. | | **504** | Timeout — a dependent service did not respond in time | Retry with a simpler request (fewer pages, shorter query). | *** ## Error Codes ### bad\_request **Status:** 400 The request is structurally valid JSON but violates a domain rule. The `detail` message explains the specific constraint. ```json theme={null} {"error": "bad_request", "detail": "Unknown content type path: 'fake:path'"} ``` Common causes: * Invalid parameter combination * Unsupported value for a domain-specific field * Operation not applicable to the current resource state *** ### unauthorized **Status:** 401 Authentication credentials were not provided or are invalid. ```json theme={null} {"error": "unauthorized", "detail": "Authentication credentials were not provided."} ``` Include a valid API key in the `Authorization: Bearer ` header, or authenticate via session cookie. *** ### email\_verification\_required **Status:** 403 Your email address has not been verified. Some operations require a verified email. ```json theme={null} {"error": "email_verification_required", "detail": "Please verify your email address to access this feature."} ``` Check your inbox for a verification email, or request a new one from your account settings. *** ### insufficient\_permissions **Status:** 403 You are authenticated but your role does not grant access to this operation. ```json theme={null} {"error": "insufficient_permissions", "detail": "You do not have permission to perform this action."} ``` Contact your workspace or company administrator to request the required permission. *** ### not\_found **Status:** 404 The requested resource does not exist, or you do not have access to it. For security, the API does not distinguish between "does not exist" and "not accessible." ```json theme={null} {"error": "not_found", "detail": "Document not found."} ``` Verify the resource ID or path. If the resource was recently created, ensure you have the appropriate workspace membership. *** ### conflict **Status:** 409 A resource with the same unique identifier (name, path, etc.) already exists. ```json theme={null} {"error": "conflict", "detail": "A resource with this name already exists."} ``` Choose a different name, or retrieve the existing resource. *** ### payload\_too\_large **Status:** 413 The uploaded file exceeds the endpoint's size limit. The `detail` field reports the actual size and the applicable limit. ```json theme={null} {"error": "payload_too_large", "detail": "File size (157286400 bytes) exceeds the 100MB async limit."} ``` Limits depend on the endpoint and mode: 20 MB for synchronous Parse, Extract, and Preview, 100 MB for asynchronous Parse and Extract. Split the document, or switch to async mode. *** ### content\_filtered **Status:** 422 The model refused to generate a response, typically because a safety filter was triggered. ```json theme={null} { "error": "content_filtered", "fields": { "query": [{"error": "content_filtered", "detail": "The model refused to generate an answer or a safety filter was triggered."}] } } ``` Rephrase your query. This is not a rate limit — the model determined the content was not appropriate to answer. *** ### context\_length\_exceeded **Status:** 422 The combined query and retrieved context exceed the model's maximum context window. ```json theme={null} { "error": "context_length_exceeded", "fields": { "query": [{"error": "context_length_exceeded", "detail": "Query + retrieved context exceeds the model's context window."}] } } ``` Reduce the number of documents in scope (use `workspace_id`, `tag_id`, or `file_id` filters), or shorten your query. *** ### validation\_error **Status:** 422 The request body is valid JSON, but one or more fields failed validation. Check the `fields` key for per-field error details. ```json theme={null} { "error": "validation_error", "fields": { "content_type_path": [{"error": "required", "detail": "Field required"}], "choices": [{"error": "invalid", "detail": "'choices' must be a non-empty list for select/multi-select attributes."}] } } ``` See [Field Error Codes](#field-error-codes) for the vocabulary of field-level `error` values. *** ### too\_many\_requests **Status:** 429 You have exceeded the rate limit for this endpoint. Wait before retrying. ```json theme={null} {"error": "too_many_requests", "detail": "Too many requests. Please try again later."} ``` *** ### internal\_server\_error **Status:** 500 An unexpected error occurred on the server. This is not caused by your request. ```json theme={null} {"error": "internal_server_error", "detail": "An unexpected error occurred. Please try again later."} ``` If the error persists, contact support with the timestamp and endpoint. *** ### model\_unavailable **Status:** 502 / 503 The requested AI model is not reachable or not currently deployed. ```json theme={null} {"error": "model_unavailable", "detail": "The requested model is currently unavailable."} ``` Retry after a short delay. If using a specific model name, verify it is supported via the models endpoint. *** ### model\_timeout **Status:** 504 The AI model did not respond within the allowed time. ```json theme={null} {"error": "model_timeout", "detail": "The model did not respond within the allowed time."} ``` Consider reducing the complexity of your request (shorter query, fewer documents in scope). *** ## Domain Error Codes Some endpoints return domain-specific `error` codes for fine-grained error handling. Each code links directly from the `doc_url` in the error response. ### Extract #### extraction\_failed **Status:** 500 The extraction pipeline failed unexpectedly. Retry the request. If the error persists, contact support. #### extraction\_timeout **Status:** 504 Extraction did not complete within the allowed time. Try extracting fewer pages at a time. #### extraction\_upstream\_error **Status:** 502 An upstream service used during extraction returned an error. Retry after a short delay. #### invalid\_page **Status:** 400 The page specification is malformed. Use integer page numbers or valid ranges. #### page\_out\_of\_range **Status:** 400 The requested page number is outside the document's actual page range. Check the document's page count first. #### document\_file\_not\_found **Status:** 404 The document referenced by `file_id` exists, but its stored file could not be found. The document may still be ingesting, or its underlying file was removed. Re-upload the document. ### Facets #### attribute\_name\_conflict **Status:** 400 Attribute name is already defined on another node in the same content type tree. Attribute names must be unique across the entire tree (root to leaves). #### attribute\_name\_reserved **Status:** 400 Attribute name collides with a system-reserved name (e.g. `id`, `path`, `type`). Choose a different name. #### attribute\_type\_immutable **Status:** 400 Cannot change an attribute's type after creation. To change the type, delete the attribute with `undefine_attribute` and recreate it with the new type. #### attribute\_value\_invalid **Status:** 400 The value does not match the attribute's type or allowed choices. For `select` attributes, the value must be one of the defined choices. For `multi-select`, it must be an array of valid choices. #### content\_type\_depth\_exceeded **Status:** 400 Tree nesting exceeds the maximum allowed depth of 4 levels (e.g. `legal:contract:nda:mutual` is depth 4). Restructure your taxonomy to stay within this limit. #### content\_type\_not\_assigned **Status:** 404 The content type is not assigned to this document. Classify the document first with the `classify` action. #### content\_type\_not\_found **Status:** 404 Content type path does not exist for your company. Verify available paths with `GET /api/v3/content-types`. If you need to create it, use the `adopt` or `define_content_type` action. #### content\_type\_sibling\_conflict **Status:** 400 The document already has a content type from the same tree root. A document can hold at most one classification per tree. Unclassify the existing one first, then classify with the new path. #### content\_type\_unknown **Status:** 400 Content type code is not recognized. Verify available content types with `GET /api/v3/content-types/seed/templates`. ### Files #### upload\_session\_expired **Status:** 400 The upload session has expired. Create a new upload session and retry. #### upload\_session\_not\_found **Status:** 404 The upload session ID does not exist. Verify the session ID or create a new one. #### STORAGE\_LIMIT\_REACHED **Status:** 400 The upload would exceed your company's custom workspace storage quota. The `detail` field reports the limit and your current usage. Delete stale documents, or ask your company admin to request a quota increase. #### UPLOADS\_DISABLED **Status:** 400 Uploads are disabled for the custom workspace because its storage limit is set to 0 MB. Ask your company admin to request a quota increase. ### Parse #### ambiguous\_input **Status:** 400 Both a file upload and a document URL were provided in the same request. Provide one or the other, not both. #### file\_rendering\_failed **Status:** 500 The file could not be rendered to images before OCR processing. The file may be corrupted or in an unsupported variant. #### invalid\_model **Status:** 400 The requested vision-language model is not available. Check supported models via the models endpoint. #### invalid\_page\_range **Status:** 400 Page range is malformed or out of bounds. Ensure start page is less than end page and both are within the document's page count. #### max\_pages\_exceeded **Status:** 400 | **Also:** `/extract` The document exceeds the maximum page count allowed for this operation. Process the document in smaller page ranges. #### max\_pages\_per\_request\_exceeded **Status:** 400 Too many pages requested in a single OCR call. Split into smaller page ranges. #### missing\_input **Status:** 400 Neither a file upload nor a document URL was provided. Provide one of them. #### no\_file **Status:** 400 The file upload field is empty or missing. Attach a file to the request. #### ocr\_concurrent\_limit **Status:** 429 Too many concurrent OCR requests are running. Wait and retry after a short delay. #### unsupported\_file\_format **Status:** 400 The file type is not supported for parsing. Check supported file types in the API documentation. #### vlm\_error **Status:** 502 The vision-language model returned an error while processing the document. Retry after a short delay. #### vlm\_unavailable **Status:** 503 The vision-language model is not reachable or not currently deployed. Retry after a short delay. ### Preview #### unsupported\_format **Status:** 400 The document's format cannot be rendered as a PDF preview. Supported types are `.pdf`, `.png`, `.jpg`, `.jpeg`, `.pptx`, `.ppt`, `.odp`, `.docx`, `.odt`, `.doc`, `.html`, and `.xhtml`. ### Search #### search\_backend\_unavailable **Status:** 500 The search backend is temporarily unreachable. Retry after a short delay; if the error persists, contact support. ### Workspaces #### datasource\_connection\_failed **Status:** 400 Could not connect to the external data source with the provided credentials. Verify your credentials and network access, then retry. #### googledrive\_folder\_inaccessible **Status:** 400 The Google Drive folder cannot be accessed with the provided service account. Verify that the service account has read access to the folder. #### sharepoint\_site\_not\_found **Status:** 400 The SharePoint site does not exist or is not accessible with the provided credentials. Verify the `site_id` or `site_name` and tenant configuration. #### unknown\_provider **Status:** 400 The datasource type is not recognized. Supported types: `googledrive`, `sharepoint`, `servicenow`, `webscrapper`. #### workspace\_already\_synced **Status:** 400 The workspace is already connected to a datasource. A workspace can only have one datasource. #### workspace\_has\_datasource **Status:** 409 The workspace already has an active datasource import running. #### workspace\_has\_documents **Status:** 400 Cannot convert to a synced workspace because it already contains manually uploaded documents. Remove existing documents first or create a new workspace. *** ## Field Error Codes When you receive a `422` response, each entry in the `fields` dict contains an `error` code from this vocabulary: | Code | Description | Example | | - | - | - | | `required` | Field is missing from the request body | `{"error": "required", "detail": "Field required"}` | | `blank` | Field is present but empty string | `{"error": "blank", "detail": "This field may not be blank."}` | | `null` | Field is present but null | `{"error": "null", "detail": "This field may not be null."}` | | `invalid` | Wrong type or format | `{"error": "invalid", "detail": "Input should be a valid integer"}` | | `invalid_choice` | Not one of the allowed values | `{"error": "invalid_choice", "detail": "Input should be 'text', 'number' or 'date'"}` | | `invalid_format` | Doesn't match the expected pattern | `{"error": "invalid_format", "detail": "Invalid email format"}` | | `too_short` | String or array below minimum length | `{"error": "too_short", "detail": "String should have at least 3 characters"}` | | `too_long` | String or array exceeds maximum length | `{"error": "too_long", "detail": "String should have at most 255 characters"}` | | `too_small` | Number below minimum value | `{"error": "too_small", "detail": "Input should be greater than or equal to 1"}` | | `too_large` | Number exceeds maximum value | `{"error": "too_large", "detail": "Input should be less than or equal to 100"}` | | `already_exists` | Duplicate value | `{"error": "already_exists", "detail": "A group with this name already exists."}` | | `does_not_exist` | Referenced resource not found | `{"error": "does_not_exist", "detail": "User with this ID does not exist."}` | ### Handling field errors in your application ```python theme={null} import requests try: response = requests.post( "https://your-instance.lighton.ai/api/v3/content-types", headers={"Authorization": f"Bearer {api_key}"}, json={"action": "define_attribute"}, ) if not response.ok: error = response.json() # Batch requests include the 0-based action index that failed if "index" in error: print(f" Failed at batch action #{error['index']}") if error["error"] == "validation_error" and "fields" in error: # Per-field errors — collect for form display or logging for field, errors in error["fields"].items(): for entry in errors: print(f" {field}: [{entry['error']}] {entry['detail']}") else: # Domain error (e.g. attribute_type_immutable, content_type_not_found) print(f"Error {error['code']}: [{error['error']}] {error['detail']}") except requests.RequestException as exc: print(f"Network error: {exc}") ``` # Browse datasource folders Source: https://developers.lighton.ai/api-reference/workspaces/browse-datasource-folders /api-reference/api-console.yaml post /api/v3/workspaces/datasource/browse ⚠️ **ALPHA ENDPOINT** - This endpoint is in alpha and subject to breaking changes. Use with caution in production environments. Browse the remote folder hierarchy of a datasource without persisting anything. Connects to the external provider using the credentials in the payload and returns the immediate subfolders of ``parent_id`` (or the top-level entries when ``parent_id`` is ``null``). Use this to power a visual folder picker before submitting a datasource configuration via `PATCH /api/v3/workspaces/{id}` with a `datasource` payload. **Access:** any authenticated user. Supported providers and credentials: - **googledrive**: `service_account_file` (JSON string of the service account key file). When ``parent_id`` is omitted, returns the folders explicitly shared with the service account. - **sharepoint**: `client_id`, `client_secret`, `tenant_id`, `instance_url`, `site_name`. When both ``drive_id`` and ``parent_id`` are ``null``, returns the site's document libraries as ``kind="library"`` entries (each entry's ``id`` is the drive id). Pass that ``id`` back as ``drive_id`` (with ``parent_id=null``) to list the library root; pass it as ``drive_id`` together with a folder's ``id`` as ``parent_id`` to descend into a folder. # Create a workspace Source: https://developers.lighton.ai/api-reference/workspaces/create-a-workspace /api-reference/api-console.yaml post /api/v3/workspaces ⚠️ **ALPHA ENDPOINT** - This endpoint is in alpha and subject to breaking changes. Use with caution in production environments. Create a new custom workspace in the authenticated user's company. Requires the company to have `allow_user_workspace_creation` enabled, unless the caller is a company or instance administrator (who may always create workspaces). Returns **403** if workspace creation is disabled for the company and the caller is not an administrator. The creator is automatically added as OWNER. # Delete a workspace Source: https://developers.lighton.ai/api-reference/workspaces/delete-a-workspace /api-reference/api-console.yaml delete /api/v3/workspaces/{id} ⚠️ **ALPHA ENDPOINT** - This endpoint is in alpha and subject to breaking changes. Use with caution in production environments. Soft-delete a custom workspace you own. The workspace and its memberships are marked as deleted but retained for the configured recovery period; collection data (documents, chunks, embeddings) is preserved until the workspace is permanently deleted by the cleanup task. The workspace can be restored within the recovery period via `PATCH` with `deleted_at=null` (use `?include_deleted=true` to address it after deletion). **Restrictions:** - PERSONAL workspaces cannot be deleted (system-managed) - Caller must be an OWNER of the workspace (or hold instance/company workspace-delete permission) # List workspaces Source: https://developers.lighton.ai/api-reference/workspaces/list-workspaces /api-reference/api-console.yaml get /api/v3/workspaces ⚠️ **ALPHA ENDPOINT** - This endpoint is in alpha and subject to breaking changes. Use with caution in production environments. Get the list of accessible workspaces for the authenticated user. Returns workspaces where the user is set as member (through any group: private, company, or custom). Please use instance or company level endpoints to access the workspaces as admin. **Filtering:** All users see only workspaces where they are members. Admin users should use `/api/v3/instance/workspaces` or `/api/v3/company/workspaces` for administrative access. Supported query filters: `workspace_type`, `document_upload_method`, `datasource_type` (`googledrive`, `sharepoint`, `servicenow`, `webscrapper`, `smb`), `name` (case-insensitive contains), `group_id`, `group_name`, `user_role`. **API keys:** `scoped_api_keys` lists your own non-revoked keys that can access the workspace. Each entry has a `scope_type`: `workspace` for keys explicitly scoped to it (listed first, with their per-workspace role), or `global` for keys with no scope rows that implicitly reach every workspace (their `role` mirrors your own role on the workspace). **Taxonomy:** Each workspace includes a `taxonomy` field summarizing content type classification. `classified_files_rate` is the proportion of documents with at least one content type (0-to-1 ratio). `root_content_types` lists top-level content type families with document counts. A workspace holding documents that are all unclassified reports a rate of `0.0` with an empty `root_content_types`; the whole `taxonomy` object is `null` only when the workspace has no documents. Results ordered by `name` ascending by default — pass `ordering` to change it. Paginated with 20 elements per page by default. # Retrieve a workspace Source: https://developers.lighton.ai/api-reference/workspaces/retrieve-a-workspace /api-reference/api-console.yaml get /api/v3/workspaces/{id} ⚠️ **ALPHA ENDPOINT** - This endpoint is in alpha and subject to breaking changes. Use with caution in production environments. Retrieve a workspace by ID. Returns workspace in V3 format. **Access:** Instance-level users (Sys Admin, Account Manager, Admin, DPO Admin) can retrieve any workspace. Company-level users (Company Admin, Company DPO) can retrieve workspaces in their company. Regular users can retrieve workspaces where they are members. **Member Visibility:** Instance-level users and company-level users see all members. Workspace OWNER sees members. EDITOR and VIEWER do not see members. **Sync status:** For synced workspaces, the response includes a `sync` block with `datasource_type`, `source_name`, `last_status`, `updated_at`, `failed_files_count`, and `next_import_date`. Use this field for polling the sync state. Non-existent and unauthorized workspaces are treated identically (existence is not disclosed). # Test datasource credentials Source: https://developers.lighton.ai/api-reference/workspaces/test-datasource-credentials /api-reference/api-console.yaml post /api/v3/workspaces/datasource/test ⚠️ **ALPHA ENDPOINT** - This endpoint is in alpha and subject to breaking changes. Use with caution in production environments. Test datasource credentials without persisting anything. Validates that the provided credentials can connect to the external source. Succeeds when the connection can be established; returns an error otherwise. No datasource or import is created. Use this before `PATCH /api/v3/workspaces/{id}` with a `datasource` payload to surface connection errors before committing the conversion. **Access:** any authenticated user. Credentials per type: - **googledrive**: `service_account_file` (JSON string of the service account key file) - **sharepoint**: `client_id`, `client_secret`, `tenant_id`, `site_id` (optional), `site_name` (optional) - **servicenow**: `instance_url`, `username`, `password` - **webscrapper**: no credentials required - **smb**: `instance_url` (`smb://host[:port]`), `username`, `password` Filter criteria per type: - **googledrive**: `folder_id` (required), `recursive` (optional) - **sharepoint**: `folder_path` (required), `recursive` (optional) - **servicenow**: `doc_type` (required, e.g. `knowledge`) - **webscrapper**: `start_url` (required) - **smb**: `share_name` (required), `path` (optional), `recursive` (optional) # Update a workspace Source: https://developers.lighton.ai/api-reference/workspaces/update-a-workspace /api-reference/api-console.yaml patch /api/v3/workspaces/{id} ⚠️ **ALPHA ENDPOINT** - This endpoint is in alpha and subject to breaking changes. Use with caution in production environments. Partially update a given workspace. **Standard update (workspace OWNER only):** - **name** (string, optional): Desired workspace name (max 100 characters, cannot be empty) - **description** (string, optional): Desired workspace description. Send empty string or null to clear. **Convert to a synced workspace** (workspace OWNER or a role granting workspace edit/delete): - **datasource** (object): Datasource configuration used to populate the workspace. Credentials are validated against the external source before persistence; use `POST /api/v3/workspaces/{id}/datasource/test` to validate them without committing. - The target workspace must be empty (no documents) and not already synced. **Edit an existing synced workspace's datasource:** - If the workspace is already synced and no successful sync has happened yet, sending a `datasource` payload edits the datasource in place (full credential re-entry required, name/filter_criteria updated, next sync re-triggered). - After the first successful sync, the field is rejected with **409 Conflict** — ingested data integrity is preserved by locking the config. Delete and recreate the workspace to change its configuration. - The datasource `type` is immutable on edit. - Edits are also rejected with 409 while a sync is currently in flight (WAITING/PROCESSING). - The current edit-availability is exposed in the response under `sync.editable` (boolean). **Restrictions:** - Only SHARED workspaces can be updated (PERSONAL workspaces cannot be modified) - Conversion is one-way: a synced workspace cannot be reverted to manual via the API # Authentication Source: https://developers.lighton.ai/authentication Authenticate API requests with a bearer token. All API requests require a bearer token in the `Authorization` header. ``` Authorization: Bearer $LIGHTON_API_KEY ``` You can create an API key from the **API Keys** section of the [console](https://console.lighton.ai), or programmatically via [`POST /api/v3/keys`](/api-reference/api-keys/create-api-key). Existing keys can be listed and revoked through the same endpoint family. ```python Python SDK theme={null} from lighton import ApiKey, LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment # Omitting expires_at creates a key that never expires key = ApiKey(name="ci-pipeline").create(client) print(key.id, key.key.get_secret_value()) # the secret is shown once, store it now # List the keys you already have, then revoke this one for existing in ApiKey.list(client): print(existing.id, existing.name, existing.prefix) key.delete() ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # expires_at is required: null creates a key that never expires response = requests.post( "https://api.lighton.ai/api/v3/keys", headers=headers, json={"name": "ci-pipeline", "expires_at": None}, ) key = response.json() print(key["id"], key["key"]) # the full key is returned only here, store it now # Revoke it when it is no longer needed response = requests.delete( f"https://api.lighton.ai/api/v3/keys/{key['id']}", headers=headers, ) print(response.status_code) # 204 ``` ```bash cURL theme={null} # Create a key. The full `key` value is in this response and nowhere else. curl https://api.lighton.ai/api/v3/keys \ -H "Authorization: Bearer $LIGHTON_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "ci-pipeline", "expires_at": null}' # Revoke it (204 No Content) curl -X DELETE https://api.lighton.ai/api/v3/keys/$KEY_ID \ -H "Authorization: Bearer $LIGHTON_API_KEY" ``` ## Error responses | Status | Meaning | | - | - | | `401` | Missing or invalid API key | | `403` | Key exists but lacks permission for this resource | | `429` | Rate limit exceeded | # Introduction Source: https://developers.lighton.ai/index Upload a PDF. Search it in milliseconds. Parse any document to clean Markdown. LightOn gives you an API to search, parse, and ingest documents at scale. Build knowledge-retrieval pipelines without managing vector databases or OCR models. ## What you can do ### Build a searchable knowledge base Ingest documents once, then query them. A persistent, indexed corpus you can search or ask questions over. Upload files to a workspace and LightOn turns them into a searchable index automatically, running the whole document-understanding pipeline for you. Hybrid semantic + lexical search with reranking across your document corpus. Returns ranked chunks with scores, source metadata, and optional page images. Retrieval-augmented generation in one call: a grounded LLM answer with the sources it used, returned synchronously or streamed token by token. ### Classify and organise documents Partition your corpus into isolated containers per team, customer, or tenant. Group documents into reusable collections that cut across workspaces. Define classification trees with custom attributes, classify files, and filter search by metadata. ### Process documents on the fly Convert or extract from a document in a single call. Nothing is stored, useful for feeding your own pipeline. Convert PDFs, Office files, and images to structured Markdown. Sync mode for quick jobs, async for large documents. Nothing stored either way. Pull typed fields out of documents using a JSON Schema you provide. Sync mode for small files, async mode for large jobs. ## Get started Get a search result in under 5 minutes. Set up your API key. Full endpoint documentation. Do you even search? # Google Drive Source: https://developers.lighton.ai/integrations/google-drive Create workspaces synced to your Google Drive. Sync a workspace to a Google Drive folder so any file added there is automatically ingested and indexed in LightOn. Files are kept in sync: additions, updates, and deletions in Drive flow through to the workspace on the next import. Synced workspaces are read-only: you cannot manually upload to or delete from them. ## From the console The fastest path is the console UI: 1. Create a new workspace from **Workspaces → New**. 2. Open the workspace, then choose **Sync from Google Drive** in its settings. 3. Paste the service account JSON, pick the folder, and trigger the first import. Use this if you'd rather not handle credentials yourself. ## From the API Convert any manual workspace into a Google Drive-synced workspace by issuing a [`PATCH /api/v3/workspaces/{id}`](/api-reference/workspaces/update-a-workspace) with a `datasource` object. Conversion is one-way: you cannot revert a synced workspace back to manual. ```python theme={null} import requests, json requests.patch( f"https://api.lighton.ai/api/v3/workspaces/{workspace_id}", headers={"Authorization": "Bearer $LIGHTON_API_KEY"}, json={ "datasource": { "type": "googledrive", "name": "Engineering Drive", "credentials": { "service_account_file": json.dumps(service_account_dict), }, "filter_criteria": { "folder_id": "1AbCdEfGhIjKlMnOpQrStUvWxYz", "recursive": True, }, }, }, ) ``` **Credentials** * `service_account_file`: JSON-encoded string of your Google service account credentials. The service account needs read access to the target folder. **Filter criteria** * `folder_id` (required): the Google Drive folder ID to sync from. It's the last path segment of the folder's URL in Drive (`drive.google.com/drive/folders/`), or use [`POST /api/v3/workspaces/datasource/browse`](/api-reference/workspaces/browse-datasource-folders) (alpha) to list the folders shared with your service account. * `recursive` (optional): include subfolders The first import is scheduled immediately. Track progress on the workspace's `sync` field returned by [`GET /api/v3/workspaces/{id}`](/api-reference/workspaces/retrieve-a-workspace). For the full request shape, see [`PATCH /api/v3/workspaces/{id}`](/api-reference/workspaces/update-a-workspace) in the API reference. # Inceptron Source: https://developers.lighton.ai/integrations/inceptron Use LightOn search as the retrieval layer for models hosted on Inceptron. [Inceptron](https://www.inceptron.io/models) is a platform for hosting and serving optimized open models (Llama, Kimi, MiniMax, GLM, and others) with best-in-class price-performance, exposed through an OpenAI-compatible endpoint. The infrastructure is enterprise-ready (ISO 27001 and GDPR compliant). Pair these models with LightOn search to build RAG pipelines where the retrieval stays on LightOn's infrastructure and the generation runs on Inceptron. The flow is: 1. Search LightOn for the passages most relevant to the user's question. 2. Pack those passages into the model's context window. 3. Call the Inceptron model to generate an answer grounded in the retrieved content. ## Prerequisites * A `LIGHTON_API_KEY`, available in the **Console → API Keys** section. * An Inceptron API key, available from the [Inceptron console](https://console.inceptron.io). Store it as `INCEPTRON_API_KEY`. * At least one workspace with indexed documents on LightOn. ## Installation ```bash theme={null} pip install requests openai ``` The `openai` package is used here only for its client; Inceptron's endpoint is fully compatible with it. ## Full example ```python theme={null} import os import requests from openai import OpenAI LIGHTON_API_KEY = os.environ["LIGHTON_API_KEY"] INCEPTRON_API_KEY = os.environ["INCEPTRON_API_KEY"] inceptron = OpenAI( base_url="https://api.inceptron.io/v1", api_key=INCEPTRON_API_KEY, ) def search(query: str, workspace_id: list[int] | None = None, max_results: int = 5) -> list[dict]: payload = {"query": query, "max_results": max_results} if workspace_id: payload["workspace_id"] = workspace_id response = requests.post( "https://api.lighton.ai/api/v3/search", headers={"Authorization": f"Bearer {LIGHTON_API_KEY}"}, json=payload, ) response.raise_for_status() return response.json()["results"] def answer(question: str, workspace_id: list[int] | None = None, model: str = "nvidia/llama-3.3-70b-instruct-fp8") -> str: results = search(question, workspace_id=workspace_id) context = "\n\n".join( f"[{r['source']['filename']}, p.{r['source']['page_start']}]\n{r['content']}" for r in results if r["content"] ) completion = inceptron.chat.completions.create( model=model, messages=[ { "role": "system", "content": ( "You are a helpful assistant. Answer the user's question using only " "the provided context. If the context does not contain enough information, " "say so.\n\nContext:\n" + context ), }, {"role": "user", "content": question}, ], ) return completion.choices[0].message.content print(answer("What is our data retention policy?")) ``` ## LightOn search as a tool Instead of always searching before calling the model, you can expose LightOn search as a tool and let the model decide when to call it. The model issues a `lighton_search` tool call when it needs context; your code executes the search and feeds the results back; the model then produces a final answer. ```python theme={null} import json import os import requests from openai import OpenAI LIGHTON_API_KEY = os.environ["LIGHTON_API_KEY"] INCEPTRON_API_KEY = os.environ["INCEPTRON_API_KEY"] inceptron = OpenAI( base_url="https://api.inceptron.io/v1", api_key=INCEPTRON_API_KEY, ) SEARCH_TOOL = { "type": "function", "function": { "name": "lighton_search", "description": ( "Search the company knowledge base for passages relevant to a query. " "Returns ranked excerpts with their source filename and page numbers." ), "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Natural-language search query.", }, "max_results": { "type": "integer", "description": "Number of passages to return (1–50, default 5).", "default": 5, }, }, "required": ["query"], }, }, } def run_search(query: str, max_results: int = 5) -> str: response = requests.post( "https://api.lighton.ai/api/v3/search", headers={"Authorization": f"Bearer {LIGHTON_API_KEY}"}, json={"query": query, "max_results": max_results}, ) response.raise_for_status() results = response.json()["results"] passages = [ f"[{r['source']['filename']}, p.{r['source']['page_start']}]\n{r['content']}" for r in results if r["content"] ] return "\n\n".join(passages) if passages else "No results found." def answer(question: str, model: str = "nvidia/llama-3.3-70b-instruct-fp8") -> str: messages = [{"role": "user", "content": question}] while True: completion = inceptron.chat.completions.create( model=model, tools=[SEARCH_TOOL], messages=messages, ) choice = completion.choices[0] if choice.finish_reason == "tool_calls": messages.append(choice.message) for call in choice.message.tool_calls: args = json.loads(call.function.arguments) result = run_search(**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, }) else: return choice.message.content print(answer("What is our data retention policy?")) ``` The loop handles the case where the model issues multiple search calls in sequence before producing a final answer. ## Scoping retrieval to a workspace Pass `workspace_id` to limit search to a specific workspace. This is useful in multi-tenant products where each customer's data lives in a dedicated workspace. ```python theme={null} answer("Summarize the onboarding checklist", workspace_id=[42]) ``` ## Choosing a model Inceptron's catalog includes several hosted models. Pass the model name to the `model` parameter: | Model | Notes | | - | - | | `nvidia/llama-3.3-70b-instruct-fp8` | Strong general-purpose model, good default choice | | `zai-org/GLM-5.1-FP8` | Capable multilingual model | | `MiniMaxAI/MiniMax-M2.5` | Long-context model | | `moonshotai/Kimi-K2.6` | Reasoning model with strong agentic abilities | | `moonshotai/Kimi-K2.6-Fast` | Faster variant of Kimi K2.6 | | `moonshotai/Kimi-K2.7-Code` | Tuned for code generation | You can list the models available to your key at any time: ```python theme={null} print([m.id for m in inceptron.models.list().data]) ``` See the [Inceptron models catalog](https://www.inceptron.io/models) for the current list, context limits, and pay-as-you-go pricing. ## A note on reasoning models The Kimi models (`moonshotai/Kimi-K2.6`, `moonshotai/Kimi-K2.6-Fast`, `moonshotai/Kimi-K2.7-Code`) are reasoning models: they spend tokens thinking before producing an answer, and they return that thinking under a separate `reasoning` field. If you set `max_tokens` too low, the model can exhaust the budget while still reasoning, so the request finishes with `finish_reason="length"` and `message.content` is `None`. Give reasoning models a generous token budget (a couple thousand tokens or more) to leave room for the final answer. The non-reasoning models in the table above are not affected. ## Streaming responses Inceptron's endpoint supports streaming. Enable it by passing `stream=True` and iterating over the response: ```python theme={null} stream = inceptron.chat.completions.create( model="nvidia/llama-3.3-70b-instruct-fp8", messages=[...], stream=True, ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True) ``` # Integrations Source: https://developers.lighton.ai/integrations/index Connect LightOn to the tools and data sources your team already uses. Integrations let LightOn pull from systems where your documents already live, instead of asking you to upload them by hand. Set one up once and the workspace stays in sync: additions, updates, and deletions in the source flow through automatically on the next import. ## Datasources Datasources sync a workspace to an external file store. The workspace becomes read-only on LightOn's side; the source system remains the single source of truth. Sync a workspace to a Google Drive folder via a service account. Sync a workspace to a SharePoint site using an Azure app registration. Both datasources can be configured from the **console UI** (create a new workspace, then open it and choose to sync it from inside the workspace's settings) or via the API by issuing [`PATCH /api/v3/workspaces/{id}`](/api-reference/workspaces/update-a-workspace) with a `datasource` object. See each datasource's page for the credentials and filter criteria it expects. ## LLM Providers LLM provider guides show how to combine LightOn retrieval with a hosted model to build RAG pipelines. LightOn handles search and ranking; the provider handles generation. Use LightOn search as the retrieval layer for vision-language and other models hosted on Lyceum. Use LightOn search as the retrieval layer for Llama, Mistral, and other models hosted on Scaleway. Use LightOn search as the retrieval layer for Qwen, Mistral, Llama, and other models hosted on OVHcloud. Use LightOn search as the retrieval layer for Llama, Kimi, MiniMax, and other models hosted on Inceptron. # Lyceum Serverless Inference Source: https://developers.lighton.ai/integrations/lyceum Use LightOn search as the retrieval layer for models hosted on Lyceum. [Lyceum](https://lyceum.technology) serves open models through an OpenAI-compatible serverless endpoint. Pair them with LightOn search to build RAG pipelines where the retrieval stays on LightOn's infrastructure and the generation runs on Lyceum. The flow is: 1. Search LightOn for the passages most relevant to the user's question. 2. Pack those passages into the model's context window. 3. Call the Lyceum model to generate an answer grounded in the retrieved content. ## Prerequisites * A `LIGHTON_API_KEY`, available in the **Console → API Keys** section. * A Lyceum API key. Store it as `LYCEUM_API_KEY`. * At least one workspace with indexed documents on LightOn. ## Installation ```bash theme={null} pip install requests openai ``` The `openai` package is used here only for its client; Lyceum's endpoint is fully compatible with it. ## Full example ```python theme={null} import os import requests from openai import OpenAI LIGHTON_API_KEY = os.environ["LIGHTON_API_KEY"] LYCEUM_API_KEY = os.environ["LYCEUM_API_KEY"] lyceum = OpenAI( base_url="https://api.lyceum.technology/api/v2/external/serverless", api_key=LYCEUM_API_KEY, ) def search(query: str, workspace_id: list[int] | None = None, max_results: int = 5) -> list[dict]: payload = {"query": query, "max_results": max_results} if workspace_id: payload["workspace_id"] = workspace_id response = requests.post( "https://api.lighton.ai/api/v3/search", headers={"Authorization": f"Bearer {LIGHTON_API_KEY}"}, json=payload, ) response.raise_for_status() return response.json()["results"] def answer(question: str, workspace_id: list[int] | None = None, model: str = "openbmb/MiniCPM-V-4_5") -> str: results = search(question, workspace_id=workspace_id) context = "\n\n".join( f"[{r['source']['filename']}, p.{r['source']['page_start']}]\n{r['content']}" for r in results if r["content"] ) completion = lyceum.chat.completions.create( model=model, messages=[ { "role": "system", "content": ( "You are a helpful assistant. Answer the user's question using only " "the provided context. If the context does not contain enough information, " "say so.\n\nContext:\n" + context ), }, {"role": "user", "content": question}, ], ) return completion.choices[0].message.content print(answer("What is our data retention policy?")) ``` ## LightOn search as a tool Instead of always searching before calling the model, you can expose LightOn search as a tool and let the model decide when to call it. The model issues a `lighton_search` tool call when it needs context; your code executes the search and feeds the results back; the model then produces a final answer. ```python theme={null} import json import os import requests from openai import OpenAI LIGHTON_API_KEY = os.environ["LIGHTON_API_KEY"] LYCEUM_API_KEY = os.environ["LYCEUM_API_KEY"] lyceum = OpenAI( base_url="https://api.lyceum.technology/api/v2/external/serverless", api_key=LYCEUM_API_KEY, ) SEARCH_TOOL = { "type": "function", "function": { "name": "lighton_search", "description": ( "Search the company knowledge base for passages relevant to a query. " "Returns ranked excerpts with their source filename and page numbers." ), "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Natural-language search query.", }, "max_results": { "type": "integer", "description": "Number of passages to return (1–50, default 5).", "default": 5, }, }, "required": ["query"], }, }, } def run_search(query: str, max_results: int = 5) -> str: response = requests.post( "https://api.lighton.ai/api/v3/search", headers={"Authorization": f"Bearer {LIGHTON_API_KEY}"}, json={"query": query, "max_results": max_results}, ) response.raise_for_status() results = response.json()["results"] passages = [ f"[{r['source']['filename']}, p.{r['source']['page_start']}]\n{r['content']}" for r in results if r["content"] ] return "\n\n".join(passages) if passages else "No results found." def answer(question: str, model: str = "openbmb/MiniCPM-V-4_5") -> str: messages = [{"role": "user", "content": question}] while True: completion = lyceum.chat.completions.create( model=model, tools=[SEARCH_TOOL], messages=messages, ) choice = completion.choices[0] if choice.finish_reason == "tool_calls": messages.append(choice.message) for call in choice.message.tool_calls: args = json.loads(call.function.arguments) result = run_search(**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, }) else: return choice.message.content print(answer("What is our data retention policy?")) ``` The loop handles the case where the model issues multiple search calls in sequence before producing a final answer. ## Multimodal input Several models on Lyceum (such as `openbmb/MiniCPM-V-4_5`) are vision-language models that accept images alongside text. Pass an `image_url` content part to describe or reason over an image: ```python theme={null} response = lyceum.chat.completions.create( model="openbmb/MiniCPM-V-4_5", messages=[ { "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, {"type": "image_url", "image_url": {"url": ""}}, ], } ], max_tokens=256, ) print(response.choices[0].message.content) ``` You can combine this with LightOn search to ground answers about an image in your indexed documents. ## Scoping retrieval to a workspace Pass `workspace_id` to limit search to a specific workspace. This is useful in multi-tenant products where each customer's data lives in a dedicated workspace. ```python theme={null} answer("Summarize the onboarding checklist", workspace_id=[42]) ``` ## Choosing a model Lyceum's catalog includes a wide range of hosted models. Pass the model name to the `model` parameter: | Model | Notes | | - | - | | `meta-llama/Llama-3.3-70B-Instruct` | Strong general-purpose model, good default choice | | `Qwen/Qwen3-235B-A22B-Instruct-2507` | Large MoE model for demanding reasoning | | `Qwen/Qwen3-32B` | Capable mid-size model, lower latency | | `google/gemma-3-27b-it` | Compact, efficient instruction-tuned model | | `openai/gpt-oss-120b` | Open-weight GPT model | | `Qwen/Qwen2.5-VL-72B-Instruct` | Vision-language model, accepts image input | | `openbmb/MiniCPM-V-4_5` | Lightweight vision-language model | You can list the models available to your key at any time: ```python theme={null} print([m.id for m in lyceum.models.list().data]) ``` Check the [Lyceum documentation](https://lyceum.technology) for the current model list and pricing. ## Streaming responses Lyceum's endpoint supports streaming. Enable it by passing `stream=True` and iterating over the response: ```python theme={null} stream = lyceum.chat.completions.create( model="openbmb/MiniCPM-V-4_5", messages=[...], stream=True, ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True) ``` # Microsoft SharePoint Source: https://developers.lighton.ai/integrations/microsoft-sharepoint Create workspaces synced to your Microsoft SharePoint. Sync a workspace to a SharePoint site so any document added there is automatically ingested and indexed in LightOn. Files stay in sync: additions, updates, and deletions in SharePoint flow through to the workspace on the next import. Synced workspaces are read-only: you cannot manually upload to or delete from them. ## From the console The fastest path is the console UI: 1. Create a new workspace from **Workspaces → New**. 2. Open the workspace, then choose **Sync from SharePoint** in its settings. 3. Paste your Azure app credentials, pick the site and folder, and trigger the first import. Use this if you'd rather not handle credentials yourself. ## From the API Convert any manual workspace into a SharePoint-synced workspace by issuing a [`PATCH /api/v3/workspaces/{id}`](/api-reference/workspaces/update-a-workspace) with a `datasource` object. Conversion is one-way: you cannot revert a synced workspace back to manual. ```python theme={null} import requests requests.patch( f"https://api.lighton.ai/api/v3/workspaces/{workspace_id}", headers={"Authorization": "Bearer $LIGHTON_API_KEY"}, json={ "datasource": { "type": "sharepoint", "name": "Legal SharePoint", "credentials": { "client_id": "00000000-0000-0000-0000-000000000000", "client_secret": "YOUR_CLIENT_SECRET", "tenant_id": "11111111-1111-1111-1111-111111111111", "site_name": "Legal", }, "filter_criteria": { "folder_path": "/Shared Documents/Contracts", "recursive": True, }, }, }, ) ``` **Credentials** * `client_id`, `client_secret`, `tenant_id`: from your Azure app registration. The app needs `Sites.Read.All` (or scoped `Sites.Selected`) Microsoft Graph permissions. * `site_id` *or* `site_name` (optional): pick the SharePoint site. If both are omitted, the root site is used. **Filter criteria** * `folder_path` (required): path to the SharePoint folder to sync from, relative to the site. Use [`POST /api/v3/workspaces/datasource/browse`](/api-reference/workspaces/browse-datasource-folders) (alpha) to walk the site's document libraries and folders if you don't know the path. * `recursive` (optional): include subfolders The first import is scheduled immediately. Track progress on the workspace's `sync` field returned by [`GET /api/v3/workspaces/{id}`](/api-reference/workspaces/retrieve-a-workspace). For the full request shape, see [`PATCH /api/v3/workspaces/{id}`](/api-reference/workspaces/update-a-workspace) in the API reference. # OVHcloud AI Endpoints Source: https://developers.lighton.ai/integrations/ovh Use LightOn search as the retrieval layer for models hosted on OVHcloud AI Endpoints. OVHcloud AI Endpoints expose hosted open models (Qwen, Mistral, Llama, gpt-oss, and others) through an OpenAI-compatible endpoint, served from OVHcloud's European infrastructure. Pair them with LightOn search to build RAG pipelines where the retrieval stays on LightOn's infrastructure and the generation runs on OVHcloud. The flow is: 1. Search LightOn for the passages most relevant to the user's question. 2. Pack those passages into the model's context window. 3. Call the OVHcloud model to generate an answer grounded in the retrieved content. ## Prerequisites * A `LIGHTON_API_KEY`, available in the **Console → API Keys** section. * An OVHcloud AI Endpoints access token, available in the OVHcloud Control Panel under **Public Cloud → AI Endpoints**. Store it as `OVH_AI_ENDPOINTS_ACCESS_TOKEN`. * At least one workspace with indexed documents on LightOn. ## Installation ```bash theme={null} pip install requests openai ``` The `openai` package is used here only for its client; OVHcloud's `/chat/completions` endpoint is fully compatible with it. ## Full example ```python theme={null} import os import requests from openai import OpenAI LIGHTON_API_KEY = os.environ["LIGHTON_API_KEY"] OVH_AI_ENDPOINTS_ACCESS_TOKEN = os.environ["OVH_AI_ENDPOINTS_ACCESS_TOKEN"] ovh = OpenAI( base_url="https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", api_key=OVH_AI_ENDPOINTS_ACCESS_TOKEN, ) def search(query: str, workspace_id: list[int] | None = None, max_results: int = 5) -> list[dict]: payload = {"query": query, "max_results": max_results} if workspace_id: payload["workspace_id"] = workspace_id response = requests.post( "https://api.lighton.ai/api/v3/search", headers={"Authorization": f"Bearer {LIGHTON_API_KEY}"}, json=payload, ) response.raise_for_status() return response.json()["results"] def answer(question: str, workspace_id: list[int] | None = None, model: str = "Meta-Llama-3_3-70B-Instruct") -> str: results = search(question, workspace_id=workspace_id) context = "\n\n".join( f"[{r['source']['filename']}, p.{r['source']['page_start']}]\n{r['content']}" for r in results if r["content"] ) completion = ovh.chat.completions.create( model=model, messages=[ { "role": "system", "content": ( "You are a helpful assistant. Answer the user's question using only " "the provided context. If the context does not contain enough information, " "say so.\n\nContext:\n" + context ), }, {"role": "user", "content": question}, ], ) return completion.choices[0].message.content print(answer("What is our data retention policy?")) ``` ## LightOn search as a tool Instead of always searching before calling the model, you can expose LightOn search as a tool and let the model decide when to call it. The model issues a `lighton_search` tool call when it needs context; your code executes the search and feeds the results back; the model then produces a final answer. ```python theme={null} import json import os import requests from openai import OpenAI LIGHTON_API_KEY = os.environ["LIGHTON_API_KEY"] OVH_AI_ENDPOINTS_ACCESS_TOKEN = os.environ["OVH_AI_ENDPOINTS_ACCESS_TOKEN"] ovh = OpenAI( base_url="https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", api_key=OVH_AI_ENDPOINTS_ACCESS_TOKEN, ) SEARCH_TOOL = { "type": "function", "function": { "name": "lighton_search", "description": ( "Search the company knowledge base for passages relevant to a query. " "Returns ranked excerpts with their source filename and page numbers." ), "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Natural-language search query.", }, "max_results": { "type": "integer", "description": "Number of passages to return (1–50, default 5).", "default": 5, }, }, "required": ["query"], }, }, } def run_search(query: str, max_results: int = 5) -> str: response = requests.post( "https://api.lighton.ai/api/v3/search", headers={"Authorization": f"Bearer {LIGHTON_API_KEY}"}, json={"query": query, "max_results": max_results}, ) response.raise_for_status() results = response.json()["results"] passages = [ f"[{r['source']['filename']}, p.{r['source']['page_start']}]\n{r['content']}" for r in results if r["content"] ] return "\n\n".join(passages) if passages else "No results found." def answer(question: str, model: str = "Meta-Llama-3_3-70B-Instruct") -> str: messages = [{"role": "user", "content": question}] while True: completion = ovh.chat.completions.create( model=model, tools=[SEARCH_TOOL], messages=messages, ) choice = completion.choices[0] if choice.finish_reason == "tool_calls": messages.append(choice.message) for call in choice.message.tool_calls: args = json.loads(call.function.arguments) result = run_search(**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, }) else: return choice.message.content print(answer("What is our data retention policy?")) ``` The loop handles the case where the model issues multiple search calls in sequence before producing a final answer. ## Scoping retrieval to a workspace Pass `workspace_id` to limit search to a specific workspace. This is useful in multi-tenant products where each customer's data lives in a dedicated workspace. ```python theme={null} answer("Summarize the onboarding checklist", workspace_id=[42]) ``` ## Choosing a model OVHcloud's catalog includes several hosted models. Pass the model name to the `model` parameter: | Model | Notes | | - | - | | `Meta-Llama-3_3-70B-Instruct` | Strong reasoning, good default choice | | `Llama-3.1-8B-Instruct` | Faster and cheaper, suitable for simpler queries | | `Mistral-Small-3.2-24B-Instruct-2506` | Compact Mistral, low latency | | `Qwen3-32B` | Strong multilingual reasoning model | | `Qwen2.5-VL-72B-Instruct` | Vision-language model, accepts image input | You can list the models available to your token at any time: ```python theme={null} print([m.id for m in ovh.models.list().data]) ``` Check the [OVHcloud AI Endpoints documentation](https://endpoints.ai.cloud.ovh.net/) for the current model list and regional availability. ## Streaming responses OVHcloud's endpoint supports streaming. Enable it by passing `stream=True` and iterating over the response: ```python theme={null} stream = ovh.chat.completions.create( model="Meta-Llama-3_3-70B-Instruct", messages=[...], stream=True, ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True) ``` ## A note on reasoning models Some OVHcloud models (for example `Qwen3-32B` and `Qwen3.6-27B`) are reasoning models. When called through the raw HTTP API they may return their chain of thought under a `reasoning` field and the final answer under `content`. The `openai` client used in the examples above surfaces the final answer in `choices[0].message.content` as usual, so no special handling is needed; read `message.reasoning` only if you want to inspect the thinking trace. # Scaleway Generative APIs Source: https://developers.lighton.ai/integrations/scaleway Use LightOn search as the retrieval layer for models hosted on Scaleway. Scaleway's Generative APIs expose hosted models (Llama, Mistral, and others) through an OpenAI-compatible endpoint. Pair them with LightOn search to build RAG pipelines where the retrieval stays on LightOn's infrastructure and the generation runs on Scaleway. The flow is: 1. Search LightOn for the passages most relevant to the user's question. 2. Pack those passages into the model's context window. 3. Call the Scaleway model to generate an answer grounded in the retrieved content. ## Prerequisites * A `LIGHTON_API_KEY`, available in the **Console → API Keys** section. * A Scaleway API key with access to Generative APIs, available in the Scaleway console under **IAM → API Keys**. * At least one workspace with indexed documents on LightOn. ## Installation ```bash theme={null} pip install requests openai ``` The `openai` package is used here only for its client; Scaleway's endpoint is fully compatible with it. ## Full example ```python theme={null} import os import requests from openai import OpenAI LIGHTON_API_KEY = os.environ["LIGHTON_API_KEY"] SCALEWAY_API_KEY = os.environ["SCALEWAY_API_KEY"] scaleway = OpenAI( base_url="https://api.scaleway.ai/v1", api_key=SCALEWAY_API_KEY, ) def search(query: str, workspace_id: list[int] | None = None, max_results: int = 5) -> list[dict]: payload = {"query": query, "max_results": max_results} if workspace_id: payload["workspace_id"] = workspace_id response = requests.post( "https://api.lighton.ai/api/v3/search", headers={"Authorization": f"Bearer {LIGHTON_API_KEY}"}, json=payload, ) response.raise_for_status() return response.json()["results"] def answer(question: str, workspace_id: list[int] | None = None, model: str = "llama-3.3-70b-instruct") -> str: results = search(question, workspace_id=workspace_id) context = "\n\n".join( f"[{r['source']['filename']}, p.{r['source']['page_start']}]\n{r['content']}" for r in results if r["content"] ) completion = scaleway.chat.completions.create( model=model, messages=[ { "role": "system", "content": ( "You are a helpful assistant. Answer the user's question using only " "the provided context. If the context does not contain enough information, " "say so.\n\nContext:\n" + context ), }, {"role": "user", "content": question}, ], ) return completion.choices[0].message.content print(answer("What is our data retention policy?")) ``` ## LightOn search as a tool Instead of always searching before calling the model, you can expose LightOn search as a tool and let the model decide when to call it. The model issues a `lighton_search` tool call when it needs context; your code executes the search and feeds the results back; the model then produces a final answer. ```python theme={null} import json import os import requests from openai import OpenAI LIGHTON_API_KEY = os.environ["LIGHTON_API_KEY"] SCALEWAY_API_KEY = os.environ["SCALEWAY_API_KEY"] scaleway = OpenAI( base_url="https://api.scaleway.ai/v1", api_key=SCALEWAY_API_KEY, ) SEARCH_TOOL = { "type": "function", "function": { "name": "lighton_search", "description": ( "Search the company knowledge base for passages relevant to a query. " "Returns ranked excerpts with their source filename and page numbers." ), "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Natural-language search query.", }, "max_results": { "type": "integer", "description": "Number of passages to return (1–50, default 5).", "default": 5, }, }, "required": ["query"], }, }, } def run_search(query: str, max_results: int = 5) -> str: response = requests.post( "https://api.lighton.ai/api/v3/search", headers={"Authorization": f"Bearer {LIGHTON_API_KEY}"}, json={"query": query, "max_results": max_results}, ) response.raise_for_status() results = response.json()["results"] passages = [ f"[{r['source']['filename']}, p.{r['source']['page_start']}]\n{r['content']}" for r in results if r["content"] ] return "\n\n".join(passages) if passages else "No results found." def answer(question: str, model: str = "llama-3.3-70b-instruct") -> str: messages = [{"role": "user", "content": question}] while True: completion = scaleway.chat.completions.create( model=model, tools=[SEARCH_TOOL], messages=messages, ) choice = completion.choices[0] if choice.finish_reason == "tool_calls": messages.append(choice.message) for call in choice.message.tool_calls: args = json.loads(call.function.arguments) result = run_search(**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, }) else: return choice.message.content print(answer("What is our data retention policy?")) ``` The loop handles the case where the model issues multiple search calls in sequence before producing a final answer. ## Scoping retrieval to a workspace Pass `workspace_id` to limit search to a specific workspace. This is useful in multi-tenant products where each customer's data lives in a dedicated workspace. ```python theme={null} answer("Summarize the onboarding checklist", workspace_id=[42]) ``` ## Choosing a model Scaleway's catalog includes several hosted models. Pass the model name to the `model` parameter: | Model | Notes | | - | - | | `llama-3.3-70b-instruct` | Strong reasoning, good default choice | | `llama-3.1-8b-instruct` | Faster and cheaper, suitable for simpler queries | | `mistral-nemo-instruct-2407` | Compact Mistral, low latency | | `mixtral-8x7b-instruct-v0.1` | MoE model, good for longer contexts | Check the [Scaleway documentation](https://www.scaleway.com/en/docs/ai-data/generative-apis/) for the current model list and regional availability. ## Streaming responses Scaleway's endpoint supports streaming. Enable it by passing `stream=True` and iterating over the response: ```python theme={null} stream = scaleway.chat.completions.create( model="llama-3.3-70b-instruct", messages=[...], stream=True, ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True) ``` # Cookies and Trackers Policy Source: https://developers.lighton.ai/legal/cookies-policy How LightOn uses cookies and trackers across its websites, Console, and Paradigm services, and how you can control them. **Cookies and Trackers Policy** Last update: July 17, 2026 # **​1​ What are cookies and trackers?** ## **​1.1​ Cookies** Technically, a cookie is a computer file, which can contain any information (random identifier, email address, session token, etc.). These files are usually small. They are stored and read by your browser or by an application which accesses the Internet (for example a mobile application on a smartphone) or by the operating system of your terminal (Windows, MacOS, Android, iOS, Linux etc.). A cookie belongs to a given domain (for example lighton.ai or paradigm.lighton.ai), has a name, a content and an expiration date. A cookie can only be read by a server in the domain to which it belongs. ### **​1.1.1​ First party cookies and third party cookies** A “first party” cookie (or simply a “normal” or “standard” cookie) is initially set by the website or the server currently browsed by the Internet user. A “third party” cookie is placed or read when the Internet user browses a website or interacts with a server and this website or this server has integrated technology from a different server (ie a "Third party") and this technology implements a tracker or a cookie owned by a domain different from that to which the server on which the Internet user browses belongs. There is no fundamental difference between a standard (1st party) cookie and a third party cookie, it just depends on the context that was associated with this cookie. For example, a cookie set by Google while the user is browsing google.com is a standard cookie for Google. But if the user browses a site different from those of Google, and this site includes functionalities provided by Google (advertising or analyzes for example), then there is a possibility that a cookie from Google may be set and / or read even if the user browses a site that is not a Google site. In this context, Google cookies are considered to be third-party cookies. Such a third party cookie may contain an identifier which allows Google to recognize the user (actually the browser), in particular if the user has a Google account and has logged into it. It is then possible for Google to link the identifier contained in the third-party cookie with various data, including data concerning the browsing history of the user on all the sites which use the same tracker technology. ## **​1.2​ Trackers** We call a tracker any technology which makes it possible to determine that we are dealing with the same user at various times. Cookies, in particular third-party cookies, are thus technologies that can be used to implement trackers: If a cookie contains a unique identifier (which can be random) and this cookie is read at several different times, the server which reads this cookie can deduce that it is the same identifier and, therefore, the same browser or the same terminal. The server which has set this cookie can then record the fact that the navigation on a given page was carried out from a browser which has already carried out navigation on another site and another page before. The browser is thus identifiable and the server which reads the cookie can associate a browsing history, recorded in a database, then analyzed. There are also other technologies allowing tracing, in particular tracing pixels or footprints of browsers. Tracking technologies are generally based on the assumption that a browser is associated with an individual. Thus, by tracing the use made of a browser it becomes possible to know the browsing history of an individual, to create standard profiles and to determine the likely centers of interest of an individual, in particular to determine content that interests him/her more than others or to recognize her/him as part of a "Cash-Back" program so as to allow discounts or refunds to be associated with this user when she/he passes order on partner sites of the cash-back program. ## **​1.3​ More details** The following resources can provide additional information and details about cookies and trackers\ [https://en.wikipedia.org/wiki/HTTP\_cookie](https://en.wikipedia.org/wiki/HTTP_cookie)\ [Magic cookie - Wikipedia](https://en.wikipedia.org/wiki/Magic_cookie)\ [https://ico.org.uk/your-data-matters/online/cookies/](https://ico.org.uk/your-data-matters/online/cookies/) # **​2​ Why do we use cookies and trackers?** We have set cookies and trackers for the needs of browsing our website and the solutions we offer to users, optimization, audience measurement, increasing our productivity, prospecting for new potential customers, our communication tools with you and the customization to your preferences of the services and offers that we or our partners provide. We use cookies to provide you with an optimal user experience tailored to your personal preferences. By setting cookies and trackers, for example, we can prevent you from re-entering the same information each time you visit one of our websites. Cookies can also be used to optimize the performance of our websites, mobile sites and mobile applications. In particular, they can make your browsing more user-friendly and help us meet your requests faster. The data processed following the implementation of these cookies and trackers are used by us, our partners and third parties in accordance with their purposes. ## **​2.1​ What types of cookies and trackers are used on our websites?** A summary table of cookies and trackers used within our websites, applications and solutions makes it possible to know the types of tracker we use and to which type of trackers each of these cookies and trackers belongs to. ### **​2.1.1​ Essential cookie and trackers** These cookies and trackers are necessary for the proper functioning of our websites and solutions, for example by keeping track of your connection to your account or by allowing the navigation context to be preserved when you change pages, if you leave our site temporarily, to allow you to communicate with us or to make it possible for our solution to use third parties’ services that rely on such essential cookies. The essential cookies are necessary for the use of our sites and solutions. Some audience and traffic analysis cookies may also be considered necessary in order to satisfy our legitimate interests to optimize our processes, increase our productivity and acquire new customers for our products and services. By definition, cookies and trackers classified as essential cannot be deactivated, except by deactivating these cookies or some of them globally, as we mention below. Disabling these cookies classified as “necessary” may have the effect of preventing the use of certain features of our sites and solutions or the display of certain pages. On the LightOn Console platform specifically, Sentry Error Reporting (automatic detection of technical errors and performance issues, without Session Replay) is treated as a technically necessary tracker within this category, as it is configured without persistent cross-session identifiers and without collection of default personal data. It is therefore enabled by default for Console SaaS users, excluding Enterprise plans, on the basis of LightOn's legitimate interest in maintaining the technical quality and security of the interface. Unlike other essential cookies and trackers, Console users and clients may nonetheless opt out of Sentry Error Reporting at any time via their account settings, without affecting their access to or the quality of the Console. The corresponding opt-out procedure is described in LightOn's online documentation. Sentry Session Replay, a separate and distinct feature allowing visual reconstruction of a user's navigation for debugging purposes, is not covered by this exemption and remains disabled by default, subject to the user's or client's prior explicit consent. ### **​2.1.2​ Functional and preferences cookies** Functional and preferences cookies and trackers are useful for an optimized and more ergonomic navigation of our websites and solutions, for example by retaining some of your preferences (language or currency preference in particular) as you defined them during a previous visit to our sites or to allow faster display of our pages. They can also be used to manage access to reserved and personal areas of our sites or our services, such as your personal account, for example by memorizing your identifiers. ### **​2.1.3​ Cookie and trackers for statistics, analysis and audience measurement** These cookies and trackers allow us to better understand the use made of our websites or solutions, what pages are most viewed, how do people find us, etc. These cookies and trackers are essential for us in particular so that we can provide solutions always better suited to the expectations of our customers and our visitors, in particular with regard to ergonomics matters. The cookies in this category used on our sites are third-party cookies that we have chosen carefully in order to achieve specific objectives. We use in particular the "Google Analytics" services offered by the American company Google, Inc, a subsidiary of the company Alphabet, Inc.\ The data collected via cookies and trackers used for the operation of Google Analytics are transmitted to and stored by Google Inc. The company Google Inc. is “Privacy Shield” certified and we have Data Protection Agreement (DP,) with it, in order to supervise data transfers to the USA and to ensure that all the necessary guarantees are provided so that these transfers are carried out in compliance with the principles and terms of the regulations concerning the data protection of European citizens. The Google Analytics service allows us to count the visitors of our web sites and applications and determine how they are used. The cookies and trackers implemented within the framework of the "Google Analytics" solution are set and / or read on the user's terminal as soon as the latter accesses a website using the "Google Analytics" service. The data generated or processed by the "Google Analytics" service concerns: your use of our sites and applications and your IP address in order to determine your connection region. This data is immediately anonymized after localization and is not communicated to us. You can get more details about the Google Analytics service by going to [http://www.google.com/analytics/learn/privacy.html](http://www.google.com/analytics/learn/privacy.html) In September 2020 and then in March 2024, Google, Inc. set up a Google Analytics functionality called “consent mode” aimed to dissociate the processings of pure analysis from those intended to supply behavioral / browsing data to Google’s advertising networks (doubleclick.net in particular). You can now decide whether the processing carried out by Google Analytics when you use our sites and applications will supply behavioral or navigation data to Google, Inc.'s advertising networks. This choice is made through our cookies and trackers consent manager (see section 4.1.1 below). ### **​2.1.4​ Marketing cookies and trackers, intended for personalization** Cookies and trackers intended for the personalization of content, essentially make it possible to identify recurring connections from the same browser, on our sites and on other websites, so as to be able to constitute an history navigation and deduce therefrom likely centers of interest of the user as well as other data, including, potentially, identifiers or contact details allowing to contact the user, in particular his/her email address.\ This makes it possible to offer, on the websites visited using the same trackers, content and advertisements adapted to the interests of the users. Note that deactivating targeting cookies and tracker does not necessarily deactivate advertisements: when you deactivate one or more trackers in this category, you will generally continue to see advertisements, but these will no longer be adapted according to the analysis of your navigation. These will then be “generic” advertisements, i.e. the same as when you use a new computer or a newly installed browser, with a newly created user account which is not linked to anything known to companies which implement these trackers. We do not advertise on our websites, but some of our partners may use the fact that you have browsed our websites to deduce probable interests therefrom for solutions similar or related to those we offer to our customers. ### **​2.1.5​ Social networks cookies and trackers** These cookies and trackers can be set on your terminal by social networks. They are intended for sharing the content of our websites on social networks. This can be the case when we add a "Like" or "Share" button which will allow us to report these actions on the social network to which they are linked. These cookies and trackers are beyond our control, as are the data processed by these social networks. For more information on these cookies and trackers, we invite you to consult the confidentiality policies specific to each of these social networking sites. ### **​2.1.6​ Pixels and Trackers in Our Electronic Communications** We do not use any pixels or trackers in our emails that require your prior consent. The only trackers that may be present in our electronic communications meet the criteria for consent exemption. More specifically, the trackers we use allow us to ensure that any recipient who has not interacted with our emails (i.e., has not opened or clicked on them) for up to 2 years (for prospects) or 3 years (for customers and users) is automatically removed from our mailing lists for future mailings: the contact is marked as inactive. Any interaction tracked in an existing email reactivates the contact as active, even if they had previously been deactivated. The trackers in our emails serve no other purpose. # **​3​ How long are cookies and traces stored?** The necessary cookies can last until you delete them, although most of them are deleted when you leave the session.\ All cookies and trackers, set directly by our sites or solutions or indirectly via the technologies we embed, have a validity period which is mentioned in the list of cookies and trackers below. # **​4​ How to control cookies and trackers?** ## **​4.1​ Control of third-party cookies** ### **​4.1.1​ For one of our sites** When you first connect, you are asked to accept or configure your preferences regarding the use we make of cookies and trackers used on the site in question. If there are more than essential cookies, you can choose what types of cookies and trackers you accept and what you refuse.\ You can change these preferences at any time via the “Cookies” link or the widget at the bottom of all the pages of the site you are currently browsing. The widget looks like the following: The LightOn cookie consent widget shown at the bottom of every page. We draw your attention to the fact that, when you oppose the installation or the use of one or more cookies or tracker on one of our sites, one or more essential cookies are set which contain a trace of your choice. If you delete these cookies which are intended to record your choices regarding cookies and trackers, it will no longer be possible to identify you as having refused or accepted the use of certain cookies and trackers. If these choice cookies are deleted, you will have to express your choice again. These choice cookies are named : CookieConsent ### **​4.1.2​ Overall, for all websites** This information is given to you for educational purposes and can not engage LightOn in any way. Browsers, including Firefox, Chrome and Safari, generally suggest not to accept third-party cookies, in order to reduce their exposure to tracking. Refer to the cookie setting sections in the via this link:\ [https://ico.org.uk/your-data-matters/online/cookies/](https://ico.org.uk/your-data-matters/online/cookies/) To globally control the third-party cookies set by other sites or services for the purposes of advertising and targeted content, you can use the following online platform:\ [http://www.youronlinechoices.com/fr/controler-ses-cookies/](http://www.youronlinechoices.com/fr/controler-ses-cookies/) Note that if you use this platform to refuse behavioral analyzes from one or more particular companies, this will be applied to all websites that use the trackers of this company. For example, if you disable DoubleClick trackers on this platform, you will theoretically no longer be tracked by DoubleClick on any website that uses DoubleClick technologies. In other words, this platform does not allow you to deactivate DoubleClick analyzes for our site only while keeping them active for other sites. This platform has a global action on all websites. It also allows, in one click, to deactivate the trackers for all affiliated companies. These deactivations are done by depositing cookies which record the fact that you do not want to be traced by a given company. So if you delete the corresponding cookies, your unsubscription from behavioral analysis programs will be lost and you will have to redo the process if you want to unsubscribe again. Finally, note that this platform does not deactivate advertisements: when you deactivate the trackers of a company via this platform, you will continue to see advertisements offered through this company, but these will no longer be adapted according to analyzes of your behavior, these will then be “generic” advertisements, i.e. the same as when you use a new computer or a newly installed browser, with a newly created user account which is not linked to anything known to companies that are members of the platform. ### **​4.1.3​ Site by site, and optionally and globally** To deactivate most of the tracking cookies globally or only on certain sites, you can use browser plug-ins, in particular [Ghostery](https://www.ghostery.com/), [uBlock](https://www.ublock.org/), [disconnect](https://disconnect.me/) and [privacy badger](https://privacybadger.org/). ### **​4.1.4​ Do Not Track** Certain browsers implement a functionality allowing you to indicate that you do not want to be tracked. This choice is transmitted to the sites which can take it into account in order to adapt their behavior. You will have more information on this feature here: [https://en.wikipedia.org/wiki/Do\_Not\_Track](https://en.wikipedia.org/wiki/Do_Not_Track) Currently, there are no legal or consensus constraints for websites to recognize and comply with the Do Not Track option. Few sites therefore respect this functionality, including ours. ## **​4.2​ Manage Facebook cookies and trackers** We can use services offered by the Facebook company.\ To find out more, please refer to what is written here: [https://www.facebook.com/policies/cookies/](https://www.facebook.com/policies/cookies/) ## **​4.3​ Manage Google cookies and trackers** Refer to this page: [https://policies.google.com/technologies/types?hl=fr](https://policies.google.com/technologies/types?hl=fr)\ For google analytics, please refer to what is written here: [https://support.google.com/analytics/answer/181881?hl=en](https://support.google.com/analytics/answer/181881?hl=fr) ## **​4.4​ Manage Linkedin cookies and trackers** We can use services offered by Linkedin.\ To find out more, please refer to what is written here: [https://www.linkedin.com/legal/privacy-policy](https://www.linkedin.com/legal/privacy-policy) and here: [https://www.linkedin.com/legal/cookie-policy](https://www.linkedin.com/legal/cookie-policy) # **​5​ Advertising** We do not broadcast advertising on our sites!\ For more information, refer to the above. # **​6​ List of cookies and trackers used on or via our website** Details of this site's use of cookies are available by clicking on the widget located at the left bottom of every page on this site. This widget looks like The LightOn cookie consent widget shown at the bottom of every page. The additional following cookies and trackers may be used on your terminal | Name | Conservation | Description | | :-: | :-: | :-: | | Sentry Error Reporting (Console) | 90 days | Technical error and performance monitoring on the Console web interface; enabled by default for SaaS users (excluding Enterprise plans) on the basis of LightOn's legitimate interest; opt-out available via account settings or parameters (details in the online documentation). | # **​7​ Changes to this cookie policy** Although most changes are likely to be minor, we may have to change its cookie use policy.\ We encourage visitors of our sites to check this page frequently for any changes to our cookie policy. The lists of cookies presented above is not exhaustive and may change, in particular because the publishers in charge of third-party cookies have full control of them and are not obliged to inform us of related changes. Nevertheless, the information given in this document should allow the user to easily control which cookies and trackers she/he accepts and refuses.\ In the event of a significant change in the use of cookies and trackers by or via our sites, we will display the information and consent banner for the use of cookies and trackers even if you have previously expressed your choices in that matter. # GDPR Compliance Source: https://developers.lighton.ai/legal/gdpr-compliance Understand how Paradigm complies with GDPR regulations and your rights regarding personal data processing. ## Overview The General Data Protection Regulation (GDPR) is a European Union regulation that governs how organizations collect, process, and protect personal data of EU residents. Paradigm is committed to full GDPR compliance and transparency in our data handling practices. ## Your Rights Under GDPR As a data subject under GDPR, you have the following rights regarding your personal data: ### Right of Access * You can request information about what personal data we process about you * You can obtain a copy of your personal data in a structured format ### Right to Rectification * You can request correction of inaccurate personal data * You can request completion of incomplete personal data ### Right to Erasure (Right to be Forgotten) * You can request deletion of your personal data under certain circumstances * This applies when data is no longer necessary for the original purpose ### Right to Restrict Processing * You can request limitation of processing under specific conditions * Data can be stored but not actively processed during restriction periods ### Right to Data Portability * You can receive your personal data in a machine-readable format * You can request direct transfer to another data controller where technically feasible ### Right to Object * You can object to processing based on legitimate interests * You can object to direct marketing at any time ## Data Processing Lawful Basis We process your personal data based on: * **Contractual necessity**: To fulfill our service obligations * **Legitimate interests**: For system security and service improvement * **Legal compliance**: To meet regulatory requirements * **Consent**: Where explicitly provided for specific processing activities ## Data Protection Measures ### Technical Safeguards * End-to-end encryption for data in transit * Encryption at rest for stored personal data * Access controls and authentication systems * Regular security assessments and updates ### Organizational Measures * Privacy by design principles in system development * Regular staff training on data protection * Data processing impact assessments * Incident response procedures ## Data Retention * Personal data is retained only as long as necessary for the stated purposes * Retention periods are clearly defined and regularly reviewed * Automated deletion processes ensure timely data removal * Special categories of data have enhanced protection measures ## International Data Transfers When transferring personal data outside the EU: * We ensure adequate protection through appropriate safeguards * Standard contractual clauses or adequacy decisions are used * Recipients are contractually bound to maintain data protection standards ## Exercising Your Rights To exercise your GDPR rights: 1. **Contact our Data Protection Officer**: * Email: [privacy@lighton.ai](mailto:privacy@lighton.ai) * Include clear identification and specific request details 2. **Response Timeline**: * We respond within 30 days of receiving your request * Complex requests may require up to 60 additional days with notification 3. **Verification Process**: * Identity verification may be required for security * Additional information may be requested to locate your data ## Complaints and Remedies If you believe your data protection rights have been violated: * **Internal Resolution**: Contact our Data Protection Officer first * **Supervisory Authority**: File a complaint with your local data protection authority * **Judicial Remedy**: Pursue legal remedies through competent courts ## Data Protection Officer Our appointed Data Protection Officer oversees GDPR compliance and serves as your point of contact for data protection matters. **Contact Information**: * Email: [privacy@lighton.ai](mailto:privacy@lighton.ai) * Role: Independent oversight of data processing activities * Responsibilities: Monitoring compliance, conducting impact assessments, serving as point of contact ## Regular Updates This GDPR compliance documentation is regularly reviewed and updated to reflect: * Changes in data processing activities * Updates to legal requirements * Improvements in data protection measures * Feedback from data subjects and authorities # Privacy Policy Source: https://developers.lighton.ai/legal/privacy-policy Privacy policy governing data processing and protection for LightOn's Paradigm AI platform and services. **Privacy Policy** April 21, 2026 \ LightOn's services and websites are intended for professional use only and are not intended for use by minors. If you are not a professional user or if you are a minor, you must not use this site or any of LightOn's services.\ LightOn is a company specializing in the creation of so-called "artificial intelligence" software programs, and more specifically in the creation of AI systems based on "LLMs" (large language models). It is the publisher of the website [www.lighton.ai](http://www.lighton.ai) and the services provided by this website, as well as the Paradigm and Console software.\ LightOn's headquarters are located 2 rue de la Bourse, 75002 Paris, France. Our company is registered in the SIREN directory under number 821100690.\ The privacy and personal data of our service users are of paramount importance to us. At LightOn, we have fundamental principles regarding confidentiality and the protection of personal data: * We ask for or process your personal information only if it is essential or if you have given us your consent to do so. * We do not share your personal data with anyone, except to comply with the law, assist you, or protect our rights, or if you have given us your explicit consent to do so. Below you will find our privacy policy, which outlines these principles. LightOn develops software designed to implement artificial intelligence services and may operate or use certain digital tools accessible via the Internet, including email tools and this website (hereinafter, the Sites, or individually, a Site). LightOn's policy respects the confidentiality of any information we may collect in connection with the operation of the digital communication services we use or the services, particularly IT services, we offer to our clients and users (hereinafter "the Services"), the Sites, our commercial management, our internal management (human resources, accounting, etc.) and the processing of requests made to our company. LightOn is committed, within the scope of its activities and in accordance with the laws in force in France and Europe, to ensuring the protection, confidentiality, and security of the personal data of users of its services, as well as to respecting their privacy. To make this policy easier to read, the text has been written on two levels: a legally binding level ("legal text") and "In a nutshell" frames, with text in italics, that are non-binding. Our goal in doing this is to help you better understand what we mean, how your data is processed, what you can expect from us, and what we expect from you. **In a nutshell**: We have written these "in a nutshell" sections to help you better understand our practices. However, the text that is legally binding, if necessary, is the text written outside the "in a nutshell" sections. We know you care about how your personal data is used and shared, and we appreciate the trust you place in us by providing it. Please be assured that we will treat it with the utmost care. By using our Services, visiting our websites, contacting us, filling out a form, responding to our requests and surveys, and, more generally, interacting with us, you agree to the practices described in this privacy policy. **In a nutshell**: By using LightOn's services, continuing to browse the site, providing information to contact us, etc., you acknowledge that you have read and accepted this policy. # **​1​ Data Controller** LightOn is the data controller for the processing of [personal data](https://www.cnil.fr/en/personal-data-definition) it carries out on its own behalf. In this context, LightOn has, where applicable, completed all necessary administrative formalities with the competent authorities. In general, LightOn complies with all laws and regulations relating to the protection of personal data in force in the countries where it operates, including but not limited to: * European Regulation 2016/679 of April 27, 2016, on the protection of natural persons with regard to the processing of personal data and on the free movement of such data, and repealing Directive 95/46/EC (General Data Protection Regulation, often referred to as the GDPR); * French Law No. 78-17 of January 6, 1978, on Information Technology, Data Files, and Civil Liberties, and its amendments. **In a nutshell**: We are responsible for the data you entrust to us for the processing we carry out on our own behalf. We provide you with our contact information so that you can reach us. For our part, we have done everything required of us by the regulations. # **​2​ Data Processor** Similarly, as a data processor for our clients who use the web applications and software provided by LightOn, we have completed all necessary administrative formalities with the relevant authorities, including European Regulation 2016/679 of April 27, 2016, on the protection of natural persons with regard to the processing of personal data and on the free movement of such data, and repealing Directive 95/46/EC (General Data Protection Regulation, often referred to as the GDPR). It should be noted that data processed through the use of LightOn Services is encrypted. A Service subscriber may explicitly grant access to certain data that they have provided and stored via the Services or that is processed within the Services, in particular to employees, service providers, or partners, various stakeholders, as well as to LightOn teams, specifically for technical support purposes. In any event, it is the LightOn customer who decides to store personal data, which belongs to them, through the use of one of the Services, to grant access to their data, and who has full control over it, including with respect to the documents that are uploaded, which only the persons to whom they have granted access may view. Each user of the Services provided by LightOn (hereinafter a "User" and in the plural "Users"), whether a casual visitor, a free user, a customer, or a guest, including one invited by a customer, is responsible for the data they provide through the use of the Services and, as such, is responsible, where applicable, for providing data in compliance with regulations. **In a nutshell**: Users of the solutions offered by LightOn are responsible for the data they have provided and that is processed through the use of LightOn's Services, including when these Services are implemented on behalf of third parties, in particular on behalf of LightOn's customers. ## **​2.1​ Specific Provisions: LightOn Console and Paradigm APIs** When the Customer submits personal data to the Paradigm APIs via Paradigm, the Console, or directly, LightOn acts as a processor within the meaning of Article 28 of the GDPR and processes such data solely in accordance with the Customer's instructions and the provisions of the Data Protection Agreement (DPA). LightOn specifically guarantees that: * **Stateless inference**: in SaaS inference mode or in any mode where the inference nodes are under LightOn's control, input data (prompts, documents, etc.) is not stored persistently on the compute nodes beyond the time strictly necessary to execute the request. Persistent storage (indexed document databases, conversation history) is enabled only when the Customer explicitly activates these features. * **No use for training purposes**: LightOn does not use the input and output data (prompts and completions) submitted or provided via the APIs to train, fine-tune, or improve its AI models, except upon the Customer's explicit written request. **In a nutshell**: When you use LightOn's systems, your data does not belong to us. You retain control over it. We use it only to provide you with the requested service, and we do not use it to improve our models unless you have explicitly asked us to. # **​3​ Purposes** ## **​3.1​ As Data Controller** LightOn processes personal data on its own behalf for one or more of the following purposes, including but not limited to: * To enable you to use our online services * To improve our services * Handling requests for information, particularly regarding the services offered (pricing, quotes, terms, etc.) * Managing relationships with our customers, prospects, website visitors, and more generally with our contacts * Sales prospecting * Providing support to customers and prospects * Managing disputes * Recruitment management * Providing relevant information (general, technical, or other) * Management of contribution programs aimed at developing new products or services or improving existing ones * Managing relationships with our partners and additional services offered to our customers * Optimizing our profitability and the commercial success of the products and offers we provide * Monitoring the technical quality of web interfaces: error detection, performance tracking, and, with prior consent, session replay for debugging purposes (see [Section 12.1](#121-technical-monitoring--sentry-console-and-paradigm-web-interfaces) below) Thus, LightOn processes your personal data within the framework of the contractual or pre-contractual relationship between you and LightOn, or within the framework of LightOn's legitimate interests in processing your personal data (see below), or when you have given us your consent to process your personal data. A contractual or pre-contractual relationship, governed by the terms of the contractual documents signed or accepted by you and by us, by our general terms and conditions (of sale, use, or service), and any specific terms and conditions attached thereto, exists when you have ordered a product from us, submitted a request to our services, used one of our services, particularly online, or have requested to be put in touch with a professional from our company or our network; when you have expressed an explicit interest in a LightOn offer; when you have used one of our websites; when you have requested information from us regarding an offer, a service, or a product; or when you have applied for a job opening. The legitimate interests that LightOn has in processing your data on its own behalf include the following: * Optimizing our profitability and the commercial success of the products and offers we provide * Ensuring our commercial success * Better understanding: * The usage, queries, and interests related to our offers, products, and services * The users of the products and services offered by LightOn, in particular through studies, analyses, and statistics, as well as through our contribution programs * Protecting ourselves against disputes, litigation, and fraud * Identifying the individuals best suited to effectively fill vacant positions within our facilities **In a nutshell**: When you provide us with your data for a specific purpose, we process that data to achieve that purpose. We may also process certain data to conduct analyses and studies to improve our services, to attract or retain our customers and users, and to manage disputes. For all of these purposes, we need personal data. As for other data concerning you, we process it only if you have consented to it. ## **​3.2​ As a Data Processor** When you use a LightOn application or Service on behalf of a LightOn client, the relationship between you and LightOn is governed by the commercial and contractual relationship with said Client, as well as by any applicable General and Special Terms and Conditions. The Services provided by LightOn enable LightOn's clients to perform processing operations that generally pursue at least one of the following objectives, without being limited to them: * Providing a chatbot system that uses the client's documents and resources to generate responses. * Providing the logic to enable users to register, configure, and use the application. LightOn may also use its expertise to conduct studies and analyses of the use of the Services, in particular for the purposes of establishing indicators, identifying trends, performing predictive processing, proposing new services, as well as improving its own productivity and that of Users. **In a nutshell**: When you use one of LightOn's services, both you and we have certain obligations and responsibilities, which, where applicable, are detailed in the Terms and Conditions governing the services provided by LightOn. The Services are intended to provide certain services to LightOn's clients, who retain control over the data they provide or that is provided by users of the applications that LightOn's clients have authorized to access the applications. # **​4​ Data Protection Service** LightOn has designated a dedicated Data Protection Office. You may contact the person or department responsible for this function at the email address [rgpd\_rssi@lighton.ai](mailto:rgpd_rssi@lighton.ai) or at LightOn's mailing address, addressing your correspondence to the attention of the Data Protection Office. # **​5​ Retention Period for Personal Data** Personal data collected by LightOn on its own behalf is retained only for as long as necessary to fulfill the purpose for which it was collected or to comply with legal and regulatory requirements. More specifically, the retention periods for the following data are: * **Data collected when purchasing a product or service**: for the duration of the contractual relationship and 3 years after the end of the contract (interim archiving in accordance with legal or regulatory obligations and liabilities, up to 10 years after the end of the business relationship, particularly with regard to billing data). * **Data collected when requesting information, making contact, etc.**: up to 3 years after the last positive interaction between LightOn and the data subject. * **Connection data collected in connection with the use of our online applications and services** (IP address, browsing history, etc.): up to 6 months. * **Usage metrics (billing and capacity)**: API call volumes, processed tokens, storage space, and aggregated timestamps, excluding the content of requests, prompts, or completions: retained for the duration of the contract and up to 13 months after its expiration, for billing, accounting reconciliation, and capacity planning purposes. * **Access logs and technical logs (security and reliability)**: timestamp, API key ID, endpoint called, source IP address, HTTP status code, latency, request headers, and error traces, excluding request content, prompts, or completions: retained for up to 90 days for the purposes of abuse detection, fraud prevention, quota enforcement, security incident investigation, and audit trail maintenance in accordance with our legal, regulatory, and contractual obligations. * **Sentry error reports (Error Reporting) and sessions recorded upon error detection (Session Replay)**: 90 days, with automatic rotation on Sentry servers. This data is collected only if the corresponding options are enabled. * **Data processed during recruitment management** (including unsolicited applications): 2 years after the last contact with the candidate, unless there is an express request to shorten this period. **In a nutshell**: We do not retain your data longer than necessary or as required by law. For example, the law requires that we retain invoices for your purchases, quotes, purchase orders, etc., for 10 years. For technical tools such as Sentry, data is automatically deleted after 90 days. The retention periods for data collected by LightOn on behalf of its clients are determined by the client for whom the data was collected. # **​6​ Scope of Data Collected** In order to use LightOn's Sites and Services, we may collect and process the following personal data: * Title, last name, and first name of individuals who have purchased a product from LightOn and those who have contacted us, job applicants, visitors to our websites, participants in one of our games or contests, etc. * Where applicable, the employer of individuals who have purchased a product from LightOn when that employer is a LightOn customer. * Title, last name, first name, and email address of users of the Services. * Email address of individuals who have contacted us via a digital channel and individuals who subscribe to our newsletters. * Email address of job applicants. * Last name, first name, phone number, and/or mailing address of individuals who have contacted us and/or placed an order. * Connection data (IP address, browsing history, technical details of devices, browser language and characteristics, etc.). * Data contained in the resumes and cover letters of job applicants. * Data provided by individuals during data processing carried out through the Services. * **LightOn account data**: professional email address, name, organization, billing information (credit card data is transmitted directly to the payment provider and is not stored by LightOn). * **Usage metrics**: API call volumes, processed tokens, storage space, and aggregated timestamps, excluding the content of requests, prompts, or completions. * **Access logs and technical logs**: timestamp, API key ID, endpoint called, source IP address, HTTP status code, latency, request headers, and error traces, excluding the content of requests, prompts, or completions. Connection data is collected and processed, including the IP address used for the connection: this is necessary to provide you with support, to implement our security measures, and to comply with our legal obligations, particularly regarding the traceability of access and usage. **In a nutshell**: We need your contact information and email address to manage your purchases and requests, to inform you as a buyer and/or user and as a visitor to our websites as well as a prospective customer, to contact you and/or respond to your requests, to process your applications for our job openings, your phone number to contact you, and connection data to assist you and fulfill our obligations, particularly regarding traceability. ## **​6.1​ Location of Processing** The processing that LightOn may perform on personal data is primarily carried out in data centers hosted by its service providers, in France or in Europe. Certain processing operations may be performed by LightOn's subcontractors, who may be located in various places. LightOn strives to work with service providers, suppliers, and subcontractors that process data within the European Union or in a country listed as having adequate safeguards (for example, by adopting programs such as the Data Privacy Framework, or by implementing Binding Corporate Rules (BCRs), standard contractual clauses, data processing agreements, etc.). If this were not the case, the contractual relationship between LightOn and these third parties would require the implementation of appropriate safeguards. LightOn works with third parties that provide all necessary safeguards, and in particular with those with whom LightOn has signed the [European Commission's standard contractual clauses](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32021D0914). In all cases, and particularly in the event of regulatory changes, LightOn takes all necessary measures to regulate cross-border data transfers and establishes, if necessary, data protection agreements (DPAs) to ensure data transfers comply with applicable law, particularly pursuant to Article 46 of the GDPR. Furthermore, LightOn may use the services of Sentry Inc. to monitor the Console and Paradigm web interfaces. Data transmitted to Sentry, if telemetry and/or error monitoring are enabled, is subject to the Standard Contractual Clauses (SCCs) adopted by the European Commission, as well as a Data Processing Agreement (DPA) available at [https://sentry.io/legal/dpa/](https://sentry.io/legal/dpa/). LightOn has also implemented Sentry Relay within its systems to filter personal data before it is transmitted to Sentry. **In a nutshell**: The data you have entrusted to us is processed in France or in a country whose laws require compliance with the basic principles of fairness, transparency, and confidentiality, or by a recipient who provides all necessary and legal guarantees to ensure that both you and we can be confident. ## **​6.2​ Recipients of Processed Data** LightOn may transmit the personal data you have provided or that it has collected to its employees, service providers, partners, or affiliated organizations that: 1. Need to know this information in order to process it on behalf of LightOn or to provide services for the purposes mentioned above, and 2. Have agreed not to disclose it to third parties. These potential recipients include, in particular, the companies that host LightOn's websites (see legal notices) and the companies that provide technical services enabling the implementation of the Services. LightOn may use the services of providers that offer messaging, accounting, opportunity management, email delivery, marketing, remote payment, monitoring, and web interface debugging services (see [Section 12.1](#121-technical-monitoring--sentry-console-and-paradigm-web-interfaces)), as well as the services of companies specializing in research, analysis, and satisfaction surveys, which may need to use your personal data to carry out the tasks entrusted to them. The complete list of these data recipients can be provided upon request. The list of processors is referenced in the DPA, available upon request at [compliance@lighton.ai](mailto:compliance@lighton.ai). Regarding the transfer of telemetry and error monitoring data, the user or customer can find detailed information on the LightOn documentation site. LightOn will not rent or sell personal data to third parties without the explicit consent of the individuals concerned or following an explicit action on the part of the individual concerned. Apart from its employees, service providers, partners, and affiliated organizations, as indicated above, LightOn discloses personal data only in the event of a subpoena, court order, or other government order, or when LightOn believes that such disclosure is necessary to protect the property or rights of LightOn, a third party, or the general public. If you are a registered user on a LightOn website or have access to the Services, have provided your email address, and have not opted out of receiving communications from LightOn, LightOn may occasionally send you an email to inform you of new features, solicit your feedback, or keep you updated on LightOn news and offers. We may also use blogs to communicate this type of information. LightOn takes all reasonable and necessary measures to protect your personal data against unauthorized access, use, modification, or improper destruction of personal (or potentially personal) data. **In a nutshell**: The data you have entrusted to us is only shared if necessary or if you have given your consent. The recipients of your data, including us, provide all necessary safeguards to ensure your peace of mind. ## **​6.3​ Transfer of Processed Data to Third Parties** The data you provide or that is collected by LightOn or on its behalf is not transferred to third parties, except, potentially, to the recipients mentioned above. **In a nutshell**: We do not share your data, except in the cases we have already mentioned. ## **​6.4​ Transfer of Data to a Third Country Outside the European Union** Some of the data you provide or that is collected by LightOn or on its behalf may be transferred to third parties operating outside the European Union. These third parties all provide sufficient safeguards to ensure the security, confidentiality, and protection of personal data. **In a nutshell**: We do not transfer your data, except in the cases we have already mentioned. When we do transfer it, it is to recipients operating in a country whose laws require compliance with the basic principles of fairness, transparency, and confidentiality, or to service providers who provide all necessary guarantees to ensure your and our peace of mind. ## **​6.5​ Sales prospecting** If you have already used our Services or ordered a product or service from us, and if you have not opted out of receiving our communications via SMS or email, LightOn may send you emails or text messages to alert you to offers for goods or products similar to those you have already ordered or purchased from us, to let you take advantage of our offers, to announce new features, to ask for your opinion, or simply to keep you informed of LightOn's latest news and offers. You always have the option to opt out of any such communications by following the unsubscribe links in our email communications, using a "STOP SMS" function if we have sent you a text message, clicking on an unsubscribe link in an email you have received, via your customer account on one of our websites, etc. Every digital communication from LightOn for commercial or informational purposes (email, SMS, etc.) contains a link or instructions allowing you to opt out of future similar communications. **In a nutshell**: If you have not opted out of receiving our digital communications related to your professional relationship with us, or if you are one of our customers, we may send you informational or promotional emails and text messages. You may opt out of receiving such emails and text messages at any time. ### **​6.5.1​ Newsletters and Digital Messages** You may receive communications from LightOn, such as emails, newsletters, text messages, etc., because your contact information is included in our subscriber list. Your contact information is included in this list either due to the context of your professional activity (B2B communications related to your professional activities) or as a registered LightOn user, or because your contact information was submitted to this list either directly by you, via a visible form on the LightOn website, or based on public information such as your LinkedIn profile and various other publicly available data. If you wish to update your email preferences or unsubscribe from one or more of our mailing lists, please click on the link provided at the end of any of the emails we send for business development purposes or use our email notifications and preferences form. # **​7​ Procedures for Exercising Rights of Access, Rectification, and Objection** As a customer or user of our Sites and Services, you have the right: * To request that the data processed about you be deleted from our files. This is known as the right to be forgotten. That said, we cannot delete your data as long as there is a business relationship between us (such as an order that has not been fully processed), a warranty related to one of your purchases, or as long as there is a dispute between you and us, or, more generally, as long as we need your data to perform a contract or fulfill one of our obligations. * To object to the collection and processing of data concerning you when it is not strictly necessary. If you are a customer or user of LightOn Services: * We need your email address so that we can send you information, confirmation emails, password recovery or activation emails, and other communications necessary to access the app, etc. * Your profile data is required to create and manage your profile and to provide the Services, including by identifying your interests and providing you with suggestions that best match your use of the Services. * To access the data collected about you and retrieve it in a standard electronic format. * To have it corrected. * To request a restriction on their processing, for example, by refusing any use for sales prospecting purposes, or by requesting that a retention period be extended (e.g., for a recorded conversation) to manage a potential dispute or litigation. * To withdraw your consent at any time to the use of optional trackers or features such as Sentry Session Replay, without this withdrawal affecting the lawfulness of processing carried out prior to the withdrawal or your access to the Services. This withdrawal can be made through your account settings. Furthermore, as a candidate who has applied for a job at our company, you have the right: * To request that the data you provided during your application, including your resume and cover letter, be deleted from our records. Until a candidate makes such a request, and for up to two years after the last contact with the candidate, it is in our legitimate interest to retain the candidate's data in order to ensure the best match between available positions and the individuals qualified to fill them. These rights may be exercised by sending an email to [rgpd\_rssi@lighton.ai](mailto:rgpd_rssi@lighton.ai) or by contacting us at the contact information listed above or available on our website. You also have the right to file a complaint with a supervisory authority and to seek judicial remedy, particularly if your requests to exercise your rights have not been processed within one month of being submitted. In France, the supervisory authority is the CNIL, which can be contacted via the appropriate forms on its website [cnil.fr](http://cnil.fr). **In a nutshell**: You have control over the data you have entrusted to us. You can request access to it or to retrieve it, request that it be deleted, or that it be used only for certain purposes and not for others, for example, that it not be used to send you information or commercial offers. To the extent possible, we will strive to fulfill your requests as quickly as possible. # **​8​ Disposition of Data** You may provide instructions regarding the retention, deletion, and disclosure of your personal data after your death in accordance with Articles 84 and 85 of French Law No. 78-17 of January 6, 1978, as amended. These instructions may be general or specific. You may submit your advance directives by contacting us via email at [rgpd\_rssi@lighton.ai](mailto:rgpd_rssi@lighton.ai). **In a nutshell**: You have control over the data you have entrusted to us, and you can provide us with instructions to be followed after your death, should we still hold data about you at that time, which we hope will be as late as possible. For example, you may ask us to delete your data or to transfer it to a specific person. # **​9​ Data Security** LightOn takes all reasonable and necessary measures to protect the data we process against unauthorized access, use, modification, or destruction of personal (or potentially personal) data. **In a nutshell**: The data you have entrusted to us is handled with care, and we take numerous measures to protect and safeguard it. For example, we may encrypt your data, grant access to this data only to those who need it, monitor the computers that store and process it, perform regular backups, and so on. We are committed to the security of our services and have implemented physical, administrative, and technical measures to prevent unauthorized access to your data. Our security policies cover security management for internal operations and our services. These policies govern all security areas applicable to the services and apply to all LightOn employees as well as our service providers and subcontractors who need access to this data. **In a nutshell**: We take the security of the data you entrust to us very seriously. We assess and respond quickly to incidents that raise suspicions of unauthorized data manipulation. Our teams are notified of these incidents and, depending on the nature of the activity, define processes and response procedures to address them. If we determine that your data has been misappropriated or otherwise improperly obtained by a third party and that this poses a significant risk to your rights or freedoms, we will inform you as soon as possible and notify the supervisory authority (the CNIL in France) within a maximum of 72 hours in accordance with Article 33 of the GDPR. **In a nutshell**: Our teams monitor and oversee all suspicious activity and are committed to minimizing such incidents as much as possible. # **​10​ Subcontracting** LightOn only uses processors who provide sufficient guarantees regarding the implementation of appropriate technical and organizational measures to ensure that processing meets the requirements of European regulations and safeguards the rights of the data subject. **In a nutshell**: When we need subcontractors, for example, to host your data or process your orders, we select only those who have provided us with sufficient guarantees to ensure that both you and we can feel confident. # **​11​ Transfer of Assets** If LightOn, or substantially all of its assets, is sold, or in the unlikely event of LightOn's cessation of business or bankruptcy, the information of users, prospects, and customers would be among the assets transferred to or acquired by a third party. You acknowledge that such transfers may occur and that any acquirer of LightOn may continue to use your personal data in accordance with this privacy policy. **In a nutshell**: If our company is sold or transferred, your data will be transferred to the new owner, as it is among our most valuable assets. But we guarantee that this data will then be handled by the new owner with at least the same level of care as we provide. # **​12​ Trackers and Cookies** Please refer to the "[Cookies and Trackers Policy](/legal/cookies-policy)" document for complete information on cookies and trackers. ## **​12.1​ Technical Monitoring: Sentry (Console and Paradigm web interfaces)** **Scope of this section**: The provisions below apply exclusively to the LightOn Console and Paradigm web interfaces. They do not apply to calls made via the Console or Paradigm APIs. LightOn uses the services of Sentry Inc. (132 Hawthorne Street, San Francisco, CA 94107, United States, [sentry.io](https://sentry.io)) to ensure the technical quality of the Console web interface. There are two levels of processing. ### **​12.1.1​ Sentry Error Reporting (error detection)** *Disabled by default on the interfaces.* Sentry Error Reporting automatically detects errors and performance issues on the web interface. * **Data collected**: error type and message, stack trace, browser and operating system, page URL, recent activity trail (breadcrumbs: navigation, HTTP request statuses, anonymized DOM interactions). * **What is not collected**: the content of prompts or API requests, API keys, billing data. `BeforeSend` filters are applied on the client side before any data is transmitted to Sentry. * **Legal basis**: LightOn's legitimate interest (Article 6(1)(f) GDPR) in maintaining the technical quality of its interface. Sentry Error Reporting is configured without setting persistent identifying cookies and is classified as a technically necessary tracker, requiring no prior consent. ### **​12.1.2​ Sentry Session Replay** *Disabled by default, subject to prior consent.* Sentry Session Replay allows for the visual reconstruction of a user's navigation on the Console for the sole purpose of technical debugging. This is not a video recording of the screen but a reconstruction of changes to the browser's DOM. * **Recorded data**: DOM changes (without field content), clicks, scrolling, page navigation, console logs, network request statuses. * **Protective measures**: LightOn applies `maskAllText: true` and `blockAllMedia: true`: all displayed or entered text and all images are masked (replaced by asterisks or opaque blocks) on the browser side before any transmission to Sentry. This measure applies without exception to Console fields, including API keys, billing information, and all user content. * **Legal basis and consent**: In accordance with the CNIL's draft recommendation on session replay tools (public consultation conducted in the first half of 2026), Session Replay is subject to the prior consent of the client and/or user under Article 82 of the French Data Protection Act. Consequently: * Sentry Session Replay is only enabled upon explicit consent obtained through the user's or client's account settings in the Console interface. * The user and/or client may withdraw their consent at any time via the account settings, without this withdrawal affecting the quality of or access to the service. * **Retention period**: 90 days on Sentry servers, with automatic deletion. Individual recordings may be deleted upon request sent to [rgpd\_rssi@lighton.ai](mailto:rgpd_rssi@lighton.ai). **In a nutshell**: To improve and debug our Console interface, we use two separate Sentry tools. The first (error detection) does not collect any information identified as personal. The second (session replay, which allows us to "replay" your browsing to find a bug) is only enabled if you give us your consent. In all cases, your entered text is masked before being sent to Sentry. # **​13​ Recording of Phone Calls and Online Chats** LightOn may record some of the calls made to its services. These recordings are used in our legitimate interest and, unless you object, for the following purposes: * Employee training and development; * Monitoring the quality of advice and customer service; * Gathering factual evidence for the prevention and resolution of disputes, litigation, and pre-litigation matters; * Protecting employees in the event of verbal abuse or incivility directed at them; * Conducting experiments related to management and quality monitoring objectives, as well as analyzing conversations using artificial intelligence techniques. They may also be used in connection with our legal obligations to: * Manage requests to exercise your rights; * Implement control measures, particularly regarding the fight against fraud and corruption. Telephone recordings are intended solely for authorized personnel and may be transmitted to and/or accessed by individuals involved in the setup and analysis of telephone conversations. They are retained for a maximum of 30 days, except in the event of a dispute, in which case they are retained for the duration of the dispute and until all avenues of appeal have been exhausted. When you communicate with our services via online chat (internal messaging chatbox), the entirety of your communications may be retained for a maximum of 2 months. The purposes of this data retention are: * Improving the chatbot service, particularly through machine learning technologies; * Enhancing employee skills; * Monitoring the quality of advice and customer service; * Gathering factual evidence for use in the prevention and resolution of disputes, litigation, and pre-litigation matters; * Protecting employees in the event of verbal abuse or incivility directed at them; * Conducting experiments related to management and quality monitoring objectives, as well as analyzing conversations using artificial intelligence techniques. This data is intended solely for authorized personnel and may be transmitted to and/or accessed by individuals involved in the setup and analysis of conversations conducted via instant messaging (chat). We recommend that you never enter confidential information through this process. All data you provide to us may be retained in an anonymized form to the greatest extent possible. Since anonymization techniques are generally at least partially automated, it is recommended that you do not provide personal data (and certainly not sensitive data) via online chat tools. In particular, it is recommended that you never provide payment information such as credit card numbers. # **​14​ Advertising** We do not display advertising on our websites. # **​15​ Changes to Our Privacy Policy** We may update, and, more importantly, improve, this privacy policy from time to time, while ensuring that we never reduce the level of protection already provided. Although most changes are likely to be minor, LightOn may modify its privacy policy at its sole discretion. LightOn encourages its customers, service users, job applicants, and website visitors to check this page frequently to stay informed of any changes to its privacy policy. Your use of a LightOn website, any of the Services, a mobile app provided by LightOn, a contact request, an order, or any other provision of personal data following any changes to the privacy policy constitutes your acceptance of those changes. **In a nutshell**: We may make changes to how we process your data, but with a level of protection that will be at least equivalent to that outlined in this policy. These changes are mostly minor. We don't routinely bother you with our new privacy policies by asking you to accept them again, but we'd appreciate it if you'd check the current policy from time to time. And remember that if you ever disagree with our policy, or if you have a question, or if you wish to exercise any of your rights, simply write to us at [rgpd\_rssi@lighton.ai](mailto:rgpd_rssi@lighton.ai). # **​16​ Intellectual Property** Our websites are published by LightOn. LightOn holds all intellectual property rights related thereto. It is prohibited to copy or download all or part of its content without the prior and express authorization of the owners or rights holders of said parts and said content. This website and all of its content (including documents, photos, logos, trademarks, and information of any kind appearing on it) are protected by various rights, including copyright. LightOn grants users of the Sites and Services permission to view the content, specifically excluding the reuse of all or part of the content of the Sites or Services for any reason whatsoever. The reuse of all or part of the Sites or Services, including this document, is strictly prohibited. This document was drafted based on materials provided by [SysStreaming](https://sysstreaming.com), which has granted LightOn the necessary rights to use them. Any unauthorized reuse of all or part of the content of this document would thus violate the rights of LightOn and SysStreaming, which reserve the right to seek compensation for any damages incurred. All reproduction rights are reserved, including for downloadable documents (PDFs as well as logos, photos, information of any kind, etc.). Any downloadable documents are also protected by copyright. We may ask for your email address to allow you to access this content, in particular to track access and any unauthorized reuse of our downloadable content, which is our legitimate interest. Any total or partial reproduction of the Sites, or any reuse of any content from the Sites or Services, by any organization whatsoever, without the express authorization of LightOn or the rights holders where applicable, is prohibited and would constitute an infringement punishable under Articles L.335-2 et seq. of the French Intellectual Property Code. The databases used by LightOn are protected by the provisions of the law of July 1, 1998, transposing into the Intellectual Property Code the European Directive of March 11, 1996, on the legal protection of databases. Similarly, any recording, broadcasting, translation, or adaptation, in whole or in part, of any element of the Sites or Services is prohibited without the express prior authorization of LightOn. **In a nutshell**: Everything found on our websites, our services, our applications, or in our documents is our property or that of third parties who have granted us a right of use and may not be reused without first obtaining our explicit consent. # **​17​ Hyperlinks** The Sites and Services may provide access to other web pages or websites, including those of partners or corresponding to search results that may have their own legal notices, which should be consulted and respected. The creation of hyperlinks pointing to the content of the Sites is permitted provided that they open in a new window. Under no circumstances may any content from the Sites be included within another site or imply or suggest that the content is not that of LightOn. LightOn reserves the right to request the removal of any link it deems non-compliant. LightOn cannot under any circumstances be held liable for information published on websites with which hyperlinks have been established, nor for any damages of any kind resulting in particular from access to such websites. LightOn is not liable for any hyperlinks on the Site that direct users to other websites. **In a nutshell**: Links to our site are permitted provided you notify us, do not attempt to misrepresent the content as your own, and we do not object. # Terms of Service Source: https://developers.lighton.ai/legal/terms-of-service These Terms of Service govern access to and use of the LightOn Console platform and its associated APIs. *Effective date: 28/05/2026* *Version 1.0* These Terms of Service ("Terms") govern access to and use of the LightOn Console platform and its associated APIs ("Console" or "Service"). By creating an account or using the Service, you accept these Terms in full. If you do not agree, you must not use the Service. ## **Article 1 – Definitions** For the purposes of these Terms, the following terms shall have the meanings set out below: **"Console"** or **"Service"**: The LightOn developer platform accessible at [https://console.lighton.ai](https://console.lighton.ai) (or any successor URL), including the web portal, REST APIs, playgrounds, usage dashboards, and any related tools or documentation made available by LightOn. **"LightOn"** or **"Provider"**: LightOn SA, a French company (*société anonyme*) registered in the Paris Trade and Companies Register (*Registre du Commerce et des Sociétés de Paris*) under number 821 100 690 00014, with its registered office at 2 Rue de la Bourse, 75002 Paris, France, represented by its President. **"Customer"** or **"you"**: Any natural person or legal entity that creates an account on the Console and uses the Service, whether under a Free Tier, a paid subscription, or an Enterprise arrangement. An account created by a natural person acting on behalf of a legal person is deemed to have been created by that legal person. **"User"**: Any natural person authorised by the Customer to access the Console under the Customer's account. **"API Key"**: A unique credential generated by the Console allowing the Customer or its applications to authenticate to and call the Console APIs. **"AI Features"**: The artificial intelligence capabilities made available through the Console APIs, including but not limited to: text generation and completion (LLM inference), embeddings, retrieval-augmented generation (RAG), semantic and hybrid search, document indexation, optical character recognition (OCR), vision and multimodal analysis or generation, and any additional features published in the Documentation. **"Input Data"**: Text, documents, images, or any other content submitted by the Customer to the Service for processing. **"Output Data"**: Text, embeddings, vectors, parsed documents, search results, or other content generated or returned by the Service in response to the Customer's requests. **"Customer Data"**: Input Data and Output Data, collectively, as well as any metadata, configuration data, and other information associated with the Customer's account and usage. **"Documentation"**: The technical documentation, API reference, integration guides, and any other materials published by LightOn at [https://docs.lighton.ai](https://docs.lighton.ai) or through the Console interface. **"Free Tier"**: A no-charge access level providing limited API call volumes and feature access as described in Article 6 – Free Tier and in the current Pricing Page. **"Paid Plan"**: A subscription or usage-based plan, as described on the Pricing Page and in Article 7 – Paid Plans and Billing. **"Enterprise Deployment"**: Any deployment mode other than the standard multi-tenant SaaS, including but not limited to On-Premises, Managed Mono-Tenant SaaS, or Hybrid deployments, governed by separate Special Terms appended to or associated with these Terms. **"Pricing Page"**: The page available at [https://lighton.ai/pricing](https://lighton.ai/pricing), as updated from time to time, setting out the rates, quotas, and available plans. **"DPA"**: LightOn's Data Processing Agreement, governing personal data processing where LightOn acts as a data processor, provided upon request at [compliance@lighton.ai](mailto:compliance@lighton.ai). **"AUP"**: LightOn's Acceptable Use Policy, set out in Article 10 – Acceptable Use Policy (AUP) of these Terms. **"EU AI Act"**: Regulation (EU) 2024/1689 of the European Parliament and of the Council on artificial intelligence. **"GDPR"**: Regulation (EU) 2016/679 of the European Parliament and of the Council on the protection of natural persons with regard to the processing of personal data. **"SLA"**: The Service Level Agreement specifying availability and support commitments, are available upon request at [compliance@lighton.ai](mailto:compliance@lighton.ai) or appended to the relevant plan or Enterprise arrangement. **"Effective Date"**: The date on which the Customer completes account registration or, in the case of Enterprise Deployments, the date of signature of the applicable Purchase Order or Enterprise Agreement. ## **Article 2 – Contractual Documents** ### **2.1 Order of precedence** The contractual relationship between LightOn and the Customer is governed, in descending order of priority, by: 1. Any signed Purchase Order or Enterprise Agreement (for Enterprise Deployments); 2. Any applicable Special Terms and Conditions (for Enterprise Deployments); 3. These Terms of Service and their appendices; 4. The applicable SLA; 5. The DPA (where personal data processing applies); 6. The Documentation. In the event of a conflict between documents of different ranks, the higher-ranking document prevails. ### **2.2 Entire agreement** Except as specified in section 2.4 below, these Terms, together with the documents listed above, constitute the entire agreement between LightOn and the Customer (hereinafter “the Parties”) and supersede all prior representations, negotiations, and agreements relating to the Service. Any general purchasing conditions or standard terms of the Customer are expressly excluded. ### **2.3 Amendments** LightOn reserves the right to amend these Terms in accordance with Article 20 – Modifications to the Terms. Amendments to Enterprise Agreements require a written addendum signed by both Parties. ### **2.4 Special conditions, prior and specific agreements** Where a Customer (or any of its affiliates) is party to a separate signed agreement with LightOn governing the use of any LightOn product (including Paradigm enterprise deployments), that agreement governs the relationship for that product. These Terms govern only the Customer’s use of the Console. In case of conflict between the two, the separate signed agreement prevails for the products it covers. ## **Article 3 – Eligibility and Account Registration** ### **3.1 Eligibility** The Service is intended exclusively for professional use by businesses, developers, and organisations ("Business Users"). By registering, you represent and warrant that: * You are at least 18 years of age; * You have the authority to bind the entity on whose behalf you are registering; * You are acting for professional or commercial purposes, not as a consumer; * Your use of the Service will comply with all applicable laws and regulations, including those of France and the European Union. The Service is not intended for consumers within the meaning of the French *Code de la consommation*. ### **3.2 Account creation** To access the Service, the Customer must create an account via the Console registration page by providing accurate and complete information, including, where applicable: * A valid professional email address; * First and family name of the person creating the account on behalf of the Customer; * Customer’s legal information: legal name and registered address; * Customer’s tax information: VAT identification number; * Acceptance of these Terms, the DPA, and the AUP. LightOn reserves the right to refuse registration at its sole discretion, including where the requested use would violate these Terms or applicable law. ### **3.3 Account security** The Customer is solely responsible for: * Maintaining the confidentiality of its account credentials; * All activities occurring under its account; * Promptly notifying LightOn at [security@lighton.ai](mailto:security@lighton.ai) of any suspected unauthorised access or breach. LightOn shall not be liable for any loss or damage arising from the Customer's failure to safeguard its credentials. ### **3.4 Organisational accounts** Where the Customer is a legal entity, the person completing registration is deemed to have authority to bind that entity. Multiple Users may be authorised under a single account, subject to the plan's seat limits. The Customer remains fully responsible for all Users' compliance with these Terms. ## **Article 4 – API Keys** ### **4.1 Generation and use** Upon account creation, the Customer may generate API Keys through the Console dashboard. Each API Key: * Is unique to the Customer's account; * Must not be shared with unauthorised third parties; * May be revoked by the Customer or by LightOn at any time. ### **4.2 Security obligations** The Customer must treat API Keys as confidential credentials and must: * Store them securely (e.g., using secret management services, environment variables) and not embed them in public source code repositories; * Rotate keys promptly upon any suspected compromise; * Implement appropriate access controls to limit key exposure to authorised systems and personnel. ### **4.3 Responsibility** Any API call made using the Customer's API Key is deemed to originate from the Customer. The Customer is responsible for all charges, data submitted, and Outputs generated through its API Keys, regardless of whether the call was authorised. ### **4.4 Revocation** LightOn may revoke an API Key immediately, without prior notice, if it reasonably suspects misuse, a security incident, or a violation of these Terms. ## **Article 5 – Description of the Service** ### **5.1 Console features** The Console provides access to the following core capabilities: * **Account management**: registration, profile management, team and user management, API key lifecycle management; * **Playground**: interactive testing environment for AI Features; * **AI API endpoints**: programmatic access to AI Features as described in the Documentation, including for instance generation, embeddings, RAG, search, indexation, OCR, and Vision; * **Usage dashboard**: real-time and historical metrics on API consumption, storage, and billing; * **Documentation and SDK access**: guides, API reference, and developer tools. ### **5.2 AI Features** The AI Features accessible through the Console include, without limitation: * **Text generation**: completion and chat-style LLM inference over available models; * **Embeddings**: dense vector representations of text and documents; * **RAG (Retrieval-Augmented Generation)**: combined retrieval and generation pipelines over indexed knowledge bases, documents, data, etc.; * **Document search and indexation**: ingestion, parsing, chunking, and semantic indexing of documents; * **OCR**: extraction of text and structured data from scanned documents and images; * **Vision**: multimodal analysis combining textual and visual inputs. The specific models, features, and capabilities available depend on the Customer's plan and are documented on the Pricing Page and/or in the Documentation. LightOn reserves the right to update, deprecate, or replace AI Features upon reasonable notice. ### **5.3 Deployment modes** The standard Console offering operates in **multi-tenant SaaS mode**, hosted by LightOn or its sub-processors on infrastructure located in the European Union. Enterprise Customers may additionally access the Service under the following deployment modes, governed by the applicable Special Terms: * **Dedicated mono-tenant SaaS**: a dedicated, logically or physically isolated instance hosted and managed by LightOn; * **Managed On-Premises**: the Console software is deployed on the Customer's own infrastructure and managed by LightOn; * **On-Premises (customer-operated)**: the Console software is deployed on and operated by the Customer on its own infrastructure; * **Hybrid On-Premises**: the back-end logic is operated by the Customer on its infrastructure, while LightOn provides stateless GPU nodes for inference computation on a shared basis. Specific terms, security obligations, SLAs, and pricing for Enterprise Deployments are set out in the applicable Special Terms and/or Purchase Order. ## **Article 6 – Free Tier** ### **6.1 Free Tier offering** LightOn offers a Free Tier allowing eligible Customers to access a limited version of Console at no charge, as described on the Pricing Page. Free Tier access can be subject to the following conditions: * Monthly call quotas and storage limits as defined on the Pricing Page; * Rate limits and throttling as described in the Documentation; * Availability on a best-efforts basis, without any SLA commitments; * Access to a limited set of models and features; The actual features and limitation applicable to Free Tier are available on the Pricing Page and/or in the Documentation. ### **6.2 No SLA on Free Tier** The Free Tier is provided "as is" and "as available." LightOn provides no uptime guarantees, no support commitments, and no data retention guarantees under the Free Tier, except as required by applicable law. ### **6.3 Transition to paid plans** If the Customer exceeds Free Tier limits, API calls may be throttled or rejected, specifically for the feature or subset for which the limit has been reached. To access higher volumes or additional features, the Customer can upgrade to a Paid Plan. LightOn may, at its discretion, notify the Customer when approaching Free Tier limits. ### **6.4 Free Tier modifications** LightOn reserves the right to modify, reduce, or discontinue the Free Tier at any time, with 30 days' prior notice by email or through the Console dashboard. ## **Article 7 – Paid Plans and Billing** ### **7.1 Plans** Paid access to the Service is available under the plans described on the Pricing Page, which may include: * **Usage-based plans**: the Customer is billed periodically based on actual consumption (API calls, tokens processed, documents indexed, storage used, etc.); * **Subscription plans**: flat-rate monthly or annual subscriptions providing agreed quotas; * **Enterprise plans**: custom pricing and terms, as set out in a Purchase Order. The actual types, features and limitations applicable to Paid Plans are available on the Pricing Page and/or in the Documentation. ### **7.2 Prices** Prices are set out on the Pricing Page. Unless otherwise stated, prices are: * Exclusive of all applicable taxes (including VAT); * Denominated in euros (€); * Subject to revision as set out in Article 7.7. ### **7.3 Payment methods and payment service provider** All payments under self-serve Plans are processed by Stripe, Inc. ("Payment Processor"), subject to Stripe's applicable terms of service. LightOn accepts payment by credit card. Additional payment methods may be made available at LightOn's discretion. Invoices are denominated in euros (EUR). For Customers paying from accounts held in another currency, Stripe applies its standard foreign exchange conversion at the time of the transaction; LightOn bears no liability for exchange rate fluctuations or conversion costs incurred by the Customer. For self-serve Plans, payment is processed automatically at the end of each billing cycle. The Customer is responsible for maintaining valid and up-to-date payment information in the Console dashboard. ### **7.4 Billing cycle and invoicing** Billing cycles and invoice issuance are as follows: * **Monthly usage-based plans**: billed in arrears at the end of each calendar month; * **Annual subscriptions**: billed in advance at the start of each annual term, unless otherwise agreed; * **Enterprise plans**: in accordance with the applicable Purchase Order. Invoices are issued electronically and may be made available in the Console dashboard and sent by email to the registered address. The Customer is responsible for providing accurate billing and tax information. Invoices are due upon receipt for monthly usage-based plans and annual subscriptions. ### **7.5 Prepaid credits** LightOn may offer prepaid credit packages. Credits: * Are non-refundable once purchased, except as required by applicable law; * Have a validity period as specified at the time of purchase; * Are non-transferable. ### **7.6 Non-payment and suspension** In the event of failed payment or non-payment: * LightOn may suspend access to the Service after providing written notice (email) and a cure period of 7 business days; * Late payment shall bear interest at three (3) times the legal interest rate, plus a flat-rate collection indemnity of €40, in accordance with Articles L.441-6 and D.441-5 of the French *Code de commerce*; * Persistent non-payment for more than 60 days may result in termination of the account pursuant to Article 18. ### **7.7 Price revisions** For subscription plans in force, prices may be revised anytime with prior 30 days notice. Existing customers may cancel their subscription at any time during the interim period, in accordance with clause 18.2. ### **7.8 Disputes** The Customer must raise any billing dispute within 15 days of receiving the invoice, by contacting [support@lighton.ai](mailto:support@lighton.ai). Undisputed amounts remain due and payable. ## **Article 8 – License and Intellectual Property** ### **8.1 License to use the Service** Subject to the Customer's full compliance with these Terms and payment of applicable fees, and except as mentioned in Purchase Orders or other contractual documents, LightOn grants the Customer a limited, non-exclusive, non-transferable, non-sublicensable, revocable license to: * Access and use the Console and its AI Features via the APIs and web interface; * Integrate the API outputs into the Customer's own applications and workflows; * Make copies of the Documentation strictly necessary for the Customer's internal use. This license is granted solely for the Customer's internal professional purposes and for integration into Customer-built products or services. ### **8.2 Restrictions** The Customer must not, and must ensure its Users do not: * Reproduce, copy, decompile, reverse engineer, disassemble, or attempt to extract the source code of the Console or any AI model made available or used through the Console, except to the extent expressly permitted by applicable law; * Modify, adapt, translate, or create derivative works of the Console software or LightOn's models; * Sublicense, rent, sell, resell, lease, or otherwise transfer access to the Service to third parties, other than as part of the Customer's own products or services that consume the APIs; * Use the Service to train, fine-tune, or distil competing AI models, or to benchmark the Service against competitors for public commercial purposes, without LightOn's prior written consent; * Circumvent or attempt to circumvent any technical access controls, rate limits, or usage restrictions; * Use the Service in a manner that would infringe any third party's intellectual property rights. ### **8.3 LightOn's intellectual property** All intellectual property rights in and to the Console, the AI Features, LightOn's models, the Documentation, and any related technology remain the exclusive property of LightOn. Nothing in these Terms transfers any intellectual property right to the Customer other than the limited license granted in Article 8.1. LightOn's trademarks, logos, and branding may not be used without prior written authorisation. ### **8.4 Customer Data and outputs** As between the Parties: * **Customer Data (Input)**: the Customer retains full ownership of all Input Data submitted to the Service. * **Output Data**: the Customer owns Output Data generated in response to its inputs, subject to any third-party rights and to the limitations described in 8.6 License granted to LightOn below. ### **8.5 Copyright and output similarity** The Customer acknowledges that the AI models provided via the Console are trained on large corpora of text and data, which may include material protected by third-party intellectual property rights. As a result: * Output Data may reflect patterns, phrasing, or structures statistically derived from such training material, and may, in certain cases, bear similarity to pre-existing copyrighted works, without LightOn necessarily being aware of this at the time of generation; * Due to the statistical nature of generative AI, Output Data produced in response to similar or identical inputs may be substantially similar or identical across different customers. LightOn makes no warranty of exclusivity or uniqueness with respect to any Output Data. The Customer is solely responsible for reviewing Output Data before any use, publication, or commercial exploitation, and for conducting any intellectual property clearance it deems necessary. LightOn disclaims all liability arising from any third-party intellectual property infringement claim relating to Output Data, where such claim results from the Customer's use, reproduction, or distribution of that Output Data. The Customer shall indemnify LightOn against any such third-party claim pursuant to Article 17.5. Nothing in this clause limits LightOn's obligations under the EU AI Act, including its transparency obligations regarding training data under Article 53(1)(d) of Regulation (EU) 2024/1689. ### **8.6 License granted to LightOn** To provide the Service, the Customer grants LightOn and its authorised sub-processors a non-exclusive, worldwide, royalty-free, limited license to host, process, store, cache, and transmit Customer Data solely to the extent necessary to deliver the Service, enforce these Terms, and comply with applicable law. This license terminates upon deletion of Customer Data as provided in Article 12 – Data Portability and Deletion. LightOn does not use Customer Data to train, fine-tune, or improve any general-purpose AI models, except where the Customer has expressly opted into such use in writing or through a clearly presented consent mechanism in the Console. ### **8.7 Feedback** Where the Customer provides LightOn with feedback, suggestions, or ideas regarding the Service ("Feedback"), LightOn may freely use such Feedback to improve its products and services without any obligation of confidentiality or compensation, unless a separate written agreement provides otherwise. ## **Article 9 – AI-Specific Provisions** ### **9.1 Nature of AI outputs** The AI Features are probabilistic systems. Output Data is generated based on statistical patterns and may be: * Inaccurate, incomplete, or inconsistent; * Similar or identical to output generated for other customers (due to model generality); * Subject to hallucinations, biases, or errors. The Customer must review Output Data before relying on it for any consequential purpose and must not treat it as professional advice (legal, medical, financial, or otherwise). ### **9.2 Customer as deployer under the EU AI Act** Where applicable, the Customer acts as a **deployer** of AI systems within the meaning of the EU AI Act. As such, the Customer is responsible for: * Assessing whether its intended use of the AI Features falls within any prohibited or high-risk use case category under the EU AI Act; * Ensuring that its deployment complies with all obligations applicable to deployers, including transparency, human oversight, and data governance requirements; * Refraining from using the Service for purposes prohibited under Article 5 of the EU AI Act, including without limitation: social scoring systems, real-time remote biometric identification in public spaces (except as permitted by law), or systems designed to exploit psychological vulnerabilities. LightOn makes no representations regarding the suitability of the AI Features for any particular use case, including high-risk use cases. ### **9.3 Non-use for prohibited AI applications** The Customer expressly agrees not to place on the market, put into service, or use the Service for any AI practice prohibited under Article 5 of the EU AI Act, as that Article may be amended or supplemented from time to time, including any practice falling within points (a) to (h) of Article 5(1). ### **9.4 AI transparency obligations** Where required by applicable law, the Customer is responsible for informing its own end-users that content has been generated or assisted by an AI system, and for providing any required disclosures regarding automated decision-making. ### **9.5 Model availability and changes** LightOn may update, replace, or retire AI models made available through the Console, with reasonable prior notice. LightOn will endeavour to maintain backward compatibility of API interfaces; however, model-level changes (e.g., updated weights, altered output characteristics) may affect Customer integrations. ## **Article 10 – Acceptable Use Policy (AUP)** ### **10.1 General obligations** The Customer agrees to use the Service only for lawful purposes and in compliance with all applicable laws and regulations. The Customer must not use the Service to process, generate, transmit, or store content that: * Is illegal, fraudulent, defamatory, threatening, harassing, or abusive; * Infringes any third party's intellectual property, privacy, or other rights; * Constitutes or facilitates discrimination based on race, ethnic origin, religion, disability, gender, sexual orientation, nationality, or any other protected characteristic; * Promotes terrorism, extremism, or violence; * Contains or distributes malware, viruses, or other malicious code; * Involves the non-consensual collection or processing of personal data; * Is designed to deceive or manipulate users in harmful ways. ### **10.2 API abuse** The Customer must not: * Conduct load tests, stress tests, or security assessments of the Service without prior written consent from LightOn; * Attempt to probe, scan, or test the vulnerability of the Service or its infrastructure without prior written consent from LightOn; * Use automated bots or scripts in a manner that creates a disproportionate load or disrupts the Service for other customers; * Attempt to access data or accounts belonging to other customers. ### **10.3 Security and sovereignty-sensitive uses** For public sector, defence, or critical infrastructure uses subject to specific security or sovereignty requirements (e.g., ANSSI SecNumCloud qualification, NATO classifications, NIS2 obligations, etc.), the Customer must notify LightOn prior to onboarding and must use an Enterprise Deployment mode specifically validated for such purposes. Multi-tenant SaaS mode is not validated for classified or Restricted (DR/TF) workloads. ### **10.4 Enforcement** LightOn may investigate suspected AUP violations and may, at its sole discretion, issue a warning, throttle access, suspend the API Key(s) concerned, or terminate the Customer's account in accordance with Article 18 – Term and Termination. LightOn may also report violations to competent law enforcement authorities where required by law. ## **Article 11 – Data Protection and Privacy** ### **11.1 General** LightOn processes personal data in accordance with applicable data protection law, including the GDPR and French *Loi n° 78-17 du 6 janvier 1978 relative à l'informatique, aux fichiers et aux libertés* ("Loi Informatique et Libertés"). LightOn's [Privacy Policy](/legal/privacy-policy) describes how LightOn processes personal data for which it acts as controller (e.g., Customer account data, billing information). ### **11.2 LightOn as data processor** Where the Customer submits Input Data containing personal data to the Service for processing, LightOn acts as a **data processor** within the meaning of Article 28 GDPR. In such cases: * The Parties' respective obligations are governed by the DPA, which is incorporated into these Terms by reference; * The Customer, as data controller, is responsible for ensuring it has a valid legal basis to process such personal data and to transfer it to LightOn for processing; * The Customer must not submit special-category data (Article 9 GDPR) or personal data of minors to the Service without a documented lawful basis and prior notification to LightOn. * LightOn must assist the Customer for relevant matters mentioned in the GDPR and/or the DPA (data subjects rights, privacy impact assessment, data breaches, etc.). ### **11.3 Data residency** For the standard multi-tenant SaaS deployment, Customer Data is processed and stored on infrastructure located in the European Union, using sub-processors listed in the DPA. The Customer may request a specific data residency zone as part of an Enterprise Deployment, subject to availability and additional charges. ### **11.4 Data minimisation and retention** LightOn applies data minimisation principles to usage and telemetry data. The specific retention periods applicable to Input Data, Output Data, logs, and usage data are set out in the DPA and the applicable SLA. In the SaaS modes where LightOn manages the inference nodes, inference and compute instances operate in a **stateless** manner: Input Data is not persistently stored on compute or inference nodes after a request is completed. ### **11.5 Customer obligations** The Customer is responsible for: * The lawfulness of all personal data submitted to the Service; * Providing appropriate notices and obtaining necessary consents from data subjects; * Responding to data subject rights requests in respect of data processed through the Service; * Maintaining its own records of processing activities as required by Article 30 GDPR. ## **Article 12 – Data Portability and Deletion** ### **12.1 Export** During the term of the Customer's subscription, the Customer may export its Customer Data (indexed documents, stored outputs, configuration data, etc.) via the Console interface or API, in standard formats as documented. LightOn will maintain export functionality for the duration of the subscription. ### **12.2 Deletion upon termination** Following termination or expiry of the Customer's account: * LightOn will delete or anonymise Customer Data within 30 days of the termination date, except where retention is required by applicable law or for legitimate legal purposes; * Backups containing Customer Data will be purged with backups rotation and overwriting; * The Customer will receive a confirmation of deletion upon request. ### **12.3 Portability for Enterprise Deployments** Where applicable, specific data portability and transition assistance obligations for Enterprise Deployments are set out in the applicable Special Terms. ## **Article 13 – Security** ### **13.1 LightOn's security measures** LightOn implements technical and organisational measures designed to protect Customer Data against unauthorised access, disclosure, alteration, or destruction, including, without limitation: * Encryption of data in transit (TLS 1.2 or higher) and at rest (AES-256 or equivalent); * Logical isolation of Customer environments in multi-tenant infrastructure; * Role-based access controls and privileged access management; * Audit logging of administrative access and API key usage; * Regular vulnerability assessments and penetration testing; * Business continuity and disaster recovery procedures; * Certifications as specified in [LightOn’s trust center](https://trust.lighton.ai). ### **13.2 Security incident notification** In the event of a personal data breach within the meaning of Article 4(12) GDPR, LightOn will notify the Customer without undue delay and, in any case, within 72 hours of becoming aware, in accordance with the Customer’s obligations under Article 33 GDPR and the DPA. ### **13.3 Customer's security obligations** The Customer is responsible for: * Implementing appropriate security measures in its own environment and applications integrating the APIs; * Protecting API Keys and ensuring they are rotated regularly; * Reporting suspected security incidents or vulnerabilities to LightOn at [security@lighton.ai](mailto:security@lighton.ai) promptly. ### **13.4 Security for Enterprise Deployments** Security requirements, penetration testing rights, audits, and specific certifications applicable to Enterprise Deployments (including on-premises modes) are governed by the applicable Special Terms and, where applicable, Security Addenda forming part of the Enterprise Agreement. ## **Article 14 – Service Levels and Availability** ### **14.1 SaaS availability commitments** For Paid Plans, LightOn commits to the availability levels, maintenance windows, and support response times set out in the applicable SLA available upon request at [support@lighton.ai](mailto:support@lighton.ai). ### **14.2 Free Tier and best-efforts basis** No SLA applies to the Free Tier. The Service is provided on a best-efforts basis under the Free Tier, and LightOn reserves the right to perform maintenance or impose throttling at any time. ### **14.3 Scheduled maintenance** LightOn will endeavour to provide advance notice of scheduled maintenance affecting the Service, in accordance with the applicable SLA. ### **14.4 Enterprise SLAs** Custom SLAs, including dedicated uptime commitments, incident escalation procedures, and premium support arrangements, may be agreed for Enterprise Deployments in the applicable Purchase Order or SLA addendum. ## **Article 15 – Confidentiality** ### **15.1 Obligations** Each Party ("Receiving Party") agrees to keep confidential all non-public information disclosed by the other Party ("Disclosing Party") in connection with the Service ("Confidential Information"), and to use such information solely for the purposes of performing or receiving the Service. Customer Data is considered the Customer's Confidential Information. LightOn's models, code, algorithms, infrastructure, pricing, and roadmap are LightOn's Confidential Information. ### **15.2 Exclusions** The confidentiality obligation does not apply to information that: * Is or becomes publicly available through no fault of the Receiving Party; * Was already known to the Receiving Party prior to disclosure; * Is independently developed by the Receiving Party without reference to the Confidential Information; * Is required to be disclosed by applicable law or court order, provided the Receiving Party gives the Disclosing Party prompt prior notice where legally permitted. ### **15.3 Duration** Confidentiality obligations survive termination of these Terms for a period of five (5) years, except for trade secrets, which remain protected for as long as they qualify as such under applicable law. ## **Article 16 – Warranties and Disclaimers** ### **16.1 LightOn's warranties** LightOn warrants that: * It has the right to grant the licenses set out in 8.1 License to use the Service; * The Service will perform substantially in accordance with the Documentation under normal use and operating conditions; * It will implement the security measures described in Article 13 – Security. ### **16.2 Disclaimers** TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW: * THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE". LIGHTON MAKES NO ADDITIONAL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. * LIGHTON DOES NOT WARRANT THAT OUTPUT DATA WILL BE ACCURATE, COMPLETE, RELIABLE, OR FREE FROM ERRORS OR BIASES. * LIGHTON DOES NOT WARRANT THAT THE SERVICE WILL BE UNINTERRUPTED, ERROR-FREE, OR FREE FROM SECURITY VULNERABILITIES. * LIGHTON IS NOT RESPONSIBLE FOR THE SUITABILITY OF THE SERVICE FOR THE CUSTOMER'S SPECIFIC USE CASE, INCLUDING ANY HIGH-RISK AI APPLICATION. ## **Article 17 – Limitation of Liability** ### **17.1 Exclusion of indirect damages** TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, NEITHER PARTY SHALL BE LIABLE TO THE OTHER FOR ANY INDIRECT, INCIDENTAL, SPECIAL, PUNITIVE, OR CONSEQUENTIAL DAMAGES (INCLUDING LOSS OF PROFITS, LOSS OF DATA, LOSS OF BUSINESS, OR REPUTATIONAL DAMAGE), ARISING OUT OF OR IN CONNECTION WITH THE SERVICE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. ### **17.2 Cap on liability** TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LIGHTON'S TOTAL AGGREGATE LIABILITY TO THE CUSTOMER, ARISING OUT OF OR RELATED TO THESE TERMS OR THE SERVICE, WHETHER IN CONTRACT, TORT, OR OTHERWISE, SHALL NOT EXCEED THE GREATER OF: (a) THE TOTAL AMOUNTS PAID BY THE CUSTOMER TO LIGHTON IN THE SIX (6) MONTHS IMMEDIATELY PRECEDING THE EVENT GIVING RISE TO THE CLAIM; OR (b) TEN THOUSAND EUROS (€10,000). ### **17.3 Free Tier** NOTWITHSTANDING ARTICLE 17.2, LIGHTON'S TOTAL LIABILITY IN RESPECT OF THE FREE TIER IS LIMITED TO ONE HUNDRED EUROS (€100). ### **17.4 Exceptions** Nothing in these Terms excludes or limits liability for: death or personal injury caused by negligence, fraud or fraudulent misrepresentation, or any other liability that cannot be excluded or limited under applicable French or EU law. ### **17.5 Customer indemnity** The Customer shall indemnify, defend, and hold harmless LightOn and its officers, directors, employees, and agents from and against any third-party claims, damages, or costs (including reasonable legal fees) arising from: (i) the Customer's use of the Service in violation of these Terms; (ii) the Customer's Input Data, including any infringement of third-party rights or data protection violations; (iii) the Customer's end-users' use of Customer-built applications powered by the Service. ## **Article 18 – Term and Termination** ### **18.1 Term** These Terms take effect on the Effective Date and remain in force until terminated as provided herein or, for Enterprise Deployments, for the term specified in the applicable Purchase Order. ### **18.2 Termination by the Customer** The Customer may terminate its account: * **Free Tier**: at any time by deleting the account through the Console interface; * **Monthly plans**: at any time, effective at the end of the current billing cycle; * **Annual subscriptions**: at any time, with written notice to [support@lighton.ai](mailto:support@lighton.ai); early termination of an annual subscription does not entitle the Customer to a refund of prepaid fees except if LightOn notified the Customer of a price increase. In that case, the prepaid amount will be refunded on a pro rata basis from the date the price change takes effect; * **Enterprise Deployments**: subject to the notice and termination provisions of the applicable Purchase Order (generally three (3) months' notice at the end of the initial term or any renewal term). ### **18.3 Termination by LightOn** LightOn may terminate the Customer's account: * Immediately, without notice, if the Customer materially breaches the AUP or engages in fraudulent, abusive, or unlawful conduct; * With 30 days' notice, for any other material breach of these Terms that the Customer fails to cure within such period; * With 90 days' notice, for the discontinuation of the Service; * Immediately, in the event of non-payment exceeding 60 days after the due date. ### **18.4 Effect of termination** Upon termination: * All licenses granted to the Customer immediately cease; * The Customer must immediately cease all use of the Service and API Keys; * All amounts due and payable as of the termination date remain due, and no refunds are owed except as required by applicable law; * LightOn will delete Customer Data as set out in Article 12 – Data Portability and Deletion. ### **18.5 Survival** The following provisions survive termination: Article 1 – Definitions, 8.3 LightOn's intellectual property, 9.1 Nature of AI outputs, Article 15 – Confidentiality, Article 16 – Warranties and Disclaimers, Article 17 – Limitation of Liability, 18.4 Effect of termination, Article 19 – Governing Law and Dispute Resolution, and Article 21 – Miscellaneous. ## **Article 19 – Governing Law and Dispute Resolution** ### **19.1 Governing law** These Terms and any disputes arising from or in connection with them shall be governed by and construed in accordance with French law, without regard to its conflict of laws rules. ### **19.2 Amicable resolution** In the event of a dispute, the Parties agree to use their best efforts to resolve it amicably. Either Party may initiate the amicable resolution process by giving written notice to the other Party describing the dispute in reasonable detail. The Parties shall negotiate in good faith for a period of 30 days from the date of such notice. ### **19.3 Jurisdiction** If the Parties fail to resolve the dispute amicably, the courts of Paris, France, shall have exclusive jurisdiction, subject to any mandatory jurisdictional rules applicable under French or EU consumer protection law. ### **19.4 Mediation** Without prejudice to the foregoing, the Parties may at any time agree to submit the dispute to mediation before a mediator appointed by mutual agreement or under the rules of the Centre de Médiation et d'Arbitrage de Paris (CMAP). ## **Article 20 – Modifications to the Terms** LightOn reserves the right to modify these Terms at any time. Modifications will be notified to the Customer by: * Email to the registered account address; and/or * A prominent notice in the Console dashboard. For **material changes**, LightOn will provide at least 30 days' advance notice. The Customer's continued use of the Service after the effective date of the modification constitutes acceptance of the modified Terms. If the Customer does not accept the modified Terms, it may terminate its account before the effective date of the modification, in which case: * For paid annual subscriptions: LightOn will provide a pro-rata refund of prepaid fees for the unused period; * For monthly and usage-based plans: no refund is owed for charges already incurred. ## **Article 21 – Miscellaneous** ### **21.1 Entire agreement** These Terms (together with the documents referred to herein) constitute the entire agreement between the Parties with respect to the Service and supersede all prior agreements and representations. ### **21.2 Severability** If any provision of these Terms is held to be invalid, illegal, or unenforceable, that provision shall be modified to the minimum extent necessary to make it enforceable. The remaining provisions shall continue in full force. ### **21.3 Waiver** The failure of either Party to enforce any provision of these Terms shall not constitute a waiver of that Party's right to enforce such provision in the future. ### **21.4 Assignment** The Customer may not assign or transfer any of its rights or obligations under these Terms without LightOn's prior written consent. LightOn may assign its rights and obligations under these Terms to a successor entity in the event of a merger, acquisition, or sale of substantially all of its assets, provided it gives the Customer notice and the successor entity assumes all obligations under these Terms. ### **21.5 Force majeure** Neither Party shall be liable for delays or failures in performance resulting from circumstances beyond its reasonable control, including but not limited to: acts of God, natural disasters, epidemics, pandemics, war, terrorism, government orders, cyberattacks not attributable to the Party's negligence, or failures of third-party infrastructure providers. The affected Party must promptly notify the other and use its best efforts to mitigate the impact. ### **21.6 Sub-processors and third parties** LightOn may use sub-processors to deliver the Service, as listed in the DPA. LightOn remains responsible for the acts and omissions of its sub-processors to the same extent as for its own acts and omissions under these Terms. ### **21.7 Export controls** The Customer must comply with all applicable export control laws and regulations, including EU dual-use regulations and any applicable sanctions. The Customer represents that neither it nor any User is the subject of any applicable trade restriction. ### **21.8 Accessibility** LightOn will make its best efforts to ensure that the console generally complies with the applicable digital accessibility standards RGAA / EN 301 549. ### **21.9 Communications and notices** All notices under these Terms must be sent in writing: * **To LightOn**: by email to [compliance@lighton.ai](mailto:compliance@lighton.ai) and/or by registered post with acknowledgment of receipt to LightOn SA, 2 Rue de la Bourse, 75002 Paris, France; * **To the Customer**: by email to the registered account address. Notices sent by email are deemed received on the business day following transmission (absent proof of non-delivery). Notices sent by registered post are deemed received three (3) business days after dispatch. Last updated: 28/05/2026. LightOn SA, 2 Rue de la Bourse, 75002 Paris, France # Terms of Use Source: https://developers.lighton.ai/legal/terms-of-use These Conditions of Use govern access and use of the Services made available by LightOn. *Effective date: August 11, 2026* These Terms of Use (hereinafter the “Terms”) govern access to and use of the services provided by LightOn. By using our services, you agree to be bound by these Terms. If you use our Services on behalf of an entity or organization, such as a business or educational institution, you may be binding that organization to these Terms and you hereby represent that you have the authority to do so. LightOn services are developed and marketed by LightOn, a French company headquartered at 2, rue de la Bourse, 75002 Paris. ## **1. Definitions** “**Data Protection** Agreement” or “**DPA**” means the agreement between LightOn and the Customer regarding the processing of Personal Data. “Administrator” means an Authorized User designated by the Customer to manage and control the Platform, the Customer’s accounts, and the Authorized Users. “**Third-Party** Applications” means all third-party applications, integrations, websites, products, or services that are linked to or interact with the Services, including all applications created by third parties using LightOn’s software development kits (“SDKs”) or other development tools. **“API Key”** means the unique identifier generated via the Console or via Paradigm that allows the Customer or its applications to authenticate and consume LightOn’s API Services. **“Customer”** means the person or organization that has entered into a contract for the provision of solutions delivered through the use of the Platform made available by LightOn for the purpose of benefiting from and/or enabling its Authorized Users to benefit from the User Services offered by LightOn. **“Account”** refers to the online space made available to Users to manage the User interface, notably by providing access to certain User information (including last name, first name, email address, phone numbers, Content, etc.), to access relevant information and documents stored on the Platform, and to use the Platform in connection with the services provided by LightOn. **“Terms”** has the meaning given to it in the preamble hereto. **“Customer Terms”** refers to the specific terms that apply to Authorized Users and bind them to the Customer. “Content” means any content that LightOn has not directly created or produced (data entered by Authorized Users, documents provided, etc.). “**User** Documentation” means any documentation or materials provided (or made available) by LightOn to the Customer or Authorized Users in connection with the Services. “**Customer** Data” refers to all information and data processed by a LightOn Service on behalf of the Customer, including all Personal Data as well as all photos, videos, audio or transcription data, images, comments, annotations, documents, user support communications, or any other content or information entered, uploaded, or processed by Users or automatically within the Platform or through the User Services, or collected, stored, or processed by LightOn on behalf of the Customer or its Authorized Users through or *via* the Platform or the User Services “**Personal** Data” means personal data or personal information (each as defined in the GDPR and in national laws implementing the GDPR, including the French Data Protection Act of January 6, 1978, as amended) contained in the Customer Data and which enables the direct or indirect identification of a natural person. “Platform” means all solutions and services provided by LightOn as part of the User Services, on all platforms such as mobile, web, or desktop applications, and hosting the User Services. The solutions and services in question include, in particular, Paradigm and LightOn Console. The Platform may be hosted: * On Premises, under the Customer’s full control * In a private cloud, under the Customer’s full control * In a private cloud managed by LightOn on behalf of the Customer (single-tenant installation, except possibly regarding the use of GPU-type nodes, used in particular for interferometry, document processing, and simulations) * In a private cloud managed and controlled by LightOn (multi-tenant installation) “Party” means individually LightOn or the Customer and jointly LightOn and the Customer. “**Confidential** Information” means information exchanged by the Parties that would reasonably be considered confidential given the nature of the information, regardless of its form, medium, or means, including, without this list being exhaustive, any information collected (or provided) by the Customer or the User on the Platform or the Services, commercial information, product, technology, and marketing information, orders, discounts, non-public payment terms, audit reports, and Customer Data, as well as information specifically designated as such by either Party. Confidential Information does not include information that (a) is or becomes publicly known through lawful means; (b) was known to the recipient prior to disclosure without any obligation of confidentiality; (c) is received from a third party without a breach of confidentiality obligations; or (d) has been independently developed by the recipient without use of or access to confidential information. “**Legal** Process” means a disclosure of information or a request for access made pursuant to law, a government regulation, a court order, a subpoena, a warrant, a regulatory request or from a government agency, or from another legal authority, legal proceeding, or similar valid process. “GDPR” means Regulation (EU) 2016/679 of the European Parliament and of the Council of April 27, 2016, on the protection of natural persons with regard to the processing of personal data and on the free movement of such data, and repealing Directive 95/46/EC. **“User** Services” means (i) Users’ access to the Platform, (ii) any hosting of the Platform and the Data and related processing, including processing performed in response to calls to the APIs exposed by the Platform, (iii) the administration and support of hosting servers, if applicable, (iv) any backup of Customer Data, (v) support and corrective and evolutionary maintenance of the Platform, and (vi) the correction of Defects. “Site” means the LightOn application website / the Platform accessible to the Customer / Users. “**User(s)**” means any person who uses the User Services, including Authorized Users and third parties who access the User Services in connection with the use and internal management of the Platform on behalf of the Customer. “**Authorized User(s)**” means employees, agents, or other third parties authorized by the Customer to access or use the User Services, including Administrators and users whose access to the platform is required or intended, including, where applicable, LightOn employees and certain service providers. ## **2. Customer Terms** The Platform may allow the Customer who has enabled access to the Platform for Authorized Users to upload its own terms and conditions. These Customer Terms govern the use of the Platform by the Customer’s Users. In the event of a conflict between the terms of the Customer’s Terms and those of LightOn’s Terms, the terms of the Customer’s Terms shall prevail by default, except in cases of obvious errors or non-compliance with applicable regulations. ## **3. Changes to the Terms** LightOn reserves the right to change or modify the Terms (or any LightOn policy or guideline) at any time and in its sole discretion to adapt to its business operations and comply with applicable regulations. If LightOn makes changes to these Terms, we will notify you of such changes either by sending you a notification, by informing you via the Site or the Platform (including by asking you to accept the new terms), and/or by updating the “Last Updated” date at the top of these Terms and publishing the revised Terms on the Platform without prior notice. Upon logging into the Platform, the User must accept the current version of the Terms by checking a box or using any other means capable of obtaining the User’s consent. The version that prevails is the one accessible online. If the User does not accept the Terms or any subsequent modifications thereto, access to the Platform, the User Services, and the User’s account may be disabled. ## **4. Acceptance of the Terms and Conditions for Use of the Platform** ### **4.1. General Provisions** By using the Platform, you acknowledge and agree that: 1. You have read LightOn’s and/or the Customer’s Terms 2. You understand all the terms of the Terms 3. You agree to be legally bound by all the provisions of the Terms and Conditions that you have read and accepted. ### **4.2. Provisions Specific to the “LightOn Console” Self-Service Offering** Notwithstanding or in addition to the provisions of Section 4.1, access to and use of the LightOn Console platform and its self-service application programming interfaces (APIs) are subject to the Customer’s independent creation of an online account. Upon doing so, the Customer fully, completely, and unreservedly accepts these General Terms of Use (GTU), the Console’s General Terms of Service, the Data Usage Policy (Privacy Policy), and the Data Protection Agreement (DPA) is mandatory and must be provided through an affirmative digital action (clickwrap mechanism via a dedicated checkbox) during the registration process. If the Customer does not accept these documents, they must not use the Service. The Parties expressly agree that the digital audit trails and connection logs automatically generated by LightOn’s identity management systems (including, but not limited to, the Customer’s unique identifier, the source public IP address, the precise timestamp of acceptance, and the cryptographic hash of the version of the accepted terms) constitute valid, legally binding, and fully enforceable proof of consent against the Customer. The requirement for a handwritten or qualified electronic signature is expressly waived for the use of the self-service Console platform. ## **5. Eligibility** The purpose of these Terms is exclusively to govern access to the Platform and User Services and to determine the terms of use thereof. They are therefore not intended to apply to any other medium belonging to LightOn or the Customer, as the case may be, that is not implemented within the Platform. The Terms apply to Users and Customers for access to and use of the Platform and User Services. As a User, you represent and warrant that: 1. You are of legal age in your country of residence to access the Platform 2. Your access to the Platform has not previously been suspended and has not been reactivated since then. 3. The name, company/organization name, username, or any other identifying information you submit when creating or using an Account does not contain offensive, obscene, racist, discriminatory, or hateful language or, more generally, any language that can be considered to be contrary to public decency. LightOn is not a party to any contract, agreement, or contractual relationship that may be entered into between the Customer and Users. The Application and associated User Services are accessible to Authorized Users who have an account with a username, associated with a password or a Customer-specific authentication system, under the terms of Section 6.3 below; **Any User using the Platform acknowledges having read and accepted all of LightOn’s Terms and Conditions and/or the Customer’s Terms and Conditions at the time of their first login to the Platform.** ## **6. LightOn Services** ### **6.1. General Description of Services** LightOn provides a suite of generative artificial intelligence and advanced enterprise data processing software solutions, including in particular: * **Paradigm**: an enterprise artificial intelligence platform equipped with a turnkey user interface (*UI Assistant*), designed for document analysis, large-scale semantic search, collaboration, and secure content generation. * **LightOn Console**: a developer platform comprising a web portal, interactive test environments (*playgrounds*), consumption monitoring dashboards, and a set of application programming interfaces (REST APIs) that enable the integration of LightOn’s AI capabilities directly into the customer’s workflows and applications. ### **6.2. Deployment Model and Infrastructure Architectures** Depending on the options subscribed to or selected online by the Customer, the Services are accessible via different infrastructure architectures, operated on the Customer’s infrastructure or LightOn’s infrastructure (hosted within the European Union): * **Shared Cloud Deployment (Multi-tenant SaaS)**: The entire Platform and the software components necessary for its operation are deployed on a shared cloud infrastructure, operated and controlled by LightOn. Each Customer’s data is strictly segmented and logically isolated to prevent any access by third parties. This is the deployment mode of The Console when you use [console.lighton.ai](http://console.lighton.ai) * **Dedicated Deployment / Private Cloud (Single-tenant SaaS)**: The Services are deployed on a dedicated cloud instance, logically or physically isolated, hosted and managed by LightOn exclusively on behalf of the Customer. The choice of hosting is under the Customer’s control from among the options offered by LightOn; for example, it is possible to choose hosting with a “SecNumCloud”-certified provider. * **Hybrid Deployment (*Sovereign Cloud Acceleration*)**: The software logic, interface, and persistent data storage are deployed on the Customer’s own infrastructure (cloud or on-premises servers). The computing resources required to run AI models (including inference and embedding calculations) are outsourced under LightOn’s control via cloud computing nodes (GPUs), which operate on a short-lived basis and, by default, without any persistent retention of the transmitted data. * **On-Premises Deployment (Self-Hosted / Sovereign)**: The entire Platform, including all software components, data storage, and necessary GPU computing resources, is installed and operated directly under the Customer’s full control on their own infrastructure (which can be configured to be completely isolated from the Internet or *air-gapped*). #### **6.2.1. Natural Language Processing Suite / 6.2.2. Content Generation System** The Services incorporate advanced generative artificial intelligence capabilities that enable Users, depending on the application or programmatic interfaces used, to perform the following operations: * **Natural Language Processing (NLP) and Text Generation**: Contextual text generation, query completion, real-time multilingual translation, automated writing, and synthesis and comparison of complex documents. * **Document analysis and advanced parsing**: Automated text extraction from digital, digitized, scanned, or handwritten documents via a state-of-the-art optical character recognition (OCR) system or any other system capable of extracting textual information from image- or graphic-based documents, converting raw documents into actionable data. * **Structured data extraction**: Targeted extraction of specific information, tables, or fields from unstructured documents, automatically rendered as structured data schemas (e.g., JSON format) according to the application’s requirements. * **Semantic search and generation-enhanced search**: Ingestion, chunking, embedding, and indexing of files to enable *grounded search* within documents provided by the Customer. * **Visual and multimodal analysis**: Image recognition and visual analysis combining textual and graphical elements (diagrams, figures, drawings, photographs) present within documents provided by the Customer. * **Knowledge management and connectors**: Organization of data into specific *workspaces* dedicated to the Customer or a team, integration of native connectors for third-party data sources (SharePoint, Teams, Google Drive, ServiceNow, etc.), and indexing via smart tagging (Knowledge Graph / *Smart Tagging*). LightOn provides support and corrective and evolutionary maintenance for the Platform, including continuous monitoring of the technical quality of the infrastructure and applications. LightOn thus deploys security patches without undue delay. The procedures for deploying software updates (monthly major releases, emergency patches, or *hotfixes*) are carried out in accordance with maintenance windows and according to the specific conditions applicable to the infrastructure architecture selected by the Customer. ### **6.3. Registration/Login** To use the Platform, a User Account must be created. Only Authorized Users with the necessary permissions may create User Accounts on behalf of a Customer. ### **6.4. Access** During the term of the contractual relationship, Authorized Users may access and use the Platform, and may allow Users for whom they have created Accounts to access and use the Platform, in particular for the purposes set forth: * in these Terms * in LightOn’s terms and conditions or in the contract between LightOn and the Customer, and * in the Customer’s Terms, if applicable. Access may be revoked, in particular: 1. at the request of Users and/or the Customer if access to or the existence of the said User’s account is no longer necessary for the purposes, 2. by an Authorized User with the necessary rights 3. upon termination of the contractual relationship. The User is solely responsible for: * Access to the Platform and the use made of their login credentials for the Platform and their API Keys by a third party. It is their responsibility to take all necessary measures to maintain the confidentiality of these credentials. LightOn is released from any liability in the event of inability to access the Platform and the Services due to an event beyond its control; * The creation and use of information shared for the purpose of accessing and/or using the Platform and/or User Services. It is the responsibility of Authorized Users to take all necessary measures to control the sharing of such information and, where applicable, to inform the individuals concerned about the processing of their personal data when they use the User Services, particularly through the Customer Terms. LightOn is released from any liability in the event of access to the Platform and the Services by any third party with knowledge of said information, due to an event beyond its control. ### **6.5. Software** The Platform is accessible online. LightOn may make the Platform available as part of the User Services. Subject to compliance with the Terms, LightOn grants the Customer, Authorized Users, Users, and any guests a limited, non-exclusive, non-transferable, and non-assignable license to access and use the Platform to the extent necessary to use the User Services. To the extent that a component of the Platform contains open-source software, the open-source license for such software shall govern with respect to that component. With respect to the elements under its control, LightOn endeavors, to the extent possible, to make the Platform and its User Services accessible at all times, 24 hours a day, 7 days a week. Nevertheless, for the proper management of the Platform, LightOn may at any time, with respect to elements under its control: 1. Suspend, interrupt, or limit access to all or part of the Platform, or to certain parts of the Platform, for Users or certain Users; 2. Remove any information that may disrupt its operation or that violates national or international laws or LightOn’s and/or the Customer’s Terms and Conditions; 3. Suspend access to the Platform in order to perform updates, in the cases provided for herein. LightOn shall not be held liable for malfunctions whose origin is linked, directly or indirectly, to an information system other than its own or those under its full control, and in particular that of the Customer or the Users. ### **6.6. Authorized Use - Restrictions.** The Customer agrees to comply with and warrants that Users whose Accounts have been created under its authority will comply with the following commitments. 1\. When using the Platform or User Services, and unless expressly authorized and duly validated by the Parties, no information, and more generally any Content, may be recorded, hosted, displayed, downloaded, modified, published, transmitted, updated, or shared that: 1. belongs to another person and to which neither the Authorized User nor the Customer has any rights; 2. is grossly offensive, harassing, profane, obscene, pornographic, pedophilic, defamatory, invasive of another’s privacy, hateful or racially or ethnically objectionable, disparaging, relating to or encouraging money laundering or gambling, or otherwise illegal in any way, or, more generally, is contrary to the purposes of the Platform, likely to infringe upon the rights of LightOn or a third party, or contrary to public decency; 3. harms minors in any way; 4. infringes any patent, trademark, copyright, or other proprietary rights, including intellectual property; 5. violates any law or regulation currently in force; 6. deceives or misleads the recipient regarding the origin of the information, or communicates any information of a grossly offensive or threatening nature; 7. impersonates another person; 8. contains software viruses or any other computer code, files, or programs designed to interrupt, destroy, or limit the functionality of any computer system; 9. threatens the unity, integrity, defense, security, or sovereignty of any country, friendly relations with foreign states, or public order; incites the commission of a recognizable offense; hinders the investigation of an offense; or insults another nation; or is misleading or known to be false in any way. For the avoidance of doubt, this restriction applies equally to any Content submitted through the Platform and to any account, company, organization, or profile identifier entered at registration or thereafter. 2\. The Customer and Users shall not: 1. Interfere with the Platform or User Services for the purpose of accessing unauthorized Content or information; 2. rent, lend, sell, sublicense, or create derivative works of the Platform and User Services, except to the extent permitted by applicable provisions and/or applicable regulations; 3. reproduce, modify, adapt, disclose, decompile, reverse engineer, disassemble, or attempt to derive the Platform and/or User Services, and more specifically the source code, the underlying ideas, algorithms, file formats, or non-public APIs of the User Services used to make the User Services available to Users, except to the extent required by applicable law and applicable provisions or express authorization granted by LightOn; 4. remove, modify, or obscure any copyright, trademark, or other proprietary rights notices on or within the Platform and the User Services, and generally infringe upon the intellectual property rights held by LightOn or third parties regarding the Platform as well as the data accessible via the Platform and/or the User Services; 5. use the Platform, applications, and User Services in a manner that could interfere with, disrupt, negatively affect, or prevent other users of the Services from fully enjoying the User Services, or that could damage, disable, overload, or impair the operation of the User Services, including the security measures taken by LightOn and/or the Customer to ensure the confidentiality and integrity of data processed in connection with the User Services; 6. attempt to gain unauthorized access to the Platform and the User Services or to its related systems or networks; 7. within the strict limits of applicable legal provisions, create a competitive product or service, or copy features or functions of the Platform and User Services; 8. disclose to any third party, without LightOn’s authorization, any information or performance analysis relating to the Platform or the User Services; 9. engage in advertising or soliciting other Users or third parties to buy or sell services, including, but not limited to, services related to those displayed via the Platform or related to LightOn; 10. transmit unsolicited commercial or spam chain letters or emails to third parties via the Platform or User Services; 11. use any information obtained from the Platform or the User Services to harass, abuse, or harm another person, or to contact, advertise to, solicit, or sell anything to another person; 12. misuse the User Services in any way, including in violation of these Terms or applicable regulations; 13. use the “LightOn” names, trademarks, and logos, as well as, more generally, any other protected names used in connection with the Platform or User Services, in your advertising and commercial materials, unless you have LightOn’s prior written consent; 14. use the Platform or User Services in a manner that violates European laws and regulations, including but not limited to those pertaining to AI. Unless otherwise agreed in writing in advance, LightOn does not authorize the use of the Platform or User Services to develop or implement artificial intelligence systems with a risk level equal to or greater than the so-called “high-risk” level as defined by European regulations, where elements under LightOn’s control are used to provide the User Services to the Customer. The Customer under whose authority the Platform or User Services are used to implement an Artificial Intelligence system with a risk level greater than or equal to the so-called “high-risk” level as defined by European regulations, assumes full responsibility and all obligations, in particular as the provider or deployer  (as applicable) of such a system, within the meaning of applicable European regulations. 15. cause or allow a third party to do the foregoing. Any use of data mining, robots, or similar tools for data collection, extraction, or monitoring of all or part of the User Services without the authorization of LightOn and/or the Customer is strictly prohibited. Where applicable, the Customer shall prevent, terminate, and notify LightOn of any unauthorized or illegal use of or access to any User account or the User Services. LightOn has no obligation to monitor the Content, documents, and data processed by the Platform or by the User Services. Under no circumstances shall LightOn assume any liability for any Content processed and/or published or for any claim, damage, or loss resulting from the use of Content and/or the appearance of Content processed by the Platform. When using the User Services in SaaS mode (notably via calls to the LightOn Console programming interfaces) or in any mode where inference nodes are under LightOn’s control, computing instances operate in a stateless manner. Data provided as input by the Customer or its Users (*prompts*, queries, documents, images) is not stored persistently on the servers or compute nodes beyond the time technically required for the execution and return of the query and for compliance and security-related processing. No persistent storage of inference data (such as conversation history or document database indexes) is enabled by default within the infrastructures hosting the inference nodes. ## **7. Customer Responsibility** ### **7.1. Fees** The Customer agrees to pay the amounts due to LightOn in accordance with the payment terms set forth at the time of the order or in any agreement governing the provision of User Services to the Customer. The Customer is responsible for paying applicable taxes. #### **7.1.2. LightOn Console Online Offering** Access to the application programming interfaces (APIs) is organized into two distinct plans, detailed on the *Pricing Page*: * **Pay-as-you-go (PAYG) Plan**: The Customer is billed monthly in arrears solely based on their actual resource usage (volume of *tokens* processed, pages parsed via OCR or similar, search queries, or storage volume used). This basic plan is provided strictly on a "*best effort*" basis, is not covered by any service level agreement (SLA), and provides access exclusively to *community* support. * **Business Plan**: This plan may include a monthly minimum charge (minimum billing amount set for informational purposes; see our current rates) that includes a basic storage quota, technical support via email or ticket, as well as specific service availability commitments (Uptime SLA). ##### **7.1.2.1. Automated Payment Terms** The calculation of usage and the collection of amounts due for Console plans (Pay-As-You-Go and Business Plan) are fully automated and delegated to a secure third-party payment service provider. The Customer agrees to maintain an active credit card or direct debit authorization on their account. The corresponding invoices are generated at the end of each monthly cycle and are accessible via the dashboard. In the event of a failed payment, LightOn reserves the right to immediately suspend the API keys following digital notice. ##### **7.1.2.2. Free Plan** LightOn offers a free plan (*Free Tier*) subject to monthly quotas and bandwidth limits. This access level is provided “as is,” without any service level agreement (SLA) or guarantee of availability or technical support. If certain quotas are exceeded, the corresponding API requests are suspended or rejected until the next billing cycle or until the user upgrades to a paid plan. ### **7.2. Third-Party Applications** Applications external to the Platform and User Services, including the Customer’s applications, particularly those interfacing with User Services via API, are subject, where applicable, to their own terms and privacy policies. The Customer is responsible for reviewing these terms and policies before using third-party applications. LightOn is not responsible for third-party applications and does not warrant or support them. By enabling a third-party application that uses or accesses Customer Data, the Customer authorizes the transfer of such data to the third-party application and, where applicable, shall ensure that a legally valid data transfer mechanism exists for the transfer or onward transfer, in accordance with all applicable privacy and data protection laws. ### **7.3. Users** The Customer is responsible for the distribution of access it grants to the Platform and for Users’ use of the User Services, as well as for their compliance with these Terms. The Customer shall ensure that all Users are of the required age to use the Platform or User Services in their country of residence at the time of such use. Where applicable, the Customer represents that it has obtained all necessary consents for the use of the User Services by its Users. ### **7.4. Customer Data** The Customer is responsible for ensuring that all Customer Data and any other content or information provided to LightOn or uploaded, published, processed, stored, or transmitted to the User Services complies with these Terms. If any Customer Data violates these Terms, LightOn may request that the Customer delete or modify the Customer Data so that it no longer violates these Terms, or LightOn may, with respect to items under its control, without being obligated to do so, delete such Customer Data or take the necessary measures to resolve the issue. ### **7.5. User Comments** #### **7.5.1. Feedback Mechanism.** The Platform may include interactive features that allow Users to provide qualitative feedback regarding the Services to other users (e.g., “like,” “dislike,” or “report”). This mechanism is designed to identify technical anomalies and improve the accuracy of the Platform’s results. The feedback mechanism is not available in ephemeral mode. 1. **Like and Dislike.** Interactions with the *Like* and *Dislike* buttons are recorded in the conversation log and are not sent to any recipient. The logs, and thus the information indicating that a user clicked a *Like/Dislike* button, are accessible in the logs only to users with the DPO role. 2. **Report**. This feature allows Users to provide information regarding a response in a conversation (“User Comments”) to the support team. #### **7.5.2. Content of comments, routing, and support team.** The recipient of User comments (the “support team”) is determined exclusively by the client via the platform’s system configuration (specifically the support email setting). 1. **Content of User Comments:** When users use the “report an issue” comment (flag), a brief report is sent to the support email address specified in the admin panel (see below) and recorded in the admin panel, accessible only to users with instance-level access. This report includes only information related to the reported message, such as the user’s query and the response in Paradigm. To facilitate troubleshooting, the user submitting the user feedback may choose to attach documents related to the interaction (“documents”) to the context shared with the support team in the user feedback (“feedback content”). Only registered Users with the DPO role can access other messages in the conversation, beyond the specific message that was reported. 2. **Customer-Controlled Routing:** The recipient of the comment content can be configured to specify an internal email address via the appropriate setting in the configuration keys, as detailed in the relevant documentation. Comment content is then routed only to the recipients designated by the Customer. In this case, LightOn has no access to the processing of said comments and assumes no responsibility in this regard. 3. **Routing to LightOn:** If the Customer configures the corresponding configuration key to route user comments to LightOn (for example, for SaaS deployments or Level 3 support), the Customer authorizes LightOn to process this data in accordance with Section 7.5.3 below. 4. **Voluntary submission:** The transmission of documents within the content of comments is strictly optional and can only be triggered by an affirmative action on the part of the User (e.g., by checking a box). 5. **Disclaimer:** The Customer acknowledges that if Users refuse to share documents, the support team’s ability to effectively diagnose and resolve specific glitches or recovery failures may be severely limited. #### **7.5.3. Limited License and Use of Data.** In the event that the content of comments is transmitted to LightOn, LightOn’s use of such data is strictly limited as follows: 1. **No Use for Model Training:** LightOn will not use the content of the comments or any associated documents to feed, train, retrain, or fine-tune fundamental or general artificial intelligence models. 2. **Benchmarking and Quality Assurance:** The Customer grants LightOn a limited, non-exclusive license to use the content of the comments solely for benchmarking and quality assurance purposes. For the purposes of this Agreement, the term “benchmarking” refers to the use of data to verify that subsequent technical modifications to the Platform or User Services (i) successfully resolve the reported issue (regression testing) or (ii) do not degrade the quality of previously successful interactions. 3. **Confidentiality:** All Feedback Content received by LightOn remains confidential and is subject to the data protection obligations set forth in Section 9. ## **8. LightOn’s Responsibilities** ### **8.1. Best-Efforts Obligation** Strictly within the scope of the provision of paid User Services to any User, LightOn is bound by an obligation of means. LightOn further reserves the right to perform technical interventions, maintenance, or updates on the Platform and the User Services under its control, which may result in a temporary interruption of access. Nevertheless, LightOn undertakes to use its best efforts to ensure that User Services are not interrupted during periods of normal or intensive use of the Platform. In any case, and unless otherwise stipulated in a service level agreement or any document of similar scope, any resulting service malfunctions or unavailability shall not give rise to any compensation for Users. In particular, LightOn does not guarantee to Users: 1. The reliability, performance, or completeness of the content published on the Platform in relation to the Users’ intended use. Users agree to use this information at their own sole risk; 2. The nature and outcome of relationships between Users, if any. LightOn cannot therefore be held liable for any direct or indirect damages claimed by Users based on the information or content published on the Platform and the User Services. LightOn undertakes to respond without undue delay to Users’ requests and complaints, as well as to requests for the removal or modification of Content published on the Platform and User Services, under the conditions set forth in the applicable provisions and in compliance with applicable legal provisions, in particular those arising from the Law on Confidence in the Digital Economy. ### **8.2. Published Data and Content** LightOn makes every effort to ensure the quality of the Data present on the Platform over which it has control. However, any Content available through the Platform and User Services is the sole responsibility of its author or the person who created or submitted it, provided that LightOn is not the author. LightOn does not control or guarantee the truthfulness, quality, accuracy, validity, relevance, or legality of the Content or the results generated by the use of the Platform or the User Services. As a service provider, LightOn has no control over User behavior; LightOn’s role is limited to facilitating access to and making available the User Services and the Platform. Consequently, LightOn cannot be held liable in this regard, particularly with respect to any damages that may result. If LightOn were to become the subject of an out-of-court or legal proceeding due to the use of the Platform or User Services, it may seek to hold the Customer and/or the User concerned liable to obtain compensation for all damages, sums, judgments, and costs that may arise from such a proceeding. The User acknowledges that LightOn, where possible, reserves the right, without being obligated to do so, to review the Content, remove it, or modify it in order to, in particular, (i) operate, secure, and improve the Platform and User Services, (ii) ensure compliance with applicable provisions, including these Terms, (iii) comply with applicable law or the decision of any competent court. ### **8.3. Relationships Between Users** Any correspondence, exchange, or relationship of any kind between Users via the Platform or User Services, including any comments, notes, or corrections associated with the Content, is binding only on those Users. Consequently, LightOn shall not be held liable for any damage or loss caused during the use of the Platform or User Services by Users. The Customer and, where applicable, the User involved undertake to LightOn to defend, indemnify, hold harmless, and release LightOn from any and all claims, costs, demands, proceedings, damages, and losses (including reasonable attorneys’ fees) incurred, suffered, or paid by LightOn, arising out of or in connection with their relationships (whether contractual or otherwise). ## **9. Data Protection** ### **9.1. Processing of Personal Data** In connection with the provision of the Platform and the delivery of User Services, LightOn acknowledges that it may be required to collect and process Personal Data on behalf of and for the Customer, particularly when LightOn controls all or part of the technical infrastructure necessary to operate the Platform. Users are informed that LightOn acts in such cases as a data processor within the meaning of data protection legislation and undertakes to process the Personal Data entrusted to it in accordance with the following provisions, and to ensure compliance with these terms by its permanent and non-permanent staff, as well as any subcontractors, in particular by imposing on them commitments similar to those set forth below. LightOn will process Personal Data only: 1. to provide, manage, protect, update, and improve the quality of the User Services under its control, 2. to enforce the Terms and exercise its rights hereunder, 3. in accordance with the law, and 4. in accordance with the Customer’s instructions. LightOn processes Users’ Personal Data in accordance with its Data Protection Policy and the measures implemented and described elsewhere, including in the legally binding documents established and accepted by the Customer and LightOn. ### **9.2. Data Protection Agreement** LightOn processes Personal Data on behalf of the Customer in accordance with the Data Protection Agreements established with the Customer. ### **9.3. Security** LightOn is committed to maintaining an information security program that complies with industry standards, consisting of technical and organizational safeguards designed to protect the data it processes. Where it has control over them, LightOn ensures the security and confidentiality of User accounts, particularly with regard to account credentials (username and passwords, API keys, etc.). Notwithstanding the foregoing, the Customer and/or User is responsible for maintaining the security and confidentiality of the credentials used to access the Platform or User Services. ### **9.4. Subcontractors** LightOn may engage subcontractors to assist in providing the User Services, provided that: * the subcontractors receiving Personal Data are subject to confidentiality obligations no less protective than those set forth in these Terms; * LightOn remains responsible for its obligations under these Terms and for the use of Personal Data by subcontractors. ## **10. Confidentiality** ### **10.1. Use** If the Parties disclose Confidential Information, the recipient shall use the disclosing Party’s Confidential Information only to exercise its rights and fulfill its obligations under the Terms and applicable provisions. The recipient shall exercise reasonable care to protect the Confidential Information. ### **10.2. Non-Disclosure** Unless required by law or regulation, the recipient shall not disclose Confidential Information to anyone, except to its affiliates, employees, agents, or subcontractors who need to know such information and who are bound by confidentiality obligations at least as protective of the Confidential Information as those described in this section. The recipient shall be liable for any breach of this section by such parties. A breach of this section may cause irreparable harm and entitle the disclosing party to an injunction. ### **10.3. Required Disclosure** The Recipient may disclose Confidential Information to the extent required by law, provided that it uses reasonable efforts to notify the Disclosing Party in advance and gives the Disclosing Party the opportunity to object to the disclosure (except as described in the “Legal Proceedings” section). Confidential Information disclosed pursuant to this section shall otherwise remain subject to the confidentiality obligations described above. ## **11. Intellectual Property Rights** ### **11.1. Ownership** Unless expressly stated otherwise, these Terms do not grant any rights, implied or otherwise, to intellectual property. The Customer owns and will continue to own all intellectual property rights in the Customer Data, including any documents or information it provides or makes available during its use of the Platform or User Services, questions asked by Users and the answers generated by the Platform, and LightOn owns and will continue to own all intellectual property rights in the Platform and the User Services, including the original source code that enables their operation. ### **11.2. Licenses** The Customer grants LightOn a worldwide, non-exclusive license to access, host, use, process, cache, copy, modify, distribute, execute, export, and display the Content and Customer Data solely for the purpose of providing the User Services or as described in the Terms. The Customer represents and warrants that it has obtained all necessary licenses or rights to grant the aforementioned license. LightOn grants the Customer a non-exclusive worldwide license to access, use, export, and display the Content and Customer Data solely for the purpose of providing the User Services or as otherwise described in the Terms. LightOn represents and warrants that it has obtained all necessary rights and licenses to grant the aforementioned license. ## **12. Suspension and Management of the Platform** Within the scope, particularly technical, controlled by LightOn, LightOn may limit or suspend, temporarily or permanently (termination), the User’s access to and use of the Platform and the User Services: 1. to the extent required by law, 2. to prevent injury, death, or a credible risk of harm to LightOn, the User Services, Users, or any third party 3. for failure to pay, 4. for repeated violations of applicable provisions and, generally, any of our policies or regulations, particularly regarding copyright, 5. at the Customer’s request. In this regard, LightOn undertakes to notify, depending on the circumstances, the User or the Customer, who then undertakes to forward the information to the Users affected by the measure, of the implementation of such a measure in order to give them the time and opportunity to provide explanations to LightOn and resolve the issue prior to suspension, if applicable. The lifting of such measures is at LightOn’s sole discretion. Notwithstanding the foregoing, in the event of a manifest violation involving offensive, discriminatory, racist, or hateful language (including in an account name, company name, or any other identifier), LightOn may proceed with the immediate deletion of the Account and associated data, without prior notice. LightOn may retain, for evidentiary purposes and for the retention period defined in Section 5 of the [Data Usage Policy](/legal/privacy-policy), the elements constituting the violation (e.g., time-stamped screenshots, registration logs), solely for the purpose of demonstrating the violation in the event of a dispute, legal proceeding, or a request from a competent authority. Provisions that by their nature are intended to survive termination shall survive, including confidentiality obligations, limitations of liability, and disclaimers. ## **13. Legal Proceedings** If LightOn receives a request from law enforcement or another duly authorized third party to provide data or information about the Customer or its Users, LightOn will comply with a valid legal proceeding only to the extent required by law. LightOn will notify the Customer and/or Users of the request unless expressly prohibited from doing so or unless such notification would jeopardize an individual’s safety. ## **14. Miscellaneous** ### **14.1. Amendments** These Terms constitute the entire agreement between Users and LightOn regarding the use of the Platform and User Services. Any other document, including any mention on the Platform, is for informational purposes only. LightOn may update these Terms at its discretion. Any updates to the Terms must be accepted by Users when they access the Platform following such an update. ### **14.2. Severability** If any provision of these Terms is deemed invalid, illegal, or unenforceable by a court decision or proves impossible to implement, the validity, legality, and enforceability of the other provisions of the Terms shall in no way be affected or impaired thereby, and each such provision of the Terms shall be valid and enforceable to the fullest extent permitted by law. In such a case, these Terms will be amended to the minimum extent necessary to remedy any invalidity, illegality, or unenforceability, while preserving to the greatest extent possible the rights and commercial expectations of the Parties hereto, as expressed in the Terms. The headings and subheadings contained in these Terms are included for convenience only and shall not be used to interpret any provision of the Terms. LightOn’s failure to enforce any provision of the Terms or to tolerate a temporary or permanent breach thereof shall in no way be construed as a waiver by LightOn of its rights hereunder. LightOn’s tolerance of a breach or imperfect performance of the Terms, or more generally of any act, failure to act, or omission by the User or the Customer that does not comply with the provisions of the Terms, shall not confer any right whatsoever on the User or the Customer benefiting from such tolerance. Users acknowledge that the Terms and Conditions shall not have the effect of creating, between them and LightOn, a partnership or association of any kind. ### **14.3. Hyperlinks** From LightOn to external sites LightOn may provide links from the Platform to other resources accessible via the Internet. In such cases, these resources are independent of the Platform and are owned by third parties. LightOn does not edit or control these sites. Links to any site or resource do not, under any circumstances, constitute an endorsement of their content, products, advertising, or any other goods or services presented on these sites. Consequently, LightOn cannot be held liable for any damage of any kind resulting from the content of these sites. By Authorized Users to the Platform The creation by Users of any hyperlink to all or part of the Platform is strictly prohibited, except: * in cases provided for by the Platform and, in particular, for the purpose of making Content accessible to Users who need to know, * prior written authorization from LightOn, requested via email. LightOn is free to refuse the creation by Authorized Users of any hyperlink to all or part of the Platform. In the event that LightOn grants its authorization, such authorization may be withdrawn at any time, without any obligation on the part of LightOn to provide justification. ### **14.4. Cookies and trackers** LightOn may automatically collect, through the Platform, standard information via the use of cookies or similar technologies. All information collected indirectly will be used solely to provide User Services, track the volume, type, and configuration of traffic using the Platform, to develop its design and layout, and for other administrative and planning purposes, and more generally to improve the User Services that LightOn offers. In particular, on the LightOn Console web interface, LightOn uses Sentry Error Reporting, a technical monitoring tool enabled by default for SaaS users (excluding Enterprise plans) on the basis of LightOn’s legitimate interest in maintaining the stability and security of the Platform. This feature does not use Session Replay, does not set persistent cross-session identifiers, and does not collect default personal data, so that it does not constitute a tracker requiring prior consent. Sentry Session Replay, which allows visual reconstruction of a user’s navigation for debugging purposes, is not available by default and is only activated upon prior explicit request. Users and Customers may opt out of Sentry Error Reporting at any time via their account settings, without this affecting their access to or the quality of the Platform. The corresponding opt-out procedure is described in LightOn’s online documentation. ### **14.5. Governing Law and Jurisdiction** These Terms shall be governed by the laws of France and the competent courts of Paris. LightOn, the Customer, and the Users agree that, in the event of difficulties in the performance of these Terms and prior to any legal proceedings, they will seek an amicable resolution to any dispute. To this end, the Party wishing to initiate this amicable procedure must notify the other Party by registered letter with acknowledgment of receipt, specifying the difficulties encountered in application or the breaches observed. This letter shall serve as a formal notice to remedy the observed breaches and as a statement of the amicable settlement proposal. The other Party shall then have fifteen (15) calendar days to provide its interpretation of the events, its own proposal for an amicable settlement of the dispute, or its refusal of an amicable settlement. The Party that initiated the amicable settlement shall then have fifteen (15) calendar days to provide its response. However, if within thirty (30) calendar days following this response, the Parties have not reached an amicable agreement, each Party shall regain its full freedom of action and its right to bring the matter before the competent courts mentioned above. # Claude Code Source: https://developers.lighton.ai/mcp-server/claude-code Use the LightOn MCP server directly from Claude Code. ## Prerequisites * [Claude Code](https://claude.ai/code) installed * A LightOn API key from the [LightOn Console](https://console.lighton.ai) ## Configuration Run this command once to register the LightOn server globally; Claude Code will store your API key and send it as a Bearer token on every request. ```bash theme={null} claude mcp add lighton https://api.lighton.ai/mcp \ --transport http --scope user \ --header "Authorization: Bearer $LIGHTON_API_KEY" ``` `$LIGHTON_API_KEY` is interpolated by your shell when you run the command and the value is stored in Claude Code's config. Make sure it is set, you can grab your key from the [LightOn Console](https://console.lighton.ai). Omit `--scope user` to register the server for the current project only. ## Verification 1. Confirm the server is registered: ```bash theme={null} claude mcp list ``` 2. Use the `/mcp` command inside a Claude Code session to view available tools and server status. 3. Test the connection with a prompt like: ``` Search my LightOn workspaces for documents about [topic] ``` You're all set. From any Claude Code session you can now ask Claude to search or ingest documents in your LightOn workspaces directly. ## References * [Claude Code docs: Model Context Protocol](https://docs.anthropic.com/en/docs/claude-code/mcp) # Cursor Source: https://developers.lighton.ai/mcp-server/cursor Use the LightOn MCP server directly from Cursor. ## Prerequisites * [Cursor](https://cursor.com) installed * A LightOn API key from the [LightOn Console](https://console.lighton.ai) ## Configuration Cursor configures MCP servers via a JSON file. Add the following to `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "lighton": { "url": "https://api.lighton.ai/mcp", "headers": { "Authorization": "Bearer ${env:LIGHTON_API_KEY}" } } } } ``` `${env:LIGHTON_API_KEY}` is Cursor's syntax for reading an environment variable at runtime. The key is never stored in the config file. Make sure `LIGHTON_API_KEY` is set in your shell environment, you can grab your key from the [LightOn Console](https://console.lighton.ai). To scope the server to a single project instead, place the same snippet in `.cursor/mcp.json` at the root of your project. ## Verification 1. Open **Settings → Features → Model Context Protocol** and confirm `lighton` appears in the list. 2. Check the connection logs via **Output** panel (`Cmd+Shift+U`) and select **MCP Logs** from the dropdown. 3. Test the connection with a prompt like: ``` Search my LightOn workspaces for documents about [topic] ``` You're all set. From any Cursor session you can now ask it to search or ingest documents in your LightOn workspaces directly. ## References * [Cursor docs: Model Context Protocol](https://cursor.com/docs/mcp) # Mistral Vibe Source: https://developers.lighton.ai/mcp-server/mistral-vibe Use the LightOn MCP server directly from Mistral Vibe. ## Prerequisites * [Mistral Vibe](https://docs.mistral.ai/mistral-vibe/introduction/configuration) installed * A LightOn API key from the [LightOn Console](https://console.lighton.ai) ## Configuration Mistral Vibe configures MCP servers via a TOML file. Add the following to `~/.vibe/config.toml`: ```toml theme={null} [[mcp_servers]] name = "lighton" transport = "http" url = "https://api.lighton.ai/mcp" api_key_env = "LIGHTON_API_KEY" api_key_header = "Authorization" api_key_format = "Bearer {token}" ``` `api_key_env` takes the **name** of an environment variable. Mistral Vibe reads the value at runtime on every request. Make sure `LIGHTON_API_KEY` is set in your shell environment, you can grab your key from the [LightOn Console](https://console.lighton.ai). To scope the server to a single project instead, place the same snippet in `.vibe/config.toml` at the root of your project. ## Verification 1. Start a Mistral Vibe session. 2. Check that the `lighton` server is listed among the active MCP servers in the session output. 3. Test the connection with a prompt like: ``` Search my LightOn workspaces for documents about [topic] ``` You're all set. From any Mistral Vibe session you can now ask it to search or ingest documents in your LightOn workspaces directly. ## References * [Mistral Vibe docs: Configuration](https://docs.mistral.ai/mistral-vibe/introduction/configuration) # OpenAI Codex Source: https://developers.lighton.ai/mcp-server/openai-codex Use the LightOn MCP server directly from OpenAI Codex. ## Prerequisites * [OpenAI Codex](https://openai.com/codex) installed * A LightOn API key from the [LightOn Console](https://console.lighton.ai) ## Configuration Codex configures MCP servers via a TOML file. Add the following to `~/.codex/config.toml`: ```toml theme={null} [mcp_servers.lighton] url = "https://api.lighton.ai/mcp" bearer_token_env_var = "LIGHTON_API_KEY" ``` `bearer_token_env_var` takes the **name** of an environment variable. Codex reads the value at runtime on every request. Make sure `LIGHTON_API_KEY` is set in your shell environment, you can grab your key from the [LightOn Console](https://console.lighton.ai). To scope the server to a single project instead, place the same snippet in `.codex/config.toml` at the root of your project. ## Verification 1. Open Codex and start a session. 2. Use the `/mcp` command to list configured servers and verify that `lighton` appears in the output. 3. Test the connection with a prompt like: ``` Search my LightOn workspaces for documents about [topic] ``` You're all set. From any Codex session you can now ask it to search or ingest documents in your LightOn workspaces directly. ## References * [Codex docs: Model Context Protocol](https://developers.openai.com/codex/mcp) # OpenCode Source: https://developers.lighton.ai/mcp-server/opencode Use the LightOn MCP server directly from OpenCode. ## Prerequisites * [OpenCode](https://opencode.ai) installed * A LightOn API key from the [LightOn Console](https://console.lighton.ai) ## Configuration OpenCode configures MCP servers via a JSON file. Add the following to `~/.config/opencode/opencode.jsonc`: ```json theme={null} { "$schema": "https://opencode.ai/config.json", "mcp": { "lighton": { "type": "remote", "url": "https://api.lighton.ai/mcp", "enabled": true, "oauth": false, "headers": { "Authorization": "Bearer {env:LIGHTON_API_KEY}" } } } } ``` `{env:LIGHTON_API_KEY}` is OpenCode's syntax for reading an environment variable at runtime. The key is never stored in the config file. Make sure `LIGHTON_API_KEY` is set in your shell environment, you can grab your key from the [LightOn Console](https://console.lighton.ai). To scope the server to a single project instead, place the same snippet in `opencode.jsonc` at the root of your project. ## Verification 1. List all configured MCP servers: ```bash theme={null} opencode mcp list ``` 2. Confirm `lighton` appears and shows as authenticated. 3. Test the connection with a prompt like: ``` Search my LightOn workspaces for documents about [topic] ``` You're all set. From any OpenCode session you can now ask it to search or ingest documents in your LightOn workspaces directly. ## References * [OpenCode docs: MCP Servers](https://opencode.ai/docs/mcp-servers/) # Quickstart Source: https://developers.lighton.ai/mcp-server/quickstart Connect to the LightOn MCP server. LightOn exposes a hosted MCP server that lets MCP-compatible clients (Claude Desktop, Claude Code, Cursor, etc.) search and ingest documents in your workspaces. ## Server URL ``` https://api.lighton.ai/mcp ``` ## Authentication The server accepts an API key in two forms. Pick whichever fits your client. Pass the key in the `Authorization` header. Recommended for clients that support custom headers. ``` Authorization: Bearer $LIGHTON_API_KEY ``` Append `?apiKey=$LIGHTON_API_KEY` to the server URL. Useful when the client only accepts a plain URL and no headers. ``` https://api.lighton.ai/mcp?apiKey=$LIGHTON_API_KEY ``` # Tools Source: https://developers.lighton.ai/mcp-server/tools Reference for all tools exposed by the LightOn MCP server. The LightOn MCP server exposes three tools. Use `list_workspaces` and `list_tags` to resolve names to IDs, then pass those IDs to `search` to scope your queries. *** ## search Retrieve the most relevant passages from your indexed documents for a natural-language query. Natural-language search query. Must be self-contained: resolve pronouns and inline any necessary context. Max 1500 characters. Restrict the search to these workspace IDs. Cannot be combined with `file_ids`. Restrict the search to specific file IDs. Cannot be combined with `workspace_ids` or `tag_ids`. Restrict the search to documents carrying any of these tag IDs. Cannot be combined with `file_ids`. Maximum number of chunks to return. Range: 1–50. Search mode. `"text"` uses hybrid keyword + vector search. `"vision"` uses VLM-embedded page image search. *** ## list\_workspaces List every workspace the caller has access to, each annotated with an auto-generated summary. Use this to resolve a workspace name to an ID before scoping a `search` call. No parameters. *** ## list\_tags List every document tag defined within the caller's company. Use this to map a human-readable label to a `tag_id` for filtering searches. No parameters. # Windsurf Source: https://developers.lighton.ai/mcp-server/windsurf Use the LightOn MCP server directly from Windsurf. ## Prerequisites * [Windsurf](https://windsurf.com) installed * A LightOn API key from the [LightOn Console](https://console.lighton.ai) ## Configuration Windsurf configures MCP servers via a JSON file. Add the following to `~/.codeium/windsurf/mcp_config.json`: ```json theme={null} { "mcpServers": { "lighton": { "serverUrl": "https://api.lighton.ai/mcp", "headers": { "Authorization": "Bearer ${env:LIGHTON_API_KEY}" } } } } ``` `${env:LIGHTON_API_KEY}` is Windsurf's syntax for reading an environment variable at runtime. The key is never stored in the config file. Make sure `LIGHTON_API_KEY` is set in your shell environment, you can grab your key from the [LightOn Console](https://console.lighton.ai). ## Verification 1. Open the MCP panel in Windsurf and confirm `lighton` appears and shows as connected. 2. Test the connection with a prompt like: ``` Search my LightOn workspaces for documents about [topic] ``` You're all set. From any Windsurf session you can now ask Cascade to search or ingest documents in your LightOn workspaces directly. ## References * [Windsurf docs: MCP](https://docs.windsurf.com/windsurf/cascade/mcp) # Microsoft Entra Configuration Source: https://developers.lighton.ai/microsoft-entra-configuration Step-by-step guide to configure the Console SAML SSO with Microsoft EntraID This guide connects a Microsoft Entra ID tenant to Console with SAML 2.0, so users sign in with their Microsoft account. It keeps the default claims Entra creates for a new application. Console identifies each user by their Entra object ID, so later changes to their email or name never break the link. Two people are involved: * **Entra administrator**: creates the Enterprise Application and assigns users. Needs the Cloud Application Administrator role or higher. * **Console administrator**: creates the SAML Social Application in the Console admin (Django admin → Social applications). | Value | Example | Chosen by | | :- | :- | :- | | Console host | `https://console.example.com` | Console administrator | | Client ID (SAML identifier) | `acme-entra-sso` | Console administrator. Lowercase letters, digits and dashes; it appears in the URLs. | | Entra tenant ID | `4eb1dc68-1234-5abc-1234-1a1234b12cd3` | Entra (read-only) | | Entra application ID | `775f0e7b-3c72-4da2-b8ef-0e41a70ae33c` | Entra (read-only) | `` **is the company name in slug form**., as it appears in the company's Console login URL (for example `entraid-sso-demo`)

The rest of this guide writes these as ``, ``, `` and ``.
## Prerequisites : enable SSO on the Console instance SSO sign-in only works once the instance-wide config key `SSO` is `True`. A Console instance administrator sets these keys in the Console admin, under **Config key values** (authentication category): | Config key | Set to | Effect | | :- | :- | :- | | `SSO` | `True` (required) | Global switch for company SAML and OIDC sign-in. When `False`, the SSO button is hidden, the company login page returns "No SSO provider matches this company", and SSO redirects return 404. | | `SSO_LDAP_SIGNUP` | `True` (optional) | Allows SSO to create accounts on first sign-in. The company's **Auto provision external users** setting must also be on. Leave `False` if users are created or invited beforehand. | Company settings, in the Console admin under **Companies**: * **Auto provision external users**: turn on together with `SSO_LDAP_SIGNUP` to create accounts automatically. * **Login method**: set to **Single Sign On** to require SSO and block password sign-in for the company's users. Leave it on **Password** while you test. Each company needs exactly one Console SSO application. The company login page is found from the company name in slug form, and two applications for the same company make it return the same "not found" error. ## Step 1 : Create the Enterprise Application in Entra 1. In the [Microsoft Entra admin center](https://entra.microsoft.com), go to **Identity → Applications → Enterprise applications**. 2. Click **New application → Create your own application**. 3. Enter a name, for example `Console SSO Demo`, and choose **Integrate any other application you don't find in the gallery (Non-gallery)**. Click **Create**. 4. In the new application, open **Single sign-on** and choose **SAML**. Entraid Saml 1a Entraid Saml 1b ## Step 2 : Basic SAML Configuration On the SAML page, edit **Basic SAML Configuration** and set: | Entra field | Value | Notes | | :- | :- | :- | | Identifier (Entity ID) | `` | Must exactly match `sp.entity_id` in the Console settings (Step 5). Mark it as **Default**. | | Reply URL (Assertion Consumer Service URL) | `https:///auth/saml//acs/` | Keep the trailing slash. Uses the Console **Client ID**, not the provider ID. | | Sign on URL (optional) | `https:///auth/saml//login/` | Lets users start sign-in from the My Apps portal. | | Relay State, Logout URL | leave empty | | Save. Leave **SAML Certificates** as they are: Console reads the signing certificate from the federation metadata URL. Entraid Saml 2a ## Step 3 : Attributes & Claims Keep the defaults Entra creates for a new application. Don't add, rename or delete claims. | Claim name | Source attribute | Used by Console as | | :- | :- | :- | | Unique User Identifier (Name ID), format *Email address* | `user.userprincipalname` | Email, only when the `emailaddress` claim is missing | | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress` | `user.mail` | Email | | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname` | `user.givenname` | First name | | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname` | `user.surname` | Last name | | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name` | `user.userprincipalname` | Username | | `http://schemas.microsoft.com/identity/claims/objectidentifier` | built in, not listed on this page | Account identifier | Entraid Saml 3a Entraid Saml 3b Entra adds `objectidentifier` (with `tenantid`, `identityprovider` and `authnmethodsreferences`) to every token automatically. You don't need to configure it. **Check user emails.** Entra omits a claim whose source attribute is empty.
If a user's **Email** (`mail`) is blank, Console falls back to the Name ID (the UPN).
Make sure each user's `mail` or UPN is the address they use in Console: **Users → the user → Properties → Contact information → Email**.
## Step 4 : Collect the Entra values and assign users The Entra administrator sends these two values from the SAML page to the Console administrator: | Entra field | Where | Example | | :- | :- | :- | | Microsoft Entra Identifier | Section 4, *Set up …* | `https://sts.windows.net//` | | App Federation Metadata Url | Section 3, *SAML Certificates* | `https://login.microsoftonline.com//federationmetadata/2007-06/federationmetadata.xml?appid=` | Entraid Saml 4a Copy the Microsoft Entra Identifier exactly, **including the trailing slash.** Then assign who may sign in: **Users and groups → Add user/group**.
A user who isn't assigned gets an Entra error (AADSTS50105) before reaching Console. Entraid Saml 4b ## Step 5 : Configure the Social Application in Console In the Console Django admin, open **Social applications → Add** and fill in: | Field | Value | | :- | :- | | Provider | `SAML` | | Provider ID | A unique slug, for example `acme-entra-sso` | | Name | An internal label shown only in the Console admin, for example `Acme Entra SSO`. The company login page shows the company name instead. | | Company | The customer's company | | Application | `Console` | | Client ID | ``, the same value as in Steps 2 and 3 | | Secret | leave empty | Set **Settings** to the JSON below, replacing the placeholders: ```json theme={null} { "sp": {"entity_id": ""}, "idp": { "entity_id": "https://sts.windows.net//", "metadata_url": "https://login.microsoftonline.com//federationmetadata/2007-06/federationmetadata.xml?appid=" }, "attribute_mapping": { "uid": "http://schemas.microsoft.com/identity/claims/objectidentifier", "email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "username": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "first_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "last_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" } } ``` | Key | Must equal | | :- | :- | | `sp.entity_id` | Entra **Identifier (Entity ID)** | | `idp.entity_id` | Entra **Microsoft Entra Identifier** | | `idp.metadata_url` | Entra **App Federation Metadata Url** | Entraid Saml 5a The admin form checks only `sp.entity_id` and `idp.entity_id`. A typo in a claim name saves without an error and leaves that field empty, so copy the claim names exactly. **Account creation.** On first sign-in, Console links the user to an existing account with the same email in this company.
To create missing users automatically, enable both the instance config key `SSO_LDAP_SIGNUP` and the company's **Auto provision external users** setting.
Otherwise, create or invite users before they sign in.
## Step 6 : Test the sign-in 1. In Entra, open **Single sign-on → Test this application** and sign in as an assigned user. 2. In a private browser window, open the Console login page, `https:///login`, click on `SSO d'entreprise`, write slugified company name (`My Company` would become `my-company`) then click on `Continuer` to arrive on the dedicated company SSO login page. Click on `Se Connecter` to start the login process with EntraID. Entraid Saml 6a Entraid Saml 6b Entraid Saml 6c As you can see, the button to start the SSO authentication is the one you put as login URL in the SSO configuration in EntraID. What happens on a user's first sign-in: | Situation | Result | | :- | :- | | An account with that email exists in the company | Linked, then signed in | | No account, and auto-provisioning is enabled | Account created in the company | | No account, and auto-provisioning is disabled | Refused | | The email belongs to an account in another company | Refused | Later sign-ins match on the Entra object ID, not the email. ## Debugging advice If sign-in still fails, SAML-tracer shows what your browser exchanges with Microsoft and Console. It's a free browser extension, and you don't need admin rights to use it. * [SAML-tracer for Chrome](https://chromewebstore.google.com/detail/saml-tracer/mpdajninpobndbfcldcmbpnnbhibjmch) * [SAML-tracer for Firefox](https://addons.mozilla.org/firefox/addon/saml-tracer/) Image **Record a sign-in** 1. Click the SAML-tracer icon in your browser toolbar to open its window. 2. In your main browser window, open `https:////login` and sign in with Microsoft. 3. In SAML-tracer, select the rows tagged **SAML** and open the **SAML** tab to read each message. **What to check** | Check | What you should see | If not | | :- | :- | :- | | 1. Console sends the request to Microsoft | A request to `login.microsoftonline.com//saml2`. Its `Issuer` is `` and its `AssertionConsumerServiceURL` is the Reply URL. | Compare Step 2 with Step 5 | | 2. Microsoft identifies the user by email | In the response sent to `/auth/saml//acs/`, the `NameID` in the `Subject` is the user's email | The user's email is empty in Entra (Step 3) | | 3. Microsoft sends the user's details | The response lists `http://schemas.microsoft.com/identity/claims/objectidentifier` and `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress` | Check the claims (Step 3) and their spelling in `attribute_mapping` (Step 5) | **Sharing the recording with support** Still stuck? Use SAML-tracer's **Export** button and send the file to LightOn support with a short description of the problem. A recording contains personal and security data: the user's email and name, a signed sign-in message from Microsoft, and possibly session cookies. Handle it like a password: * Record with a test account when you can. * In the export dialog, choose to mask or remove cookie values. * Send it only to LightOn support, through your usual support channel. Never post it in a public forum, a shared chat or a ticket that other people can read. * After recording, sign out of Console and delete the file once the issue is resolved. # Prompting Source: https://developers.lighton.ai/prompting/index Best practices for writing effective prompts with LightOn APIs. Coming soon. # Quickstart Source: https://developers.lighton.ai/quickstart Get a search result in under 5 minutes. ## 1. Get your API key Create one from the **API Keys** section of the [console](https://console.lighton.ai) and copy it. You will not see it again. Then set it as an environment variable: ```bash theme={null} export LIGHTON_API_KEY=your_api_key_here ``` See [Authentication](/authentication) for more on key management. ## 2. Upload a document ```python Python SDK theme={null} from lighton import File, LightOn, Workspace workspace_id = 42 # replace with your workspace ID with LightOn() as client: # reads LIGHTON_API_KEY from the environment workspace = Workspace.get(client, workspace_id) doc = workspace.ingest(File(path="handbook.pdf")) print(doc.id, doc.status) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn, Workspace } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // reads LIGHTON_API_KEY from the environment const workspace = await Workspace.get(client, workspaceId) const doc = await workspace.ingest(new File({ path: "handbook.pdf" })) console.log(doc.id, doc.status) ``` ```python Plain Python theme={null} import os import requests workspace_id = 42 # replace with your workspace ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} with open("handbook.pdf", "rb") as f: response = requests.post( "https://api.lighton.ai/api/v3/files", headers=headers, data={"workspace_id": workspace_id}, files={"file": f}, ) print(response.json()) ``` The response returns the new file with its `id` and a `status` of `pending`: ```json theme={null} {"id": 12345, "status": "pending", "workspace_id": 42} ``` Indexing takes a few seconds. Poll until `status` is `embedded`: ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # the ID returned by the upload above with LightOn() as client: doc = File.get(client, file_id) doc.wait() # polls until ingestion reaches a terminal status print(doc.status) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // the ID returned by the upload above const client = new LightOn() const doc = await File.get(client, fileId) await doc.wait() // polls until ingestion reaches a terminal status console.log(doc.status) ``` ```python Plain Python theme={null} import os import time import requests file_id = 1234 # the ID returned by the upload above headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} for _ in range(60): # give up after ~2 minutes response = requests.get( f"https://api.lighton.ai/api/v3/files/{file_id}", headers=headers, ) status = response.json()["status"] if status == "embedded": break time.sleep(2) print(status) ``` ## 3. Search it ```python Python SDK theme={null} from lighton import LightOn workspace_id = 42 # replace with your workspace ID with LightOn() as client: response = client.search( "What is the vacation policy at LightOn?", workspaces=[workspace_id], max_results=3, ) for result in response.results: source = result.source print(f"[p.{source.page_start}–{source.page_end}, score={result.score:.2f}]") print(result.content[:120]) print() ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() const response = await client.search("What is the vacation policy at LightOn?", { workspaces: [workspaceId], maxResults: 3, }) for (const result of response.results) { const source = result.source console.log(`[p.${source.pageStart}–${source.pageEnd}, score=${result.score.toFixed(2)}]`) console.log((result.content ?? "").slice(0, 120)) console.log() } ``` ```python Plain Python theme={null} import os import requests workspace_id = 42 # replace with your workspace ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json={ "query": "What is the vacation policy at LightOn?", "workspace_id": [workspace_id], "max_results": 3, }, ) for result in response.json()["results"]: print(f"[p.{result['source']['page_start']}–{result['source']['page_end']}, score={result['score']:.2f}]") print(result["content"][:120]) print() ``` ``` [p.4–4, score=0.94] Employees are entitled to 25 days of paid leave per year... [p.4–4, score=0.87] Unused vacation days carry over up to a maximum of 10 days... [p.7–7, score=0.71] Public holidays are in addition to the annual leave entitlement... ``` Three API calls: upload, wait, search. That's the full pipeline. Learn how API keys work and how to handle auth errors. # SDKs Source: https://developers.lighton.ai/sdks/index Official client libraries for the LightOn API. ## Supported languages [`lighton-sdk`](https://github.com/lightonai/lighton-python-sdk): typed models, pagination and rate-limit pacing handled for you. Requires Python 3.11+. [`@lighton-ai/sdk`](https://github.com/lightonai/lighton-typescript-sdk): typed models, pagination and rate-limit pacing handled for you. Requires Node 22+, also runs on Bun, Deno, edge runtimes, and in the browser. ## Other languages No official client yet. Every endpoint is a plain HTTP call, so any HTTP library works: see the [API reference](/api-reference/introduction) for the request shapes, and the **Plain Python** tab of any tutorial snippet for a worked example you can port. # Python quick start Source: https://developers.lighton.ai/sdks/python/quickstart Install lighton-sdk and get your first grounded answer. [`lighton-sdk`](https://github.com/lightonai/lighton-python-sdk) wraps the LightOn API: typed models, pagination handled for you, and rate-limit pacing built in. It's the code shown in the **Python SDK** tab of every tutorial snippet. Requires Python 3.11 or later. ## Install ```bash theme={null} uv add lighton-sdk ``` Set your API key in the environment. `LightOn()` reads it from there, so it never appears in your code: ```bash theme={null} export LIGHTON_API_KEY="..." ``` ## Your first result ```python theme={null} from lighton import LightOn, Workspace with LightOn() as client: # reads LIGHTON_API_KEY from the environment workspace = Workspace(name="Docs").create(client) workspace.ingest_many(["docs/**/*.pdf"], wait=True) answer = client.ask("What were Q4 revenues?", workspaces=[workspace]) print(answer.answer) ``` ## Where to go next Every public class, method, and type, generated from the installed SDK. Task-by-task walkthroughs, each with a **Python SDK** tab. For `parse`, `extract`, async jobs, and agent-framework integrations (LangChain, LangGraph, LlamaIndex, OpenAI Agents, CrewAI), see the [SDK README](https://github.com/lightonai/lighton-python-sdk#readme). # Python SDK reference Source: https://developers.lighton.ai/sdks/python/reference Every public class, method, and type in lighton-sdk 1.2.0. Generated from [`lighton-sdk`](https://github.com/lightonai/lighton-python-sdk) **1.2.0**. Install it and see runnable examples in the [quick start](/sdks/python/quickstart). ## Client ### LightOn #### `LightOn.ask()` ```python theme={null} ask( query: str, *, workspaces: list[Workspace | int] | None = None, tags: list[Tag | int | str] | None = None, files: list[File | int] | None = None, content_type: list[ContentType | str] | None = None, attribute: list[str] | None = None, max_results: int | None = None, relevance_scoring: RelevanceScoring | None = None, model: str | None = None, schema: type[BaseModel] | dict[str, Any] | None = None, stream: bool = False, ) -> AskResponse | Generator[AskEvent, None, None] ``` POST /api/v3/ask, ask a grounded question over indexed documents. **Arguments** | Name | Type | Description | | - | - | - | | `query` | `str` | Natural-language question (max 1500 chars). | | `workspaces` | `list[Workspace \| int] \| None` | Restrict to these workspaces (Workspace objects or ids). Excludes files. | | `tags` | `list[Tag \| int \| str] \| None` | Restrict to documents carrying any of these tags, Tag objects, ids, or names (OR-matched). Names are resolved via Tag.list() and must exist. Excludes files. | | `files` | `list[File \| int] \| None` | Restrict to these files (File objects or ids). Excludes workspaces and tags. | | `content_type` | `list[ContentType \| str] \| None` | Restrict to these content-type paths, ContentType objects or path strings (OR-matched, exact-or-subtree, e.g. "legal" also matches "legal:contract"; wildcards `legal:contract*`, `*nda*`). | | `attribute` | `list[str] \| None` | Restrict by attribute value, e.g. `["fiscal_year:2024\|2025", "status:active"]`. Entries are ANDed, `\|` ORs within one entry. Also `name` (has any value), `name:>value`, `name:prefix*`, `name:*text*`. | | `max_results` | `int \| None` | Chunks to retrieve for context (1–50; server default 10). | | `relevance_scoring` | `RelevanceScoring \| None` | RelevanceScoring, .scoring\_and\_filtering (default), .scoring\_only, or .none. | | `model` | `str \| None` | LLM for answer generation; platform default if omitted. | | `schema` | `type[BaseModel] \| dict[str, Any] \| None` | Constrain the answer to structured output, a pydantic model class or a JSON-Schema dict (same inputs as `extract`, sent as the API's `response_format`; must describe an object). The answer then comes back as JSON *text* in `.answer`, parse it with `YourModel.model_validate_json(resp.answer)`. | | `stream` | `bool` | Return an iterator of Server-Sent Events instead of the whole answer, for showing it as it generates. Composes with `schema`: the tokens then spell out the JSON, so concatenate them and parse at the end. | **Returns** | Type | Description | | - | - | | `AskResponse \| Generator[AskEvent, None, None]` | `AskResponse` normally. When `stream=True`, an iterator of `SourcesEvent` (once, before generation), `TokenEvent` (repeatedly) and `DoneEvent` (last). Being a generator, nothing is sent until you start iterating, so request errors surface on the first step, not here. Iterate it fully or close it, so the connection is released. | **Raises** | Exception | When | | - | - | | `StreamError` | If the server reports a failure mid-stream; the answer is incomplete at that point. | #### `LightOn.close()` ```python theme={null} close() -> None ``` #### `LightOn.extract()` ```python theme={null} extract( schema: type[BaseModel] | dict[str, Any], *, path: str | Path | None = None, url: str | None = None, file: File | int | None = None, options: dict[str, Any] | None = None, mode: ExecMode = ExecMode.SYNC, wait: bool = False, timeout: float = 300.0, ) -> ExtractJobResponse | ExtractJob ``` POST /api/v3/extract, extract structured data from a document. Pass exactly one of: path: A local file to upload (multipart). url: A publicly accessible URL to fetch. file: An already-ingested file (File object or id), no re-upload, the server reads the document it already has. **Arguments** | Name | Type | Description | | - | - | - | | `schema` | `type[BaseModel] \| dict[str, Any]` | The guided-generation schema driving extraction, either a pydantic model class (converted to a vLLM `response_format` JSON Schema) or a dict already holding a valid such schema. | | `options` | `dict[str, Any] \| None` | Free-form request options; currently `{"async": bool}`. | | `mode` | `ExecMode` | ExecMode.SYNC (default) runs inline and returns the extracted data. ExecMode.ASYNC queues the job and returns an `ExtractJob` handle, call `.poll()` until `.succeeded`. | | `wait` | `bool` | Async only. Block until the job is terminal, so the returned `ExtractJob` already carries its `result`. | | `timeout` | `float` | Seconds to wait when wait=True before raising TimeoutError. | **Returns** | Type | Description | | - | - | | `ExtractJobResponse \| ExtractJob` | `ExtractJobResponse` (with data) when sync; an `ExtractJob` when async, pollable (wait=False) or already finished (wait=True). | **Raises** | Exception | When | | - | - | | `ValueError` | If not exactly one of path/url/file is given, or wait=True without ExecMode.ASYNC (sync already blocks). | | `TimeoutError` | If wait=True and `timeout` elapses first. | | `LightOnError` | If wait=True and the job ends in failure. | #### `LightOn.parse()` ```python theme={null} parse( *, path: str | Path | None = None, url: str | None = None, mode: ExecMode = ExecMode.SYNC, wait: bool = False, timeout: float = 300.0, ) -> ParseResponse | ParseJob ``` POST /api/v3/parse, parse a document into per-page text. Pass exactly one of: path: A local file to upload (multipart). url: A publicly accessible URL to fetch. **Arguments** | Name | Type | Description | | - | - | - | | `mode` | `ExecMode` | ExecMode.SYNC (default) runs inline and returns the full `ParseResponse`. ExecMode.ASYNC queues the job and returns a `ParseJob` handle, call `.poll()` until `.succeeded`. | | `wait` | `bool` | Async only. Block until the job is terminal, so the returned `ParseJob` already carries its `result`. | | `timeout` | `float` | Seconds to wait when wait=True before raising TimeoutError. | **Returns** | Type | Description | | - | - | | `ParseResponse \| ParseJob` | `ParseResponse` when sync; a `ParseJob` when async, pollable (wait=False) or already finished (wait=True). | **Raises** | Exception | When | | - | - | | `ValueError` | If not exactly one of path/url is given, or wait=True without ExecMode.ASYNC (sync already blocks). | | `TimeoutError` | If wait=True and `timeout` elapses first. | | `LightOnError` | If wait=True and the job ends in failure. | #### `LightOn.search()` ```python theme={null} search( query: str, *, workspaces: list[Workspace | int] | None = None, tags: list[Tag | int | str] | None = None, files: list[File | int] | None = None, content_type: list[ContentType | str] | None = None, attribute: list[str] | None = None, max_results: int | None = None, mode: SearchMode | None = None, relevance_scoring: RelevanceScoring | None = None, include_image: bool | None = None, include_bboxes: bool | None = None, ) -> SearchResponse ``` POST /api/v3/search, retrieve relevant passages (no generation). **Arguments** | Name | Type | Description | | - | - | - | | `query` | `str` | Natural-language search query (max 4000 chars). | | `workspaces` | `list[Workspace \| int] \| None` | Restrict to these workspaces (Workspace objects or ids). Excludes files. | | `tags` | `list[Tag \| int \| str] \| None` | Restrict to documents carrying any of these tags, Tag objects, ids, or names (OR-matched). Names are resolved via Tag.list() and must exist. Excludes files. | | `files` | `list[File \| int] \| None` | Restrict to these files (File objects or ids). Excludes workspaces and tags. | | `content_type` | `list[ContentType \| str] \| None` | Restrict to these content-type paths, ContentType objects or path strings (OR-matched, exact-or-subtree, e.g. "legal" also matches "legal:contract"; wildcards `legal:contract*`, `*nda*`). | | `attribute` | `list[str] \| None` | Restrict by attribute value, e.g. `["fiscal_year:2024\|2025", "status:active"]`. Entries are ANDed, `\|` ORs within one entry. Also `name` (has any value), `name:>value`, `name:prefix*`, `name:*text*`. | | `max_results` | `int \| None` | Chunks to return after reranking (1–100; server default 10). | | `mode` | `SearchMode \| None` | SearchMode.text (hybrid keyword+vector) or .vision (page-image). | | `relevance_scoring` | `RelevanceScoring \| None` | RelevanceScoring, .scoring\_and\_filtering (default), .scoring\_only, or .none. | | `include_image` | `bool \| None` | Attach a base64 page image to each result. | | `include_bboxes` | `bool \| None` | Attach chunk bounding boxes (PDF text-mode only). | **Returns** | Type | Description | | - | - | | `SearchResponse` | The ranked search results. | ### LightOnConfiguration Non-essential client knobs. api\_key stays a direct LightOn() argument. | Field | Type | Default | Description | | - | - | - | - | | `base_url` | `str` | `'https://api.lighton.ai'` | | | `timeout` | `Timeout` | `Timeout(connect=5.0, read=120.0, write=120.0, pool=120.0)` | | | `retries` | `int` | `3` | Connection-level retries (httpx transport). | | `transport` | `httpx.BaseTransport \| None` | `None` | | | `max_requests_per_minute` | `int \| None` | `1000` | Pace ALL requests to stay under this per-minute cap (min-interval gate in \_request). Defaults to 1000, the API's limit for most endpoints; override if your account differs. Set None to disable pacing entirely. | | `rate_limit_retries` | `int` | `3` | On HTTP 429, retry this many times, waiting the Retry-After header when present (else exponential backoff). 0 disables. | ## Workspaces & files ### Workspace | Field | Type | Default | Description | | - | - | - | - | | `id` | `int \| None` | `None` | Server-assigned id; None until created/retrieved. | | `name` | `str` | *required* | Workspace display name. | | `description` | `str` | `''` | Free-text workspace description. | | `workspace_type` | `str \| None` | `None` | Workspace type (read-only). | | `document_upload_method` | `str \| None` | `None` | How documents are uploaded to this workspace (read-only). | | `files_count` | `int \| None` | `None` | Number of files in the workspace (read-only). | | `used_storage` | `float \| None` | `None` | Bytes of storage used (read-only). | | `created_at` | `datetime.datetime \| None` | `None` | Creation timestamp (read-only). | | `updated_at` | `datetime.datetime \| None` | `None` | Last-update timestamp (read-only). | | `user_role` | `enums.Role \| None` | `None` | Your role on this workspace, owner/editor/viewer (read-only). | | `taxonomy` | `types.workspace.WorkspaceTaxonomy \| None` | `None` | Classification coverage and per-root document counts (read-only). Only `list()` returns it; the detail endpoint omits the key, so `get()`/`refresh()` leave an already-loaded value in place. | | `sync` | `types.workspace.WorkspaceSync \| None` | `None` | External datasource this workspace imports from (read-only); None when documents are uploaded directly. | #### `Workspace.create()` ```python theme={null} create(client: LightOn) -> Workspace ``` Create this workspace and bind the client for later lifecycle calls. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to create the workspace with and bind to `self`. | **Returns** | Type | Description | | - | - | | `Workspace` | `self`, updated with the server-assigned id and read-only fields. | #### `Workspace.delete()` ```python theme={null} delete() -> None ``` Delete this resource and clear its local id. #### `Workspace.get()` *Classmethod.* ```python theme={null} get(client: LightOn, id: int | str) -> Self ``` Fetch a single resource by id. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client used to make the request and bind to the result. | | `id` | `int \| str` | The resource id to retrieve. | **Returns** | Type | Description | | - | - | | `Self` | The resource, bound to `client`. | #### `Workspace.ingest()` ```python theme={null} ingest( file: File, *, wait: bool = False, timeout: float = 300.0, tags: list[int] | None = None, ) -> File ``` Upload a File into this workspace, uploading is the ingestion. Non-blocking by default: the returned File is 'pending', poll it via refresh()/wait(). Pass wait=True to block until ingestion is terminal. **Arguments** | Name | Type | Description | | - | - | - | | `file` | `File` | The File to upload; its workspace\_id is set to this workspace. | | `wait` | `bool` | If True, block until ingestion reaches a terminal status. | | `timeout` | `float` | Seconds to wait when wait=True before raising TimeoutError. | | `tags` | `list[int] \| None` | Optional tag ids to assign to the document on upload. | **Returns** | Type | Description | | - | - | | `File` | The created File, bound to this workspace's client. | **Raises** | Exception | When | | - | - | | `ValueError` | If this workspace has not been created/retrieved yet. | #### `Workspace.ingest_many()` ```python theme={null} ingest_many( files: list[File | str | Path], *, mode: ExecMode = ExecMode.SYNC, ignore_errors: bool = False, wait: bool = False, timeout: float = 300.0, max_workers: int = 8, tags: list[int] | None = None, ) -> BatchIngest | BatchIngestJob ``` Upload many files into this workspace, concurrently. Every local path is validated to exist **before** any upload starts (fail fast). Staying under the API rate limit and the 429 cooldown are handled by the client (see LightOnConfiguration.max\_requests\_per\_minute / rate\_limit\_retries), so they apply across uploads and status polls alike. **Arguments** | Name | Type | Description | | - | - | - | | `files` | `list[File \| str \| Path]` | Items to ingest, File objects, path strings, or Paths (mixed). A string with glob characters (`*?[`) is expanded (recursive `**` supported) to its matching files; duplicates are ignored. | | `mode` | `ExecMode` | ExecMode.SYNC blocks and returns a BatchIngest; ExecMode.ASYNC returns a BatchIngestJob you poll for progress/failures. | | `ignore_errors` | `bool` | If False (default), the first failure raises (sync) or surfaces on job.wait() (async). If True, failures are collected and the batch continues. | | `wait` | `bool` | If True, wait for each upload's ingestion to reach a terminal status (embedded); if False, return once uploads are accepted. | | `timeout` | `float` | Per-file seconds to wait for ingestion when wait=True. | | `max_workers` | `int` | Concurrent upload/poll threads. | | `tags` | `list[int] \| None` | Optional tag ids assigned to every uploaded document. | **Returns** | Type | Description | | - | - | | `BatchIngest \| BatchIngestJob` | A BatchIngest (SYNC) or a BatchIngestJob (ASYNC). | **Raises** | Exception | When | | - | - | | `ValueError` | If this workspace has no id, or an item is a File with no path. | | `FileNotFoundError` | If any path is missing and ignore\_errors is False. | #### `Workspace.list()` *Classmethod.* ```python theme={null} list(client: LightOn, **params: object) -> list[Self] ``` List every resource, following pagination to the end. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client used to make the request and bind to each result. | | `**params` | `object` | Optional query filters (e.g. workspace\_id) sent on the first page. | **Returns** | Type | Description | | - | - | | `list[Self]` | All matching resources, each bound to `client`. | #### `Workspace.refresh()` ```python theme={null} refresh() -> Self ``` Re-fetch this resource from the API (GET). **Returns** | Type | Description | | - | - | | `Self` | `self`, updated with the latest field values. | #### `Workspace.save()` ```python theme={null} save() -> Workspace ``` Persist local edits to name/description (PATCH). **Returns** | Type | Description | | - | - | | `Workspace` | `self`, refreshed with the server's response. | ### WorkspaceTaxonomy How much of a workspace is classified, and under which roots. The cheapest way to see classification coverage without listing files. Only the **list** endpoint returns it (`Workspace.list()`); the detail endpoint omits the key entirely, so `get()`/`refresh()` leave whatever was already there rather than clearing it. | Field | Type | Default | Description | | - | - | - | - | | `classified_files_rate` | `float` | *required* | Fraction of the workspace's files that carry a content type, 0 to 1. | | `root_content_types` | `list` | `[]` | Per-root document counts, one entry per root content type. | ### RootContentType How many of a workspace's documents sit under one root content type. | Field | Type | Default | Description | | - | - | - | - | | `path` | `str` | *required* | Root content-type path, e.g. legal. | | `label` | `str` | *required* | Human-readable label for that root. | | `count` | `int` | *required* | Documents classified under it, including children. | ### WorkspaceSync The external datasource a workspace imports from, when one is connected. None on a workspace whose documents were uploaded directly. | Field | Type | Default | Description | | - | - | - | - | | `name` | `str \| None` | `None` | Sync configuration name. | | `datasource_type` | `str \| None` | `None` | Kind of connected datasource (read-only). | | `source_name` | `str \| None` | `None` | Name of the remote source. | | `last_status` | `str \| None` | `None` | Outcome of the last import run. | | `updated_at` | `datetime.datetime \| None` | `None` | When the sync last ran; None if it never has. | | `next_import_date` | `datetime.datetime \| None` | `None` | When the next import is due; None if none is scheduled. | | `failed_files_count` | `int \| None` | `None` | Files the last run could not import. | | `editable` | `bool \| None` | `None` | Whether the caller may change this configuration. | | `instance_url` | `str \| None` | `None` | Remote instance URL, if any. | | `tenant_id` | `str \| None` | `None` | Remote tenant identifier, if any. | | `site_name` | `str \| None` | `None` | Remote site name, if any. | | `client_id` | `str \| None` | `None` | Remote client identifier, if any. | | `filter_criteria` | `Any` | `None` | Datasource-specific import filter, passed through as-is. | ### File | Field | Type | Default | Description | | - | - | - | - | | `id` | `int \| None` | `None` | Server-assigned id; None until created/retrieved. | | `path` | `pathlib.Path \| None` | `None` | Local source file to upload; set before create(), never in a response. | | `workspace_id` | `int \| None` | `None` | Target workspace for upload; required to create (Workspace.ingest fills it). | | `filename` | `str \| None` | `None` | Document filename; defaults to path.name on upload. | | `title` | `str \| None` | `None` | Document title; defaults to the filename server-side. | | `external_metadata` | `types.file.ExternalMetadata \| None` | `None` | Origin of the document in a third-party system. Settable on create() and save(); the response returns the same shape, so it round-trips. | | `status` | `enums.FileStatus \| None` | `None` | Ingestion pipeline status (read-only). | | `status_detail` | `str \| None` | `None` | Free-text error detail, present only on failure (read-only). | | `pending_reprocess` | `enums.ReprocessLevel \| None` | `None` | Reprocessing queued but not started (read-only). While non-null, `status` and the file-derived fields still describe the *previous* run; it clears the moment processing starts. `update` means a file replacement. | | `extension` | `str \| None` | `None` | File extension of the document (read-only). | | `total_pages` | `int \| None` | `None` | Total page count of the document (read-only). | | `size` | `int \| None` | `None` | File size in bytes (read-only). | | `thumbnail` | `types.file.Thumbnail \| None` | `None` | Thumbnail status and URL (read-only). Check `status` is READY before calling download\_thumbnail(), which 404s otherwise. | | `created_at` | `datetime.datetime \| None` | `None` | Creation timestamp (read-only). | | `updated_at` | `datetime.datetime \| None` | `None` | Last-update timestamp (read-only). | #### `File.classify()` ```python theme={null} classify(content_type: ContentType | str) -> File ``` Assign a content type to this file (ContentType object or path string). **Arguments** | Name | Type | Description | | - | - | - | | `content_type` | `ContentType \| str` | The content type to assign, e.g. "legal:contract:nda". | **Returns** | Type | Description | | - | - | | `File` | `self`. | **Raises** | Exception | When | | - | - | | `ValueError` | If this file has not been created/retrieved yet. | #### `File.clear_attribute()` ```python theme={null} clear_attribute(content_type: ContentType | str, name: str) -> File ``` Clear an attribute value under an assigned content type. **Arguments** | Name | Type | Description | | - | - | - | | `content_type` | `ContentType \| str` | The assigned content type (object or path string). | | `name` | `str` | Attribute identifier to clear. | **Returns** | Type | Description | | - | - | | `File` | `self`. | #### `File.create()` ```python theme={null} create( client: LightOn, *, tags: list[int] | None = None, external_metadata: ExternalMetadata | None = None, ) -> File ``` Upload the file (multipart), this starts ingestion. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to upload with and bind to `self`. | | `tags` | `list[int] \| None` | Optional tag ids to assign to the document on upload. | | `external_metadata` | `ExternalMetadata \| None` | Where this document came from in a third-party system. Overrides the `external_metadata` field if both are set. | **Returns** | Type | Description | | - | - | | `File` | `self`, updated with the server-assigned id and initial status. | **Raises** | Exception | When | | - | - | | `ValueError` | If `path` or `workspace_id` is not set. | #### `File.delete()` ```python theme={null} delete() -> None ``` Delete this resource and clear its local id. #### `File.delete_many()` *Classmethod.* ```python theme={null} delete_many(client: LightOn, files: Sequence[File | int]) -> None ``` Delete many files in one request (POST /files/bulk-delete). All-or-nothing: if any id is unknown (or not yours), the API rejects the whole call with 404 and deletes **nothing**, which this surfaces as a `NotFoundError`. There is no partial-success result to report, so a failure raises rather than returning a per-file report: nothing was deleted, and retrying with the ids you can account for is the fix. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to delete with. | | `files` | `Sequence[File \| int]` | The files to delete, File objects or bare ids (mix freely). Any sequence, so `File.list(...)` passes straight in. Empty is a no-op (the endpoint rejects an empty list). | **Returns** | Type | Description | | - | - | | `None` | None. Any File objects passed in have their `id` cleared, as delete() does. | **Raises** | Exception | When | | - | - | | `NotFoundError` | If any id is unknown; no file is deleted in that case. | #### `File.download()` ```python theme={null} download(purpose: DownloadPurpose | str = DownloadPurpose.original) -> bytes ``` Download this document's stored bytes (GET /files/\/download). **Arguments** | Name | Type | Description | | - | - | - | | `purpose` | `DownloadPurpose \| str` | Which stored version to fetch, `original` (default), the `rendered_pdf`, or the `transcript`. The server falls back to `original` when the requested purpose has no associated file, so this never 404s just because a rendition is missing. | **Returns** | Type | Description | | - | - | | `bytes` | The file content, as bytes. Write it yourself: `Path("out.pdf").write_bytes(doc.download())`. | **Raises** | Exception | When | | - | - | | `ValueError` | If this file has not been created/retrieved yet. | #### `File.download_thumbnail()` ```python theme={null} download_thumbnail() -> bytes ``` Download this document's 256x256 WebP thumbnail. Generated asynchronously and **independently of ingestion**, so an embedded file may still have none. Check the `thumbnail` field first, a fetch before it is READY raises `NotFoundError`: doc.refresh() if doc.thumbnail and doc.thumbnail.status is ThumbnailStatus.READY: image = doc.download\_thumbnail() **Returns** | Type | Description | | - | - | | `bytes` | The WebP image, as bytes. | **Raises** | Exception | When | | - | - | | `ValueError` | If this file has not been created/retrieved yet. | | `NotFoundError` | If no thumbnail exists (status is not READY). | #### `File.facets()` ```python theme={null} facets() -> list[Facet] ``` List this file's assigned content types and their attribute values. **Returns** | Type | Description | | - | - | | `list[Facet]` | One Facet per assigned content type (with its attribute values). | **Raises** | Exception | When | | - | - | | `ValueError` | If this file has not been created/retrieved yet. | #### `File.get()` *Classmethod.* ```python theme={null} get(client: LightOn, id: int | str) -> Self ``` Fetch a single resource by id. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client used to make the request and bind to the result. | | `id` | `int \| str` | The resource id to retrieve. | **Returns** | Type | Description | | - | - | | `Self` | The resource, bound to `client`. | #### `File.get_by_name()` *Classmethod.* ```python theme={null} get_by_name(client: LightOn, name: str, *, workspace: Workspace | int) -> list[File] ``` Fetch every file with this user-facing name in a workspace. Matches `title`, not `filename`: the server uniquifies filenames on upload ("report.pdf" is stored as something like "report\_20260728\_c9be.pdf"), so the name you uploaded never matches the stored one. A title defaults to the uploaded filename without its extension, so "report.pdf" and "report" both find that upload. Titles are not unique the way stored filenames are, uploading the same document twice gives both copies the same title, so this returns every match rather than picking one. Check the length if you need exactly one. The API's `title` filter is a case-insensitive *partial* match, so the candidates it returns are narrowed to an exact title match here. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to query with and bind to the results. | | `name` | `str` | The file's title, with or without an extension (e.g. "report.pdf"). | | `workspace` | `Workspace \| int` | The workspace to search in (Workspace object or id). | **Returns** | Type | Description | | - | - | | `list[File]` | Every File with that title, each bound to `client`; empty if none match. | **Raises** | Exception | When | | - | - | | `ValueError` | If the workspace has not been created/retrieved (has no id). | #### `File.list()` *Classmethod.* ```python theme={null} list(client: LightOn, **params: object) -> list[Self] ``` List every resource, following pagination to the end. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client used to make the request and bind to each result. | | `**params` | `object` | Optional query filters (e.g. workspace\_id) sent on the first page. | **Returns** | Type | Description | | - | - | | `list[Self]` | All matching resources, each bound to `client`. | #### `File.pages()` ```python theme={null} pages() -> list[Page] ``` Fetch the parsed text of this document, one entry per page. The platform stores what it parsed at ingestion, so this reads it back instead of re-uploading and re-parsing a document it already has. The result is the **same** `{index, markdown}` shape `parse` returns (literally the same `Page` model), so code can move between parsing a local file and reading an ingested one without reshaping anything. Costs a request with `include_content=true` and is not part of `refresh()`, because the text can be large and most callers don't want it. **Returns** | Type | Description | | - | - | | `list[Page]` | One Page per page, `index` and `markdown`. Empty if the document has no stored text. A document ingested before per-page text was stored comes back as a single page (index 1). | **Raises** | Exception | When | | - | - | | `ValueError` | If this file has not been created/retrieved yet. | #### `File.refresh()` ```python theme={null} refresh() -> Self ``` Re-fetch this resource from the API (GET). **Returns** | Type | Description | | - | - | | `Self` | `self`, updated with the latest field values. | #### `File.replace()` ```python theme={null} replace(path: str | Path, *, wait: bool = False, timeout: float = 300.0) -> File ``` Replace this document's content in place (PATCH, multipart `file` part). The document keeps its id, title, tags, and content-type classifications, and is re-ingested from the new content, so every reference to the id survives what used to need a delete plus a re-upload. The new file may be of a different type. `filename` follows the new file, but `title` (the user-facing name, what get\_by\_name matches) is preserved, so a replaced document is still found under the name it was uploaded with. Addressed by **id**, never by name: titles and filenames aren't unique, so resolve to the one document you mean first (`File.get(client, id)`, or check the length of `File.get_by_name(...)`) before replacing. **Arguments** | Name | Type | Description | | - | - | - | | `path` | `str \| Path` | Local file whose content replaces the current one. | | `wait` | `bool` | If True, block until the re-ingestion reaches a terminal status. | | `timeout` | `float` | Seconds to wait when wait=True before raising TimeoutError. | **Returns** | Type | Description | | - | - | | `File` | `self`. Note the fields absorbed from the response still describe the *previous* content (see below); refresh() once re-ingestion has run. | **Raises** | Exception | When | | - | - | | `ValueError` | If this file has not been created/retrieved yet. | | `TimeoutError` | If wait=True and `timeout` elapses. | | `LightOnError` | If wait=True and the re-ingestion fails. | #### `File.save()` ```python theme={null} save( *, tags: list[Tag | int | str] | None = None, external_metadata: ExternalMetadata | None = None, ) -> File ``` Persist local edits to `title`, plus whatever you pass explicitly. `filename` is immutable server-side. `title` is a plain model field: set it and save. `tags` and `external_metadata` are **arguments, not fields**, because neither is a plain set server-side, and a keyword at the call site says which one you're doing: * `tags` **replaces** every tag on the document, auto-assigned included. Pass `[]` to remove them all; `tag()`/`untag()` stay the additive path. (It also can't be a field: the response returns tag *objects*, not the `list[int]` the request takes, which would clash on absorb.) * `external_metadata` **merges** into what's stored, including inside `additional_metadata`, so a partial update leaves the other keys alone. The field of the same name stays readable and round-trips; it just isn't what gets sent, so assigning it can't read as a whole-value set. Omitting either leaves that part of the document untouched, so a plain `save()` only ever writes the title. Clearing metadata is partial, and the shape is the API's, verified live: `doc_type=""` blanks it, and a null *inside* `additional_metadata` (`{"version": None}`) nulls that key. `external_id` can be neither blanked (422 blank) nor nulled (422 null), and the whole record can't be dropped: the schema offers a null `external_metadata`, but the endpoint takes only form/multipart, where a bare `null` is rejected as "must be a JSON object", and `application/json` is 415. So the null branch is unreachable. ponytail: `exclude_none=True` is load-bearing, not tidiness. It keeps an unset field out of the payload (a None `doc_type` would 422), while an empty string still goes through, which is what makes the blank-clear work. **Arguments** | Name | Type | Description | | - | - | - | | `tags` | `list[Tag \| int \| str] \| None` | Replacement tags, Tag objects, ids, or names (mix freely). None (the default) leaves the document's tags untouched; `[]` clears them. | | `external_metadata` | `ExternalMetadata \| None` | Origin fields to merge in. None (the default) leaves the stored metadata untouched. | **Returns** | Type | Description | | - | - | | `File` | `self`, refreshed with the server's response (so `external_metadata` shows the merged result, not the partial value you sent). | **Raises** | Exception | When | | - | - | | `ValueError` | If this file isn't persisted, or a name/Tag can't resolve. | #### `File.set_attribute()` ```python theme={null} set_attribute(content_type: ContentType | str, name: str, value: object) -> File ``` Set an attribute value under an assigned content type. **Arguments** | Name | Type | Description | | - | - | - | | `content_type` | `ContentType \| str` | The assigned content type (object or path string). | | `name` | `str` | Attribute identifier (snake\_case). | | `value` | `object` | The value; shape depends on the attribute type (string, number, date "YYYY-MM-DD", bool, or list\[str] for multi-select). | **Returns** | Type | Description | | - | - | | `File` | `self`. | #### `File.tag()` ```python theme={null} tag(tags: list[Tag | int | str]) -> File ``` Assign tags to this file (POST /files/\/tags). **Arguments** | Name | Type | Description | | - | - | - | | `tags` | `list[Tag \| int \| str]` | Tags to add, Tag objects, ids, or names (mix freely). Names are resolved via Tag.list() and must exist. Empty is a no-op. | **Returns** | Type | Description | | - | - | | `File` | `self`, refreshed from the response. | **Raises** | Exception | When | | - | - | | `ValueError` | If this file isn't persisted, or a name/Tag can't resolve. | #### `File.unclassify()` ```python theme={null} unclassify(content_type: ContentType | str) -> File ``` Remove a content-type assignment from this file. **Arguments** | Name | Type | Description | | - | - | - | | `content_type` | `ContentType \| str` | The content type to unassign (object or path string). | **Returns** | Type | Description | | - | - | | `File` | `self`. | #### `File.untag()` ```python theme={null} untag(tags: list[Tag | int | str]) -> File ``` Remove tags from this file (DELETE /files/\/tags/\, one each). **Arguments** | Name | Type | Description | | - | - | - | | `tags` | `list[Tag \| int \| str]` | Tags to remove, Tag objects, ids, or names (mix freely). Names are resolved via Tag.list() and must exist. Empty is a no-op. | **Returns** | Type | Description | | - | - | | `File` | `self`. | **Raises** | Exception | When | | - | - | | `ValueError` | If this file isn't persisted, or a name/Tag can't resolve. | #### `File.wait()` ```python theme={null} wait(timeout: float = 300.0, poll: float = 2.0) -> File ``` Block (polling) until ingestion reaches a terminal state. ponytail: dumb poll loop, the API offers no webhook. Run in a thread (or use wait\_all) for concurrency; the SDK stays sync. **Arguments** | Name | Type | Description | | - | - | - | | `timeout` | `float` | Max seconds to wait before raising TimeoutError. | | `poll` | `float` | Seconds to sleep between status checks. A pending reprocess counts as *not* terminal: while `pending_reprocess` is set the queued work has not started and `status` still reports the previous run, so trusting it would call a replace() done before it began. | **Returns** | Type | Description | | - | - | | `File` | `self`, once `status` is a terminal-success state (embedded/parsed) and no reprocess is queued. | **Raises** | Exception | When | | - | - | | `TimeoutError` | If `timeout` elapses before a terminal status. | | `LightOnError` | If ingestion ends in a terminal-failure state. | ### ExternalMetadata Where a document came from in a third-party system. How a synced document keeps its origin id: set it on upload (or with save()) and it survives on the File, so a later sync can match the platform document back to the record it was ingested from. Updates **merge** server-side, including into `additional_metadata`: patching one key leaves the others in place, there is no way to drop a key this way. | Field | Type | Default | Description | | - | - | - | - | | `external_id` | `str \| None` | `None` | Document id in the source system; required the first time. | | `doc_type` | `str \| None` | `None` | Document type in the source system, e.g. 'incident'. | | `additional_metadata` | `Any` | `None` | Arbitrary JSON (url, version, timestamps...), passed through as-is. | ### Thumbnail Whether a file's 256x256 WebP thumbnail exists yet, and where it lives. Generation is asynchronous and independent of ingestion, so check `status` before fetching: `File.download_thumbnail()` 404s while it isn't READY. | Field | Type | Default | Description | | - | - | - | - | | `status` | `enums.ThumbnailStatus \| None` | `None` | MISSING, PROCESSING, or READY (read-only). | | `url` | `str \| None` | `None` | Relative URL to the image; None unless status is READY. | ### wait\_all ```python theme={null} wait_all(files: list[File], timeout: float = 300.0) -> list[File] ``` Wait for many ingestions concurrently (threads, not async, sync SDK). **Arguments** | Name | Type | Description | | - | - | - | | `files` | `list[File]` | The files to wait on; each is polled via File.wait. | | `timeout` | `float` | Max seconds to wait per file before that wait raises TimeoutError. | **Returns** | Type | Description | | - | - | | `list[File]` | The same files, once each has reached a terminal-success status. | **Raises** | Exception | When | | - | - | | `TimeoutError` | If any file does not finish within `timeout`. | | `LightOnError` | If any file's ingestion ends in a terminal-failure state. | ## Tags & content types ### Tag | Field | Type | Default | Description | | - | - | - | - | | `id` | `int \| None` | `None` | Server-assigned id; None until created/retrieved. | | `name` | `str` | *required* | Tag name. | | `description` | `str` | `''` | Free-text description (max 500 chars). | | `auto_assign` | `bool` | `True` | If True the system may auto-assign this tag; else user-only. | | `document_count` | `int \| None` | `None` | Number of documents carrying this tag (read-only). | | `created_at` | `datetime.datetime \| None` | `None` | Creation timestamp (read-only). | | `updated_at` | `datetime.datetime \| None` | `None` | Last-update timestamp (read-only). | #### `Tag.create()` ```python theme={null} create(client: LightOn) -> Tag ``` Create this tag and bind the client for later lifecycle calls. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to create the tag with and bind to `self`. | **Returns** | Type | Description | | - | - | | `Tag` | `self`, updated with the server-assigned id and read-only fields. | #### `Tag.delete()` ```python theme={null} delete() -> None ``` Delete this resource and clear its local id. #### `Tag.get()` *Classmethod.* ```python theme={null} get(client: LightOn, id: int | str) -> NoReturn ``` **Raises** | Exception | When | | - | - | | `NotImplementedError` | the tags API has no single-tag GET; use Tag.list(client) | #### `Tag.list()` *Classmethod.* ```python theme={null} list(client: LightOn, **params: object) -> list[Self] ``` List every resource, following pagination to the end. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client used to make the request and bind to each result. | | `**params` | `object` | Optional query filters (e.g. workspace\_id) sent on the first page. | **Returns** | Type | Description | | - | - | | `list[Self]` | All matching resources, each bound to `client`. | #### `Tag.refresh()` ```python theme={null} refresh() -> NoReturn ``` **Raises** | Exception | When | | - | - | | `NotImplementedError` | the tags API has no single-tag GET; use Tag.list(client) | ### ContentType A node in the content-type taxonomy. | Field | Type | Default | Description | | - | - | - | - | | `path` | `str` | *required* | Full taxonomy path, e.g. legal:contract:nda. | | `code` | `str` | *required* | This node's own code segment. | | `label` | `str` | *required* | Human-readable label. | | `description` | `str` | `''` | Free-text description. | | `source` | `str \| None` | `None` | Where the type is defined (read-only). | | `attributes` | `list` | `[]` | Attribute definitions (present when include\_attributes=True). | | `children` | `list` | `[]` | Child content types. | #### `ContentType.adopt()` *Classmethod.* ```python theme={null} adopt(client: LightOn, paths: list[str]) -> list[ContentType] ``` Import starter trees from the template catalog into your taxonomy. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `paths` | `list[str]` | Template root paths to import, e.g. `["legal", "finance"]` (see `templates()`). | **Returns** | Type | Description | | - | - | | `list[ContentType]` | The imported top-level nodes. | #### `ContentType.batch()` *Classmethod.* ```python theme={null} batch(client: LightOn, actions: list[dict[str, Any]]) -> list[dict[str, Any]] ``` Apply several taxonomy actions in one request (POST /content-types/batch). Each entry is the body a single-action method would send, so a tree and its attributes land together instead of one round trip each: ContentType.batch(client, \[ \{"action": "adopt", "content\_types": \["legal"]}, \{"action": "define\_attribute", "content\_type\_path": "legal", "name": "jurisdiction", "attribute\_type": "select", "choices": \["FR", "US"]}, ]) **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `actions` | `list[dict[str, Any]]` | The action bodies, in order. | **Returns** | Type | Description | | - | - | | `list[dict[str, Any]]` | One `{"status": ..., "data": ...}` entry per action, in the same order. `data` is a node for the content-type actions and an attribute for the attribute ones, so it's left as raw dicts rather than guessed into one model. | #### `ContentType.define()` *Classmethod.* ```python theme={null} define( client: LightOn, code: str, label: str, *, parent: ContentType | str | None = None, description: str | None = None, inherit_attributes: bool | None = None, ) -> ContentType ``` Create or update one node (POST /content-types, `define_content_type`). Idempotent: defining an existing code again updates it, so this is also how you rename a node. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `code` | `str` | This node's own segment, lowercase alphanumeric with hyphens (e.g. "employment-contract"); the server rejects anything else. | | `label` | `str` | Human-readable label. | | `parent` | `ContentType \| str \| None` | Parent node or path; omit for a root node. | | `description` | `str \| None` | Optional free-text description. | | `inherit_attributes` | `bool \| None` | Whether children inherit this node's attributes (server default True). | **Returns** | Type | Description | | - | - | | `ContentType` | The created (or updated) node. | #### `ContentType.define_attribute()` *Classmethod.* ```python theme={null} define_attribute( client: LightOn, content_type: ContentType | str, name: str, attribute_type: AttributeType | str, *, choices: list[str] | None = None, label: str | None = None, description: str | None = None, required: bool | None = None, ) -> Attribute ``` Create or update an attribute column on a node. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `content_type` | `ContentType \| str` | The node to define it on (object or path string). | | `name` | `str` | Attribute identifier in snake\_case. | | `attribute_type` | `AttributeType \| str` | AttributeType, or the equivalent string. | | `choices` | `list[str] \| None` | Allowed values; **required** for select and multi-select, and rejected by the server for every other type. | | `label` | `str \| None` | Human-readable label (defaults to a title-cased `name`). | | `description` | `str \| None` | Optional description. | | `required` | `bool \| None` | Whether the schema requires a value (server default False). | **Returns** | Type | Description | | - | - | | `Attribute` | The created (or updated) attribute definition. | **Raises** | Exception | When | | - | - | | `ValueError` | If a select/multi-select is missing `choices`, which the API rejects with a 422 anyway, caught here to save the round trip. | #### `ContentType.list()` *Classmethod.* ```python theme={null} list( client: LightOn, *, path: str | None = None, depth: int | None = None, include_attributes: bool = False, query: str | None = None, ) -> list[ContentType] ``` List the content-type taxonomy (top-level nodes, each with `children`). **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to query with. | | `path` | `str \| None` | Restrict to the subtree rooted at this path. | | `depth` | `int \| None` | How many levels of children to return. | | `include_attributes` | `bool` | Populate each node's `attributes` definitions. | | `query` | `str \| None` | Free-text filter over labels/paths. | **Returns** | Type | Description | | - | - | | `list[ContentType]` | The top-level content-type nodes. | #### `ContentType.templates()` *Classmethod.* ```python theme={null} templates(client: LightOn) -> list[Template] ``` List the starter taxonomies you can `adopt()` (GET /content-types/templates). **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to query with. | **Returns** | Type | Description | | - | - | | `list[Template]` | The template root nodes, each with its `children` and an `attributes` map covering the whole subtree. | #### `ContentType.undefine()` *Classmethod.* ```python theme={null} undefine(client: LightOn, content_type: ContentType | str) -> None ``` Delete a node **and cascade its whole subtree**. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `content_type` | `ContentType \| str` | The node to delete (object or path string). | **Returns** | Type | Description | | - | - | | `None` | None. | #### `ContentType.undefine_attribute()` *Classmethod.* ```python theme={null} undefine_attribute(client: LightOn, content_type: ContentType | str, name: str) -> None ``` Remove an attribute column from a node. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `content_type` | `ContentType \| str` | The node it's defined on (object or path string). | | `name` | `str` | Attribute identifier to remove. | **Returns** | Type | Description | | - | - | | `None` | None. | ### Template A starter taxonomy from the catalog, what `adopt()` imports. Same tree as a `ContentType` except for `attributes`: on a template it's a **map** from node path to that node's attribute definitions (the whole tree's attributes hang off the root), not this node's own list. | Field | Type | Default | Description | | - | - | - | - | | `path` | `str` | *required* | Full taxonomy path, e.g. legal:contract:nda. | | `code` | `str` | *required* | This node's own code segment. | | `label` | `str` | *required* | Human-readable label. | | `description` | `str` | `''` | Free-text description. | | `source` | `str \| None` | `None` | Where the type is defined (read-only). | | `attributes` | `dict` | `{}` | Attribute definitions per node path, for the whole subtree. | | `children` | `list` | `[]` | Child content types. | #### `Template.adopt()` *Classmethod.* ```python theme={null} adopt(client: LightOn, paths: list[str]) -> list[ContentType] ``` Import starter trees from the template catalog into your taxonomy. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `paths` | `list[str]` | Template root paths to import, e.g. `["legal", "finance"]` (see `templates()`). | **Returns** | Type | Description | | - | - | | `list[ContentType]` | The imported top-level nodes. | #### `Template.batch()` *Classmethod.* ```python theme={null} batch(client: LightOn, actions: list[dict[str, Any]]) -> list[dict[str, Any]] ``` Apply several taxonomy actions in one request (POST /content-types/batch). Each entry is the body a single-action method would send, so a tree and its attributes land together instead of one round trip each: ContentType.batch(client, \[ \{"action": "adopt", "content\_types": \["legal"]}, \{"action": "define\_attribute", "content\_type\_path": "legal", "name": "jurisdiction", "attribute\_type": "select", "choices": \["FR", "US"]}, ]) **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `actions` | `list[dict[str, Any]]` | The action bodies, in order. | **Returns** | Type | Description | | - | - | | `list[dict[str, Any]]` | One `{"status": ..., "data": ...}` entry per action, in the same order. `data` is a node for the content-type actions and an attribute for the attribute ones, so it's left as raw dicts rather than guessed into one model. | #### `Template.define()` *Classmethod.* ```python theme={null} define( client: LightOn, code: str, label: str, *, parent: ContentType | str | None = None, description: str | None = None, inherit_attributes: bool | None = None, ) -> ContentType ``` Create or update one node (POST /content-types, `define_content_type`). Idempotent: defining an existing code again updates it, so this is also how you rename a node. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `code` | `str` | This node's own segment, lowercase alphanumeric with hyphens (e.g. "employment-contract"); the server rejects anything else. | | `label` | `str` | Human-readable label. | | `parent` | `ContentType \| str \| None` | Parent node or path; omit for a root node. | | `description` | `str \| None` | Optional free-text description. | | `inherit_attributes` | `bool \| None` | Whether children inherit this node's attributes (server default True). | **Returns** | Type | Description | | - | - | | `ContentType` | The created (or updated) node. | #### `Template.define_attribute()` *Classmethod.* ```python theme={null} define_attribute( client: LightOn, content_type: ContentType | str, name: str, attribute_type: AttributeType | str, *, choices: list[str] | None = None, label: str | None = None, description: str | None = None, required: bool | None = None, ) -> Attribute ``` Create or update an attribute column on a node. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `content_type` | `ContentType \| str` | The node to define it on (object or path string). | | `name` | `str` | Attribute identifier in snake\_case. | | `attribute_type` | `AttributeType \| str` | AttributeType, or the equivalent string. | | `choices` | `list[str] \| None` | Allowed values; **required** for select and multi-select, and rejected by the server for every other type. | | `label` | `str \| None` | Human-readable label (defaults to a title-cased `name`). | | `description` | `str \| None` | Optional description. | | `required` | `bool \| None` | Whether the schema requires a value (server default False). | **Returns** | Type | Description | | - | - | | `Attribute` | The created (or updated) attribute definition. | **Raises** | Exception | When | | - | - | | `ValueError` | If a select/multi-select is missing `choices`, which the API rejects with a 422 anyway, caught here to save the round trip. | #### `Template.list()` *Classmethod.* ```python theme={null} list( client: LightOn, *, path: str | None = None, depth: int | None = None, include_attributes: bool = False, query: str | None = None, ) -> list[ContentType] ``` List the content-type taxonomy (top-level nodes, each with `children`). **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to query with. | | `path` | `str \| None` | Restrict to the subtree rooted at this path. | | `depth` | `int \| None` | How many levels of children to return. | | `include_attributes` | `bool` | Populate each node's `attributes` definitions. | | `query` | `str \| None` | Free-text filter over labels/paths. | **Returns** | Type | Description | | - | - | | `list[ContentType]` | The top-level content-type nodes. | #### `Template.templates()` *Classmethod.* ```python theme={null} templates(client: LightOn) -> list[Template] ``` List the starter taxonomies you can `adopt()` (GET /content-types/templates). **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to query with. | **Returns** | Type | Description | | - | - | | `list[Template]` | The template root nodes, each with its `children` and an `attributes` map covering the whole subtree. | #### `Template.undefine()` *Classmethod.* ```python theme={null} undefine(client: LightOn, content_type: ContentType | str) -> None ``` Delete a node **and cascade its whole subtree**. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `content_type` | `ContentType \| str` | The node to delete (object or path string). | **Returns** | Type | Description | | - | - | | `None` | None. | #### `Template.undefine_attribute()` *Classmethod.* ```python theme={null} undefine_attribute(client: LightOn, content_type: ContentType | str, name: str) -> None ``` Remove an attribute column from a node. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to write with. | | `content_type` | `ContentType \| str` | The node it's defined on (object or path string). | | `name` | `str` | Attribute identifier to remove. | **Returns** | Type | Description | | - | - | | `None` | None. | ### Attribute One attribute of a content type, a definition, or a value set on a file. Carries both the schema (type/required/choices) and, when read from a file's facets, the current `value`. `value` is None for bare definitions / when unset. | Field | Type | Default | Description | | - | - | - | - | | `name` | `str` | *required* | Attribute identifier in snake\_case. | | `label` | `str` | `''` | Human-readable label. | | `type` | `str` | `''` | text, number, date, boolean, select, or multi-select. | | `value` | `Any` | `None` | Current value on the file; None for a definition/unset. | | `required` | `bool` | `False` | Whether the schema requires it. | | `choices` | `list` | `[]` | Allowed values for select/multi-select. | | `description` | `str` | `''` | Optional attribute description. | ### Facet A content type assigned to a file, with the file's attribute values on it. | Field | Type | Default | Description | | - | - | - | - | | `path` | `str` | *required* | Assigned content-type path on the file. | | `label` | `str` | *required* | Human-readable content-type label. | | `attributes` | `list` | `[]` | Attribute values set on the file. | ## API keys ### ApiKey | Field | Type | Default | Description | | - | - | - | - | | `id` | `str \| None` | `None` | Server-assigned id; None until created/retrieved. | | `name` | `str` | *required* | API key display name. | | `expires_at` | `datetime.datetime \| None` | `None` | Expiry timestamp; None for no expiry. | | `scopes` | `list` | `[]` | Per-workspace access scopes; empty for an unscoped key. | | `prefix` | `str \| None` | `None` | Non-secret key prefix for identification (read-only). | | `created_at` | `datetime.datetime \| None` | `None` | Creation timestamp (read-only). | | `key` | `pydantic.types.SecretStr \| None` | `None` | Plaintext secret, returned ONLY by create(), once. Use .get\_secret\_value(). | #### `ApiKey.create()` ```python theme={null} create(client: LightOn) -> ApiKey ``` Create this API key and bind the client for later lifecycle calls. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client to create the key with and bind to `self`. | **Returns** | Type | Description | | - | - | | `ApiKey` | `self`, updated with the id and the one-time plaintext `key`. | #### `ApiKey.delete()` ```python theme={null} delete() -> None ``` Delete this resource and clear its local id. #### `ApiKey.get()` *Classmethod.* ```python theme={null} get(client: LightOn, id: int | str) -> Self ``` Fetch a single resource by id. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client used to make the request and bind to the result. | | `id` | `int \| str` | The resource id to retrieve. | **Returns** | Type | Description | | - | - | | `Self` | The resource, bound to `client`. | #### `ApiKey.list()` *Classmethod.* ```python theme={null} list(client: LightOn, **params: object) -> list[Self] ``` List every resource, following pagination to the end. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `LightOn` | The client used to make the request and bind to each result. | | `**params` | `object` | Optional query filters (e.g. workspace\_id) sent on the first page. | **Returns** | Type | Description | | - | - | | `list[Self]` | All matching resources, each bound to `client`. | #### `ApiKey.refresh()` ```python theme={null} refresh() -> Self ``` Re-fetch this resource from the API (GET). **Returns** | Type | Description | | - | - | | `Self` | `self`, updated with the latest field values. | #### `ApiKey.save()` ```python theme={null} save() -> ApiKey ``` Persist local edits to name/scopes (PATCH). **Returns** | Type | Description | | - | - | | `ApiKey` | `self`, refreshed with the server's response (never re-includes `key`). | ### ApiKeyScope | Field | Type | Default | Description | | - | - | - | - | | `workspace_id` | `int` | *required* | Workspace this scope grants access to. | | `role` | `Role` | *required* | Access role granted on the workspace. | ## Jobs & batches ### ParseJob | Field | Type | Default | Description | | - | - | - | - | | `id` | `str` | *required* | Async job id; poll its endpoint for status. | | `status` | `str` | *required* | Current status; compare against JobStatus (see .succeeded). | | `created_at` | `pydantic.types.AwareDatetime \| None` | `None` | When the job was accepted. | | `completed_at` | `pydantic.types.AwareDatetime \| None` | `None` | Set once the job is terminal (success or failure). | | `processing_time_ms` | `int \| None` | `None` | Wall-clock processing time, populated once terminal. | | `progress` | `types.api.JobProgress \| None` | `None` | Live progress (percentage, pages) while in flight. | | `document` | `types.api.ParseDocument \| None` | `None` | Parsed-document metadata, once completed. | | `result` | `types.api.ParseResult \| None` | `None` | Per-page text, populated once completed. | | `usage` | `types.api.ParseUsage \| None` | `None` | Pages processed, populated once completed. | | `error` | `types.api.ParseError \| None` | `None` | Failure detail on a terminal-failed job, else null. | #### `ParseJob.poll()` ```python theme={null} poll(*, page: int | None = None) -> Self ``` Re-fetch this job's status from the API, updating this object in place. **Arguments** | Name | Type | Description | | - | - | - | | `page` | `int \| None` | Page of `result.data` to fetch (extract only, results paginate). | **Returns** | Type | Description | | - | - | | `Self` | `self`, refreshed with the latest status/result. | **Raises** | Exception | When | | - | - | | `ValueError` | If the job isn't bound to a client (built by hand, not returned from parse/extract). | #### `ParseJob.wait()` ```python theme={null} wait(timeout: float = 300.0, poll: float = 2.0) -> Self ``` Block (polling) until the job is terminal, mirrors `File.wait`. ponytail: dumb poll loop, the API offers no webhook. **Arguments** | Name | Type | Description | | - | - | - | | `timeout` | `float` | Max seconds to wait before raising TimeoutError. | | `poll` | `float` | Seconds to sleep between status checks. | **Returns** | Type | Description | | - | - | | `Self` | `self`, once the job has completed successfully. | **Raises** | Exception | When | | - | - | | `TimeoutError` | If `timeout` elapses before the job is terminal. | | `LightOnError` | If the job ends in a terminal-failure state. | ### ExtractJob | Field | Type | Default | Description | | - | - | - | - | | `id` | `str` | *required* | Async job id; poll its endpoint for status. | | `status` | `str` | *required* | Current status; compare against JobStatus (see .succeeded). | | `created_at` | `pydantic.types.AwareDatetime \| None` | `None` | When the job was accepted. | | `completed_at` | `pydantic.types.AwareDatetime \| None` | `None` | Set once the job is terminal (success or failure). | | `processing_time_ms` | `int \| None` | `None` | Wall-clock processing time, populated once terminal. | | `progress` | `types.api.JobProgress \| None` | `None` | Live progress (percentage, pages) while in flight. | | `document` | `types.api.ExtractDocument \| None` | `None` | Source-document metadata, once completed. | | `result` | `types.api.ExtractResult \| None` | `None` | Extracted data, populated once completed. | | `usage` | `types.api.ExtractUsage \| None` | `None` | Pages processed, populated once completed. | #### `ExtractJob.poll()` ```python theme={null} poll(*, page: int | None = None) -> Self ``` Re-fetch this job's status from the API, updating this object in place. **Arguments** | Name | Type | Description | | - | - | - | | `page` | `int \| None` | Page of `result.data` to fetch (extract only, results paginate). | **Returns** | Type | Description | | - | - | | `Self` | `self`, refreshed with the latest status/result. | **Raises** | Exception | When | | - | - | | `ValueError` | If the job isn't bound to a client (built by hand, not returned from parse/extract). | #### `ExtractJob.wait()` ```python theme={null} wait(timeout: float = 300.0, poll: float = 2.0) -> Self ``` Block (polling) until the job is terminal, mirrors `File.wait`. ponytail: dumb poll loop, the API offers no webhook. **Arguments** | Name | Type | Description | | - | - | - | | `timeout` | `float` | Max seconds to wait before raising TimeoutError. | | `poll` | `float` | Seconds to sleep between status checks. | **Returns** | Type | Description | | - | - | | `Self` | `self`, once the job has completed successfully. | **Raises** | Exception | When | | - | - | | `TimeoutError` | If `timeout` elapses before the job is terminal. | | `LightOnError` | If the job ends in a terminal-failure state. | ### BatchIngestJob A running (or finished) batch ingestion. Returned by `Workspace.ingest_many(mode=ExecMode.ASYNC)`. Uploads (and, when `wait=True`, ingestion polls) run in a background thread; read `progress`, `succeeded`, and `failed` at any time, or block with `wait()`. #### `BatchIngestJob.poll()` ```python theme={null} poll() -> BatchProgress ``` Return the current progress. Mirror of the parse/extract job's poll(); state updates itself in the background thread, so this just snapshots it. #### `BatchIngestJob.wait()` ```python theme={null} wait(timeout: float | None = None) -> BatchIngest ``` Block until the batch finishes, then return its result. **Arguments** | Name | Type | Description | | - | - | - | | `timeout` | `float \| None` | Max seconds to wait for the whole batch; None waits forever. | **Returns** | Type | Description | | - | - | | `BatchIngest` | The terminal `BatchIngest`. | **Raises** | Exception | When | | - | - | | `TimeoutError` | If the batch doesn't finish within `timeout`. | | `Exception` | The first upload/ingestion error, re-raised, when the batch ran with `ignore_errors=False`. | | `_error` | | ### BatchIngest Terminal result of a batch: the Files that succeeded and what failed. | Field | Type | Default | Description | | - | - | - | - | | `succeeded` | `list` | `[]` | Files that succeeded. | | `failed` | `list` | `[]` | Failures, each with its cause. | ### BatchProgress Live snapshot of a running batch (see `BatchIngestJob.progress`). | Field | Type | Default | Description | | - | - | - | - | | `total` | `int` | *required* | Total items requested. | | `uploaded` | `int` | *required* | Uploads accepted so far. | | `ingested` | `int` | *required* | Files that reached a terminal-OK status (only advances when wait=True). | | `failed` | `int` | *required* | Failures so far. | | `done` | `bool` | *required* | True once the batch is terminal. | ### FailedIngest One file that didn't make it. | Field | Type | Default | Description | | - | - | - | - | | `source` | `Path` | *required* | The local path that failed. | | `error` | `Exception` | *required* | The exception that caused the failure. | | `file` | `file.File \| None` | `None` | The uploaded File when the failure was at ingestion (not the upload itself); None if the upload failed or the path was missing. | ## Streaming events ### AskEvent ```python theme={null} AskEvent(...) ``` Represent a PEP 604 union type E.g. for int | str ### SourcesEvent The retrieved chunks, sent once before generation starts. | Field | Type | Default | Description | | - | - | - | - | | `type` | `Literal` | `'sources'` | Event discriminator. | | `results` | `list` | `[]` | Retrieved chunks used as context, the same items `ask` returns. | ### TokenEvent One chunk of the answer as it generates. | Field | Type | Default | Description | | - | - | - | - | | `type` | `Literal` | `'token'` | Event discriminator. | | `text` | `str` | `''` | Answer text to append; may be several tokens. | ### DoneEvent Generation finished; no further events follow. | Field | Type | Default | Description | | - | - | - | - | | `type` | `Literal` | `'done'` | Event discriminator. | ## Enums ### AttributeType Type of a content-type attribute column. `select`/`multi_select` require `choices`; the API also accepts the aliases `multiselect` and `richtext` for the hyphenated values used here. | Member | Value | | - | - | | `text` | `text` | | `number` | `number` | | `date` | `date` | | `boolean` | `boolean` | | `select` | `select` | | `multi_select` | `multi-select` | | `rich_text` | `rich-text` | ### DownloadPurpose Which stored version of a file to download. The server falls back to `original` when the requested purpose has no associated file. | Member | Value | | - | - | | `original` | `original` | | `rendered_pdf` | `rendered_pdf` | | `transcript` | `transcript` | ### ExecMode Execution mode for parse/extract: run inline or queue as an async job. Uppercase members (unlike the other enums here) so the async member can be `ASYNC`, lowercase `async` is a Python keyword and can't be a member name. | Member | Value | | - | - | | `SYNC` | `sync` | | `ASYNC` | `async` | ### FileStatus Ingestion pipeline status for a File. | Member | Value | | - | - | | `pending` | `pending` | | `pending_conversion` | `pending_conversion` | | `converting` | `converting` | | `parsing` | `parsing` | | `parsing_failed` | `parsing_failed` | | `embedding` | `embedding` | | `embedding_failed` | `embedding_failed` | | `embedded` | `embedded` | | `parsed` | `parsed` | | `fail` | `fail` | | `updating` | `updating` | ### JobStatus Status of an async parse/extract job. Only `pending` (initial) and `completed` (success) are documented by the API, the schema types `status` as a bare string with no enum and doesn't publish the failure vocabulary. This enum is for call-site comparisons (StrEnum members equal their string values), NOT to validate the response field, so an unrecognized server value compares unequal rather than erroring. Detect terminal-failure via `completed_at` being set without `completed` (or, for parse, the `error` block) rather than a status string. | Member | Value | | - | - | | `pending` | `pending` | | `completed` | `completed` | ### RelevanceScoring Cross-encoder relevance scoring step for search. | Member | Value | | - | - | | `none` | `none` | | `scoring_only` | `scoring_only` | | `scoring_and_filtering` | `scoring_and_filtering` | ### ReprocessLevel Reprocessing level queued on a File (`pending_reprocess`), `update` = replacement. | Member | Value | | - | - | | `reparse` | `reparse` | | `rechunk` | `rechunk` | | `reembed` | `reembed` | | `reembed_vision` | `reembed_vision` | | `update` | `update` | ### Role Access role granted by an API-key scope on a workspace. | Member | Value | | - | - | | `viewer` | `viewer` | | `editor` | `editor` | | `owner` | `owner` | ### SearchMode Retrieval mode for search/ask. | Member | Value | | - | - | | `text` | `text` | | `vision` | `vision` | ### ThumbnailStatus Whether a file's thumbnail has been generated (uppercase, as the API sends). | Member | Value | | - | - | | `MISSING` | `MISSING` | | `PROCESSING` | `PROCESSING` | | `READY` | `READY` | ## Exceptions Importable from `lighton.exceptions`. Every method that performs a request can raise these, so they are listed once here rather than repeated on each method. A method's own **Raises** block covers only what it raises directly. | Exception | Inherits | Raised when | | - | - | - | | `LightOnError` | `Exception` | Base class for every error raised by this SDK. | | `LightOnAPIError` | `LightOnError` | The API returned a non-2xx response. | | `LightOnConnectionError` | `LightOnError` | Transport failure before any response was received (DNS, timeout, reset). | | `MalformedResponseError` | `LightOnError` | A 2xx response body was not valid JSON. | | `StreamError` | `LightOnError` | The server sent an `error` event partway through a stream. Not a `LightOnAPIError`: the HTTP response was a perfectly good 200 and the failure happened during generation, so there is no status code to carry. The answer is incomplete, which is why this raises instead of arriving as one more event a caller could mistake for a finished answer. `body` holds the payload. | | `AuthenticationError` | `LightOnAPIError` | 401, bad or missing API key (the request is not authenticated). | | `NotFoundError` | `LightOnAPIError` | 404, the resource does not exist. | | `PermissionDeniedError` | `LightOnAPIError` | 403, authenticated, but the key lacks permission for this operation. Distinct from `AuthenticationError`: the credentials are valid, but the caller isn't allowed (e.g. an endpoint that requires the CompanyAdmin role). | | `RateLimitError` | `LightOnAPIError` | 429, too many requests. `retry_after` is the seconds to wait before retrying, from the `Retry-After` response header when the server sends it (else None). | | `ServerError` | `LightOnAPIError` | 5xx, the API failed to handle the request. | | `MaintenanceError` | `ServerError` | 503 during a planned maintenance window, not a crash. A `ServerError` subclass, so existing `except ServerError` handlers keep working; catch this specifically to tell "come back later" apart from "this broke", since only one of the two is worth retrying. `mode` is `full_shutdown` or `warning_banner` (both block the request), `reason` is operator-supplied text, `started_at` is when the window opened, and `endpoint_categories` names the affected categories, empty meaning every endpoint. The untouched payload is always on `.body`. | # TypeScript quick start Source: https://developers.lighton.ai/sdks/typescript/quickstart Install @lighton-ai/sdk and get your first grounded answer. [`@lighton-ai/sdk`](https://github.com/lightonai/lighton-typescript-sdk) wraps the LightOn API: typed models, pagination handled for you, and rate-limit pacing built in. It's the code shown in the **TypeScript SDK** tab of every tutorial snippet. Requires Node 22 or later. Also runs on Bun, Deno, Cloudflare Workers, Vercel Edge, and in the browser, with no runtime dependencies. ## Install ```bash theme={null} npm install @lighton-ai/sdk ``` Set your API key in the environment. `new LightOn()` reads it from there, so it never appears in your code: ```bash theme={null} export LIGHTON_API_KEY="..." ``` ## Your first result ```typescript theme={null} import { LightOn, Workspace } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment const workspace = await new Workspace({ name: "Docs" }).create(client) await workspace.ingestMany(["docs/**/*.pdf"], { wait: true }) const answer = await client.ask("What were Q4 revenues?", { workspaces: [workspace] }) console.log(answer.answer) ``` ## Where to go next Every public class, method, and type, generated from the installed SDK. Task-by-task walkthroughs, each with a **TypeScript SDK** tab. For structured output with Zod, async jobs, error handling, and agent-framework integrations (Vercel AI SDK, LangChain.js), see the [SDK README](https://github.com/lightonai/lighton-typescript-sdk#readme). # TypeScript SDK reference Source: https://developers.lighton.ai/sdks/typescript/reference Every public class, method, and type in @lighton-ai/sdk 1.0.0. Generated from [`@lighton-ai/sdk`](https://github.com/lightonai/lighton-typescript-sdk) **1.0.0**. Install it and see runnable examples in the [quick start](/sdks/typescript/quickstart). ## Client ### LightOn ```typescript theme={null} new LightOn(apiKey?: string, config?: LightOnConfiguration) ``` | Name | Type | Description | | - | - | - | | `apiKey?` | `string` | Falls back to `LIGHTON_API_KEY` in the environment. | | `config?` | `LightOnConfiguration` | Optional client knobs, see `LightOnConfiguration`. | | Error | When | | - | - | | `Error` | If no API key is given and none is in the environment. | #### `LightOn.ask()` ```typescript theme={null} ask(query: string, options?: AskOptions & { stream?: false; }): Promise ask(query: string, options: AskOptions & { stream: true; }): AsyncGenerator ``` **Arguments** | Name | Type | Description | | - | - | - | | `query` | `string` | | | `options?` | `AskOptions & { stream?: false; }` | | | `options.maxResults?` | `number` | Chunks to retrieve for context (1 to 50; server default 10). | | `options.relevanceScoring?` | `RelevanceScoring` | Defaults to `scoringAndFiltering` server-side. | | `options.model?` | `string` | LLM for answer generation; platform default if omitted. | | `options.schema?` | `SchemaInput` | Constrain the answer to structured output: a Zod schema or a plain JSON Schema object (the same inputs `extract` takes, sent as the API's `response_format`; it must describe an object). The answer then comes back as JSON *text* in `.answer`. The SDK does not parse it back for you, so that a schema mismatch surfaces where you can see it. | | `options.workspaces?` | `readonly IdRef[]` | Restrict to these workspaces. Excludes `files`. | | `options.tags?` | `readonly TagRef[]` | Restrict to documents carrying any of these tags (OR-matched). Accepts `Tag` objects, ids, or names; names are resolved via the tags endpoint and must exist. Excludes `files`. | | `options.files?` | `readonly IdRef[]` | Restrict to these files. Excludes `workspaces` and `tags`. | | `options.contentType?` | `readonly PathRef[]` | Restrict to these content-type paths (OR-matched, exact-or-subtree, so `legal` also matches `legal:contract`). Wildcards: `legal:contract*`, `*nda*`. | | `options.attribute?` | `readonly string[]` | Restrict by attribute value, e.g. `["fiscal_year:2024\|2025", "status:active"]`. Entries are ANDed, `\|` ORs within one entry. Also `name` (has any value), `name:>value`, `name:prefix*`, `name:*text*`. | | `options.signal?` | `AbortSignal` | Caller cancellation. | | `options.stream?` | `false` | | **Returns** | Type | Description | | - | - | | `Promise \| AsyncGenerator` | | #### `LightOn.close()` ```typescript theme={null} close(): void ``` Abort every in-flight request. The client is not reusable afterwards. #### `LightOn.extract()` ```typescript theme={null} extract(schema: SchemaInput, options: ExtractOptions & { mode?: "sync"; }): Promise extract(schema: SchemaInput, options: ExtractAsyncOptions): Promise ``` **Arguments** | Name | Type | Description | | - | - | - | | `schema` | `SchemaInput` | | | `options` | `ExtractOptions & { mode?: "sync"; }` | | | `options.file?` | `FileSource` | A local path, `File`, or `{filename, blob}` to upload. | | `options.url?` | `string` | A publicly accessible URL for the server to fetch. | | `options.ingested?` | `IdRef` | An already-ingested file (a `File` or its id). No re-upload: the server reads the document it already has, which is the cheapest of the three sources. | | `options.options?` | `Record` | Free-form request options, merged into the body. | | `options.signal?` | `AbortSignal` | Caller cancellation. | | `options.mode?` | `"sync"` | | **Returns** | Type | Description | | - | - | | `Promise \| Promise` | | #### `LightOn.parse()` ```typescript theme={null} parse(options: ParseOptions & { mode?: "sync"; }): Promise parse(options: ParseAsyncOptions): Promise ``` **Arguments** | Name | Type | Description | | - | - | - | | `options` | `ParseOptions & { mode?: "sync"; }` | | | `options.file?` | `FileSource` | A local path, `File`, or `{filename, blob}` to upload. Excludes `url`. | | `options.url?` | `string` | A publicly accessible URL for the server to fetch. Excludes `file`. | | `options.signal?` | `AbortSignal` | Caller cancellation. | | `options.mode?` | `"sync"` | | **Returns** | Type | Description | | - | - | | `Promise \| Promise` | | #### `LightOn.search()` ```typescript theme={null} search(query: string, options?: SearchOptions): Promise ``` **Arguments** | Name | Type | Description | | - | - | - | | `query` | `string` | | | `options?` | `SearchOptions` | | | `options.maxResults?` | `number` | Chunks to return after reranking (1 to 100; server default 10). | | `options.mode?` | `SearchMode` | `text` (hybrid keyword + vector) or `vision` (page image). | | `options.relevanceScoring?` | `RelevanceScoring` | Defaults to `scoringAndFiltering` server-side. | | `options.includeImage?` | `boolean` | Attach a base64 page image to each result. | | `options.includeBboxes?` | `boolean` | Attach chunk bounding boxes (PDF text-mode only). | | `options.workspaces?` | `readonly IdRef[]` | Restrict to these workspaces. Excludes `files`. | | `options.tags?` | `readonly TagRef[]` | Restrict to documents carrying any of these tags (OR-matched). Accepts `Tag` objects, ids, or names; names are resolved via the tags endpoint and must exist. Excludes `files`. | | `options.files?` | `readonly IdRef[]` | Restrict to these files. Excludes `workspaces` and `tags`. | | `options.contentType?` | `readonly PathRef[]` | Restrict to these content-type paths (OR-matched, exact-or-subtree, so `legal` also matches `legal:contract`). Wildcards: `legal:contract*`, `*nda*`. | | `options.attribute?` | `readonly string[]` | Restrict by attribute value, e.g. `["fiscal_year:2024\|2025", "status:active"]`. Entries are ANDed, `\|` ORs within one entry. Also `name` (has any value), `name:>value`, `name:prefix*`, `name:*text*`. | | `options.signal?` | `AbortSignal` | Caller cancellation. | **Returns** | Type | Description | | - | - | | `Promise` | | ### LightOnConfiguration Non-essential client knobs. `apiKey` stays a direct `LightOn()` argument, so a config object can be shared or logged without carrying a secret. | Field | Type | Description | | - | - | - | | `baseUrl?` | `string` | API root, host only. The SDK appends `/api/v3/...` itself. | | `timeout?` | `number` | Whole-request timeout in milliseconds. Default 120\_000. ponytail: one timeout, where the Python SDK splits connect (5s) from read (120s). `fetch` cannot express the split, and `AbortSignal.timeout` covers the case that actually hurts. Move to a custom dispatcher if a connect-only deadline is needed. | | `retries?` | `number` | Connection-level retries, with exponential backoff. Does not cover HTTP errors. | | `fetch?` | `typeof globalThis.fetch` | The `fetch` to send through. Defaults to the global one. This is the seam tests use to answer requests without a network, the way the Python SDK injects an `httpx.MockTransport`. It is also where a proxy dispatcher goes. | | `maxRequestsPerMinute?` | `number \| null` | Pace ALL requests to stay under this per-minute cap (a min-interval gate in `request`). Defaults to 1000, the API's limit for most endpoints; override if your account differs. Set null to disable pacing entirely. | | `rateLimitRetries?` | `number` | On HTTP 429, retry this many times, waiting the `Retry-After` header when present (else exponential backoff). 0 disables. | ### DEFAULT\_BASE\_URL ```typescript theme={null} const DEFAULT_BASE_URL: "https://api.lighton.ai" ``` Client configuration. ### VERSION ```typescript theme={null} const VERSION: string ``` ## Workspaces & files ### Workspace ```typescript theme={null} new Workspace(init?: WorkspaceInit) ``` | Name | Type | Description | | - | - | - | | `init?` | `WorkspaceInit` | | | Field | Type | Description | | - | - | - | | `id` | `number \| null` | Server-assigned id; null until created or retrieved. | | `name` | `string` | | | `description` | `string` | | | `workspaceType?` | `string \| null` | Workspace type (read-only). | | `documentUploadMethod?` | `string \| null` | How documents are uploaded to this workspace (read-only). | | `filesCount?` | `number \| null` | | | `usedStorage?` | `number \| null` | Bytes of storage used (read-only). | | `createdAt?` | `string \| null` | | | `updatedAt?` | `string \| null` | | | `userRole?` | `Role \| null` | Your role on this workspace (read-only). Null when you hold none. | | `taxonomy?` | `WorkspaceTaxonomy \| null` | Classification coverage and per-root document counts (read-only). Only `list()` returns it. The detail endpoint omits the key entirely, so `get()` and `refresh()` neither populate it nor clear an already-loaded value. Re-list for fresh coverage numbers. | | `sync?` | `WorkspaceSync \| null` | External datasource this workspace imports from (read-only); null when documents are uploaded directly. | #### `Workspace.get()` ```typescript theme={null} static get(client: Transport, id: number | string): Promise ``` Fetch a single workspace by id. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to request with and bind to the result. | | `id` | `number \| string` | The workspace id to retrieve. | **Returns** | Type | Description | | - | - | | `Promise` | The workspace, bound to `client`. | #### `Workspace.list()` ```typescript theme={null} static list(client: Transport, options?: WorkspaceListOptions): Promise ``` List every workspace, following pagination to the end. Only a listing returns `taxonomy` and the other listing-only extras. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to request with and bind to each result. | | `options?` | `WorkspaceListOptions` | Endpoint filters under `filters`, by their API names. | | `options.filters?` | `WorkspaceFilters` | Endpoint filters under their API names, e.g. `name`, `user_role`, `ordering`. | **Returns** | Type | Description | | - | - | | `Promise` | Every matching workspace, bound to `client`. | #### `Workspace.create()` ```typescript theme={null} create(client: Transport): Promise ``` Create this workspace and bind the client for later lifecycle calls. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to create the workspace with and bind to `this`. | **Returns** | Type | Description | | - | - | | `Promise` | `this`, updated with the server-assigned id and read-only fields. | #### `Workspace.delete()` ```typescript theme={null} delete(): Promise ``` Delete this resource and clear its local id. **Returns** | Type | Description | | - | - | | `Promise` | | #### `Workspace.ingest()` ```typescript theme={null} ingest(file: File, options?: IngestOptions): Promise ``` Upload a File into this workspace. Uploading *is* the ingestion. Non-blocking by default: the returned File is `pending`, poll it with `refresh()` or `wait()`. Pass `wait: true` to block until ingestion is terminal. **Arguments** | Name | Type | Description | | - | - | - | | `file` | `File` | The File to upload; its `workspaceId` is set to this workspace. | | `options?` | `IngestOptions` | `wait`, `timeoutMs`, and tag ids. See `IngestOptions`. | | `options.wait?` | `boolean` | Block until ingestion reaches a terminal status. | | `options.tags?` | `number[]` | Tag ids to assign to the document on upload. | | `options.timeoutMs?` | `number` | Milliseconds before giving up. Default 300\_000. | | `options.pollMs?` | `number` | Milliseconds between status checks. Default 2000. | **Returns** | Type | Description | | - | - | | `Promise` | The created File, bound to this workspace's client. | **Throws** | Error | When | | - | - | | `Error` | If this workspace has not been created or retrieved yet. | #### `Workspace.ingestMany()` ```typescript theme={null} ingestMany(files: readonly (File | string)[], options?: BatchOptions & { mode?: typeof ExecMode.sync; }): Promise ingestMany(files: readonly (File | string)[], options: BatchOptions & { mode: typeof ExecMode.async; }): Promise ``` Upload many files into this workspace, concurrently. Every local path is validated to exist **before** any upload starts. Staying under the API rate limit and honoring the 429 cooldown are the client's job, so they apply across uploads and status polls alike. **Arguments** | Name | Type | Description | | - | - | - | | `files` | `readonly (File \| string)[]` | Items to ingest: `File` objects and path strings, mixed. A string containing `*`, `?` or `[` is expanded as a glob (`**` works); duplicates are ignored. Files carrying a `blob` need no filesystem. | | `options?` | `BatchOptions & { mode?: typeof ExecMode.sync; }` | Execution mode, error handling and concurrency. See `BatchOptions`. | | `options.ignoreErrors?` | `boolean` | If false (the default), the first failure throws, inline or from `job.wait()`. If true, failures are collected and the batch carries on. | | `options.wait?` | `boolean` | Wait for each upload's ingestion to finish, not just to be accepted. | | `options.timeoutMs?` | `number` | Per-file milliseconds to wait for ingestion when `wait` is set. Default 300\_000. | | `options.pollMs?` | `number` | Milliseconds between ingestion status checks. Default 2000. | | `options.maxConcurrency?` | `number` | Concurrent uploads and polls. Default 8. | | `options.tags?` | `number[]` | Tag ids assigned to every uploaded document. | | `options.mode?` | `typeof ExecMode.sync` | | **Returns** | Type | Description | | - | - | | `Promise \| Promise` | A `BatchIngest` inline, or a `BatchIngestJob` with `mode: "async"`. | **Throws** | Error | When | | - | - | | `Error` | If this workspace has no id, or an item has neither path nor blob. | | `Error` | If any path is missing and `ignoreErrors` is unset. | #### `Workspace.refresh()` ```typescript theme={null} refresh(): Promise ``` Re-fetch this resource from the API. **Returns** | Type | Description | | - | - | | `Promise` | `this`, updated with the latest field values. | #### `Workspace.save()` ```typescript theme={null} save(): Promise ``` Persist local edits to name and description. **Returns** | Type | Description | | - | - | | `Promise` | `this`, refreshed with the server's response. | ### WorkspaceInit | Field | Type | Description | | - | - | - | | `name?` | `string` | | | `description?` | `string` | | ### WorkspaceListOptions | Field | Type | Description | | - | - | - | | `filters?` | `WorkspaceFilters` | Endpoint filters under their API names, e.g. `name`, `user_role`, `ordering`. | ### IngestOptions | Field | Type | Description | | - | - | - | | `wait?` | `boolean` | Block until ingestion reaches a terminal status. | | `tags?` | `number[]` | Tag ids to assign to the document on upload. | | `timeoutMs?` | `number` | Milliseconds before giving up. Default 300\_000. | | `pollMs?` | `number` | Milliseconds between status checks. Default 2000. | ### WorkspaceTaxonomy How much of a workspace is classified, and under which roots. The cheapest way to see classification coverage without listing files. Only the list endpoint returns it; the detail endpoint omits the key entirely, so `get()` and `refresh()` leave whatever was already there rather than clearing it. | Field | Type | Description | | - | - | - | | `classifiedFilesRate` | `number` | Fraction of the workspace's files that carry a content type, 0 to 1. | | `rootContentTypes` | `RootContentType[]` | Per-root document counts, one entry per root content type. | ### RootContentType How many of a workspace's documents sit under one root content type. | Field | Type | Description | | - | - | - | | `path` | `string` | Root content-type path, e.g. `legal`. | | `label` | `string` | Human-readable label for that root. | | `count` | `number` | Documents classified under it, including children. | ### WorkspaceSync The external datasource a workspace imports from, when one is connected. Null on a workspace whose documents were uploaded directly. | Field | Type | Description | | - | - | - | | `name?` | `string \| null` | | | `datasourceType?` | `string \| null` | | | `sourceName?` | `string \| null` | | | `lastStatus?` | `string \| null` | Outcome of the last import run. | | `updatedAt?` | `string \| null` | When the sync last ran; null if it never has. | | `nextImportDate?` | `string \| null` | When the next import is due; null if none is scheduled. | | `failedFilesCount?` | `number \| null` | Files the last run could not import. | | `editable?` | `boolean \| null` | Whether the caller may change this configuration. | | `instanceUrl?` | `string \| null` | | | `tenantId?` | `string \| null` | | | `siteName?` | `string \| null` | | | `clientId?` | `string \| null` | | | `filterCriteria?` | `unknown` | Datasource-specific import filter, passed through as-is. | ### File ```typescript theme={null} new File(init?: FileInit) ``` | Name | Type | Description | | - | - | - | | `init?` | `FileInit` | | | Field | Type | Description | | - | - | - | | `id` | `number \| null` | Server-assigned id; null until created or retrieved. | | `path?` | `string` | Local source path; set before create(), never present in a response. | | `blob?` | `Blob` | Source bytes, as an alternative to `path`. Never present in a response. | | `workspaceId?` | `number \| null` | | | `filename?` | `string \| null` | | | `title?` | `string \| null` | | | `externalMetadata?` | `ExternalMetadata \| null` | | | `status?` | `FileStatus \| null` | Ingestion pipeline status (read-only). | | `statusDetail?` | `string \| null` | Free-text error detail, present only on failure (read-only). | | `pendingReprocess?` | `ReprocessLevel \| null` | Reprocessing queued but not started (read-only). While this is set, `status` and the file-derived fields still describe the *previous* run; it clears the moment processing starts. `update` means a file replacement. | | `extension?` | `string \| null` | | | `totalPages?` | `number \| null` | | | `size?` | `number \| null` | | | `thumbnail?` | `Thumbnail \| null` | Check `status` is `READY` before calling `downloadThumbnail()`. | | `createdAt?` | `string \| null` | | | `updatedAt?` | `string \| null` | | #### `File.deleteMany()` ```typescript theme={null} static deleteMany(client: Transport, files: readonly (File | number)[]): Promise ``` Delete many files in one request. All-or-nothing: if any id is unknown (or not yours), the API rejects the whole call with 404 and deletes **nothing**, which surfaces as a `NotFoundError`. There is no partial-success result to report, so a failure throws rather than returning a per-file report: nothing was deleted, and retrying with the ids you can account for is the fix. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to delete with. | | `files` | `readonly (File \| number)[]` | Files or bare ids, mixed. Empty is a local no-op, because the endpoint rejects an empty list. | **Returns** | Type | Description | | - | - | | `Promise` | | **Throws** | Error | When | | - | - | | `NotFoundError` | If any id is unknown; no file is deleted in that case. | #### `File.get()` ```typescript theme={null} static get(client: Transport, id: number | string): Promise ``` Fetch a single file by id. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to request with and bind to the result. | | `id` | `number \| string` | The file id to retrieve. | **Returns** | Type | Description | | - | - | | `Promise` | The file, bound to `client`. | #### `File.getByName()` ```typescript theme={null} static getByName(client: Transport, name: string, workspace: IdRef): Promise ``` Fetch every file with this user-facing name in a workspace. Matches `title`, not `filename`: the server uniquifies filenames on upload (`report.pdf` is stored as something like `report_20260728_c9be.pdf`), so the name you uploaded never matches the stored one. A title defaults to the uploaded filename without its extension, so `report.pdf` and `report` both find that upload. Titles are not unique the way stored filenames are, so this returns every match rather than picking one. The API's `title` filter is a case-insensitive *partial* match, so candidates are narrowed to an exact title match here. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to query with and bind to the results. | | `name` | `string` | The file's title, with or without an extension. | | `workspace` | `IdRef` | The workspace to search in (a `Workspace` or its id). | **Returns** | Type | Description | | - | - | | `Promise` | Every File with that title; empty if none match. | **Throws** | Error | When | | - | - | | `Error` | If the workspace has not been created or retrieved. | #### `File.list()` ```typescript theme={null} static list(client: Transport, options?: FileListOptions): Promise ``` List files, following pagination to the end. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to request with and bind to each result. | | `options?` | `FileListOptions` | `workspaceId`, `title`, and any other endpoint filter under `filters`. | | `options.workspaceId?` | `number` | | | `options.title?` | `string` | | | `options.filters?` | `FileFilters` | Any other filter the endpoint accepts, under its API name: `status`, `tag_id`, `external_metadata__external_id`, `ordering`, date ranges... Where it overlaps `workspaceId` or `title`, those win. | **Returns** | Type | Description | | - | - | | `Promise` | Every matching file. | #### `File.classify()` ```typescript theme={null} classify(contentType: PathRef): Promise ``` Assign a content type to this file. **Arguments** | Name | Type | Description | | - | - | - | | `contentType` | `PathRef` | | **Returns** | Type | Description | | - | - | | `Promise` | | #### `File.clearAttribute()` ```typescript theme={null} clearAttribute(contentType: PathRef, name: string): Promise ``` Clear an attribute value under an assigned content type. **Arguments** | Name | Type | Description | | - | - | - | | `contentType` | `PathRef` | | | `name` | `string` | | **Returns** | Type | Description | | - | - | | `Promise` | | #### `File.create()` ```typescript theme={null} create(client: Transport, options?: CreateOptions): Promise ``` Upload the file. This starts ingestion. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to upload with and bind to `this`. | | `options?` | `CreateOptions` | Tags to assign, and external metadata. See `CreateOptions`. | | `options.tags?` | `number[]` | Tag ids to assign on upload. | | `options.externalMetadata?` | `ExternalMetadata` | Overrides the `externalMetadata` field when both are set. | **Returns** | Type | Description | | - | - | | `Promise` | `this`, updated with the server-assigned id and initial status. | **Throws** | Error | When | | - | - | | `Error` | If no source or no `workspaceId` is set. | #### `File.delete()` ```typescript theme={null} delete(): Promise ``` Delete this resource and clear its local id. **Returns** | Type | Description | | - | - | | `Promise` | | #### `File.download()` ```typescript theme={null} download(purpose?: DownloadPurpose | string): Promise ``` Download this document's stored bytes. **Arguments** | Name | Type | Description | | - | - | - | | `purpose?` | `DownloadPurpose \| string` | Which stored version to fetch. The server falls back to `original` when the requested purpose has no associated file, so this never 404s just because a rendition is missing. | **Returns** | Type | Description | | - | - | | `Promise` | The file content. | #### `File.downloadThumbnail()` ```typescript theme={null} downloadThumbnail(): Promise ``` Download this document's 256x256 WebP thumbnail. Generated asynchronously and **independently of ingestion**, so an embedded file may still have none. Check the `thumbnail` field first. **Returns** | Type | Description | | - | - | | `Promise` | The WebP image. | **Throws** | Error | When | | - | - | | `NotFoundError` | If no thumbnail exists (status is not `READY`). | #### `File.facets()` ```typescript theme={null} facets(): Promise ``` List this file's assigned content types and their attribute values. **Returns** | Type | Description | | - | - | | `Promise` | | #### `File.pages()` ```typescript theme={null} pages(): Promise ``` Fetch the parsed text of this document, one entry per page. The platform stores what it parsed at ingestion, so this reads it back instead of re-uploading and re-parsing a document it already has. The result is the **same** `{index, markdown}` shape `parse` returns, so code can move between parsing a local file and reading an ingested one without reshaping anything. A method, not a field: the text can be large, and most callers of `refresh()` don't want it riding along. **Returns** | Type | Description | | - | - | | `Promise` | One Page per page. Empty if the document has no stored text. | #### `File.refresh()` ```typescript theme={null} refresh(): Promise ``` Re-fetch this resource from the API. **Returns** | Type | Description | | - | - | | `Promise` | `this`, updated with the latest field values. | #### `File.replace()` ```typescript theme={null} replace(source: string | Blob | { filename: string; blob: Blob; }, options?: WaitOptions & { wait?: boolean; }): Promise ``` Replace this document's content in place. The document keeps its id, title, tags and content-type classifications, and is re-ingested from the new content, so every reference to the id survives what used to need a delete plus a re-upload. The new file may be of a different type. `filename` follows the new file, but `title` is preserved, so a replaced document is still found under the name it was uploaded with. Addressed by **id**, never by name: titles and filenames aren't unique, so resolve to the one document you mean first. **Arguments** | Name | Type | Description | | - | - | - | | `source` | `string \| Blob \| { filename: string; blob: Blob; }` | Local path, `File`, or `{filename, blob}` whose content replaces the current one. | | `options?` | `WaitOptions & { wait?: boolean; }` | `wait` to block until re-ingestion is terminal, plus `timeoutMs`. | | `options.timeoutMs?` | `number` | Milliseconds before giving up. Default 300\_000. | | `options.pollMs?` | `number` | Milliseconds between status checks. Default 2000. | | `options.wait?` | `boolean` | | **Returns** | Type | Description | | - | - | | `Promise` | `this`. Without `wait`, the absorbed fields still describe the *previous* content, see `pendingReprocess`. | #### `File.save()` ```typescript theme={null} save(options?: SaveOptions): Promise ``` Persist local edits to `title`, plus whatever you pass explicitly. `filename` is immutable server-side. `title` is a plain field: set it and save. `tags` and `externalMetadata` are **options, not fields**, because neither is a plain set server-side, and naming them at the call site says which one you are doing. Omitting either leaves that part of the document untouched, so a bare `save()` only ever writes the title. **Arguments** | Name | Type | Description | | - | - | - | | `options?` | `SaveOptions` | Replacement tags and metadata to merge. See `SaveOptions`. | | `options.tags?` | `readonly TagRef[]` | Replacement tags: this **replaces** every tag on the document, auto-assigned included. Omit to leave tags untouched; pass `[]` to clear them all. | | `options.externalMetadata?` | `ExternalMetadata` | Origin fields to **merge** into what is stored. Omit to leave it untouched. | **Returns** | Type | Description | | - | - | | `Promise` | `this`, refreshed with the server's response, so `externalMetadata` shows the merged result rather than the partial value you sent. | #### `File.setAttribute()` ```typescript theme={null} setAttribute(contentType: PathRef, name: string, value: unknown): Promise ``` Set an attribute value under an assigned content type. **Arguments** | Name | Type | Description | | - | - | - | | `contentType` | `PathRef` | The assigned content type, or its path. | | `name` | `string` | Attribute identifier (snake\_case). | | `value` | `unknown` | The value; its shape follows the attribute type (string, number, date `YYYY-MM-DD`, boolean, or string\[] for multi-select). | **Returns** | Type | Description | | - | - | | `Promise` | | #### `File.tag()` ```typescript theme={null} tag(tags: readonly TagRef[]): Promise ``` Assign tags to this file. **Arguments** | Name | Type | Description | | - | - | - | | `tags` | `readonly TagRef[]` | Tags to add: `Tag` objects, ids, or names, mixed. Empty is a no-op. | **Returns** | Type | Description | | - | - | | `Promise` | `this`, refreshed from the response. | #### `File.unclassify()` ```typescript theme={null} unclassify(contentType: PathRef): Promise ``` Remove a content-type assignment from this file. **Arguments** | Name | Type | Description | | - | - | - | | `contentType` | `PathRef` | | **Returns** | Type | Description | | - | - | | `Promise` | | #### `File.untag()` ```typescript theme={null} untag(tags: readonly TagRef[]): Promise ``` Remove tags from this file, one request each: there is no bulk tag delete. **Arguments** | Name | Type | Description | | - | - | - | | `tags` | `readonly TagRef[]` | Tags to remove: `Tag` objects, ids, or names, mixed. Empty is a no-op. | **Returns** | Type | Description | | - | - | | `Promise` | `this`. | #### `File.wait()` ```typescript theme={null} wait(options?: WaitOptions): Promise ``` Poll until ingestion reaches a terminal state. A pending reprocess counts as *not* terminal: while `pendingReprocess` is set the queued work has not started and `status` still reports the previous run, so trusting it would call a `replace()` done before it began. ponytail: a plain poll loop, because the API offers no webhook. Use `waitAll` to run several concurrently. **Arguments** | Name | Type | Description | | - | - | - | | `options?` | `WaitOptions` | `timeoutMs` and `pollMs`. See `WaitOptions`. | | `options.timeoutMs?` | `number` | Milliseconds before giving up. Default 300\_000. | | `options.pollMs?` | `number` | Milliseconds between status checks. Default 2000. | **Returns** | Type | Description | | - | - | | `Promise` | `this`, once `status` is terminal-success and no reprocess is queued. | **Throws** | Error | When | | - | - | | `Error` | If the timeout elapses first. | | `LightOnError` | If ingestion ends in a terminal-failure state. | ### LightOnFile Alias of [`File`](#file). ### FileInit | Field | Type | Description | | - | - | - | | `path?` | `string` | Local path to upload. Node-family runtimes only; elsewhere pass `blob`. | | `blob?` | `Blob` | File bytes, for runtimes with no filesystem. Needs `filename`. | | `workspaceId?` | `number` | Target workspace. `Workspace.ingest()` fills this in for you. | | `filename?` | `string` | Document filename; defaults to the path's basename on upload. | | `title?` | `string` | Document title; defaults to the filename server-side. | | `externalMetadata?` | `ExternalMetadata` | | ### FileListOptions | Field | Type | Description | | - | - | - | | `workspaceId?` | `number` | | | `title?` | `string` | | | `filters?` | `FileFilters` | Any other filter the endpoint accepts, under its API name: `status`, `tag_id`, `external_metadata__external_id`, `ordering`, date ranges... Where it overlaps `workspaceId` or `title`, those win. | ### CreateOptions | Field | Type | Description | | - | - | - | | `tags?` | `number[]` | Tag ids to assign on upload. | | `externalMetadata?` | `ExternalMetadata` | Overrides the `externalMetadata` field when both are set. | ### SaveOptions | Field | Type | Description | | - | - | - | | `tags?` | `readonly TagRef[]` | Replacement tags: this **replaces** every tag on the document, auto-assigned included. Omit to leave tags untouched; pass `[]` to clear them all. | | `externalMetadata?` | `ExternalMetadata` | Origin fields to **merge** into what is stored. Omit to leave it untouched. | ### WaitOptions | Field | Type | Description | | - | - | - | | `timeoutMs?` | `number` | Milliseconds before giving up. Default 300\_000. | | `pollMs?` | `number` | Milliseconds between status checks. Default 2000. | ### FileSource A local path (Node-family runtimes), a `File`/`Blob`, or a `Blob` with a name. ```typescript theme={null} type FileSource = string | Blob | { filename: string; blob: Blob; } ``` ### ExternalMetadata Where a document came from in a third-party system. Set it on upload (or with `save()`) and it survives on the File, so a later sync can match the platform document back to the record it was ingested from. Updates **merge** server-side, including into `additionalMetadata`: patching one key leaves the others in place. There is no replace mode and no way to drop the record. What you *can* clear, verified against the live API: | to remove | how | | - | - | | `docType` | `{ docType: "" }` | | one key of `additionalMetadata` | `{ additionalMetadata: { version: null } }` | | `externalId` | not possible, it can only be overwritten | | the whole record | not possible | | Field | Type | Description | | - | - | - | | `externalId?` | `string \| null` | Document id in the source system; required the first time. | | `docType?` | `string \| null` | Document type in the source system, e.g. `incident`. | | `additionalMetadata?` | `unknown` | Arbitrary JSON (url, version, timestamps), passed through as-is. | ### Thumbnail Whether a file's 256x256 WebP thumbnail exists yet, and where it lives. Generation is asynchronous and independent of ingestion, so check `status` before fetching: `file.downloadThumbnail()` 404s while it isn't `READY`. | Field | Type | Description | | - | - | - | | `status?` | `ThumbnailStatus \| null` | | | `url?` | `string \| null` | Relative URL to the image; null unless status is `READY`. | ### Page One page of a document. Defined once and reused everywhere: `parse` returns these, and so does `file.pages()`, so code moves between parsing a local file and reading an ingested one without reshaping. Never redefine this shape per app. | Field | Type | Description | | - | - | - | | `index` | `number` | | | `markdown` | `string` | | ### `waitAll()` ```typescript theme={null} export declare function waitAll(files: readonly File[], options?: WaitOptions): Promise ``` Wait for many ingestions at once. **Arguments** | Name | Type | Description | | - | - | - | | `files` | `readonly File[]` | The files to wait on; each is polled via `file.wait()`. | | `options?` | `WaitOptions` | Passed to each `wait()`. | | `options.timeoutMs?` | `number` | Milliseconds before giving up. Default 300\_000. | | `options.pollMs?` | `number` | Milliseconds between status checks. Default 2000. | **Returns** | Type | Description | | - | - | | `Promise` | The same files, once each has reached a terminal-success status. | **Throws** | Error | When | | - | - | | `Error` | If any file does not finish in time. | | `LightOnError` | If any file's ingestion ends in a terminal-failure state. | ## Tags & content types ### Tag ```typescript theme={null} new Tag(init?: TagInit) ``` | Name | Type | Description | | - | - | - | | `init?` | `TagInit` | | | Field | Type | Description | | - | - | - | | `id` | `number \| null` | Server-assigned id; null until created or retrieved. | | `name` | `string` | | | `description` | `string` | | | `autoAssign` | `boolean` | | | `documentCount?` | `number \| null` | Number of documents carrying this tag (read-only). | | `createdAt?` | `string \| null` | | | `updatedAt?` | `string \| null` | | #### `Tag.get()` ```typescript theme={null} static get(): Promise ``` Not available: the tags API exposes no single-tag GET. Declared so the failure is a clear message at the call site rather than a 404 from a URL that was never going to exist. Use `Tag.list` instead. **Returns** | Type | Description | | - | - | | `Promise` | | **Throws** | Error | When | | - | - | | `Error` | Always. | #### `Tag.list()` ```typescript theme={null} static list(client: Transport, options?: TagListOptions): Promise ``` List every tag, following pagination to the end. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to request with and bind to each result. | | `options?` | `TagListOptions` | Endpoint filters under `filters`, by their API names. | | `options.filters?` | `TagFilters` | Endpoint filters under their API names, e.g. `name`, `auto_assign`. | **Returns** | Type | Description | | - | - | | `Promise` | Every matching tag, bound to `client`. | #### `Tag.create()` ```typescript theme={null} create(client: Transport): Promise ``` Create this tag and bind the client for later lifecycle calls. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to create the tag with and bind to `this`. | **Returns** | Type | Description | | - | - | | `Promise` | `this`, updated with the server-assigned id. | #### `Tag.delete()` ```typescript theme={null} delete(): Promise ``` Delete this resource and clear its local id. **Returns** | Type | Description | | - | - | | `Promise` | | #### `Tag.refresh()` ```typescript theme={null} refresh(): Promise ``` Re-fetch this resource from the API. **Returns** | Type | Description | | - | - | | `Promise` | | **Throws** | Error | When | | - | - | | `Error` | Always: the tags API exposes no single-tag GET. | ### TagInit | Field | Type | Description | | - | - | - | | `name?` | `string` | | | `description?` | `string` | | | `autoAssign?` | `boolean` | If true the system may auto-assign this tag; else it is user-only. | ### TagListOptions | Field | Type | Description | | - | - | - | | `filters?` | `TagFilters` | Endpoint filters under their API names, e.g. `name`, `auto_assign`. | ### TagRef A `Tag`, its id, or its name. The three mix freely in one list. ```typescript theme={null} type TagRef = number | string | Tag | { readonly id: number | string | null; } ``` ### ContentType A node in the content-type taxonomy. | Field | Type | Description | | - | - | - | | `path` | `string` | Full taxonomy path, e.g. `legal:contract:nda`. | | `code` | `string` | This node's own code segment. | | `label` | `string` | | | `description?` | `string` | | | `source?` | `string \| null` | Where the type is defined (read-only). | | `attributes?` | `Attribute[]` | Attribute definitions, present when `includeAttributes` was set. | | `children?` | `ContentType[]` | | #### `ContentType.adopt()` ```typescript theme={null} adopt(client: Transport, paths: readonly string[]): Promise ``` Import starter trees from the template catalog into your taxonomy. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to write with. | | `paths` | `readonly string[]` | Template root paths to import, e.g. `["legal", "finance"]`. | **Returns** | Type | Description | | - | - | | `Promise` | The imported top-level nodes. | #### `ContentType.batch()` ```typescript theme={null} batch(client: Transport, actions: readonly Record[]): Promise ``` Apply several taxonomy actions in one request. Each entry is the body a single-action method would send, so a tree and its attributes land together instead of one round trip each: ```ts theme={null} await ContentType.batch(client, [ \{ action: "adopt", content_types: ["legal"] \}, \{ action: "define_attribute", content_type_path: "legal", name: "jurisdiction", attribute_type: "select", choices: ["FR", "US"], \}, ]) ``` The entries are wire bodies, so their keys are the server's, not the SDK's. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to write with. | | `actions` | `readonly Record[]` | The action bodies, in order. | **Returns** | Type | Description | | - | - | | `Promise` | One result per action, in the same order. | #### `ContentType.define()` ```typescript theme={null} define(client: Transport, code: string, label: string, options?: DefineOptions): Promise ``` Create or update one node. Idempotent: defining an existing code again updates it, so this is also how you rename a node. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to write with. | | `code` | `string` | This node's own segment: lowercase alphanumeric with hyphens, e.g. `employment-contract`. The server rejects anything else. | | `label` | `string` | Human-readable label. | | `options?` | `DefineOptions` | Parent, description, and attribute inheritance. | | `options.parent?` | `PathRef` | Parent node or path; omit for a root node. | | `options.description?` | `string` | | | `options.inheritAttributes?` | `boolean` | Whether children inherit this node's attributes (server default true). | **Returns** | Type | Description | | - | - | | `Promise` | The created (or updated) node. | #### `ContentType.defineAttribute()` ```typescript theme={null} defineAttribute(client: Transport, contentType: PathRef, name: string, attributeType: AttributeType | string, options?: DefineAttributeOptions): Promise ``` Create or update an attribute column on a node. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to write with. | | `contentType` | `PathRef` | The node to define it on, or its path. | | `name` | `string` | Attribute identifier, snake\_case. | | `attributeType` | `AttributeType \| string` | An `AttributeType`, or the equivalent string. | | `options?` | `DefineAttributeOptions` | Choices, label, description and whether it is required. | | `options.choices?` | `string[]` | Allowed values. **Required** for `select` and `multi-select`, and rejected by the server for every other type. | | `options.label?` | `string` | Human-readable label; defaults to a title-cased `name`. | | `options.description?` | `string` | | | `options.required?` | `boolean` | Whether the schema requires a value (server default false). | **Returns** | Type | Description | | - | - | | `Promise` | The created (or updated) attribute definition. | **Throws** | Error | When | | - | - | | `Error` | If a `select`/`multi-select` is missing `choices`. The API rejects that with a 422 anyway; catching it here saves the round trip. | #### `ContentType.list()` ```typescript theme={null} list(client: Transport, options?: ContentTypeListOptions): Promise ``` List the content-type taxonomy: top-level nodes, each carrying its `children`. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to query with. | | `options?` | `ContentTypeListOptions` | Subtree, depth, attribute and search filters. | | `options.path?` | `string` | Restrict to the subtree rooted at this path. | | `options.depth?` | `number` | How many levels of children to return. | | `options.includeAttributes?` | `boolean` | Populate each node's `attributes` definitions. | | `options.query?` | `string` | Free-text filter over labels and paths. | **Returns** | Type | Description | | - | - | | `Promise` | The top-level content-type nodes. | #### `ContentType.templates()` ```typescript theme={null} templates(client: Transport): Promise ``` List the starter taxonomies you can `ContentType.adopt`. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to query with. | **Returns** | Type | Description | | - | - | | `Promise` | The template root nodes, each with its `children` and an `attributes` map covering the whole subtree. | #### `ContentType.undefine()` ```typescript theme={null} undefine(client: Transport, contentType: PathRef): Promise ``` Delete a node **and cascade its whole subtree**. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to write with. | | `contentType` | `PathRef` | The node to delete, or its path. | **Returns** | Type | Description | | - | - | | `Promise` | | #### `ContentType.undefineAttribute()` ```typescript theme={null} undefineAttribute(client: Transport, contentType: PathRef, name: string): Promise ``` Remove an attribute column from a node. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to write with. | | `contentType` | `PathRef` | The node it is defined on, or its path. | | `name` | `string` | Attribute identifier to remove. | **Returns** | Type | Description | | - | - | | `Promise` | | ### ContentTypeListOptions | Field | Type | Description | | - | - | - | | `path?` | `string` | Restrict to the subtree rooted at this path. | | `depth?` | `number` | How many levels of children to return. | | `includeAttributes?` | `boolean` | Populate each node's `attributes` definitions. | | `query?` | `string` | Free-text filter over labels and paths. | ### DefineOptions | Field | Type | Description | | - | - | - | | `parent?` | `PathRef` | Parent node or path; omit for a root node. | | `description?` | `string` | | | `inheritAttributes?` | `boolean` | Whether children inherit this node's attributes (server default true). | ### DefineAttributeOptions | Field | Type | Description | | - | - | - | | `choices?` | `string[]` | Allowed values. **Required** for `select` and `multi-select`, and rejected by the server for every other type. | | `label?` | `string` | Human-readable label; defaults to a title-cased `name`. | | `description?` | `string` | | | `required?` | `boolean` | Whether the schema requires a value (server default false). | ### Template A starter taxonomy from the catalog, what `adopt()` imports. The same tree as a `ContentType` except for `attributes`: on a template it is a **map** from node path to that node's attribute definitions, with the whole subtree's attributes hanging off the root, rather than this node's own list. A sibling interface, not an extension: TypeScript cannot re-type an inherited member, so the Python subclass (which needs a `# type: ignore` for exactly this) has no direct equivalent. | Field | Type | Description | | - | - | - | | `attributes?` | `Record` | Attribute definitions per node path, for the whole subtree. | | `path` | `string` | Full taxonomy path, e.g. `legal:contract:nda`. | | `code` | `string` | This node's own code segment. | | `label` | `string` | | | `description?` | `string` | | | `source?` | `string \| null` | Where the type is defined (read-only). | | `children?` | `ContentType[]` | | ### Attribute One attribute of a content type: a definition, or a value set on a file. Carries both the schema (`type`/`required`/`choices`) and, when read from a file's facets, the current `value`. `value` is absent for a bare definition or when unset. | Field | Type | Description | | - | - | - | | `name` | `string` | Attribute identifier, snake\_case. | | `label?` | `string` | | | `type?` | `string` | One of the `AttributeType` values. | | `value?` | `unknown` | Current value on the file; absent for a definition or when unset. | | `required?` | `boolean` | | | `choices?` | `string[]` | Allowed values, for `select` and `multi-select`. | | `description?` | `string` | | ### Facet A content type assigned to a file, with the file's attribute values on it. | Field | Type | Description | | - | - | - | | `path` | `string` | Assigned content-type path on the file. | | `label` | `string` | | | `attributes` | `Attribute[]` | Attribute values set on the file. | ### BatchActionResult One result of a `ContentType.batch` call. | Field | Type | Description | | - | - | - | | `status` | `number` | HTTP status this action would have returned on its own. | | `data?` | `unknown` | The node or attribute the action produced. Left as-is, including its wire naming: `data` is one of the opaque keys the camelCase boundary does not rewrite, because which shape it holds depends on the action. Reach for the single-action methods when you want a typed result. | ## API keys ### ApiKey ```typescript theme={null} new ApiKey(init?: ApiKeyInit) ``` | Name | Type | Description | | - | - | - | | `init?` | `ApiKeyInit` | | | Field | Type | Description | | - | - | - | | `id` | `string \| null` | String id, unlike every other resource. | | `name` | `string` | | | `expiresAt?` | `string \| null` | | | `scopes` | `ApiKeyScope[]` | | | `prefix?` | `string \| null` | Non-secret key prefix for identification (read-only). | | `createdAt?` | `string \| null` | | | `key?` | `string \| null` | The plaintext secret. Returned by `create()` only, once. Readable as `apiKey.key`, but non-enumerable, so `console.log`, `util.inspect`, `JSON.stringify` and object spread all leave it out. This is the TypeScript counterpart of the Python SDK's `SecretStr`: a key logged by accident is the likeliest way one leaks. | #### `ApiKey.get()` ```typescript theme={null} static get(client: Transport, id: string): Promise ``` Fetch a single API key by id. The plaintext secret is never included. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to request with and bind to the result. | | `id` | `string` | The key id to retrieve. | **Returns** | Type | Description | | - | - | | `Promise` | The key, bound to `client`. | #### `ApiKey.list()` ```typescript theme={null} static list(client: Transport, options?: ApiKeyListOptions): Promise ``` List every API key, following pagination to the end. The plaintext secret is never included. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to request with and bind to each result. | | `options?` | `ApiKeyListOptions` | Endpoint filters under `filters`, by their API names. | | `options.filters?` | `ApiKeyFilters` | Endpoint filters under their API names, e.g. `is_expired`. | **Returns** | Type | Description | | - | - | | `Promise` | Every matching API key, bound to `client`. | #### `ApiKey.create()` ```typescript theme={null} create(client: Transport): Promise ``` Create this API key and bind the client for later lifecycle calls. **Arguments** | Name | Type | Description | | - | - | - | | `client` | `Transport` | The client to create the key with and bind to `this`. | **Returns** | Type | Description | | - | - | | `Promise` | `this`, updated with the id and the one-time plaintext `key`. | #### `ApiKey.delete()` ```typescript theme={null} delete(): Promise ``` Delete this resource and clear its local id. **Returns** | Type | Description | | - | - | | `Promise` | | #### `ApiKey.refresh()` ```typescript theme={null} refresh(): Promise ``` Re-fetch this resource from the API. **Returns** | Type | Description | | - | - | | `Promise` | `this`, updated with the latest field values. | #### `ApiKey.save()` ```typescript theme={null} save(): Promise ``` Persist local edits to name and scopes. **Returns** | Type | Description | | - | - | | `Promise` | `this`, refreshed with the server's response, which never re-includes `key`. | ### ApiKeyInit | Field | Type | Description | | - | - | - | | `name?` | `string` | | | `expiresAt?` | `string \| null` | Expiry timestamp (ISO-8601); omit for no expiry. | | `scopes?` | `ApiKeyScope[]` | Per-workspace access scopes; omit or leave empty for an unscoped key. | ### ApiKeyScope Access granted on one workspace. | Field | Type | Description | | - | - | - | | `workspaceId` | `number` | | | `role` | `Role` | | ### ApiKeyListOptions | Field | Type | Description | | - | - | - | | `filters?` | `ApiKeyFilters` | Endpoint filters under their API names, e.g. `is_expired`. | ## Verb options ### ScopeOptions | Field | Type | Description | | - | - | - | | `workspaces?` | `readonly IdRef[]` | Restrict to these workspaces. Excludes `files`. | | `tags?` | `readonly TagRef[]` | Restrict to documents carrying any of these tags (OR-matched). Accepts `Tag` objects, ids, or names; names are resolved via the tags endpoint and must exist. Excludes `files`. | | `files?` | `readonly IdRef[]` | Restrict to these files. Excludes `workspaces` and `tags`. | | `contentType?` | `readonly PathRef[]` | Restrict to these content-type paths (OR-matched, exact-or-subtree, so `legal` also matches `legal:contract`). Wildcards: `legal:contract*`, `*nda*`. | | `attribute?` | `readonly string[]` | Restrict by attribute value, e.g. `["fiscal_year:2024\|2025", "status:active"]`. Entries are ANDed, `\|` ORs within one entry. Also `name` (has any value), `name:>value`, `name:prefix*`, `name:*text*`. | | `signal?` | `AbortSignal` | Caller cancellation. | ### AskOptions | Field | Type | Description | | - | - | - | | `maxResults?` | `number` | Chunks to retrieve for context (1 to 50; server default 10). | | `relevanceScoring?` | `RelevanceScoring` | Defaults to `scoringAndFiltering` server-side. | | `model?` | `string` | LLM for answer generation; platform default if omitted. | | `schema?` | `SchemaInput` | Constrain the answer to structured output: a Zod schema or a plain JSON Schema object (the same inputs `extract` takes, sent as the API's `response_format`; it must describe an object). The answer then comes back as JSON *text* in `.answer`. The SDK does not parse it back for you, so that a schema mismatch surfaces where you can see it. | | `workspaces?` | `readonly IdRef[]` | Restrict to these workspaces. Excludes `files`. | | `tags?` | `readonly TagRef[]` | Restrict to documents carrying any of these tags (OR-matched). Accepts `Tag` objects, ids, or names; names are resolved via the tags endpoint and must exist. Excludes `files`. | | `files?` | `readonly IdRef[]` | Restrict to these files. Excludes `workspaces` and `tags`. | | `contentType?` | `readonly PathRef[]` | Restrict to these content-type paths (OR-matched, exact-or-subtree, so `legal` also matches `legal:contract`). Wildcards: `legal:contract*`, `*nda*`. | | `attribute?` | `readonly string[]` | Restrict by attribute value, e.g. `["fiscal_year:2024\|2025", "status:active"]`. Entries are ANDed, `\|` ORs within one entry. Also `name` (has any value), `name:>value`, `name:prefix*`, `name:*text*`. | | `signal?` | `AbortSignal` | Caller cancellation. | ### SearchOptions | Field | Type | Description | | - | - | - | | `maxResults?` | `number` | Chunks to return after reranking (1 to 100; server default 10). | | `mode?` | `SearchMode` | `text` (hybrid keyword + vector) or `vision` (page image). | | `relevanceScoring?` | `RelevanceScoring` | Defaults to `scoringAndFiltering` server-side. | | `includeImage?` | `boolean` | Attach a base64 page image to each result. | | `includeBboxes?` | `boolean` | Attach chunk bounding boxes (PDF text-mode only). | | `workspaces?` | `readonly IdRef[]` | Restrict to these workspaces. Excludes `files`. | | `tags?` | `readonly TagRef[]` | Restrict to documents carrying any of these tags (OR-matched). Accepts `Tag` objects, ids, or names; names are resolved via the tags endpoint and must exist. Excludes `files`. | | `files?` | `readonly IdRef[]` | Restrict to these files. Excludes `workspaces` and `tags`. | | `contentType?` | `readonly PathRef[]` | Restrict to these content-type paths (OR-matched, exact-or-subtree, so `legal` also matches `legal:contract`). Wildcards: `legal:contract*`, `*nda*`. | | `attribute?` | `readonly string[]` | Restrict by attribute value, e.g. `["fiscal_year:2024\|2025", "status:active"]`. Entries are ANDed, `\|` ORs within one entry. Also `name` (has any value), `name:>value`, `name:prefix*`, `name:*text*`. | | `signal?` | `AbortSignal` | Caller cancellation. | ### ParseOptions | Field | Type | Description | | - | - | - | | `file?` | `FileSource` | A local path, `File`, or `{filename, blob}` to upload. Excludes `url`. | | `url?` | `string` | A publicly accessible URL for the server to fetch. Excludes `file`. | | `signal?` | `AbortSignal` | Caller cancellation. | ### ParseAsyncOptions | Field | Type | Description | | - | - | - | | `mode` | `typeof ExecMode.async` | | | `wait?` | `boolean` | Block until the job is terminal, so the returned job already carries its result. | | `timeoutMs?` | `number` | With `wait`, how long to wait before throwing. Default 300\_000. | | `file?` | `FileSource` | A local path, `File`, or `{filename, blob}` to upload. Excludes `url`. | | `url?` | `string` | A publicly accessible URL for the server to fetch. Excludes `file`. | | `signal?` | `AbortSignal` | Caller cancellation. | ### ExtractOptions | Field | Type | Description | | - | - | - | | `file?` | `FileSource` | A local path, `File`, or `{filename, blob}` to upload. | | `url?` | `string` | A publicly accessible URL for the server to fetch. | | `ingested?` | `IdRef` | An already-ingested file (a `File` or its id). No re-upload: the server reads the document it already has, which is the cheapest of the three sources. | | `options?` | `Record` | Free-form request options, merged into the body. | | `signal?` | `AbortSignal` | Caller cancellation. | ### ExtractAsyncOptions | Field | Type | Description | | - | - | - | | `mode` | `typeof ExecMode.async` | | | `wait?` | `boolean` | Block until the job is terminal, so the returned job already carries its result. | | `timeoutMs?` | `number` | With `wait`, how long to wait before throwing. Default 300\_000. | | `file?` | `FileSource` | A local path, `File`, or `{filename, blob}` to upload. | | `url?` | `string` | A publicly accessible URL for the server to fetch. | | `ingested?` | `IdRef` | An already-ingested file (a `File` or its id). No re-upload: the server reads the document it already has, which is the cheapest of the three sources. | | `options?` | `Record` | Free-form request options, merged into the body. | | `signal?` | `AbortSignal` | Caller cancellation. | ## Responses ### AskResponse A grounded answer plus the chunks it was grounded in. | Field | Type | Description | | - | - | - | | `answer` | `string` | | | `results` | `AskResultItem[]` | | ### AskResultItem One retrieved chunk used as context by `ask`. | Field | Type | Description | | - | - | - | | `bboxes?` | `{ height: number; origin: string; pageNumber: number; unit: string; width: number; x: number; y: number; }[]` | | | `chunkId` | `string` | | | `content` | `string \| null` | | | `image?` | `{ b64Content: string; }` | | | `score` | `number` | | | `scores` | `{ keyword: number \| null; multivector: number \| null; relevance: number \| null; text: number \| null; vision: number \| null; }` | | | `source` | `{ contentTypes?: { [x: string]: unknown; }[] \| undefined; externalMetadata: { additionalMetadata: { [key: string]: unknown; }; externalId: string; externalUrl: string \| null; } \| null; fileId: number; filename: string; mimeType: string \| null; pageEnd: number \| null; pageStart: number \| null; sizeBytes: number \| null; tags: { id: number; name: string; }[]; title: string \| null; totalPages: number; }` | | | `warnings?` | `{ code: string; reason?: string \| undefined; }[]` | | | `workspace` | `{ id: number; name: string; } \| null` | | ### SearchResponse Ranked passages, with optional warnings and scoring breakdown. | Field | Type | Description | | - | - | - | | `explain?` | `{ [key: string]: unknown; }` | | | `results` | `SearchResultItem[]` | | | `warnings?` | `{ code: string; reason?: string \| undefined; }[]` | | ### SearchResultItem One ranked passage returned by `search`. | Field | Type | Description | | - | - | - | | `bboxes?` | `{ height: number; origin: string; pageNumber: number; unit: string; width: number; x: number; y: number; }[]` | | | `chunkId` | `string` | | | `content` | `string \| null` | | | `image?` | `{ b64Content: string; }` | | | `score` | `number` | | | `scores` | `{ keyword: number \| null; multivector: number \| null; relevance: number \| null; text: number \| null; vision: number \| null; }` | | | `source` | `{ contentTypes?: { [x: string]: unknown; }[] \| undefined; externalMetadata: { additionalMetadata: { [key: string]: unknown; }; externalId: string; externalUrl: string \| null; } \| null; fileId: number; filename: string; mimeType: string \| null; pageEnd: number \| null; pageStart: number \| null; sizeBytes: number \| null; tags: { id: number; name: string; }[]; title: string \| null; totalPages: number; }` | | | `workspace` | `{ id: number; name: string; } \| null` | | ### ParseResponse A completed synchronous `parse`. | Field | Type | Description | | - | - | - | | `completedAt` | `string` | | | `createdAt` | `string` | | | `document` | `ParseDocument` | | | `id` | `string` | | | `processingTimeMs` | `number` | | | `result` | `ParseResult` | | | `status` | `string` | | | `usage` | `ParseUsage` | | ### ParseResult | Field | Type | Description | | - | - | - | | `pages` | `Page[]` | | ### ParseDocument | Field | Type | Description | | - | - | - | | `fileSizeBytes` | `number` | | | `filename` | `string` | | | `mimeType` | `string` | | | `pageCount` | `number \| null` | | ### ParseUsage | Field | Type | Description | | - | - | - | | `pagesProcessed` | `number` | | ### ParseError | Field | Type | Description | | - | - | - | | `message` | `string` | | ### ExtractJobResponse An `extract` job, whether it ran inline or was queued. | Field | Type | Description | | - | - | - | | `completedAt` | `string \| null` | | | `createdAt` | `string \| null` | | | `document` | `ExtractDocument \| null` | | | `id` | `string` | | | `processingTimeMs` | `number \| null` | | | `progress` | `JobProgress \| null` | | | `result` | `ExtractResult \| null` | | | `status` | `string` | | | `usage` | `ExtractUsage \| null` | | ### ExtractResult | Field | Type | Description | | - | - | - | | `data` | `{ [key: string]: unknown; }[] \| null` | | | `pagination` | `{ hasNext: boolean; hasPrev: boolean; page: number; pageSize: number; totalItems: number; totalPages: number; } \| null` | | ### ExtractDocument | Field | Type | Description | | - | - | - | | `fileSizeBytes` | `number \| null` | | | `filename` | `string \| null` | | | `mimeType` | `string \| null` | | | `pageCount` | `number \| null` | | ### ExtractUsage | Field | Type | Description | | - | - | - | | `pagesProcessed` | `number \| null` | | ### JobProgress Progress reported by a running async job. | Field | Type | Description | | - | - | - | | `pagesProcessed` | `number` | | | `percentage` | `number` | | ## Jobs & batches ### Job A queued parse or extract job. Terminal state is `completedAt` being set, not a status string: the API documents only `pending` and `completed` and publishes no failure vocabulary, so a job that ends badly is one whose `completedAt` is set while `succeeded` is false. ```typescript theme={null} new Jobprotected constructor(transport: Transport, path: string, data: Record) ``` | Name | Type | Description | | - | - | - | | `transport` | `Transport` | | | `path` | `string` | | | `data` | `Record` | | | Field | Type | Description | | - | - | - | | `id` | `string` | | | `status` | `string` | | | `createdAt?` | `string \| null` | | | `completedAt?` | `string \| null` | | | `processingTimeMs?` | `number \| null` | | | `progress?` | `JobProgress \| null` | | | `done` | `boolean` | Whether the job is terminal, successfully or not. | | `succeeded` | `boolean` | Whether the job finished successfully. The one success state. | #### `Job.poll()` ```typescript theme={null} poll(options?: { page?: number; }): Promise ``` Re-fetch the job, updating it in place. **Arguments** | Name | Type | Description | | - | - | - | | `options?` | `{ page?: number; }` | `page` selects a page of results; extract paginates, parse ignores it. | | `options.page?` | `number` | | **Returns** | Type | Description | | - | - | | `Promise` | This job, updated, so `while (!(await job.poll()).succeeded)` reads naturally. | #### `Job.wait()` ```typescript theme={null} wait(options?: { timeoutMs?: number; pollMs?: number; }): Promise ``` Poll until the job is terminal. **Arguments** | Name | Type | Description | | - | - | - | | `options?` | `{ timeoutMs?: number; pollMs?: number; }` | `timeoutMs` before giving up (default 300\_000), `pollMs` between polls (default 2000). | | `options.timeoutMs?` | `number` | | | `options.pollMs?` | `number` | | **Returns** | Type | Description | | - | - | | `Promise` | This job, finished. | **Throws** | Error | When | | - | - | | `Error` | If the timeout elapses first. | | `LightOnError` | If the job ends in failure. | ### ParseJob An async `parse` job. Differs from `ExtractJob` only in what it carries. | Field | Type | Description | | - | - | - | | `document?` | `ParseDocument \| null` | | | `result?` | `ParseResult \| null` | | | `usage?` | `ParseUsage \| null` | | | `error?` | `ParseError \| null` | Set when the parse failed. Only parse reports failure this way. | | `id` | `string` | | | `status` | `string` | | | `createdAt?` | `string \| null` | | | `completedAt?` | `string \| null` | | | `processingTimeMs?` | `number \| null` | | | `progress?` | `JobProgress \| null` | | | `done` | `boolean` | Whether the job is terminal, successfully or not. | | `succeeded` | `boolean` | Whether the job finished successfully. The one success state. | #### `ParseJob.poll()` ```typescript theme={null} poll(options?: { page?: number; }): Promise ``` Re-fetch the job, updating it in place. **Arguments** | Name | Type | Description | | - | - | - | | `options?` | `{ page?: number; }` | `page` selects a page of results; extract paginates, parse ignores it. | | `options.page?` | `number` | | **Returns** | Type | Description | | - | - | | `Promise` | This job, updated, so `while (!(await job.poll()).succeeded)` reads naturally. | #### `ParseJob.wait()` ```typescript theme={null} wait(options?: { timeoutMs?: number; pollMs?: number; }): Promise ``` Poll until the job is terminal. **Arguments** | Name | Type | Description | | - | - | - | | `options?` | `{ timeoutMs?: number; pollMs?: number; }` | `timeoutMs` before giving up (default 300\_000), `pollMs` between polls (default 2000). | | `options.timeoutMs?` | `number` | | | `options.pollMs?` | `number` | | **Returns** | Type | Description | | - | - | | `Promise` | This job, finished. | **Throws** | Error | When | | - | - | | `Error` | If the timeout elapses first. | | `LightOnError` | If the job ends in failure. | ### ExtractJob An async `extract` job. | Field | Type | Description | | - | - | - | | `document?` | `ExtractDocument \| null` | | | `result?` | `ExtractResult \| null` | | | `usage?` | `ExtractUsage \| null` | | | `id` | `string` | | | `status` | `string` | | | `createdAt?` | `string \| null` | | | `completedAt?` | `string \| null` | | | `processingTimeMs?` | `number \| null` | | | `progress?` | `JobProgress \| null` | | | `done` | `boolean` | Whether the job is terminal, successfully or not. | | `succeeded` | `boolean` | Whether the job finished successfully. The one success state. | #### `ExtractJob.poll()` ```typescript theme={null} poll(options?: { page?: number; }): Promise ``` Re-fetch the job, updating it in place. **Arguments** | Name | Type | Description | | - | - | - | | `options?` | `{ page?: number; }` | `page` selects a page of results; extract paginates, parse ignores it. | | `options.page?` | `number` | | **Returns** | Type | Description | | - | - | | `Promise` | This job, updated, so `while (!(await job.poll()).succeeded)` reads naturally. | #### `ExtractJob.wait()` ```typescript theme={null} wait(options?: { timeoutMs?: number; pollMs?: number; }): Promise ``` Poll until the job is terminal. **Arguments** | Name | Type | Description | | - | - | - | | `options?` | `{ timeoutMs?: number; pollMs?: number; }` | `timeoutMs` before giving up (default 300\_000), `pollMs` between polls (default 2000). | | `options.timeoutMs?` | `number` | | | `options.pollMs?` | `number` | | **Returns** | Type | Description | | - | - | | `Promise` | This job, finished. | **Throws** | Error | When | | - | - | | `Error` | If the timeout elapses first. | | `LightOnError` | If the job ends in failure. | ### BatchIngestJob A running (or finished) batch ingestion. Returned by `Workspace.ingestMany(files, { mode: "async" })`. Uploads (and, when `wait` is set, ingestion polls) run in the background; read `progress`, `succeeded` and `failed` at any time, or block with `wait()`. ```typescript theme={null} new BatchIngestJob(client: Transport, workspaceId: number, files: File[], prefailed: FailedIngest[], options: BatchOptions) ``` | Name | Type | Description | | - | - | - | | `client` | `Transport` | | | `workspaceId` | `number` | | | `files` | `File[]` | | | `prefailed` | `FailedIngest[]` | | | `options` | `BatchOptions` | | | Field | Type | Description | | - | - | - | | `done` | `boolean` | True once every file has reached a terminal state, ok or failed. | | `progress` | `BatchProgress` | A snapshot of the counts, safe to read while the batch runs. | | `succeeded` | `readonly File[]` | Files that have succeeded so far. | | `failed` | `readonly FailedIngest[]` | Failures so far, available mid-run. | | `result` | `BatchIngest` | Current succeeded/failed as a `BatchIngest`; terminal once `done`. | #### `BatchIngestJob.poll()` ```typescript theme={null} poll(): BatchProgress ``` The current progress. The mirror of a parse/extract job's `poll()`, except that nothing needs fetching: the batch is driven from this process, so its state is already current. **Returns** | Type | Description | | - | - | | `BatchProgress` | | #### `BatchIngestJob.wait()` ```typescript theme={null} wait(options?: { timeoutMs?: number; }): Promise ``` Block until the batch finishes, then return its result. **Arguments** | Name | Type | Description | | - | - | - | | `options?` | `{ timeoutMs?: number; }` | `timeoutMs` for the whole batch; omit to wait indefinitely. | | `options.timeoutMs?` | `number` | | **Returns** | Type | Description | | - | - | | `Promise` | The terminal `BatchIngest`. | **Throws** | Error | When | | - | - | | `Error` | If the batch doesn't finish in time. | | `Error` | The first upload or ingestion error, rethrown, when the batch ran without `ignoreErrors`. | ### BatchOptions | Field | Type | Description | | - | - | - | | `ignoreErrors?` | `boolean` | If false (the default), the first failure throws, inline or from `job.wait()`. If true, failures are collected and the batch carries on. | | `wait?` | `boolean` | Wait for each upload's ingestion to finish, not just to be accepted. | | `timeoutMs?` | `number` | Per-file milliseconds to wait for ingestion when `wait` is set. Default 300\_000. | | `pollMs?` | `number` | Milliseconds between ingestion status checks. Default 2000. | | `maxConcurrency?` | `number` | Concurrent uploads and polls. Default 8. | | `tags?` | `number[]` | Tag ids assigned to every uploaded document. | ### BatchIngest The terminal outcome of a batch. | Field | Type | Description | | - | - | - | | `succeeded` | `File[]` | | | `failed` | `FailedIngest[]` | | | `ok` | `boolean` | Whether every item made it. | ### BatchProgress A snapshot of a running batch. | Field | Type | Description | | - | - | - | | `total` | `number` | | | `uploaded` | `number` | Uploads the API has accepted. | | `ingested` | `number` | Files that have finished ingesting. Stays 0 unless the batch is waiting. | | `failed` | `number` | | | `done` | `boolean` | | ### FailedIngest One item of a batch that did not make it. | Field | Type | Description | | - | - | - | | `source` | `string` | The path or pattern it came from; empty when the source was a blob. | | `error` | `Error` | Why it failed: the upload error, the ingestion error, or a missing path. | | `file?` | `File` | The File, when the failure was at *ingestion* rather than upload. | ## Streaming events ### AskEvent ```typescript theme={null} type AskEvent = SourcesEvent | TokenEvent | DoneEvent ``` ### SourcesEvent The retrieved chunks, sent once before generation starts. | Field | Type | Description | | - | - | - | | `type` | `"sources"` | | | `results` | `AskResultItem[]` | Retrieved chunks used as context, the same items `ask` returns. | ### TokenEvent One chunk of the answer as it generates. | Field | Type | Description | | - | - | - | | `type` | `"token"` | | | `text` | `string` | Answer text to append; may be several tokens. | ### DoneEvent Generation finished; no further events follow. | Field | Type | Description | | - | - | - | | `type` | `"done"` | | ## Schemas ### `asJsonSchema()` ```typescript theme={null} export declare function asJsonSchema(schema: SchemaInput): Promise ``` Either guided-generation input, as the self-contained schema to send. **Arguments** | Name | Type | Description | | - | - | - | | `schema` | `SchemaInput` | A Zod schema, or a plain object holding a JSON Schema. | **Returns** | Type | Description | | - | - | | `Promise` | A self-contained JSON Schema, free of `$defs` and `$ref`. | **Throws** | Error | When | | - | - | | `TypeError` | If `schema` is neither. | ### `normalizeJsonSchema()` ```typescript theme={null} export declare function normalizeJsonSchema(schema: JsonSchema): JsonSchema ``` Normalize a JSON Schema into the self-contained shape vLLM wants. `$defs`/`$ref` inlined, nullable `anyOf` collapsed to `type: [X, "null"]`, and the draft-2020-12 `$schema` marker added (an existing one is kept). **Arguments** | Name | Type | Description | | - | - | - | | `schema` | `JsonSchema` | A JSON Schema, possibly carrying `$defs`/`$ref`. | **Returns** | Type | Description | | - | - | | `JsonSchema` | An equivalent self-contained schema, free of `$defs` and `$ref`. | **Throws** | Error | When | | - | - | | `TypeError` | If a `#/$defs/` ref has no target. | ### SchemaInput Either input `ask` and `extract` accept for guided generation. A Zod schema is converted with Zod's own `toJSONSchema`, imported only when one is actually passed, so `zod` stays an optional peer dependency that costs nothing to callers who hand over a plain JSON Schema instead. ```typescript theme={null} type SchemaInput = JsonSchema | StandardSchemaLike ``` ### JsonSchema A JSON Schema, as a plain object. ```typescript theme={null} type JsonSchema = Record ``` ## List filters ### FileFilters Filters for `File.list`, e.g. `status`, `tag_id`, `external_metadata__external_id`. | Field | Type | Description | | - | - | - | | `page_size?` | `number` | | | `attribute?` | `string` | | | `content_type?` | `string` | | | `created_at_after?` | `string` | | | `created_at_before?` | `string` | | | `extension?` | `string` | | | `external_metadata__doc_type?` | `string` | | | `external_metadata__external_id?` | `string` | | | `filename?` | `string` | | | `group_id?` | `string` | | | `include_details?` | `boolean` | | | `max_documents?` | `number` | | | `ordering?` | `string` | | | `owner_id?` | `string` | | | `search?` | `string` | | | `search_details?` | `boolean` | | | `search_details_chunks_limit?` | `number` | | | `status?` | `"converting" \| "embedded" \| "embedding" \| "embedding_failed" \| "fail" \| "parsed" \| "parsing" \| "parsing_failed" \| "pending" \| "pending_conversion" \| "updating"` | | | `status_vision?` | `"-" \| "embedded" \| "fail" \| "pending" \| "processing"` | | | `tag_id?` | `string` | | | `title?` | `string` | | | `total_pages_max?` | `number \| null` | | | `total_pages_min?` | `number \| null` | | | `updated_at_after?` | `string` | | | `updated_at_before?` | `string` | | | `upload_session_uuid?` | `string` | | | `workspace_id?` | `string` | | ### WorkspaceFilters Filters for `Workspace.list`, e.g. `name`, `user_role`, `ordering`. | Field | Type | Description | | - | - | - | | `page_size?` | `number` | | | `group_id?` | `number` | | | `ordering?` | `"-created_at" \| "-name" \| "-updated_at" \| "created_at" \| "name" \| "updated_at"` | | | `name?` | `string` | | | `datasource_type?` | `"googledrive" \| "servicenow" \| "sharepoint" \| "smb" \| "webscrapper"` | | | `document_upload_method?` | `"manual" \| "synced"` | | | `group_name?` | `string` | | | `user_role?` | `"editor" \| "owner" \| "viewer"` | | | `workspace_type?` | `"personal" \| "public" \| "shared"` | | ### TagFilters Filters for `Tag.list`, e.g. `name`, `auto_assign`. | Field | Type | Description | | - | - | - | | `page_size?` | `number` | | | `auto_assign?` | `boolean` | | | `name?` | `string` | | ### ApiKeyFilters Filters for `ApiKey.list`, e.g. `is_expired`. | Field | Type | Description | | - | - | - | | `is_expired?` | `boolean` | | | `page_size?` | `number` | | ## Enums ### AttributeType Type of a content-type attribute column. `select` and `multiSelect` require `choices`; the API also accepts the aliases `multiselect` and `richtext` for the hyphenated values used here. | Member | Value | | - | - | | `text` | `text` | | `number` | `number` | | `date` | `date` | | `boolean` | `boolean` | | `select` | `select` | | `multiSelect` | `multi-select` | | `richText` | `rich-text` | ### DownloadPurpose Which stored version of a file to download. The server falls back to `original` when the requested purpose has no associated file. | Member | Value | | - | - | | `original` | `original` | | `renderedPdf` | `rendered_pdf` | | `transcript` | `transcript` | ### ExecMode Execution mode for parse/extract: run inline or queue as an async job. | Member | Value | | - | - | | `sync` | `sync` | | `async` | `async` | ### FileStatus Ingestion pipeline status for a File. | Member | Value | | - | - | | `pending` | `pending` | | `pendingConversion` | `pending_conversion` | | `converting` | `converting` | | `parsing` | `parsing` | | `parsingFailed` | `parsing_failed` | | `embedding` | `embedding` | | `embeddingFailed` | `embedding_failed` | | `embedded` | `embedded` | | `parsed` | `parsed` | | `fail` | `fail` | | `updating` | `updating` | ### JobStatus Status of an async parse/extract job. Only `pending` (initial) and `completed` (success) are documented by the API; the schema types `status` as a bare string with no enum and doesn't publish the failure vocabulary. This is for call-site comparisons, NOT to validate the response field, so an unrecognized server value compares unequal rather than erroring. Detect terminal failure via `completedAt` being set without `completed` (or, for parse, the `error` block) rather than a status string. | Member | Value | | - | - | | `pending` | `pending` | | `completed` | `completed` | ### RelevanceScoring Cross-encoder relevance scoring step for search. | Member | Value | Description | | - | - | - | | `none` | `none` | Skip scoring, return all candidates. | | `scoringOnly` | `scoring_only` | Score but don't filter. | | `scoringAndFiltering` | `scoring_and_filtering` | Score and drop below threshold. | ### ReprocessLevel Reprocessing level queued on a File (`pendingReprocess`), `update` = replacement. | Member | Value | | - | - | | `reparse` | `reparse` | | `rechunk` | `rechunk` | | `reembed` | `reembed` | | `reembedVision` | `reembed_vision` | | `update` | `update` | ### Role Access role granted by an API-key scope on a workspace. | Member | Value | | - | - | | `viewer` | `viewer` | | `editor` | `editor` | | `owner` | `owner` | ### SearchMode Retrieval mode for search and ask. | Member | Value | Description | | - | - | - | | `text` | `text` | Hybrid keyword + vector. | | `vision` | `vision` | VLM-embedded page image. | ### ThumbnailStatus Whether a file's thumbnail has been generated (uppercase, as the API sends). | Member | Value | | - | - | | `MISSING` | `MISSING` | | `PROCESSING` | `PROCESSING` | | `READY` | `READY` | ## Errors Everything the SDK throws derives from `LightOnError`, importable from `@lighton-ai/sdk`. Every method that performs a request can throw these, so they are listed once here rather than repeated on each method. A method's own **Throws** block covers only what it throws directly. | Error | Extends | Thrown when | | - | - | - | | `LightOnError` | `Error` | Base class for every error raised by this SDK. | | `LightOnConnectionError` | `LightOnError` | Transport failure before any response was received (DNS, timeout, reset). | | `MalformedResponseError` | `LightOnError` | A 2xx response body was not valid JSON. | | `StreamError` | `LightOnError` | The server sent an `error` event partway through a stream. Not a `LightOnAPIError`: the HTTP response was a perfectly good 200 and the failure happened during generation, so there is no status code to carry. The answer is incomplete, which is why this throws instead of arriving as one more event a caller could mistake for a finished answer. `body` holds the payload. | | `LightOnAPIError` | `LightOnError` | The API returned a non-2xx response. | | `AuthenticationError` | `LightOnAPIError` | 401, bad or missing API key (the request is not authenticated). | | `PermissionDeniedError` | `LightOnAPIError` | 403, authenticated, but the key lacks permission for this operation. Distinct from `AuthenticationError`: the credentials are valid, but the caller isn't allowed (e.g. an endpoint that requires the CompanyAdmin role). | | `NotFoundError` | `LightOnAPIError` | 404, the resource does not exist. | | `RateLimitError` | `LightOnAPIError` | 429, too many requests. `retryAfter` is the seconds to wait before retrying, from the `Retry-After` response header when the server sends it (else null). | | `ServerError` | `LightOnAPIError` | 5xx, the API failed to handle the request. | | `MaintenanceError` | `ServerError` | 503 during a planned maintenance window, not a crash. A `ServerError` subclass, so existing `catch (e) { if (e instanceof ServerError) }` handlers keep working; check for this specifically to tell "come back later" apart from "this broke", since only one of the two is worth retrying. `mode` is `full_shutdown` or `warning_banner` (both block the request), `reason` is operator-supplied text, `startedAt` is when the window opened, and `endpointCategories` names the affected categories, empty meaning every endpoint. The untouched payload is always on `.body`. | # How to submit a report to the Support team Source: https://developers.lighton.ai/support/index On this page, you will find the official methods to report a bug, submit a feature request, or ask a question to our team. ## Contact us by email **[support@lighton.ai](mailto:support@lighton.ai)** This method is recommended if you need to provide a large amount of details. Please do not hesitate to send us a screenshot of what you see. ## Optimal content of a request Please provide as much detail as possible so we can analyze and resolve the issue more effectively. ### For an issue, please include: * A **clear and detailed description of the issue** * The **steps you followed to reproduce the issue** * The **expected result**: what you think should happen when following the reproduction steps * The **observed result**: what you actually observe when following the reproduction steps * **Screenshots** and/or a **video showing the issue and/or an error message** * Any **information** you can give us is **relevant** for exemple: * **Operating system** (Windows, Mac, Linux, …) * **Browser** and **version** * Approximate **date and time** **Console errors:** If you are comfortable with technical details, you can check your browser console (F12 key on Chrome or Firefox): * Right-click on the web page * Select "Inspect" or "Inspect Element" * Click on the "Console" tab * Capture any error messages ### For a feature request, please provide: * An **exhaustive description of the expected feature** * The **need or objective** that this feature would address * A **use case** # Asking questions Source: https://developers.lighton.ai/tutorials/ask Get a grounded, LLM-generated answer over your documents with the sources it used. Illustration of the Ask use case. Ask is retrieval-augmented generation in a single call. It runs the same retrieval pipeline as [Search](/tutorials/search), then passes the retrieved passages to an LLM that writes a natural-language answer grounded in them, and hands back both the answer and the sources it drew from. Use Ask when you want a direct answer to a question. Use [Search](/tutorials/search) when you want the raw passages and intend to do your own ranking, display, or generation on top of them. Ask is intended for **basic, single-turn question answering**. It runs one retrieval and one generation with a fixed prompt and no tool use, memory, or follow-up reasoning. For advanced RAG (multi-step retrieval, query rewriting, conversational context, custom prompts, or your own choice of model), call [`POST /api/v3/search`](/tutorials/search) directly and drive generation from within your own agentic loop. This tutorial walks through [`POST /api/v3/ask`](/api-reference/ask/ask-a-question-over-your-documents). For the full schema and every parameter, see the [API reference](/api-reference/introduction). Each request costs one search-with-generation credit. ## Your first question ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment response = client.ask("What is our JWT token expiry policy?") print(response.answer) for result in response.results: print(f" ↳ {result.source.filename}, p.{result.source.page_start}") ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment const response = await client.ask("What is our JWT token expiry policy?") console.log(response.answer) for (const result of response.results) { console.log(` ↳ ${result.source.filename}, p.${result.source.pageStart}`) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/ask", headers=headers, json={"query": "What is our JWT token expiry policy?"}, ) body = response.json() print(body["answer"]) for result in body["results"]: print(f" ↳ {result['source']['filename']}, p.{result['source']['page_start']}") ``` By default this searches every document your API key can reach, retrieves the top 10 passages, and generates an answer with the flagship model. The query is capped at 1500 characters. ## Reading the response The response has two fields: * **`answer`**: the LLM-generated answer, grounded in the retrieved passages. * **`results`**: the ranked chunks used as context, in the same shape as a [Search](/tutorials/search#reading-the-response) result (`chunk_id`, `content`, `score`, `scores`, `source`, `workspace`). Use these to show citations or let users open the source document. ```json theme={null} { "results": [ { "chunk_id": "550e8400-e29b-41d4-a716-446655440000", "content": "JWT tokens are signed using RS256 and expire after 1 hour.", "score": 0.87, "scores": { "text": 0.91, "vision": null, "keyword": 0.43, "multivector": 12.4, "relevance": 0.95 }, "source": { "file_id": 512, "filename": "auth-system.pdf", "title": "Authentication System Design", "mime_type": "pdf", "size_bytes": 482113, "page_start": 3, "page_end": 4, "total_pages": 12, "tags": [{"id": 7, "name": "security"}], "external_metadata": null }, "workspace": {"id": 42, "name": "Engineering Docs"} } ], "answer": "JWT tokens are signed using RS256 and expire after 1 hour (auth-system.pdf, page 3)." } ``` ## Scoping to a subset of documents Ask uses the same scoping rules as Search. Pass `workspace_id` and/or `tag_id` to narrow the corpus, or `file_id` to target specific files. `file_id` is mutually exclusive with `workspace_id` and `tag_id`. ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: response = client.ask( "What are the GDPR data retention requirements?", tags=[7], # or workspaces=[42], or files=[101, 102] ) print(response.answer) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() const response = await client.ask("What are the GDPR data retention requirements?", { tags: [7], // or workspaces: [42], or files: [101, 102] }) console.log(response.answer) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/ask", headers=headers, json={ "query": "What are the GDPR data retention requirements?", "tag_id": [7], # or "workspace_id": [42], or "file_id": [101, 102] }, ) print(response.json()["answer"]) ``` `max_results` (default 10, range 1 to 50) controls how many passages are retrieved and fed to the model as context. More context can improve answer quality on broad questions, at the cost of latency. ## Choosing a model Omit `model` and you get the default configured for your organization, which is what most apps want. To name one explicitly, pass a technical name your deployment actually offers: `GET /api/v3/instance/models` lists exactly what this endpoint accepts, covering the publicly available models, your organization's default, and any model your organization registered itself (`custom-{company_id}-{uuid}`). Any other value is rejected with a `422`. Read the list at startup rather than hardcoding a name: which models a deployment offers is an instance-level setting, so a name that works on one deployment can be rejected on another. ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: response = client.ask( "Summarize the incident response playbook.", model="alfred-ft5", # replace with a model from GET /api/v3/instance/models ) print(response.answer) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() const response = await client.ask("Summarize the incident response playbook.", { model: "alfred-ft5", // replace with a model from GET /api/v3/instance/models }) console.log(response.answer) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/ask", headers=headers, json={ "query": "Summarize the incident response playbook.", "model": "alfred-ft5", # replace with a model from GET /api/v3/instance/models }, ) print(response.json()["answer"]) ``` ## Getting the answer as JSON When the answer feeds a downstream system rather than a human reader, pass `response_format` with a JSON Schema. The model is then constrained to produce valid JSON matching it, so you can parse the result instead of writing a text scraper. The schema must be an object schema: `type` set to `"object"`, plus a `properties` map. Anything else is rejected with a `422`. In the SDK the parameter is `schema`, and it accepts a pydantic model as well as a raw schema dict. ```python Python SDK theme={null} from lighton import LightOn from pydantic import BaseModel class Findings(BaseModel): findings: list[str] confidence: float with LightOn() as client: response = client.ask( "What are the key findings?", max_results=5, schema=Findings, # or a raw JSON Schema dict ) # `answer` is JSON text matching the schema, not free text print(Findings.model_validate_json(response.answer)) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" import { z } from "zod" const Findings = z.object({ findings: z.array(z.string()), confidence: z.number(), }) const client = new LightOn() const response = await client.ask("What are the key findings?", { maxResults: 5, schema: Findings, // or a raw JSON Schema object }) // `answer` is JSON text matching the schema, not free text console.log(Findings.parse(JSON.parse(response.answer))) ``` ```python Plain Python theme={null} import json import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/ask", headers=headers, json={ "query": "What are the key findings?", "max_results": 5, "response_format": { "type": "object", "properties": { "findings": {"type": "array", "items": {"type": "string"}}, "confidence": {"type": "number"}, }, "required": ["findings", "confidence"], }, }, ) # `answer` is a JSON string matching the schema, not free text print(json.loads(response.json()["answer"])) ``` `answer` still comes back as a string, but that string is now JSON conforming to your schema. Parse it to get the object: ```json theme={null} { "findings": [ "Latency regressed 18% after the March migration.", "Error budget was exhausted in two of six services." ], "confidence": 0.82 } ``` This works in streaming mode too: concatenate the `token` events, then parse the assembled string once the `done` event arrives. ## Streaming the answer For chat-style UIs where you want to show the answer as it's written, set `stream: true`. The response is a stream of [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) instead of a single JSON body: | Event | Payload | | - | - | | `sources` | The retrieved chunks (same shape as `results`), emitted first. | | `token` | An incremental piece of the answer. Many of these arrive in sequence. | | `done` | The stream is complete. | | `error` | Generation failed; the stream ends. | ```python Python SDK theme={null} from lighton import LightOn, SourcesEvent, TokenEvent with LightOn() as client: # reads LIGHTON_API_KEY from the environment for event in client.ask("What is our JWT token expiry policy?", stream=True): if isinstance(event, SourcesEvent): sources = event.results # retrieved chunks, arrive once up front elif isinstance(event, TokenEvent): print(event.text, end="", flush=True) # stream the answer to your UI print() ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment let sources for await (const event of client.ask("What is our JWT token expiry policy?", { stream: true })) { if (event.type === "sources") { sources = event.results // retrieved chunks, arrive once up front } else if (event.type === "token") { process.stdout.write(event.text) // stream the answer to your UI } } console.log() ``` ```python Plain Python theme={null} import json import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} with requests.post( "https://api.lighton.ai/api/v3/ask", headers=headers, json={"query": "What is our JWT token expiry policy?", "stream": True}, stream=True, ) as response: event = None for line in response.iter_lines(decode_unicode=True): if not line: continue if line.startswith("event:"): event = line[len("event:"):].strip() elif line.startswith("data:"): data = json.loads(line[len("data:"):].strip()) if event == "sources": sources = data # retrieved chunks, arrive once up front elif event == "token": print(data, end="", flush=True) # stream answer to the UI elif event == "error": raise RuntimeError(data) ``` Sources arrive first so you can render citations before the answer starts streaming. Stop reading once you receive the `done` event. The SDK parses the stream for you and yields one typed event per message: `SourcesEvent` once up front, then a `TokenEvent` per chunk, then `DoneEvent`. If the server fails partway through, it raises `StreamError` rather than handing you a half-finished answer. ## Common errors | Status | Cause | | - | - | | `400` | Request body is not parsable JSON | | `403` | None of the provided filters resolve to authorized resources | | `422` | Validation error, e.g. unsupported `model`, or `file_id` combined with `workspace_id`/`tag_id` | | `429` | Rate limit exceeded | | `404` | A supported model is not currently available on the backend | | `503` | Model temporarily unavailable. Retry later | | `504` | Model did not respond in time. Retry later | # Extracting structured data Source: https://developers.lighton.ai/tutorials/extract Pull typed fields out of documents using a JSON Schema you provide. Illustration of the Extract use case. Extract takes a document and a JSON Schema, and returns the fields described by the schema as a structured object. You define what you want in the schema, the API finds it in the document: invoices, forms, ID documents, contracts, anything. By default the call is **synchronous**: you send the request, the API blocks until the extraction is done, and the response comes back with the full result. For larger documents, opt into **async mode** with `options.async = true`: you get back a job ID, and you poll until done. Full request/response schema for [`POST /api/v3/extract`](/api-reference/extract/extract-structured-data-from-a-document) and [`GET /api/v3/extract/{job_id}`](/api-reference/extract/get-the-status-and-result-of-an-async-extract-job) lives in the [API reference](/api-reference/introduction). ## When to use Extract | | Input | Output | Best for | | - | - | - | - | | **Search** | Query string | Ranked text chunks | Finding passages across many documents | | **Parse** | Document file | Markdown text | Converting a document to clean text | | **Extract** | Document + JSON Schema | Structured object | Pulling typed fields from forms, invoices, contracts | Use Extract when you need machine-readable values out of a document, mapped to fields you've named. Extract applies your schema **independently to every page** of the document: `result.data` comes back with one object per page. That makes it ideal for **mechanical, repetitive document processing**, where each page is a self-contained record (a stack of invoices, a batch of forms, a multi-page table) and you want the same fields pulled from every one. If instead you need a **single structured JSON for the whole document** (synthesizing information spread across pages into one consolidated object), Extract is the wrong tool. Use the [`POST /api/v3/search`](/tutorials/search) endpoint inside your own agentic loop to retrieve the relevant passages, then have your model generate the structured output from them. ## Sync extraction (small documents) Sync mode handles documents up to **20 MB, 15 pages** and returns the full result in one response. ```python Python SDK theme={null} from lighton import LightOn from pydantic import BaseModel, Field class Invoice(BaseModel): invoice_number: str = Field(description="The invoice reference number") total: float = Field(description="The total amount due") due_date: str | None = Field(None, description="Due date in ISO format") with LightOn() as client: # reads LIGHTON_API_KEY from the environment # A pydantic model is converted to the guided-generation schema for you. # Field descriptions steer the model, so write them as instructions. response = client.extract( schema=Invoice, url="https://example.com/invoices/inv-2025-004.pdf", ) print(response.status) print(response.result.data) # one object per page ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" import { z } from "zod" const Invoice = z.object({ invoice_number: z.string().describe("The invoice reference number"), total: z.number().describe("The total amount due"), due_date: z.string().nullable().default(null).describe("Due date in ISO format"), }) const client = new LightOn() // reads LIGHTON_API_KEY from the environment // A Zod schema is converted to the guided-generation schema for you. // Field descriptions steer the model, so write them as instructions. const response = await client.extract(Invoice, { url: "https://example.com/invoices/inv-2025-004.pdf", }) console.log(response.status) console.log(response.result?.data) // one object per page ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/extract", headers=headers, json={ "document": "https://example.com/invoices/inv-2025-004.pdf", "schema": { "type": "object", "properties": { "invoice_number": {"type": "string", "description": "The invoice reference number"}, "total": {"type": "number", "description": "The total amount due"}, "due_date": {"type": "string", "description": "Due date in ISO format"}, }, }, }, ) result = response.json() print(result["status"]) # → completed print(result["result"]["data"]) # one object per page ``` `result.data` holds one object per page, for example `[{"invoice_number": "INV-2025-004", "total": 4750.0, "due_date": "2025-12-01"}, ...]`. You can also send a file via `multipart/form-data` instead of a URL: ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # `schema` also takes a raw JSON Schema dict, sent as-is after validation response = client.extract( schema={"type": "object", "properties": {"invoice_number": {"type": "string"}}}, path="invoice.pdf", ) print(response.result.data) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // The schema can also be a raw JSON Schema object, sent as-is after validation const response = await client.extract( { type: "object", properties: { invoice_number: { type: "string" } } }, { file: "invoice.pdf" }, ) console.log(response.result?.data) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} with open("invoice.pdf", "rb") as f: response = requests.post( "https://api.lighton.ai/api/v3/extract", headers=headers, files={"file": f}, data={"schema": '{"type": "object", "properties": {"invoice_number": {"type": "string"}}}'}, ) print(response.json()["result"]["data"]) ``` In multipart requests, `schema` arrives as a JSON-encoded string and is decoded server-side. ### Extracting from a document you already ingested If the document is already in the platform, you don't need to re-upload it. Pass `file_id` instead of `document` or a file part (`file` in the SDK, which also takes a `File` object), and the API extracts from the stored copy. Get the id from [`GET /api/v3/files`](/api-reference/files/list-files-accessible-to-the-authenticated-user), or from the response of the [`POST /api/v3/files`](/api-reference/files/upload-a-file) call that ingested it. ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: response = client.extract( schema={ "type": "object", "properties": { "invoice_number": {"type": "string"}, "total": {"type": "number"}, }, }, file=42, # replace with your file ID, or a File object ) print(response.result.data) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() const response = await client.extract( { type: "object", properties: { invoice_number: { type: "string" }, total: { type: "number" }, }, }, { ingested: 42, // replace with your file ID, or a File object }, ) console.log(response.result?.data) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} file_id = 42 # replace with your file ID, from GET /api/v3/files response = requests.post( "https://api.lighton.ai/api/v3/extract", headers=headers, json={ "file_id": file_id, "schema": { "type": "object", "properties": { "invoice_number": {"type": "string"}, "total": {"type": "number"}, }, }, }, ) print(response.json()["result"]["data"]) ``` `file_id`, `document`, and a multipart file part are mutually exclusive: send exactly one. Sending none returns a `400`, and a `file_id` your API key can't read returns a `404`. ## Async extraction (large documents) For documents up to **100 MB, 1000 pages**, set `options.async = true`. The API returns a `202 Accepted` immediately with a job ID. Poll [`GET /api/v3/extract/{job_id}`](/api-reference/extract/get-the-status-and-result-of-an-async-extract-job) until `status` is `completed` or `failed`. Recommended cadence: 1 s for the first 10 s, then 5 s, capped at 30 s. The SDK does this for you when you pass `wait=True`; drop it and call `job.poll()` yourself if you want to report progress as it runs. ```python Python SDK theme={null} from lighton import ExecMode, LightOn with LightOn() as client: # wait=True polls for you and returns once the job is terminal job = client.extract( schema={"type": "object", "properties": {"title": {"type": "string"}}}, url="https://example.com/large-report.pdf", mode=ExecMode.ASYNC, wait=True, ) print(job.result.data) ``` ```typescript TypeScript SDK theme={null} import { ExecMode, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // wait: true polls for you and resolves once the job is terminal const job = await client.extract( { type: "object", properties: { title: { type: "string" } } }, { url: "https://example.com/large-report.pdf", mode: ExecMode.async, wait: true, }, ) console.log(job.result?.data) ``` ```python Plain Python theme={null} import os import time import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Returns 202 Accepted immediately with a job ID instead of blocking response = requests.post( "https://api.lighton.ai/api/v3/extract", headers=headers, json={ "document": "https://example.com/large-report.pdf", "schema": {"type": "object", "properties": {"title": {"type": "string"}}}, "options": {"async": True}, }, ) job_id = response.json()["id"] # e.g. "ext_0196e4b2a3c14d5e8f7a9b2c1d0e3f4a" for _ in range(120): # give up after ~4 minutes response = requests.get( f"https://api.lighton.ai/api/v3/extract/{job_id}", headers=headers, ) data = response.json() if data["status"] in ("completed", "failed"): break time.sleep(2) print(data["result"]["data"]) ``` ## Reading the response Sync and async responses share the same shape: ```json theme={null} { "id": "ext_0196e4b2a3c14d5e8f7a9b2c1d0e3f4a", "status": "completed", "created_at": "2026-03-31T10:00:00+00:00", "completed_at": "2026-03-31T10:00:04+00:00", "processing_time_ms": 3200, "document": { "filename": "invoice.pdf", "page_count": 3, "file_size_bytes": 245120, "mime_type": "application/pdf" }, "result": { "data": [ {"invoice_number": "INV-2026-001", "total": null}, {"invoice_number": null, "total": 1250.00} ], "pagination": { "page": 1, "page_size": 15, "total_items": 3, "total_pages": 1, "has_next": false, "has_prev": false } }, "usage": { "pages_processed": 3 } } ``` `result.data` is one entry per page, each shaped like your schema. Fields that weren't found on a given page are `null`. When the document has more than 15 pages, `result.data` is paginated: request additional pages with `?page=N` on the [`GET /api/v3/extract/{job_id}`](/api-reference/extract/get-the-status-and-result-of-an-async-extract-job) endpoint. ## Common errors | Status | Cause | | - | - | | `400` | No document, file, or `file_id` supplied; unsupported format; or page limit exceeded | | `401` | Missing or invalid API key | | `404` | Extract job not found, or `file_id` is unknown, unauthorized, or its stored file is missing | | `413` | File exceeds the size limit (20 MB sync, 100 MB async) | | `422` | JSON Schema is malformed, uses unsupported features, or exceeds limits | | `429` | Rate limit exceeded (6 requests/second per tenant) | | `503` | Parsing backend is overloaded. Retry later | # Classify files and set attribute values Source: https://developers.lighton.ai/tutorials/facets/apply Turn documents into structured, queryable data. This tutorial uses [`POST /api/v3/files/{file_id}/facets`](/api-reference/facets/classify-file-and-set-attribute-values) and [`GET /api/v3/files/{file_id}/facets`](/api-reference/facets/list-file-classifications-and-attribute-values). The full schema lives in the [API reference](/api-reference/introduction). Your classification tree exists. Now you'll classify actual documents and fill in their metadata, turning unstructured files into queryable, structured data. Applying facets to a document is a two-step act: 1. **Classify:** tell LightOn *what type* this document is (`contract:nda`) 2. **Set values:** fill in the attribute values for that classification (`counterparty`, `jurisdiction`, ...) All actions are **idempotent**. Send a **JSON array** to batch multiple actions. LightOn queues exactly one BM25 reindex per batch, regardless of how many actions it contains. ## Step 1: Upload a document Before classifying, you need a file. If you already have one, skip this step and use your existing `file_id`. Replace `workspace_id` with your own. You can list your workspaces via [`GET /api/v3/workspaces`](/api-reference/workspaces/list-workspaces). ```python Python SDK theme={null} from lighton import File, LightOn, Workspace workspace_id = 42 # replace with your workspace ID with LightOn() as client: # reads LIGHTON_API_KEY from the environment workspace = Workspace.get(client, workspace_id) # Uploading is the ingestion. Non-blocking: the returned File is "pending". # Pass wait=True to block until it's embedded and searchable. doc = workspace.ingest( File(path="acme_nda_2025.pdf", title="NDA with Acme Corp") ) print(doc.id, doc.status) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn, Workspace } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // reads LIGHTON_API_KEY from the environment const workspace = await Workspace.get(client, workspaceId) // Uploading is the ingestion. Non-blocking: the returned File is "pending". // Pass { wait: true } to block until it's embedded and searchable. const doc = await workspace.ingest(new File({ path: "acme_nda_2025.pdf", title: "NDA with Acme Corp" })) console.log(doc.id, doc.status) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} workspace_id = 42 # replace with your workspace ID with open("acme_nda_2025.pdf", "rb") as f: response = requests.post( "https://api.lighton.ai/api/v3/files", headers=headers, files={"file": f}, data={"workspace_id": workspace_id, "title": "NDA with Acme Corp"}, ) print(response.json()) ``` The response has an `id` field: that's your `file_id`. Ingestion is asynchronous (`status: pending`), but you don't need to wait before classifying. ## Step 2: Classify the document Tell LightOn this file is a `contract:nda`: ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: doc = File.get(client, file_id) doc.classify("contract:nda") print(f"classified file {doc.id} as contract:nda") ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() const doc = await File.get(client, fileId) await doc.classify("contract:nda") console.log(`classified file ${doc.id} as contract:nda`) ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( f"https://api.lighton.ai/api/v3/files/{file_id}/facets", headers=headers, json={"action": "classify", "content_type_path": "contract:nda"}, ) print(response.json()) ``` Returns `201 Created` (or `200` if already classified, since classify is idempotent). This is lightweight: it only creates the link between the file and the content type path. No attribute values are set yet. A file can hold classifications from multiple trees, but only **one per tree**. If you need to reclassify within the same tree, see [Rules & constraints](/tutorials/facets/rules#my-classification-was-rejected) for what's allowed. ## Step 3: Set attribute values Fill in the structured metadata. Notice that you can set attributes like `counterparty` and `jurisdiction` on `contract:nda` even though they were defined on the parent `contract` node. This works because `inherit_attributes` is `true` in the classification tree you built earlier. ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: doc = File.get(client, file_id) # One request per attribute; the value type follows the attribute definition doc.set_attribute("contract:nda", "counterparty", "Acme Corp") doc.set_attribute("contract:nda", "jurisdiction", ["FR", "DE"]) doc.set_attribute("contract:nda", "effective_date", "2025-03-01") doc.set_attribute("contract:nda", "signed", True) doc.set_attribute("contract:nda", "is_mutual", True) doc.set_attribute("contract:nda", "duration_years", 3) print(f"set 6 attributes on file {doc.id}") ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() const doc = await File.get(client, fileId) // One request per attribute; the value type follows the attribute definition await doc.setAttribute("contract:nda", "counterparty", "Acme Corp") await doc.setAttribute("contract:nda", "jurisdiction", ["FR", "DE"]) await doc.setAttribute("contract:nda", "effective_date", "2025-03-01") await doc.setAttribute("contract:nda", "signed", true) await doc.setAttribute("contract:nda", "is_mutual", true) await doc.setAttribute("contract:nda", "duration_years", 3) console.log(`set 6 attributes on file ${doc.id}`) ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} url = f"https://api.lighton.ai/api/v3/files/{file_id}/facets" actions = [ {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "counterparty", "value": "Acme Corp"}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "jurisdiction", "value": ["FR", "DE"]}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "effective_date", "value": "2025-03-01"}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "signed", "value": True}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "is_mutual", "value": True}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "duration_years", "value": 3}, ] for payload in actions: response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` The file must be classified before you can set values. Each attribute type enforces strict validation. See [Rules & constraints](/tutorials/facets/rules#my-value-was-rejected) for the full type-by-type reference. ## The efficient way: batch everything in one call In production, always batch the classify + all `set_value` actions together. This triggers exactly one BM25 reindex, not seven. ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: doc = File.get(client, file_id) # The SDK sends one request per action. To classify and set every value in a # single round trip, call /facets/batch directly (see the Plain Python tab). doc.classify("contract:nda") doc.set_attribute("contract:nda", "counterparty", "Acme Corp") doc.set_attribute("contract:nda", "jurisdiction", ["FR", "DE"]) doc.set_attribute("contract:nda", "effective_date", "2025-03-01") doc.set_attribute("contract:nda", "signed", True) doc.set_attribute("contract:nda", "is_mutual", True) doc.set_attribute("contract:nda", "duration_years", 3) print(f"classified and populated file {doc.id}") ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() const doc = await File.get(client, fileId) // The SDK sends one request per action. To classify and set every value in a // single round trip, call /facets/batch directly (see the Plain Python tab). await doc.classify("contract:nda") await doc.setAttribute("contract:nda", "counterparty", "Acme Corp") await doc.setAttribute("contract:nda", "jurisdiction", ["FR", "DE"]) await doc.setAttribute("contract:nda", "effective_date", "2025-03-01") await doc.setAttribute("contract:nda", "signed", true) await doc.setAttribute("contract:nda", "is_mutual", true) await doc.setAttribute("contract:nda", "duration_years", 3) console.log(`classified and populated file ${doc.id}`) ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( f"https://api.lighton.ai/api/v3/files/{file_id}/facets/batch", headers=headers, json={ "actions": [ {"action": "classify", "content_type_path": "contract:nda"}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "counterparty", "value": "Acme Corp"}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "jurisdiction", "value": ["FR", "DE"]}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "effective_date", "value": "2025-03-01"}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "signed", "value": True}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "is_mutual", "value": True}, {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "duration_years", "value": 3}, ], }, ) print(response.json()) ``` ## Step 4: Read back the full facets on a file ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: doc = File.get(client, file_id) # One Facet per assigned content type, each with its attribute values for facet in doc.facets(): print(facet.path, {a.name: a.value for a in facet.attributes}) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() const doc = await File.get(client, fileId) // One Facet per assigned content type, each with its attribute values for (const facet of await doc.facets()) { console.log(facet.path, Object.fromEntries(facet.attributes.map((a) => [a.name, a.value]))) } ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.get( f"https://api.lighton.ai/api/v3/files/{file_id}/facets", headers=headers, ) print(response.json()) ``` The response includes: * `labels`: breadcrumb from root to leaf (e.g. `["Contract", "Non-Disclosure Agreement"]`), useful for display in a UI * Attributes from parent nodes (`contract`), like `counterparty` and `jurisdiction`, are included alongside attributes defined directly on `contract:nda`, like `is_mutual` and `duration_years` * `can_edit: true`: whether the current user can modify this file's facets ## Multi-classification: a document with two types A file can be classified under multiple content types, but only from **different trees**. You cannot have two classifications from the same tree (see [Rules & constraints](/tutorials/facets/rules#my-classification-was-rejected)). For example, if you have a second tree called `regulation`, you can classify a file as both `contract:nda` and `regulation:gdpr`: ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: doc = File.get(client, file_id) # The file is already classified as contract:nda (what it IS). # Add a second classification from a different tree: the investment team # also tracks this NDA as part of due diligence on Acme Corp (what it's FOR). doc.classify("finance:investment:due-diligence") doc.set_attribute( "finance:investment:due-diligence", "target_company", "Acme Corp" ) print([facet.path for facet in doc.facets()]) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() const doc = await File.get(client, fileId) // The file is already classified as contract:nda (what it IS). // Add a second classification from a different tree: the investment team // also tracks this NDA as part of due diligence on Acme Corp (what it's FOR). await doc.classify("finance:investment:due-diligence") await doc.setAttribute("finance:investment:due-diligence", "target_company", "Acme Corp") console.log((await doc.facets()).map((facet) => facet.path)) ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # The file is already classified as contract:nda (what it IS). # Add a second classification from a different tree: the investment team # also tracks this NDA as part of due diligence on Acme Corp (what it's FOR). response = requests.post( f"https://api.lighton.ai/api/v3/files/{file_id}/facets/batch", headers=headers, json={ "actions": [ {"action": "classify", "content_type_path": "finance:investment:due-diligence"}, {"action": "set_value", "content_type_path": "finance:investment:due-diligence", "attribute_name": "target_company", "value": "Acme Corp"}, ], }, ) print(response.json()) ``` The file now has two classifications from two independent trees. Each classification has its own set of attribute values. They don't interact. ## Remove a classification or a value ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: doc = File.get(client, file_id) # Clear one value but keep the classification doc.clear_attribute("contract:nda", "duration_years") # Or drop the classification entirely, along with all its values doc.unclassify("contract:nda") print([facet.path for facet in doc.facets()]) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() const doc = await File.get(client, fileId) // Clear one value but keep the classification await doc.clearAttribute("contract:nda", "duration_years") // Or drop the classification entirely, along with all its values await doc.unclassify("contract:nda") console.log((await doc.facets()).map((facet) => facet.path)) ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} url = f"https://api.lighton.ai/api/v3/files/{file_id}/facets" response_clear = requests.post( url, headers=headers, json={"action": "clear_value", "content_type_path": "contract:nda", "attribute_name": "duration_years"}, ) response_unclassify = requests.post( url, headers=headers, json={"action": "unclassify", "content_type_path": "contract:nda"}, ) print(response_clear.status_code, response_unclassify.status_code) ``` Both actions return `204 No Content` on success. `unclassify` cascades: it removes the classification and all its attribute values. See [Rules & constraints](/tutorials/facets/rules#what-happens-when-i-delete-something) for all cascade behaviors. ## Action reference | Action | What it does | | - | - | | `classify` | Assign a content type path to a file (idempotent) | | `unclassify` | Remove a classification and cascade-delete all its attribute values | | `set_value` | Set an attribute value on a classified file (idempotent) | | `clear_value` | Remove a single attribute value | # Building a classification tree Source: https://developers.lighton.ai/tutorials/facets/define Create content types and custom attributes: the schema that powers all facet queries. This tutorial uses [`POST /api/v3/content-types`](/api-reference/facets/define-content-types-and-attributes) and [`GET /api/v3/content-types`](/api-reference/facets/list-content-types). The full schema lives in the [API reference](/api-reference/introduction). After this tutorial you'll have a content type tree with custom attributes, the schema that powers all facet queries. You define this once at the company level, then apply it to as many files as you need. There are two ways to build your schema: 1. **Adopt starter templates:** import ready-made trees for common verticals and customize them 2. **Build from scratch:** create your own trees, nodes, and attributes entirely ## Option A: Adopt starter templates LightOn ships with starter templates for legal, finance, healthcare, tech, and manufacturing. Browse what's available, adopt the ones you need, then customize. ```python Python SDK theme={null} from lighton import ContentType, LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment for template in ContentType.templates(client): print(f"{template.code:20s} {template.label}") ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment for (const template of await ContentType.templates(client)) { console.log(`${template.code.padEnd(20)} ${template.label}`) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Browse available starter templates (roots only) response = requests.get( "https://api.lighton.ai/api/v3/content-types/templates", headers=headers, params={"depth": 0}, ) for tree in response.json().get("content_types", []): print(f"{tree['code']:20s} {tree['label']}") ``` This lists all available template roots. To adopt one or more: ```python Python SDK theme={null} from lighton import ContentType, LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment # Adopt the legal and finance starter trees roots = ContentType.adopt(client, ["legal", "finance"]) print([root.path for root in roots]) ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment // Adopt the legal and finance starter trees const roots = await ContentType.adopt(client, ["legal", "finance"]) console.log(roots.map((root) => root.path)) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Adopt the legal and finance starter trees response = requests.post( "https://api.lighton.ai/api/v3/content-types", headers=headers, json={"action": "adopt", "content_types": ["legal", "finance"]}, ) print(response.json()) ``` Once adopted, the trees become **your company's own** content types, fully editable. You can rename nodes, add children, define new attributes, or delete what you don't need using the same actions described below. Here's what you get with **legal** and **finance**: ```mermaid theme={null} graph TD legal["legal"] contract["legal:contract"] nda["legal:contract:nda"] sla["legal:contract:sla"] msa["legal:contract:msa"] litigation["legal:litigation"] compliance["legal:compliance"] legal --> contract legal --> litigation legal --> compliance contract --> nda contract --> sla contract --> msa ``` Each node carries its own attributes and inherits from its parent. For example, `legal` defines `confidentiality_level` and `jurisdiction`. `legal:contract` adds `parties`, `contract_value`, and `effective_date`. `legal:contract:nda` adds `counterparty`, `is_mutual`, and `duration_years`, giving it 8 attributes total. Here's a second schema with its attributes. A file can be classified under multiple trees at once: for example, an NDA could be both a `legal:contract:nda` and a `finance:investment:due-diligence` if it's part of an investment deal. ```mermaid theme={null} graph TD finance["finance
currency · fiscal_year"]:::fin reporting["finance:reporting
+ reporting_date · fiscal_period · is_audited"]:::fin investment["finance:investment
+ asset_class · investment_value"]:::fin dd["finance:investment:due-diligence
+ target_company"]:::fin memo["finance:investment:investment-memo
+ recommendation"]:::fin risk["finance:risk
+ regulatory_framework · risk_level"]:::fin treasury["finance:treasury
+ instrument_type"]:::fin finance --> reporting finance --> investment finance --> risk finance --> treasury investment --> dd investment --> memo classDef fin fill:#e8f4f8,stroke:#1f99bb ``` **Adoption behavior:** * Trees are adopted **wholesale**: you cannot pick individual branches from a template. * Adoption is **idempotent**: re-adopting the same tree updates existing nodes based on their path. It does not auto-delete nodes you've removed or added since the last adoption. * After adoption, all content types are fully yours. The link to the original template is gone. ## Option B: Build from scratch Here's what we're going to build: ```mermaid theme={null} graph TD contract["contract
counterparty · jurisdiction · effective_date · signed"] contract --> nda["contract:nda
is_mutual · duration_years"] contract --> sa["contract:service-agreement
contract_value · payment_terms"] sa --> fp["contract:service-agreement:fixed-price"] ``` All writes go to a single endpoint with an `action` field. Every action is **idempotent** and safe to replay. You can send a single action object or a **JSON array** to batch multiple actions in one call. ## Step 1: Create your first Content Type Start from zero and create a root content type called `contract`. ```python Python SDK theme={null} from lighton import ContentType, LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment contract = ContentType.define( client, code="contract", label="Contract", description="Any legally binding agreement between two or more parties", ) print(contract.path, contract.label) ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment const contract = await ContentType.define(client, "contract", "Contract", { description: "Any legally binding agreement between two or more parties", }) console.log(contract.path, contract.label) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/content-types", headers=headers, json={ "action": "define_content_type", "code": "contract", "label": "Contract", "description": "Any legally binding agreement between two or more parties", }, ) print(response.json()) ``` Returns `201 Created` on first call, `200 OK` if the node already exists (idempotent update). This makes scripts safe to run in CI pipelines or onboarding automations. `path` is the unique identifier you'll use everywhere: to classify files, define attributes, and filter searches. For a root node, `path` equals `code`. `inherit_attributes` defaults to `true`, which means any attributes you define on `contract` are automatically available to all child types (`contract:nda`, `contract:service-agreement`, etc.). You define shared fields once on the parent. ## Step 2: Build a tree of Content Types Add child nodes. The path separator is `:`. LightOn builds the full path for you from `parent_path` + `code`. ```python Python SDK theme={null} from lighton import ContentType, LightOn child_types = [ { "parent": "contract", "code": "nda", "label": "Non-Disclosure Agreement", "description": "Confidentiality agreement preventing disclosure of proprietary information", }, { "parent": "contract", "code": "service-agreement", "label": "Service Agreement", "description": "Agreement defining the delivery of services between a provider and a client", }, { "parent": "contract:service-agreement", "code": "fixed-price", "label": "Fixed Price", "description": "Service agreement with a fixed total price, regardless of time spent", }, ] with LightOn() as client: # reads LIGHTON_API_KEY from the environment for spec in child_types: node = ContentType.define(client, **spec) print(node.path, node.label) ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const childTypes = [ { parent: "contract", code: "nda", label: "Non-Disclosure Agreement", description: "Confidentiality agreement preventing disclosure of proprietary information", }, { parent: "contract", code: "service-agreement", label: "Service Agreement", description: "Agreement defining the delivery of services between a provider and a client", }, { parent: "contract:service-agreement", code: "fixed-price", label: "Fixed Price", description: "Service agreement with a fixed total price, regardless of time spent", }, ] const client = new LightOn() // reads LIGHTON_API_KEY from the environment for (const { code, label, ...options } of childTypes) { const node = await ContentType.define(client, code, label, options) console.log(node.path, node.label) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} url = "https://api.lighton.ai/api/v3/content-types" child_types = [ { "action": "define_content_type", "parent_path": "contract", "code": "nda", "label": "Non-Disclosure Agreement", "description": "Confidentiality agreement preventing disclosure of proprietary information", }, { "action": "define_content_type", "parent_path": "contract", "code": "service-agreement", "label": "Service Agreement", "description": "Agreement defining the delivery of services between a provider and a client", }, { "action": "define_content_type", "parent_path": "contract:service-agreement", "code": "fixed-price", "label": "Fixed Price", "description": "Service agreement with a fixed total price, regardless of time spent", }, ] for payload in child_types: response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` Your tree is now: ``` contract contract:nda contract:service-agreement contract:service-agreement:fixed-price ``` You can go up to 4 levels deep. A company can have multiple independent trees (different roots). Each tree is completely isolated. ### Read your classification tree at any time ```python Python SDK theme={null} from lighton import ContentType, LightOn with LightOn() as client: # The subtree rooted at "contract", two levels deep for content_type in ContentType.list(client, path="contract", depth=2): print(content_type.path, content_type.label) for child in content_type.children: print(" ", child.path, child.label) ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // The subtree rooted at "contract", two levels deep for (const contentType of await ContentType.list(client, { path: "contract", depth: 2 })) { console.log(contentType.path, contentType.label) for (const child of contentType.children ?? []) { console.log(" ", child.path, child.label) } } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.get( "https://api.lighton.ai/api/v3/content-types", headers=headers, params={"path": "contract", "depth": 2, "include_attributes": False}, ) print(response.json()) ``` ## Step 3: Define attributes Attributes are the typed fields you want to store per document. Define them on a node, and if `inherit_attributes: true`, all children get them too. See [Rules & constraints](/tutorials/facets/rules#my-attribute-wont-save) for naming restrictions, type immutability, and other validation rules. **Write descriptions for machines, not just humans.** The `description` you give an attribute is what LightOn reads when it infers search filters from a natural-language query, so the wording changes which filter syntax it picks: * **Person or entity names:** include "name", "author", "examiner", or "inventor" (e.g. `"Full legal name of the other party in the agreement"`). This signals a partial match is acceptable, so a query for "Acme" becomes `counterparty:*Acme*`. * **Codes and identifiers:** include "code", "classification", or "identifier" (e.g. `"USPC classification code for the patent"`). This signals exact match only, so the value is never guessed from surrounding topic words. ### Shared attributes on `contract` (inherited by all subtypes) ```python Python SDK theme={null} from lighton import ContentType, LightOn attributes = [ { "name": "counterparty", "label": "Counterparty", "attribute_type": "text", "description": "Full legal name of the other party in the agreement", "required": True, }, { "name": "jurisdiction", "label": "Jurisdiction", "attribute_type": "multi-select", "description": "Countries whose law governs this agreement", "choices": ["FR", "US", "UK", "DE", "CH", "BE", "NL"], }, { "name": "effective_date", "label": "Effective Date", "attribute_type": "date", "description": "Date from which the contract is legally binding", }, { "name": "signed", "label": "Signed", "attribute_type": "boolean", "description": "Whether the contract has been signed by all parties", }, ] with LightOn() as client: # reads LIGHTON_API_KEY from the environment for spec in attributes: attribute = ContentType.define_attribute(client, "contract", **spec) print(attribute.name, attribute.type) ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const attributes = [ { name: "counterparty", label: "Counterparty", attributeType: "text", description: "Full legal name of the other party in the agreement", required: true, }, { name: "jurisdiction", label: "Jurisdiction", attributeType: "multi-select", description: "Countries whose law governs this agreement", choices: ["FR", "US", "UK", "DE", "CH", "BE", "NL"], }, { name: "effective_date", label: "Effective Date", attributeType: "date", description: "Date from which the contract is legally binding", }, { name: "signed", label: "Signed", attributeType: "boolean", description: "Whether the contract has been signed by all parties", }, ] const client = new LightOn() // reads LIGHTON_API_KEY from the environment for (const { name, attributeType, ...options } of attributes) { const attribute = await ContentType.defineAttribute(client, "contract", name, attributeType, options) console.log(attribute.name, attribute.type) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} url = "https://api.lighton.ai/api/v3/content-types" attributes = [ { "action": "define_attribute", "content_type_path": "contract", "name": "counterparty", "label": "Counterparty", "attribute_type": "text", "description": "Full legal name of the other party in the agreement", "required": True, }, { "action": "define_attribute", "content_type_path": "contract", "name": "jurisdiction", "label": "Jurisdiction", "attribute_type": "multi-select", "description": "Countries whose law governs this agreement", "choices": ["FR", "US", "UK", "DE", "CH", "BE", "NL"], }, { "action": "define_attribute", "content_type_path": "contract", "name": "effective_date", "label": "Effective Date", "attribute_type": "date", "description": "Date from which the contract is legally binding", }, { "action": "define_attribute", "content_type_path": "contract", "name": "signed", "label": "Signed", "attribute_type": "boolean", "description": "Whether the contract has been signed by all parties", }, ] for payload in attributes: response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` Because `contract` has `inherit_attributes: true`, every child type (`contract:nda`, `contract:service-agreement`, etc.) automatically exposes these four attributes. You don't need to redefine them. ### NDA-specific attributes (only on `contract:nda`) ```python Python SDK theme={null} from lighton import ContentType, LightOn attributes = [ { "name": "is_mutual", "label": "Mutual", "attribute_type": "boolean", "description": "True if confidentiality obligations apply to both parties", }, { "name": "duration_years", "label": "Duration (years)", "attribute_type": "number", "description": "Number of years the confidentiality obligation lasts", }, ] with LightOn() as client: # reads LIGHTON_API_KEY from the environment for spec in attributes: attribute = ContentType.define_attribute(client, "contract:nda", **spec) print(attribute.name, attribute.type) ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const attributes = [ { name: "is_mutual", label: "Mutual", attributeType: "boolean", description: "True if confidentiality obligations apply to both parties", }, { name: "duration_years", label: "Duration (years)", attributeType: "number", description: "Number of years the confidentiality obligation lasts", }, ] const client = new LightOn() // reads LIGHTON_API_KEY from the environment for (const { name, attributeType, ...options } of attributes) { const attribute = await ContentType.defineAttribute(client, "contract:nda", name, attributeType, options) console.log(attribute.name, attribute.type) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} url = "https://api.lighton.ai/api/v3/content-types" attributes = [ { "action": "define_attribute", "content_type_path": "contract:nda", "name": "is_mutual", "label": "Mutual", "attribute_type": "boolean", "description": "True if confidentiality obligations apply to both parties", }, { "action": "define_attribute", "content_type_path": "contract:nda", "name": "duration_years", "label": "Duration (years)", "attribute_type": "number", "description": "Number of years the confidentiality obligation lasts", }, ] for payload in attributes: response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ### Service agreement-specific attributes ```python Python SDK theme={null} from lighton import ContentType, LightOn attributes = [ { "name": "contract_value", "label": "Contract Value (EUR)", "attribute_type": "number", "description": "Total value of the service agreement in euros", }, { "name": "payment_terms", "label": "Payment Terms", "attribute_type": "select", "description": "How payment is structured over the duration of the agreement", "choices": ["monthly", "quarterly", "milestone", "upfront", "upon-delivery"], }, ] with LightOn() as client: # reads LIGHTON_API_KEY from the environment for spec in attributes: attribute = ContentType.define_attribute( client, "contract:service-agreement", **spec ) print(attribute.name, attribute.type) ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const attributes = [ { name: "contract_value", label: "Contract Value (EUR)", attributeType: "number", description: "Total value of the service agreement in euros", }, { name: "payment_terms", label: "Payment Terms", attributeType: "select", description: "How payment is structured over the duration of the agreement", choices: ["monthly", "quarterly", "milestone", "upfront", "upon-delivery"], }, ] const client = new LightOn() // reads LIGHTON_API_KEY from the environment for (const { name, attributeType, ...options } of attributes) { const attribute = await ContentType.defineAttribute( client, "contract:service-agreement", name, attributeType, options, ) console.log(attribute.name, attribute.type) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} url = "https://api.lighton.ai/api/v3/content-types" attributes = [ { "action": "define_attribute", "content_type_path": "contract:service-agreement", "name": "contract_value", "label": "Contract Value (EUR)", "attribute_type": "number", "description": "Total value of the service agreement in euros", }, { "action": "define_attribute", "content_type_path": "contract:service-agreement", "name": "payment_terms", "label": "Payment Terms", "attribute_type": "select", "description": "How payment is structured over the duration of the agreement", "choices": ["monthly", "quarterly", "milestone", "upfront", "upon-delivery"], }, ] for payload in attributes: response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ## How inheritance works Attribute inheritance is resolved **at query time**: when you read a file's facets, LightOn walks up the tree and collects attributes from each ancestor with `inherit_attributes: true`. Adding an attribute to a parent immediately makes it available on all descendants, no migration needed. For details on breaking the chain or shadowing inherited attributes, see [Rules & constraints](/tutorials/facets/rules#how-does-inheritance-work). ## Full script: build the contract classification tree in one batch call All schema operations can be batched in a single request by sending an `{"actions": [...]}` object to the `/batch` endpoint. LightOn processes them in order and fails fast on the first error. This is the recommended approach for onboarding automations. ```python Python SDK theme={null} from lighton import ContentType, LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment results = ContentType.batch(client, [ {"action": "define_content_type", "code": "contract", "label": "Contract", "description": "Any legally binding agreement between two or more parties"}, {"action": "define_content_type", "parent_path": "contract", "code": "nda", "label": "Non-Disclosure Agreement", "description": "Confidentiality agreement preventing disclosure of proprietary information"}, {"action": "define_content_type", "parent_path": "contract", "code": "service-agreement", "label": "Service Agreement", "description": "Agreement defining the delivery of services between a provider and a client"}, {"action": "define_content_type", "parent_path": "contract:service-agreement", "code": "fixed-price", "label": "Fixed Price", "description": "Service agreement with a fixed total price regardless of time spent"}, {"action": "define_attribute", "content_type_path": "contract", "name": "counterparty", "label": "Counterparty", "attribute_type": "text", "description": "Full legal name of the other party in the agreement", "required": True}, {"action": "define_attribute", "content_type_path": "contract", "name": "jurisdiction", "label": "Jurisdiction", "attribute_type": "multi-select", "description": "Countries whose law governs this agreement", "choices": ["FR", "US", "UK", "DE", "CH", "BE", "NL"]}, {"action": "define_attribute", "content_type_path": "contract", "name": "effective_date", "label": "Effective Date", "attribute_type": "date", "description": "Date from which the contract is legally binding"}, {"action": "define_attribute", "content_type_path": "contract", "name": "signed", "label": "Signed", "attribute_type": "boolean", "description": "Whether the contract has been signed by all parties"}, {"action": "define_attribute", "content_type_path": "contract:nda", "name": "is_mutual", "label": "Mutual", "attribute_type": "boolean", "description": "True if confidentiality obligations apply to both parties"}, {"action": "define_attribute", "content_type_path": "contract:nda", "name": "duration_years", "label": "Duration (years)", "attribute_type": "number", "description": "Number of years the confidentiality obligation lasts"}, {"action": "define_attribute", "content_type_path": "contract:service-agreement", "name": "contract_value", "label": "Contract Value (EUR)", "attribute_type": "number", "description": "Total value of the service agreement in euros"}, {"action": "define_attribute", "content_type_path": "contract:service-agreement", "name": "payment_terms", "label": "Payment Terms", "attribute_type": "select", "description": "How payment is structured over the duration of the agreement", "choices": ["monthly", "quarterly", "milestone", "upfront", "upon-delivery"]}, ]) print(results) ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment const results = await ContentType.batch(client, [ { action: "define_content_type", code: "contract", label: "Contract", description: "Any legally binding agreement between two or more parties" }, { action: "define_content_type", parent_path: "contract", code: "nda", label: "Non-Disclosure Agreement", description: "Confidentiality agreement preventing disclosure of proprietary information" }, { action: "define_content_type", parent_path: "contract", code: "service-agreement", label: "Service Agreement", description: "Agreement defining the delivery of services between a provider and a client" }, { action: "define_content_type", parent_path: "contract:service-agreement", code: "fixed-price", label: "Fixed Price", description: "Service agreement with a fixed total price regardless of time spent" }, { action: "define_attribute", content_type_path: "contract", name: "counterparty", label: "Counterparty", attribute_type: "text", description: "Full legal name of the other party in the agreement", required: true }, { action: "define_attribute", content_type_path: "contract", name: "jurisdiction", label: "Jurisdiction", attribute_type: "multi-select", description: "Countries whose law governs this agreement", choices: ["FR", "US", "UK", "DE", "CH", "BE", "NL"] }, { action: "define_attribute", content_type_path: "contract", name: "effective_date", label: "Effective Date", attribute_type: "date", description: "Date from which the contract is legally binding" }, { action: "define_attribute", content_type_path: "contract", name: "signed", label: "Signed", attribute_type: "boolean", description: "Whether the contract has been signed by all parties" }, { action: "define_attribute", content_type_path: "contract:nda", name: "is_mutual", label: "Mutual", attribute_type: "boolean", description: "True if confidentiality obligations apply to both parties" }, { action: "define_attribute", content_type_path: "contract:nda", name: "duration_years", label: "Duration (years)", attribute_type: "number", description: "Number of years the confidentiality obligation lasts" }, { action: "define_attribute", content_type_path: "contract:service-agreement", name: "contract_value", label: "Contract Value (EUR)", attribute_type: "number", description: "Total value of the service agreement in euros" }, { action: "define_attribute", content_type_path: "contract:service-agreement", name: "payment_terms", label: "Payment Terms", attribute_type: "select", description: "How payment is structured over the duration of the agreement", choices: ["monthly", "quarterly", "milestone", "upfront", "upon-delivery"] }, ]) console.log(results) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/content-types/batch", headers=headers, json={ "actions": [ {"action": "define_content_type", "code": "contract", "label": "Contract", "description": "Any legally binding agreement between two or more parties"}, {"action": "define_content_type", "parent_path": "contract", "code": "nda", "label": "Non-Disclosure Agreement", "description": "Confidentiality agreement preventing disclosure of proprietary information"}, {"action": "define_content_type", "parent_path": "contract", "code": "service-agreement", "label": "Service Agreement", "description": "Agreement defining the delivery of services between a provider and a client"}, {"action": "define_content_type", "parent_path": "contract:service-agreement", "code": "fixed-price", "label": "Fixed Price", "description": "Service agreement with a fixed total price regardless of time spent"}, {"action": "define_attribute", "content_type_path": "contract", "name": "counterparty", "label": "Counterparty", "attribute_type": "text", "description": "Full legal name of the other party in the agreement", "required": True}, {"action": "define_attribute", "content_type_path": "contract", "name": "jurisdiction", "label": "Jurisdiction", "attribute_type": "multi-select", "description": "Countries whose law governs this agreement", "choices": ["FR", "US", "UK", "DE", "CH", "BE", "NL"]}, {"action": "define_attribute", "content_type_path": "contract", "name": "effective_date", "label": "Effective Date", "attribute_type": "date", "description": "Date from which the contract is legally binding"}, {"action": "define_attribute", "content_type_path": "contract", "name": "signed", "label": "Signed", "attribute_type": "boolean", "description": "Whether the contract has been signed by all parties"}, {"action": "define_attribute", "content_type_path": "contract:nda", "name": "is_mutual", "label": "Mutual", "attribute_type": "boolean", "description": "True if confidentiality obligations apply to both parties"}, {"action": "define_attribute", "content_type_path": "contract:nda", "name": "duration_years", "label": "Duration (years)", "attribute_type": "number", "description": "Number of years the confidentiality obligation lasts"}, {"action": "define_attribute", "content_type_path": "contract:service-agreement", "name": "contract_value", "label": "Contract Value (EUR)", "attribute_type": "number", "description": "Total value of the service agreement in euros"}, {"action": "define_attribute", "content_type_path": "contract:service-agreement", "name": "payment_terms", "label": "Payment Terms", "attribute_type": "select", "description": "How payment is structured over the duration of the agreement", "choices": ["monthly", "quarterly", "milestone", "upfront", "upon-delivery"]}, ], }, ) print(response.json()) ``` The response is a `{"results": [...]}` object with one entry per action, in order. Each entry has `status` (`201` created, `200` updated) and a `data` object. If any action fails, the batch stops at that point. Actions before the failure are committed; actions after are not. ## Verify: read your complete classification tree ```python Python SDK theme={null} from lighton import ContentType, LightOn with LightOn() as client: # include_attributes populates each node's attribute definitions for content_type in ContentType.list( client, path="contract", include_attributes=True ): print(content_type.path, content_type.label) for attribute in content_type.attributes: print(" ", attribute.name, attribute.type, attribute.choices) ``` ```typescript TypeScript SDK theme={null} import { ContentType, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // includeAttributes populates each node's attribute definitions for (const contentType of await ContentType.list(client, { path: "contract", includeAttributes: true, })) { console.log(contentType.path, contentType.label) for (const attribute of contentType.attributes ?? []) { console.log(" ", attribute.name, attribute.type, attribute.choices) } } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.get( "https://api.lighton.ai/api/v3/content-types", headers=headers, params={"path": "contract", "include_attributes": True}, ) print(response.json()) ``` The response shows own attributes per node. Inherited attributes are resolved when you read a file's facets, not pre-expanded here. ## Action reference | Action | What it does | | - | - | | `define_content_type` | Create or update a content type node (idempotent) | | `undefine_content_type` | Delete a node, its subtree, and all file classifications + values under it | | `define_attribute` | Create or update an attribute on a content type (idempotent) | | `undefine_attribute` | Delete an attribute and all its stored values across all files | # Filtering documents by metadata Source: https://developers.lighton.ai/tutorials/facets/filter Query documents by content type and attribute value using structured facet filters. This tutorial uses [`GET /api/v3/files`](/api-reference/files/list-files-accessible-to-the-authenticated-user). The full schema for every filter parameter lives in the [API reference](/api-reference/introduction). Your files are classified and enriched with metadata. Now your app needs to query them: not by what the text says, but by *what they are* and *what their metadata contains*. Imagine you've uploaded 200 contracts and classified them with facets. Your app needs to answer: **"Show all unsigned French NDAs with Acme."** Here's how to build that query, step by step. ## Filter by content type The `content_type` parameter filters files by their classification path. ### Exact match + subtree A plain path matches the exact node **and all its descendants**: ```python theme={null} # All NDAs response = requests.get( f"{base_url}/api/v3/files", headers=headers, params={"content_type": "contract:nda"}, ) # All contracts (NDAs, service agreements, fixed-price, any node in the tree) response = requests.get( f"{base_url}/api/v3/files", headers=headers, params={"content_type": "contract"}, ) ``` ### OR across types Comma-separate multiple paths for an OR query: ```python theme={null} # NDAs or service agreements response = requests.get( f"{base_url}/api/v3/files", headers=headers, params={"content_type": "contract:nda,contract:service-agreement"}, ) ``` ### Wildcard: contains Wrap in `*` to match any path containing the substring: ```python theme={null} params={"content_type": "*nda*"} ``` ### Wildcard: prefix Append `*` for a prefix match: ```python theme={null} # contract:service-agreement, contract:service-agreement:fixed-price, etc. params={"content_type": "contract:service*"} ``` ## Filter by attribute value The `attribute` parameter filters by attribute name and optionally by value and operator. ### Has any value ```python theme={null} # Files that have any counterparty value set params={"attribute": "counterparty"} ``` ### Exact match ```python theme={null} params={"attribute": "counterparty:Acme Corp"} ``` ### OR across values Use `|` (pipe) as the OR delimiter. It's the recommended separator and avoids ambiguity with commas in multi-key values: ```python theme={null} # Files where jurisdiction is FR or DE params={"attribute": "jurisdiction:FR|DE"} ``` ### Comparison operators Use `>`, `<`, `>=`, `<=` for numbers and dates: ```python theme={null} params={"attribute": "contract_value:>50000"} params={"attribute": "contract_value:>=100000"} params={"attribute": "effective_date:>2024-01-01"} params={"attribute": "effective_date:<=2025-12-31"} ``` ### Prefix match ```python theme={null} # Matches "Acme Corp", "Acme Ltd", etc. params={"attribute": "counterparty:Acme*"} ``` ### Boolean ```python theme={null} params={"attribute": "signed:true"} params={"attribute": "is_mutual:false"} ``` ### Date shortcuts LightOn expands partial dates into ranges automatically: ```python theme={null} params={"attribute": "effective_date:2025"} # any date in 2025 (Jan 1 – Dec 31) params={"attribute": "effective_date:2025-03"} # any date in March 2025 (1st – 31st) params={"attribute": "effective_date:2025-03-01"} # exact date ``` For details on date ambiguity, multi-select containment, and which types are filterable, see [Rules & constraints](/tutorials/facets/rules#what-can-i-filter-on). ## Combining filters (AND logic) When you repeat `attribute=`, each additional filter **narrows** the results (AND logic). For example, to find files where jurisdiction is FR **and** signed is true: ```python theme={null} params={"attribute": ["jurisdiction:FR", "signed:true"]} ``` ## Scoping an attribute to a specific content type When a file has multiple classifications, the same attribute name may exist under different paths. Use the explicit scope syntax to be precise. When you include a `content_type` filter alongside `attribute` filters, attributes are **automatically scoped** to that content type. You don't need the explicit scope syntax unless you're filtering across multiple content types: ```python theme={null} # counterparty on contract:nda specifically, not on any other classification params={"attribute": "content_type(contract:nda).counterparty:Acme Corp"} ``` ## Putting it all together Here's the full query from our scenario: all signed French NDAs. ```python Python SDK theme={null} from lighton import File, LightOn with LightOn() as client: # Signed French NDAs. File.list passes filters straight through as query # params and follows pagination to the end. docs = File.list( client, content_type="contract:nda", attribute=["jurisdiction:FR", "signed:true"], ) for doc in docs: print(doc.id, doc.title) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.get( "https://api.lighton.ai/api/v3/files", headers=headers, params={ "content_type": "contract:nda", "attribute": ["jurisdiction:FR", "signed:true"], }, ) print(response.json()) ``` The response is a paginated list of file objects. If no files match, you get an empty `results` array with a `200` status code. ## Full operator reference | Syntax | Operator | Works on | Example | | - | - | - | - | | `name:value` | Equals | all types | `counterparty:Acme Corp` | | `name:a\|b` | OR | all types | `jurisdiction:FR\|DE` | | `name:>value` | Greater than | number, date | `contract_value:>50000` | | `name:=value` | Greater than or equal | number, date | `effective_date:>=2024-01-01` | | `name:<=value` | Less than or equal | number, date | `effective_date:<=2025-12-31` | | `name:prefix*` | Starts with | text | `counterparty:Acme*` | | `name` | Has any value | all types | `counterparty` | Repeated `?attribute=` → AND. Use `|` (pipe) for OR within a value. Comma-separated `?content_type=a,b` → OR across content types. ## Combining facets with semantic search `content_type` and `attribute` are supported directly in the request body of both `POST /api/v3/search` and `POST /api/v3/ask`. No need to pre-filter files: just add facet fields to your query and LightOn narrows the corpus for you. ### Search by content type Scope a search to a specific document type. LightOn returns ranked chunks from matching files only. ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment # Find relevant passages across all NDAs response = client.search( "termination clause with 30-day notice", content_type=["contract:nda"], ) for result in response.results: print(result.source.filename, result.score) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment // Find relevant passages across all NDAs const response = await client.search("termination clause with 30-day notice", { contentType: ["contract:nda"], }) for (const result of response.results) { console.log(result.source.filename, result.score) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Find relevant passages across all NDAs response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json={ "query": "termination clause with 30-day notice", "content_type": ["contract:nda"], }, ) print(response.json()) ``` ### Search by attributes Scope a search by attribute values, without specifying a content type. ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment # Find passages only in unsigned French contracts response = client.search( "liability cap and indemnification", attribute=["jurisdiction:FR", "signed:false"], ) for result in response.results: print(result.source.filename, result.score) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment // Find passages only in unsigned French contracts const response = await client.search("liability cap and indemnification", { attribute: ["jurisdiction:FR", "signed:false"], }) for (const result of response.results) { console.log(result.source.filename, result.score) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Find passages only in unsigned French contracts response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json={ "query": "liability cap and indemnification", "attribute": ["jurisdiction:FR", "signed:false"], }, ) print(response.json()) ``` ### Search by content type and attributes Combine both for precise scoping: only unsigned French NDAs. ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment # Find passages in unsigned French NDAs specifically response = client.search( "termination clause with 30-day notice", content_type=["contract:nda"], attribute=["jurisdiction:FR", "signed:false"], ) for result in response.results: print(result.source.filename, result.score) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment // Find passages in unsigned French NDAs specifically const response = await client.search("termination clause with 30-day notice", { contentType: ["contract:nda"], attribute: ["jurisdiction:FR", "signed:false"], }) for (const result of response.results) { console.log(result.source.filename, result.score) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Find passages in unsigned French NDAs specifically response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json={ "query": "termination clause with 30-day notice", "content_type": ["contract:nda"], "attribute": ["jurisdiction:FR", "signed:false"], }, ) print(response.json()) ``` ### Ask: get an LLM answer from facet-filtered documents `POST /api/v3/ask` works the same way. LightOn retrieves relevant passages from matching documents, then generates a grounded answer. ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment # Ask a question scoped to unsigned French NDAs response = client.ask( "What are the termination conditions across these NDAs?", content_type=["contract:nda"], attribute=["jurisdiction:FR", "signed:false"], ) print(response.answer) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment // Ask a question scoped to unsigned French NDAs const response = await client.ask("What are the termination conditions across these NDAs?", { contentType: ["contract:nda"], attribute: ["jurisdiction:FR", "signed:false"], }) console.log(response.answer) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Ask a question scoped to unsigned French NDAs response = requests.post( "https://api.lighton.ai/api/v3/ask", headers=headers, json={ "query": "What are the termination conditions across these NDAs?", "content_type": ["contract:nda"], "attribute": ["jurisdiction:FR", "signed:false"], }, ) print(response.json()) ``` ### BM25 enrichment Facets also improve search quality automatically. When you set attribute values, LightOn appends the content type labels and attribute values to the file's BM25 lexical index. A query like *"unsigned NDAs with Acme"* benefits from the enriched index even if the document body doesn't mention "unsigned" or "Acme" explicitly. This reindexing happens after every write. ### Inferring filters from natural language The examples above require your app to know the filters upfront. If your users type free-text queries and you want the right `content_type` and `attribute` filters inferred automatically, see [Narrowing search with Agentic](/tutorials/facets/scope). # Organizing documents with metadata Source: https://developers.lighton.ai/tutorials/facets/overview Structured metadata on top of your documents: filter, classify, and enrich search. LightOn's **Facets** let you organise documents with tree-based content types and custom attributes. Classify files by type, set attribute values, and query by those fields instead of relying on full-text search alone. You build classification trees once at the company level, then classify and enrich documents as they flow in, via the API or automatically with AI. > *"Give me all NDAs, signed, valid in France, where the counterparty is Acme."* That query doesn't rely on finding those exact words in the document body. It relies on **structured metadata** you've attached to the file: a type (`nda`), a flag (`signed = true`), a jurisdiction, a counterparty name. This works for any document type: contracts, invoices, HR policies, technical specs, whatever your app needs. ## Workspaces, tags, or facets? Facets aren't the only way to organise documents. [Workspaces](/tutorials/workspaces) are containers that isolate a team's or customer's files, and [tags](/tutorials/tags) are flat labels that group files into collections (even across workspaces) with zero schema to design. The three compose: a file lives in one workspace, can carry several tags, and can be classified with facets. | | Workspaces | Tags | Facets | | - | - | - | - | | **What it is** | A container; every file lives in exactly one | Flat, reusable labels | Typed, hierarchical metadata with a schema | | **A file belongs to** | exactly one workspace | many tags, across workspaces | many content types, across workspaces | | **Best for** | Isolating teams, customers, tenants | Cross-cutting collections (project, topic) | Precise structured queries | | **Setup cost** | Create a workspace | Create a tag | Design a content-type tree | | **Scope a query with** | `workspace_id` | `tag_id` | `content_type` / `attribute` | | **Access control** | Yes: API keys can be scoped to a workspace with a per-key role | No: not a permission boundary | No: not a permission boundary | Only [workspaces](/tutorials/workspaces) are a permission boundary: API keys can be scoped to specific workspaces, so segment data that needs different permission levels with workspaces rather than facets. Reach for the simplest layer that solves your problem. The rest of this tutorial covers facets. ## How it works Facets are built on three layers: ``` Layer 1: Schema (define once) "A contract has: counterparty (text), jurisdiction (multi-select), signed (boolean)" Layer 2: Classification (per file) "This file is a contract:nda" Layer 3: Values (per file, per classification) "This file's counterparty is Acme Corp, jurisdiction is FR, signed is true" ``` You define Layer 1 once at the company level. You apply Layers 2 and 3 to individual files. The workflow maps to three API operations: 1. **Build your classification trees:** create content types and their attributes (`POST /api/v3/content-types`), or adopt a ready-made starter kit (legal, finance, healthcare, tech, manufacturing) via [`GET /api/v3/content-types/templates`](/api-reference/facets/list-content-type-templates) 2. **Classify files & set values:** assign a content type to each file and fill in its attribute values (`POST /api/v3/files/{id}/facets`) 3. **Query by metadata:** filter files by content type and attribute values (`GET /api/v3/files`) ## Glossary ### Content Type A **Content Type** is a node in your company's classification tree: a named, hierarchical label that describes what a document *is*. Content types live in a **tree** up to 4 levels deep. The path between levels uses `:` as separator. ```mermaid theme={null} graph TD contract["contract
Contract"] contract --> nda["contract:nda
Non-Disclosure Agreement"] contract --> sa["contract:service-agreement
Service Agreement"] sa --> fp["contract:service-agreement:fixed-price
Fixed Price"] ``` Each Content Type has: * `code`: a short kebab-case identifier, unique among siblings (`nda`, `service-agreement`) * `label`: the human-readable name (`"Non-Disclosure Agreement"`) * `description`: explains what this type *means*, used by AI to classify documents automatically * `inherit_attributes`: whether documents classified here also get the parent node's attributes ### Attribute An **Attribute** is a custom attribute attached to a Content Type. It describes what data a document of that type should carry. | Field | Meaning | | - | - | | `name` | machine name, e.g. `jurisdiction` | | `label` | display name, e.g. `"Jurisdiction"` | | `attribute_type` | the data type (see table below) | | `required` | whether this field must have a value | | `choices` | for select/multi-select: the allowed values | | `description` | explains what this field means, used by AI to extract values | **Available attribute types:** | Type | `attribute_type` | What you store | | - | - | - | | Short text | `"text"` | Any string | | Rich text | `"rich-text"` | Markdown string | | Number | `"number"` | Stored as a float (e.g. `50000`, `"1.5"`) | | Date | `"date"` | ISO 8601 string: `"2025-01-31"` | | Boolean | `"boolean"` | `true` or `false` | | Single option | `"select"` | One string from a fixed list (`choices`) | | Multiple options | `"multi-select"` | Array of strings from a fixed list (`choices`) | ### Classification A **Classification** is the link between a specific file and a Content Type path. It answers the question: *"What type is this document?"* One file can have **multiple classifications**. A single document might be both a `contract:nda` *and* a `contract:data-processing-agreement` at the same time. ### Attribute Value An **Attribute Value** is the actual data stored for a specific attribute on a specific classification of a specific file. When a file is classified as `contract:nda` and you set `counterparty = "Acme Corp"`, you've created an Attribute Value: `{file: #1234, path: "contract:nda", name: "counterparty", value: "Acme Corp"}`. Values are scoped by content type path. If a file has two classifications, each classification has its own set of values. They don't mix. ### Inherited Attributes When a Content Type node has `inherit_attributes: true`, files classified at any *descendant* node automatically have access to the attributes defined on that ancestor. ``` contract ← defines: counterparty (text), jurisdiction (multi-select) contract:nda ← inherit_attributes: true → also exposes: counterparty, jurisdiction ``` If `contract:nda` also defines its own attribute `is_mutual` (boolean), then a file classified as `contract:nda` sees all three fields: `counterparty` and `jurisdiction` (inherited from `contract`) plus `is_mutual` (defined directly on `contract:nda`). ## Why it matters | Without Facets | With Facets | | - | - | | Find NDAs → search "NDA" in file names | Filter `?content_type=contract:nda`: precise, instant | | Find French contracts → full-text search for "France" | Filter `?attribute=jurisdiction:FR`: structured, no false positives | | Find unsigned contracts → manual review | Filter `?attribute=signed:false` | | Build a dashboard of contracts by type | Group by `content_type`: structured counts, no parsing | | AI answers "what NDAs do we have with Acme?" | BM25 index enriched with metadata: finds the right documents | Facets also enrich the BM25 lexical search index automatically. When you set attribute values on a file, LightOn reindexes that file's text to include the content type labels and attribute values. Semantic search queries benefit from this enrichment without any extra work on your side. ## Limits at a glance | Area | Constraint | | - | - | | Tree depth | 4 levels max | | Code format | Lowercase alphanumeric + hyphens (`kebab-case`) | | Path length | 768 characters max | | Attribute types | 7: `text`, `number`, `date`, `boolean`, `select`, `multi-select`, `rich-text` | | Reserved names | Some attribute names are reserved by the system and rejected on creation | | Classifications per tree | 1 per file per tree (multiple trees allowed) | | Type immutability | `attribute_type` cannot be changed after creation | | Filterable types | `text`, `number`, `date`, `boolean`, `select`. `multi-select` and `rich-text` are not filterable | | Scope | Content types are company-scoped, not workspace-scoped | For the full list, see the [Rules & constraints reference](/tutorials/facets/rules). ## Next steps Create content types and custom attributes Classify files and set attribute values Query by content type and attribute value Infer content type and attribute filters from natural language # Rules & constraints Source: https://developers.lighton.ai/tutorials/facets/rules Troubleshoot errors and understand how Facets behaves when you hit a limit. This page covers every constraint enforced by the Facets API, organized by the problem you're trying to solve. If your request failed or something behaved unexpectedly, find your scenario below. ## My content type won't create You're calling `define_content_type` and getting an error. | What you tried | Why it failed | Error | | - | - | - | | Used uppercase, spaces, or underscores in `code` | Codes must be lowercase alphanumeric with hyphens only: `^[a-z0-9]+(?:-[a-z0-9]+)*$`. Examples: `nda`, `service-agreement`. | `422` | | Created a child with the same code as an existing sibling | Codes must be unique among siblings (same parent). | `409` | | Created a root with a code that already exists | Root codes must be unique per company. | `409` | | Created a path that already exists (e.g. `contract:nda`) | Full paths must be unique per company. | `409` | | Added a 5th level to the tree | Trees can be at most **4 levels deep**. | `400` | | Path exceeds 768 characters | Maximum path length is 768 characters. | `400` | **Good to know:** * A company can have multiple independent trees (different roots). They don't interact. * Content types are company-scoped, not workspace-scoped. * You **cannot reparent** a node. To move it, delete it with `undefine_content_type` and recreate it under the new parent. Deleting cascades: see [What happens when I delete something?](#what-happens-when-i-delete-something) below. ## My attribute won't save You're calling `define_attribute` and getting an error. | What you tried | Why it failed | Error | | - | - | - | | Changed an attribute's type (e.g. `text` → `number`) | `attribute_type` is **immutable** once created. Delete the attribute and recreate it. | `400` | | Used a name that already exists on this content type | Attribute names must be unique within a content type. | `409` | | Used a name that exists elsewhere in the same tree | Names can't conflict across the tree. This prevents ambiguity when attributes are inherited. | `400` | | Used a reserved name like `content_type`, `status`, or `page` | Some names are reserved by the system. | `400` | | Created a `select` or `multi-select` without `choices` | These types require a non-empty `choices` array. | `400` | **Good to know:** * If you omit `label`, it's auto-generated from `name`: `fiscal_year` becomes `"Fiscal Year"`. * 7 attribute types are available: `text`, `number`, `date`, `boolean`, `select`, `multi-select`, `rich-text`. ## My classification was rejected You're calling `classify` on a file and getting an error. | What you tried | Why it failed | Error | | - | - | - | | Classified a file that's already classified in the **same tree** under a sibling path (e.g. `contract:nda` → `contract:service-agreement`) | A file can only have **one classification per tree**. You can't jump between siblings. Unclassify first, then classify the new path. | `400` | | Called `set_value` on a file that isn't classified yet | You must classify a file before setting any attribute values. | `400` | ### Reclassification: what's allowed When a file is already classified and you call `classify` with a different path in the **same tree**, the result depends on the relationship: | You're moving to... | What happens | | - | - | | The **same path** | Nothing changes. Idempotent, returns `200`. | | A **descendant** (e.g. `contract` → `contract:nda`) | Auto-migrates. All attribute values are preserved. | | An **ancestor** (e.g. `contract:nda` → `contract`) | Auto-migrates. Child-specific values (like `is_mutual`) are **deleted** because they don't exist on the ancestor. | | A **sibling** (e.g. `contract:nda` → `contract:service-agreement`) | **Rejected.** Unclassify first, then classify the new path. | A file **can** be classified under multiple trees at the same time (e.g. one `contract:nda` classification and one `regulation:gdpr` classification). ## My value was rejected You're calling `set_value` and getting a `422`. | Attribute type | What's accepted | What's rejected | | - | - | - | | `text`, `rich-text` | String | Anything else | | `number` | Integer, float, or numeric string (`50000`, `"1.5"`) | Booleans, non-numeric strings | | `date` | ISO 8601 string, normalized to `YYYY-MM-DD` | Non-date strings | | `boolean` | `true` or `false` (real booleans) | `"true"`, `1`, `"yes"` | | `select` | Single string matching a defined choice | String not in choices | | `multi-select` | List of strings, each matching a defined choice | Single string, values not in choices | To **remove** a value, use `clear_value`. Passing `null` to `set_value` is rejected with `422`. ## What happens when I delete something? Deleting things in Facets cascades. Here's exactly what gets removed: | You delete... | What's also removed | | - | - | | A **content type** (`undefine_content_type`) | Its entire subtree, all file classifications referencing those paths, and all their attribute values. | | A **classification** (`unclassify`) | All attribute values stored under that classification on that file. | | An **attribute** (`undefine_attribute`) | All stored values for that attribute across every file. | | **Choices** from a `select`/`multi-select` | Any stored values that referenced the removed choices are deleted across all files. | All deletions are **irreversible**. ## What can I filter on? Not all attribute types support filtering via `GET /api/v3/files?attribute=`. | Type | Filterable? | Why | | - | - | - | | `text` | Yes | Indexed in `value_text` | | `number` | Yes | Indexed in `value_number` | | `date` | Yes | Indexed in `value_date` | | `boolean` | Yes | Indexed in `value_boolean` | | `select` | Yes | Indexed in `value_text` | | `multi-select` | No | Not indexed. Use `select` if you need filtering | | `rich-text` | No | Not indexed. Use `text` if you need filtering | ### Multi-select containment When filtering on a `multi-select` attribute, the filter uses **containment**: `jurisdiction:FR` matches files with `["FR", "DE"]`, `["FR"]`, or any list containing `"FR"`. ### Date shortcuts and ambiguity LightOn expands partial dates into ranges: * `effective_date:2025` → any date in 2025 (Jan 1 – Dec 31) * `effective_date:2025-03` → any date in March 2025 * `effective_date:2025-03-01` → exact date A 4-digit value like `2025` is **ambiguous**: it could be a year, a number, or text. LightOn searches across all typed columns with an OR. Use full `YYYY-MM-DD` format for exact date matches. ## How does inheritance work? Attribute inheritance is resolved **at query time**, not stored. When you read a file's facets, LightOn walks up the tree and collects attributes from each ancestor with `inherit_attributes: true`. | Scenario | What happens | | - | - | | You add an attribute to a parent node | Immediately available on all descendants. No migration needed. | | You set `inherit_attributes: false` on a node | Breaks the chain. That node and its descendants won't inherit attributes from above. | | A child defines an attribute with the **same name** as an inherited one | The child's definition **shadows** the parent's. The child's type, choices, and required flag take precedence. | ## Batch behavior When you send a JSON array of actions, LightOn processes them **in order** and **fails fast** on the first error: * Actions before the failure are **committed**. * Actions after the failure are **skipped**. * BM25 reindex is scheduled **once per batch**, not per action. All schema actions (`adopt`, `define_content_type`, `define_attribute`, `undefine_*`) are **idempotent** and safe to replay. # Narrowing search with Agentic Source: https://developers.lighton.ai/tutorials/facets/scope Detect content type and attribute filters from a natural-language query before searching, asking, or listing files. This tutorial uses [`POST /api/v3/content-types/scope`](/api-reference/facets/resolve-search-scope). The full schema lives in the [API reference](/api-reference/introduction). Your app receives free-text queries, but your corpus has structured metadata: content types, dates, jurisdictions, statuses. **Agentic** bridges the gap — it infers the right content type and attribute filters from a natural-language query *before* searching, so semantic search runs on a narrowed, relevant subset instead of the entire corpus. Without Agentic, a query like *"rejected electronics patents filed after 2023"* runs semantic search across all 20,000 documents. With Agentic, the API first infers `content_type: patent:electricity` and `attribute: decision_status:REJECTED, filing_date:>=2023-01-01`, then searches only the 5,409 matching documents. ```mermaid theme={null} graph LR Q[User query] --> S[POST /content-types/scope] S --> G[Scored CTs + attributes] S --> PC[prompt_context] PC -->|Pass model| CM[Completion mode] PC -->|No model| PM[Prompt mode] CM --> SC[scope_completion] PM --> LLM[Your LLM] LLM --> F[content_type + attribute] SC --> F F --> Search[POST /search] F --> Ask[POST /ask] F --> Files[GET /files] ``` The endpoint supports two modes: * **Completion mode** — pass a `model` parameter and the API calls the LLM for you. Returns `scope_completion` with parsed, normalized filters ready to pass to search. * **Prompt mode** — omit `model` and the API returns `prompt_context`, a pre-built text block you feed to your own LLM. Both paths produce the same output: `content_type` and `attribute` filters that map directly to the search API parameters. ### When you don't need scope inference If your app already knows the right filters (e.g. the user picks "NDA" from a dropdown), skip the scope endpoint. Pass `content_type` and `attribute` directly to [`POST /api/v3/search`](/api-reference/search/search-document-chunks), [`POST /api/v3/ask`](/api-reference/ask/ask-a-question-over-your-documents), or [`GET /api/v3/files`](/api-reference/files/list-files-accessible-to-the-authenticated-user). See [Filtering documents by metadata](/tutorials/facets/filter) for the full syntax. ## Quickstart: scope then search Neither [SDK](/sdks) wraps the scope endpoint yet, so the snippets on this page are plain HTTP only. The `content_type` and `attribute` filters they produce are supported by `client.search(...)` and `client.ask(...)`. The simplest integration — one call to infer scope, one call to search within it. ```python title="scope_completion.py" theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Infer content type and attribute filters for a search query. # The model parameter tells the API to call the LLM and return # parsed, normalized filters in scope_completion. response = requests.post( "https://api.lighton.ai/api/v3/content-types/scope", headers=headers, json={ "query": "rejected electronics patents filed after 2023", "model": "mistral-large-latest", # replace with your model }, ) print(response.json()) ``` The response includes ranked content types with their attribute definitions and the parsed scope: ```json theme={null} { "has_signal": true, "groups": [ { "root": "patent", "root_label": "Patent Classification", "max_score": 4.2, "content_types": [ { "path": "patent:electricity", "label": "Electricity", "root": "patent", "score": 4.2, "chunk_count": 5409, "doc_count": 5409, "attributes": [ { "name": "decision_status", "label": "Decision Status", "type": "select", "required": false, "description": "Patent application decision", "choices": ["Accepted", "Rejected"] }, { "name": "filing_date", "label": "Filing Date", "type": "date", "required": false, "description": "Date the patent was filed", "choices": [] } ] } ] } ], "scope_completion": { "content_type": "patent:electricity", "attribute": ["decision_status:REJECTED", "filing_date:>=2023-01-01"], "raw_output": "{\"content_type\":\"patent:electricity\",\"attribute\":[\"decision_status:REJECTED\",\"filing_date:>=2023-01-01\"]}", "normalized": false, "warnings": [] }, "prompt_context": "Content types (by relevance):\n 1. Electricity (patent:electricity) — score: 4.20, 5409 chunks *\n\nRelevant filters:\n - decision_status \"Decision Status\" (select: Accepted, Rejected)\n - filing_date \"Filing Date\" (date: >=, <=)\n ...\n\nRULES:\n 1. Use null content_type when ...\n\nOUTPUT FORMAT:\n {\"content_type\": \"\" or null, \"attribute\": [...]}", "prompt_version": "t:a1b2c3.d:d4e5f6" } ``` Extract `content_type` and `attribute` from `scope_completion` and pass them to your search call: ```python title="scope_then_search.py" theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} query = "rejected electronics patents filed after 2023" # Step 1: Infer scope from the query (completion mode) scope_response = requests.post( "https://api.lighton.ai/api/v3/content-types/scope", headers=headers, json={ "query": query, "model": "mistral-large-latest", # replace with your model }, ) # Step 2: Build a scoped search from the inferred filters scope = scope_response.json().get("scope_completion", {}) search_body = {"query": query} if scope.get("content_type"): search_body["content_type"] = [scope["content_type"]] if scope.get("attribute"): search_body["attribute"] = scope["attribute"] # Step 3: Search within the inferred scope response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json=search_body, ) print(response.json()) ``` `scope_completion.content_type` and `scope_completion.attribute` map directly to the search API parameters — no transformation needed. If the LLM call fails, `scope_completion` is still returned with a `warnings` array explaining the failure, and the rest of the response (scores, groups, attributes) remains usable. For the full attribute filter syntax (operators, OR logic, date shortcuts), see [Filtering documents by metadata](/tutorials/facets/filter). ## Prompt mode: bring your own LLM Completion mode handles everything in one call, but it uses the auto-generated prompt as-is. If you need more control, use **prompt mode**: omit the `model` parameter and the API returns `prompt_context` instead of calling an LLM. Without `model`, the scope endpoint does **not** return `scope_completion`. You get `prompt_context`, `groups`, and scores, but you need your own LLM call to produce the `content_type` and `attribute` filters. Why choose prompt mode: * **Add domain-specific rules** that the auto-generated prompt doesn't know about (e.g. *"'Project Aurora' is our internal name for renewable energy patents"*) * **Add few-shot examples** that teach the LLM patterns specific to your data * **Post-process the LLM output** with validation or normalization before searching * **Use a model not available** through the completion mode ### Step 1: Get the prompt context ````python title="scope_prompt.py" theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Get prompt_context: an LLM-ready text block with ranked content types, # attribute definitions, filter syntax, and inference rules. # Feed this to your own LLM alongside the user query. response = requests.post( "https://api.lighton.ai/api/v3/content-types/scope", headers=headers, json={ "query": "rejected electronics patents filed after 2023", }, ) print(response.json()) ```python prompt_context = response.json()["prompt_context"] user_query = "rejected electronics patents filed after 2023" # Add business rules only someone familiar with this corpus would know domain_rules = """ ADDITIONAL RULES: - "Project Aurora" is our internal name for the renewable energy initiative: always map it to patent:electricity:h02 - Our fiscal year starts in April: "this year" means >= 2025-04-01, "last year" means >= 2024-04-01 AND <= 2025-03-31 - When users say "pending", they mean decision_status:Accepted (our process labels accepted-but-not-granted as "pending") """ # Few-shot examples that correct patterns your LLM gets wrong examples = """ Examples: Query: "Project Aurora patents from last year" Answer: {"content_type": "patent:electricity:h02", "attribute": ["filing_date:>=2024-04-01", "filing_date:<=2025-03-31"]} Query: "pending applications from the energy team" Answer: {"content_type": "patent:electricity", "attribute": ["decision_status:Accepted"]} """ prompt = f"""{prompt_context} {domain_rules} {examples} Query: "{user_query}" Respond with ONLY JSON: {{"content_type": "path or null", "attribute": ["filter1", "filter2"] or []}}""" ```` ### Step 2: Call your LLM and search Send this prompt to any LLM (temperature 0, max 200 tokens). Parse the JSON response and pass `content_type` and `attribute` to your search call, just like with completion mode: ```python theme={null} import json raw = your_llm_client.chat(prompt, temperature=0, max_tokens=200) scope = json.loads(raw) # scope = {"content_type": "patent:electricity", "attribute": ["decision_status:REJECTED"]} search_body = {"query": user_query} if scope.get("content_type"): search_body["content_type"] = [scope["content_type"]] if scope.get("attribute"): search_body["attribute"] = scope["attribute"] results = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json=search_body, ) ``` Here's a representative example of what `prompt_context` looks like for a patent corpus: ``` Content types (by relevance): 1. Electricity (patent:electricity) — score: 1.85, 15 chunks * 2. Basic Electric Elements (patent:electricity:h01) — score: 0.91, 3 chunks Relevant filters: - filing_date "Filing Date" (date: >=, <=) ↳ query match: temporal Other available attributes: - decision_status "Decision Status" (select: Accepted, Rejected) - abstract "Abstract" (rich-text) — only filter when the query explicitly names this field; use *value* wildcard RULES: 1. Use null content_type when the query targets attributes (dates, names, codes) without naming a topic area from the tree. 2. CT DEPTH: prefer section-level paths (e.g., patent:electricity), NOT subclass-level. 3. For date attributes: only filter if the query mentions a specific date, period, or 'recent'. 4. DATE RANGES: for periods, use two attribute entries: one with >= for the start and one with <= for the end. 5. FIELD NAMES: use the exact field name in filters, not the human-readable label. 6. MINIMALITY: only add filters directly stated or clearly implied. DATE GUIDE: Quarter: Q1=Jan 1-Mar 31, Q2=Apr 1-Jun 30, Q3=Jul 1-Sep 30, Q4=Oct 1-Dec 31 EXAMPLES: Query: "recent Electricity" Answer: {"content_type": "patent:electricity", "attribute": ["filing_date:>=2024-01-01"]} Query: "Electricity from Q1 2023" Answer: {"content_type": "patent:electricity", "attribute": ["filing_date:>=2023-01-01", "filing_date:<=2023-03-31"]} Query: "Accepted documents" Answer: {"content_type": null, "attribute": ["decision_status:Accepted"]} OUTPUT FORMAT: {"content_type": "" or null, "attribute": ["field_name:value", ...] or []} - content_type: best-matching CT path from the tree above, or null - attribute: filters using exact field names and syntax from above ``` The sections are generated dynamically from your schema: * **Content types** — ranked by query relevance. The `*` marks the top match. * **Relevant filters** — attributes the API detected as related to the query (e.g., date keywords triggered `filing_date`). Shows filter syntax per type. * **Other available attributes** — remaining attributes you can filter on. * **Rules** — inference guidelines for the LLM. Adapted to your schema (e.g., date rules only appear if you have date attributes). * **Date guide** — period interpretation. Helps the LLM translate "Q1" into concrete dates. * **Examples** — few-shot demonstrations generated from your actual content types and attributes. * **Output format** — the JSON schema the LLM should respond with. ## Schema context: pre-load the catalog Omit `query` to get the full content type catalog — useful for system prompts, tool descriptions, or schema exploration. If you have few content types, this avoids scoring latency. ```python theme={null} response = requests.post( "https://api.lighton.ai/api/v3/content-types/scope", headers=headers, json={}, ) data = response.json() # data["groups"] — all CTs with attributes (score=0, chunk_count=0) # data["prompt_context"] — same catalog as LLM-ready text ``` `groups` contains all your content types with their attributes (`score=0.0` and `chunk_count=0` since there's no query to rank against). `doc_count` reflects corpus size. `prompt_context` contains the same catalog formatted as text for LLM consumption. ## Understanding the response ### Content types and scoring The `groups` array contains content types grouped by root schema (e.g., "Patent Classification" and "SIC Industry" are separate roots). Each group represents an independent taxonomy. * **`score`** — relevance to the query. Higher = stronger match. Comparable across requests. * **`chunk_count`** — how many retrieval chunks matched this content type for this specific query. Measures evidence strength. * **`doc_count`** — total documents classified under this content type in your corpus. Measures coverage. * **`max_score`** — highest score in a root group. Compare roots at a glance without iterating content types. ### Attribute definitions Each content type includes its attribute definitions in `attributes`. Use these to build dynamic filter UIs, validate user input, or understand what `scope_completion.attribute` refers to. ```json theme={null} { "name": "decision_status", "label": "Decision Status", "type": "select", "required": false, "description": "Patent application decision", "choices": ["Accepted", "Rejected"] } ``` Attribute types: `date`, `select`, `multi-select`, `text`, `number`, `boolean`, `rich-text`. For `select` and `multi-select`, `choices` lists the valid values. You never need to parse `prompt_context` to know what attributes exist — they're here as structured data. ### Prompt version (evaluation) `prompt_version` is a fingerprint in the format `t:.d:`: * `t:` hashes the prompt template (rules, patterns, syntax). Changes when the platform updates its inference logic. * `d:` hashes the per-request data (scored paths, attribute definitions). Changes when your corpus or schema changes. Same `prompt_version` = same prompt was generated. Useful for A/B testing LLMs on the same prompt, or detecting when a platform update changed the prompt your users receive. ## Scoring and confidence Scores are the primary signal for deciding whether to scope your search. The top content type's `score` tells you how relevant it is to the query. `has_signal` is a convenience shortcut — it's `true` when the top score meets a default confidence threshold. For custom logic, compare `groups[].content_types[].score` against your own threshold, or pass the `threshold` request parameter to adjust the cutoff. When the top score is low, groups and attributes are still returned — you can present them to users as suggestions or apply them conditionally based on your own rules. ## When Agentic adds the most value | Query type | Example | Without Agentic | With Agentic | | - | - | - | - | | Structured metadata | "examiner Chaki", "filed Jan 2023" | Semantic search cannot handle dates or person names | Filters by metadata: **critical value** | | Mixed | "rejected electronics patents" | Partial results | CT + attribute filters: **high value** | | Pure topic | "energy conversion methods" | Works well | CT narrows domain: moderate value | Agentic delivers the most value on queries that combine free text with structured constraints: dates, statuses, person names, classification codes. These represent roughly 65% of real-world queries and are exactly where semantic search alone falls short. # Uploading & managing files Source: https://developers.lighton.ai/tutorials/files Upload documents into LightOn so they become searchable in seconds. Before you can search, your documents need to be in LightOn. Uploading a file triggers an ingestion pipeline that parses the content, splits it into chunks, generates embeddings, and indexes everything. The whole process typically takes a few seconds for a standard PDF. Ingestion is asynchronous: the upload returns immediately with a `pending` status, and you poll [`GET /api/v3/files/{id}`](/api-reference/files/retrieve-a-single-file-by-id) for completion. This tutorial covers [`POST /api/v3/files`](/api-reference/files/upload-a-file) and [`GET /api/v3/files`](/api-reference/files/list-files-accessible-to-the-authenticated-user). The full schema for every endpoint and parameter lives in the [API reference](/api-reference/introduction). ## Upload a file Send the file as `multipart/form-data` with the destination `workspace_id`: ```python Python SDK theme={null} from lighton import File, LightOn, Workspace workspace_id = 42 # replace with your workspace ID with LightOn() as client: # reads LIGHTON_API_KEY from the environment workspace = Workspace.get(client, workspace_id) doc = workspace.ingest(File(path="handbook.pdf")) print(doc.id, doc.status) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn, Workspace } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // reads LIGHTON_API_KEY from the environment const workspace = await Workspace.get(client, workspaceId) const doc = await workspace.ingest(new File({ path: "handbook.pdf" })) console.log(doc.id, doc.status) ``` ```python Plain Python theme={null} import os import requests workspace_id = 42 # replace with your workspace ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} with open("handbook.pdf", "rb") as f: response = requests.post( "https://api.lighton.ai/api/v3/files", headers=headers, data={"workspace_id": workspace_id}, files={"file": f}, ) file = response.json() print(file["id"], file["status"], file["upload_session_uuid"]) ``` The response is a 201 with the new file record, including an `upload_session_uuid` you can use later to find every file uploaded in the same batch. ## Wait for indexing to complete Poll [`GET /api/v3/files/{id}`](/api-reference/files/retrieve-a-single-file-by-id) until `status` reaches `embedded`: ```python Python SDK theme={null} from lighton import File, FileStatus, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: doc = File.get(client, file_id) doc.wait() # polls refresh() until ingestion reaches a terminal status if doc.status == FileStatus.embedded: print("Ready to search") else: print("Ingestion failed:", doc.status, doc.status_detail) ``` ```typescript TypeScript SDK theme={null} import { File, FileStatus, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() const doc = await File.get(client, fileId) await doc.wait() // polls refresh() until ingestion reaches a terminal status if (doc.status === FileStatus.embedded) { console.log("Ready to search") } else { console.log("Ingestion failed:", doc.status, doc.statusDetail) } ``` ```python Plain Python theme={null} import os import time import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} for _ in range(60): # give up after ~2 minutes response = requests.get( f"https://api.lighton.ai/api/v3/files/{file_id}", headers=headers, ) body = response.json() if body["status"] == "embedded": print("Ready to search") break if body["status"] in ("parsing_failed", "embedding_failed", "fail"): print("Ingestion failed:", body.get("status_detail")) break time.sleep(2) ``` The `status` field moves through these stages: | Status | What's happening | | - | - | | `pending` | Queued for processing | | `parsing` | Extracting text from the document | | `parsing_failed` | Parsing failed, see `status_detail` | | `embedding` | Generating vector embeddings | | `embedding_failed` | Embedding failed, see `status_detail` | | `embedded` | Indexed and ready to search | | `updating` | Re-indexing in progress | | `fail` | Generic failure, see `status_detail` | `status_vision` tracks the same lifecycle for vision/image embeddings: `pending`, `processing`, `embedded`, `fail`, or `-` (not available for this file). One caveat: `status` describes the run that has already happened. If reprocessing is queued but hasn't started, `pending_reprocess` is non-null and `status` still reports the *previous* run. That only comes up after a [file replacement](#replacing-a-documents-file), where trusting `status` alone would report success against the old file. ## Organising documents with tags and titles Add a human-readable title and assign tag IDs at upload time. Tags can be sent as a JSON-encoded array string or as repeated form fields with the same name. ```python Python SDK theme={null} from lighton import File, LightOn, Workspace workspace_id = 42 # replace with your workspace ID with LightOn() as client: workspace = Workspace.get(client, workspace_id) doc = workspace.ingest( File(path="q4-report.pdf", title="Q4 Financial Report"), tags=[1, 2], # tag IDs from Tag.list(client) ) print(doc.id, doc.title) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn, Workspace } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() const workspace = await Workspace.get(client, workspaceId) const doc = await workspace.ingest( new File({ path: "q4-report.pdf", title: "Q4 Financial Report" }), { tags: [1, 2] }, // tag IDs from Tag.list(client) ) console.log(doc.id, doc.title) ``` ```python Plain Python theme={null} import os import requests workspace_id = 42 # replace with your workspace ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} with open("q4-report.pdf", "rb") as f: response = requests.post( "https://api.lighton.ai/api/v3/files", headers=headers, data={ "workspace_id": workspace_id, "title": "Q4 Financial Report", "tags": [3, 7], # list of tag IDs }, files={"file": f}, ) print(response.json()) ``` If a tag ID is invalid, the file is still created but the response is a `207` (multi-status) with a `message` explaining which tags were rejected. To replace tags after upload, [`PATCH /api/v3/files/{id}`](/api-reference/files/update-file-metadata-or-replace-its-file) with a new `tags` array. It replaces *all* existing tags, manual and auto-assigned. Send `[0]` (sentinel) to remove every tag when using multipart format. To add tags without touching existing ones, [`POST /api/v3/files/{id}/tags`](/api-reference/files/add-tags-to-a-file). ## Tracking documents from external systems If you're ingesting documents from a third-party system (ServiceNow, Confluence, SharePoint, etc.), store the source identifier in `external_metadata`. This lets you find the LightOn file later given only the external ID, and surface the original URL in your UI. ```python Python SDK theme={null} from lighton import ExternalMetadata, File, LightOn workspace_id = 42 # replace with your workspace ID with LightOn() as client: # reads LIGHTON_API_KEY from the environment doc = File( path="srv-456789.pdf", workspace_id=workspace_id, external_metadata=ExternalMetadata( external_id="SRV-456789", doc_type="incident", additional_metadata={ "external_url": "https://servicenow.example.com/incident/SRV-456789", }, ), ).create(client) print(doc.id, doc.external_metadata) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // reads LIGHTON_API_KEY from the environment const doc = await new File({ path: "srv-456789.pdf", workspaceId, externalMetadata: { externalId: "SRV-456789", docType: "incident", additionalMetadata: { external_url: "https://servicenow.example.com/incident/SRV-456789", }, }, }).create(client) console.log(doc.id, doc.externalMetadata) ``` ```python Plain Python theme={null} import json import os import requests workspace_id = 42 # replace with your workspace ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} with open("srv-456789.pdf", "rb") as f: response = requests.post( "https://api.lighton.ai/api/v3/files", headers=headers, data={ "workspace_id": workspace_id, "external_metadata": json.dumps({ "external_id": "SRV-456789", "doc_type": "incident", "additional_metadata": { "external_url": "https://servicenow.example.com/incident/SRV-456789", }, }), }, files={"file": f}, ) print(response.json()) ``` `external_id` is required when creating; `doc_type` and `additional_metadata` are optional. When sent via `multipart/form-data`, the whole `external_metadata` value must be a JSON string. Updates merge rather than replace, including inside `additional_metadata`, so you can patch one key and leave the rest alone. `external_id` cannot be changed on a document imported from a datasource: the sync matches its documents on it, so a rename would detach the document from its source. Retrieve it later by external ID: ``` GET /api/v3/files?external_metadata__external_id=SRV-456789 ``` ## Listing and filtering your documents [`GET /api/v3/files`](/api-reference/files/list-files-accessible-to-the-authenticated-user) supports rich filtering. A few common patterns: ```python Python SDK theme={null} from lighton import File, LightOn with LightOn() as client: # File.list passes filters through as query params and follows pagination File.list(client, workspace_id=42) File.list(client, search="security policy", search_details=True) File.list(client, tag_id=3, extension="pdf", ordering="-created_at") docs = File.list(client, total_pages_min=10, total_pages_max=50) for doc in docs: print(doc.id, doc.title, doc.total_pages) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // File.list passes filters through as query params and follows pagination await File.list(client, { workspaceId: 42 }) await File.list(client, { filters: { search: "security policy", search_details: true } }) await File.list(client, { filters: { tag_id: "3", extension: "pdf", ordering: "-created_at" } }) const docs = await File.list(client, { filters: { total_pages_min: 10, total_pages_max: 50 } }) for (const doc of docs) { console.log(doc.id, doc.title, doc.totalPages) } ``` ```python Plain Python theme={null} import os import requests url = "https://api.lighton.ai/api/v3/files" headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # All files in a workspace requests.get(url, headers=headers, params={"workspace_id": "42"}) # Semantic search across filenames and titles, with the top chunk inline requests.get(url, headers=headers, params={"search": "security policy", "search_details": True}) # PDFs tagged 'legal', most recent first requests.get(url, headers=headers, params={"tag_id": "3", "extension": "pdf", "ordering": "-created_at"}) # Files in a 10–50 page window response = requests.get(url, headers=headers, params={"total_pages_min": 10, "total_pages_max": 50}) print(response.json()) ``` Set `include_details=true` to receive the `signature` (TLSH hash for duplicate detection) and `parser` fields on each result. For advanced metadata filtering, see the [Facets tutorials](/tutorials/facets/overview): classify files by type, set custom attributes, then filter with operators. See the [filter reference](/tutorials/facets/filter) for the full DSL. ## Downloading a document Once a file is ingested, [`GET /api/v3/files/{id}/download`](/api-reference/files/download-a-document-file) gives you the stored bytes back, so your app can offer the original document next to the answer it grounded. ```python Python SDK theme={null} from pathlib import Path from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: # reads LIGHTON_API_KEY from the environment doc = File.get(client, file_id) content = doc.download() # or download("rendered_pdf") / download("transcript") Path("downloaded.pdf").write_bytes(content) print(len(content)) ``` ```typescript TypeScript SDK theme={null} import { writeFile } from "node:fs/promises" import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() // reads LIGHTON_API_KEY from the environment const doc = await File.get(client, fileId) const content = await doc.download() // or download("rendered_pdf") / download("transcript") await writeFile("downloaded.pdf", content) console.log(content.length) ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.get( f"https://api.lighton.ai/api/v3/files/{file_id}/download", headers=headers, params={"purpose": "original"}, # or "rendered_pdf", "transcript" ) with open("downloaded.pdf", "wb") as f: f.write(response.content) print(response.status_code, len(response.content)) ``` The `purpose` parameter picks which stored version you get: | Purpose | What you get | | - | - | | `original` (default) | The exact bytes you uploaded | | `rendered_pdf` | A PDF rendition, for in-browser viewers that only handle PDF | | `transcript` | Sanitized markdown text, for audio and video sources | If the requested purpose has no associated file, the API falls back to `original` rather than returning a `404`. ### Thumbnails Every document also gets a 256x256 WebP thumbnail, handy for a document picker or a result list. Generation runs asynchronously and separately from ingestion, so an `embedded` file may still have none. Check the `thumbnail.status` field on the file before fetching, or [`GET /api/v3/files/{id}/thumbnail`](/api-reference/files/get-document-thumbnail) returns a `404`: ```python Python SDK theme={null} from pathlib import Path from lighton import File, LightOn, ThumbnailStatus file_id = 1234 # replace with your file ID with LightOn() as client: # reads LIGHTON_API_KEY from the environment doc = File.get(client, file_id) if doc.thumbnail and doc.thumbnail.status is ThumbnailStatus.READY: Path("thumbnail.webp").write_bytes(doc.download_thumbnail()) print(doc.thumbnail.url) ``` ```typescript TypeScript SDK theme={null} import { writeFile } from "node:fs/promises" import { File, LightOn, ThumbnailStatus } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() // reads LIGHTON_API_KEY from the environment const doc = await File.get(client, fileId) if (doc.thumbnail?.status === ThumbnailStatus.READY) { await writeFile("thumbnail.webp", await doc.downloadThumbnail()) console.log(doc.thumbnail.url) } ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} file = requests.get( f"https://api.lighton.ai/api/v3/files/{file_id}", headers=headers, ).json() if file["thumbnail"]["status"] == "READY": thumbnail = requests.get( f"https://api.lighton.ai/api/v3/files/{file_id}/thumbnail", headers=headers, ) with open("thumbnail.webp", "wb") as f: f.write(thumbnail.content) print(thumbnail.status_code, len(thumbnail.content)) ``` `status` is `MISSING`, `PROCESSING`, or `READY`. `url` is populated only once it reads `READY`. ## Reading a document's parsed text The platform already parsed your document at ingestion, so you don't need to re-upload it to [`/parse`](/tutorials/parse) to read the text. Pass `include_content=true` to the file detail endpoint and you get the same `{index, markdown}` page shape `/parse` returns: ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: # reads LIGHTON_API_KEY from the environment doc = File.get(client, file_id) for page in doc.pages(): # same {index, markdown} shape `parse` returns print(f"--- page {page.index} ---") print(page.markdown[:200]) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() // reads LIGHTON_API_KEY from the environment const doc = await File.get(client, fileId) for (const page of await doc.pages()) { // same {index, markdown} shape `parse` returns console.log(`--- page ${page.index} ---`) console.log(page.markdown.slice(0, 200)) } ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.get( f"https://api.lighton.ai/api/v3/files/{file_id}", headers=headers, params={"include_content": True}, ) for page in response.json().get("pages", []): print(f"--- page {page['index']} ---") print(page["markdown"][:200]) ``` ```json theme={null} { "id": 1234, "title": "Acme NDA 2025", "pages": [ {"index": 1, "markdown": "# Non-Disclosure Agreement\n\nThis NDA is entered into..."}, {"index": 2, "markdown": "## 3. Term\n\nThe obligations survive for three years..."} ] } ``` The text can be large, so `include_content` is off by default. Ask for it only when you need the content, not on every list call. ## Replacing a document's file When the source document changes, you don't have to delete and re-upload: send the new file as the `file` part of [`PATCH /api/v3/files/{id}`](/api-reference/files/update-file-metadata-or-replace-its-file) and the document is re-ingested in place. It keeps its `id`, `title`, manual tags, content types and attribute values, so every reference you stored still resolves. ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: # reads LIGHTON_API_KEY from the environment doc = File.get(client, file_id) doc.replace("q4-report.pdf", wait=True) # wait=True also waits out pending_reprocess print(doc.id, doc.status, doc.filename) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() // reads LIGHTON_API_KEY from the environment const doc = await File.get(client, fileId) await doc.replace("q4-report.pdf", { wait: true }) // wait: true also waits out pendingReprocess console.log(doc.id, doc.status, doc.filename) ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} with open("q4-report.pdf", "rb") as f: response = requests.patch( f"https://api.lighton.ai/api/v3/files/{file_id}", headers=headers, files={"file": f}, ) body = response.json() print(body["status"], body["pending_reprocess"]) # 'embedded' 'update': the new file is queued ``` Only the file and what is derived from it (text, chunks, embeddings, auto-assigned tags, summaries) is rebuilt. The new file may be of a different type than the one it replaces. You can send `title`, `tags` or `external_metadata` in the same request: the metadata changes apply immediately, the file is queued. The response describes the document as it **still is**. `status` typically reads `embedded` (the previous run), and `filename`, `size`, `total_pages` and `summaries` still describe the old file. The one field that tells you a replacement is pending is `pending_reprocess`, which reads `update` until the pipeline picks the document up. So poll until `pending_reprocess` is `null` **and** `status` is terminal. Treating a terminal `status` on its own as completion reports success against the previous file. The SDK's `wait()` already accounts for this, which is what `replace(..., wait=True)` uses. The document keeps serving its previous content until processing actually starts, so search, extract and download stay consistent in the meantime. Once conversion begins, a document whose new file needs rendering has no rendered PDF yet, and `download?purpose=rendered_pdf` serves the original instead. Gate an in-browser viewer on `status`, not on the download succeeding. Replacing a file needs an owner or editor role in the document's workspace, and is rejected in workspaces configured for synced documents. The storage quota is checked on the size difference, not the full new size. ## Deleting files Single delete: ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: File.get(client, file_id).delete() print(f"deleted file {file_id}") ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() await (await File.get(client, fileId)).delete() console.log(`deleted file ${fileId}`) ``` ```python Plain Python theme={null} import os import requests file_id = 1234 # replace with your file ID headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.delete( f"https://api.lighton.ai/api/v3/files/{file_id}", headers=headers, ) print(response.status_code) ``` Bulk delete: ```python Python SDK theme={null} from lighton import File, LightOn file_ids = [123, 124, 125] # replace with your file IDs with LightOn() as client: # reads LIGHTON_API_KEY from the environment File.delete_many(client, file_ids) print(f"deleted {len(file_ids)} files") ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileIds = [123, 124, 125] // replace with your file IDs const client = new LightOn() // reads LIGHTON_API_KEY from the environment await File.deleteMany(client, fileIds) console.log(`deleted ${fileIds.length} files`) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/files/bulk-delete", headers=headers, json={"ids": [123, 124, 125]}, ) print(response.json()) ``` Both return `204 No Content` on success. Files in synced (datasource-managed) workspaces cannot be deleted manually. The API returns `400`. ## Common errors | Status | Cause | | - | - | | `400` | Validation error, unsupported file type, or synced-workspace constraint | | `401` | Missing or invalid API key | | `403` | Permission denied (no upload/delete rights) | | `404` | File does not exist or is not accessible | | `429` | Too many concurrent uploads for this session | # From documents to answers Source: https://developers.lighton.ai/tutorials/from-documents-to-answers What retrieval-augmented generation is, how retrieval and generation differ, and which LightOn endpoint to reach for. Have you ever asked a chatbot a question and gotten a confident, well-written answer that was simply *wrong*? You've met the core limitation of a language model on its own: it can only draw on what it absorbed during training. It doesn't know your contracts, your wiki, last quarter's report, or anything published after its cutoff. **Retrieval-augmented generation (RAG)** is the standard fix, and it's what most of the LightOn API is built to power. ## The two halves of RAG RAG combines two very different kinds of models. Understanding the split is the key to using the API well. Given a query, they **find** the most relevant pieces of text in a corpus. They don't write anything, they rank existing passages by how well they match the meaning of the query. Fast, factual, and grounded in your actual documents. Given a prompt, they **write** new text, fluent, coherent, in natural language. On their own they have no access to your data and can hallucinate. Their strength is synthesis and phrasing, not recall. A retrieval model can tell you *which paragraph* answers a question but not phrase the answer. A generative model can write a beautiful answer but doesn't know your documents. **RAG bolts them together:** retrieve the relevant passages first, then hand those passages to the generative model as context so its answer is grounded in real source material instead of its training-time memory. RAG flow: a question goes through retrieval over your indexed documents to fetch relevant passages, which generation turns into a grounded answer. Ask does both halves in one call. The payoff: answers that cite real sources, stay current as you add documents, and can be traced back to where they came from. Retrieval quality is the ceiling on answer quality. A generative model can only be as accurate as the passages it's given. If retrieval surfaces the wrong context, even the best model will write a confident, wrong answer. That's why LightOn invests heavily in the retrieval pipeline: hybrid semantic + lexical search with a reranker on top. ## How retrieval works under the hood When you ingest a document, LightOn turns it into a retrieval-ready index for you. Behind a single API call sits a full document-understanding pipeline: it reads the layout and structure of the document, breaks the content into meaningful units, builds rich semantic representations of them, and indexes everything for fast retrieval, with a lot of careful work in between to keep results accurate across messy, real-world files. You never have to run a vector database, an OCR model, or an embedding pipeline yourself. At query time, retrieval runs a **hybrid** lookup (vector search for meaning *and* lexical search for exact terms), then a **reranker** scores every candidate against the full query and returns the best passages. ## Mapping use cases to endpoints LightOn exposes each piece of this stack as an endpoint, so you can use as much or as little of the RAG pattern as you need. ### Build a searchable knowledge base Ingest documents once into a persistent, indexed corpus, then retrieve over it as often as you like. **Ingestion.** Upload documents and LightOn turns them into a searchable index automatically, running the whole document-understanding pipeline for you. This is the "your documents" box in the diagram above. **Retrieval only.** Send a natural-language query, get back ranked passages with scores and sources. Use this when you want the raw material and intend to do your own ranking, display, or generation on top. **Retrieval + generation.** Full RAG in a single call: it retrieves *and* generates a grounded answer with citations. The fastest path from question to cited answer. The choice between **Search** and **Ask** is the choice between owning the generation step or not: * Reach for **Ask** for straightforward, single-turn question answering. One retrieval, one generation, with a fixed prompt: minimal code on your side. * Reach for **Search** when you want to drive generation yourself: multi-step retrieval, query rewriting, conversational memory, custom prompts, or your own choice of model. Call `Search` inside your own agentic loop, then feed the passages to whatever generative model you prefer. ### Process documents on the fly Sometimes you don't want a persistent corpus at all, you just want to turn one document into something machine-readable for your own pipeline. Nothing is stored. **Document to clean Markdown.** The parsing step from the ingestion pipeline, exposed on its own. Useful when you want to feed text to your own LLM, store it, or display it. **Document to typed fields.** Give it a JSON Schema and it pulls those fields out of every page. Built for mechanical, repetitive processing: stacks of invoices, batches of forms. ## Demystifying agentic RAG, it's just a loop! Agentic RAG loop: a question goes to an agent (LLM) that repeatedly sends a query to Search over your indexed documents and reads the returned passages, looping until it has enough to write the grounded answer. "Agentic RAG" sounds like it needs a framework. It doesn't. At its core it's a `while` loop where a model decides what to look up, reads what comes back, and chooses whether to search again or write the answer. [Search](/tutorials/search) is the only retrieval tool you need inside that loop. Plain [Ask](/tutorials/ask) runs one retrieval and one generation. That covers a direct question like "What is the JWT token expiry policy". It falls short when a question needs several lookups, or when the right query only becomes clear after you've seen the first results. Take "How does our token expiry compare to the OAuth refresh window, and which one expires first?". Answering it well means retrieving the token policy, separately retrieving the refresh window, then reasoning over both. The loop has three moving parts: 1. The model proposes a search query. 2. You call Search and hand the passages back to the model. 3. The model either proposes another query or decides it has enough to answer. Give the model Search as a tool and let it drive: ```python Python SDK theme={null} from lighton import LightOn # Open the client once for the process lifetime, not once per turn of the loop client = LightOn() # reads LIGHTON_API_KEY from the environment def search(query, max_results=5): return client.search(query, max_results=max_results).results # `llm` is any chat model that supports tool calling. SEARCH_TOOL = { "name": "search_documents", "description": "Search the document corpus and return relevant passages.", "input_schema": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"], }, } messages = [{ "role": "user", "content": "How does our token expiry compare to the OAuth refresh window, and which one expires first?", }] while True: reply = llm.chat(messages=messages, tools=[SEARCH_TOOL]) if reply.tool_call: results = search(reply.tool_call.input["query"]) passages = "\n\n".join(r.content for r in results) messages.append(reply.message) messages.append({"role": "tool", "content": passages}) continue print(reply.text) # no more searches needed: this is the grounded answer break ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" // `llm` is any chat model that supports tool calling. declare const llm: { chat(args: { messages: unknown[]; tools: unknown[] }): Promise<{ toolCall?: { input: { query: string } } message: unknown text: string }> } // Create the client once for the process lifetime, not once per turn of the loop const client = new LightOn() // reads LIGHTON_API_KEY from the environment async function search(query: string, maxResults = 5) { return (await client.search(query, { maxResults })).results } const SEARCH_TOOL = { name: "search_documents", description: "Search the document corpus and return relevant passages.", input_schema: { type: "object", properties: { query: { type: "string" } }, required: ["query"], }, } const messages: unknown[] = [ { role: "user", content: "How does our token expiry compare to the OAuth refresh window, and which one expires first?", }, ] while (true) { const reply = await llm.chat({ messages, tools: [SEARCH_TOOL] }) if (reply.toolCall) { const results = await search(reply.toolCall.input.query) const passages = results.map((r) => r.content).join("\n\n") messages.push(reply.message) messages.push({ role: "tool", content: passages }) continue } console.log(reply.text) // no more searches needed: this is the grounded answer break } ``` ```python Plain Python theme={null} import os import requests HEADERS = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} def search(query, max_results=5): response = requests.post( "https://api.lighton.ai/api/v3/search", headers=HEADERS, json={"query": query, "max_results": max_results}, ) return response.json()["results"] # `llm` is any chat model that supports tool calling. SEARCH_TOOL = { "name": "search_documents", "description": "Search the document corpus and return relevant passages.", "input_schema": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"], }, } messages = [{ "role": "user", "content": "How does our token expiry compare to the OAuth refresh window, and which one expires first?", }] while True: reply = llm.chat(messages=messages, tools=[SEARCH_TOOL]) if reply.tool_call: results = search(reply.tool_call.input["query"]) passages = "\n\n".join(r["content"] for r in results) messages.append(reply.message) messages.append({"role": "tool", "content": passages}) continue print(reply.text) # no more searches needed: this is the grounded answer break ``` That is the whole pattern. On each turn the model reads the passages it has gathered so far and decides the next query, so multi-step retrieval falls out of the loop on its own. When the model stops calling the tool, its final message is the answer, generated over everything it retrieved. Because you own the loop, you also own the parts Ask keeps fixed: the system prompt, which model runs, how many rounds you allow before forcing an answer, and any reranking or filtering you apply to the passages before they go back to the model. Reach for this when a single query can't capture the question, or when one answer depends on the result of an earlier lookup. For straightforward, single-turn question answering, [Ask](/tutorials/ask) does the same retrieve-then-generate step in one call. Of course, advanced deep-search use cases may call for more than this: specialized sub-agents, planning steps, parallel retrieval. At that point you genuinely do want a framework. That's out of scope for this tutorial, but the loop above is the foundation it all builds on. ## Ship your first RAG Build the corpus everything else retrieves over. See retrieval in action and tune it to your needs. Get a grounded, cited answer in one call. Do you even search? # Tutorials Source: https://developers.lighton.ai/tutorials/index Step-by-step guides for building common document intelligence workflows. Each tutorial walks through one endpoint end to end, with copy-pasteable examples. New to retrieval-augmented generation? Start with [From documents to answers](/tutorials/from-documents-to-answers) to learn the concepts and pick the right endpoint. Otherwise, jump straight to [Uploading & managing files](/tutorials/files), then [Searching documents](/tutorials/search). ## Build a searchable knowledge base Ingest documents once, then query them. A persistent, indexed corpus you can search or ask questions over. Upload documents into LightOn so they become searchable in seconds, plus tagging, metadata, listing, and filtering. Find the most relevant passages with a natural-language query, with scoping, reranking, and vision-mode search. Get a grounded, LLM-generated answer with the sources it used, returned synchronously or streamed token by token. ## Classify and organise documents Three layers that compose: partition files into workspaces, group them with tags, and enrich them with structured facets. Pick the simplest one that solves your problem. ### Workspaces Hard containers that isolate a team's, customer's, or tenant's documents. The only layer that's also a permission boundary, via workspace-scoped API keys. Isolate documents per team, customer, or tenant, and scope API keys for permission-level segmentation. ### Tags Flat, reusable labels that group documents into collections, even across workspaces. Build reusable collections that cut across workspaces, then scope search and ask to a collection. ### Facets Typed, hierarchical metadata for precise structured queries: classify by content type and filter by attribute values. Understand content types, attributes, and how facets enrich search. Build classification trees with custom attributes, from starter templates or from scratch. Assign content types to files, set attribute values, and read back structured metadata. Scope search and ask queries by content type and attribute values. ## Process documents on the fly Convert or extract from a document in a single call. Nothing is stored, useful for feeding your own pipeline. Convert any document to clean Markdown, synchronously for quick jobs or async for large documents. Pull typed fields out of documents using a JSON Schema you provide, in sync or async mode. # Parsing documents Source: https://developers.lighton.ai/tutorials/parse Convert any document to clean Markdown, synchronously or as an async job. Illustration of the Parse use case. Parse converts a document to structured Markdown without storing or indexing it. By default the call is synchronous: send a file, get Markdown back immediately. For larger documents, an async mode queues the job and lets you poll for the result. Use Parse when you want the text content of a document for your own purposes: feeding it to an LLM, storing it in your database, displaying it in a UI, or pre-processing it before ingestion. Nothing is saved on LightOn's side. If you want the document to be **searchable**, use [Files](/tutorials/files) instead: it ingests and indexes the document permanently. The full schema for [`POST /api/v3/parse`](/api-reference/parse/parse-a-document-to-markdown) is in the [API reference](/api-reference/introduction). ## Parse a document From a local file: ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment doc = client.parse(path="report.pdf") # One entry per page, each with a 1-based index and its markdown markdown = "\n\n".join(page.markdown for page in doc.result.pages) print(markdown[:200]) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment const doc = await client.parse({ file: "report.pdf" }) // One entry per page, each with a 1-based index and its markdown const markdown = doc.result.pages.map((page) => page.markdown).join("\n\n") console.log(markdown.slice(0, 200)) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} with open("report.pdf", "rb") as f: response = requests.post( "https://api.lighton.ai/api/v3/parse", headers=headers, files={"file": f}, ) pages = response.json()["result"]["pages"] # Each page is {"index": , "markdown": }. Join them for the full document: markdown = "\n\n".join(page["markdown"] for page in pages) print(markdown[:200]) ``` Or from a publicly reachable URL: ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: doc = client.parse(url="https://example.com/whitepaper.pdf") print(doc.status) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() const doc = await client.parse({ url: "https://example.com/whitepaper.pdf" }) console.log(doc.status) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/parse", headers=headers, json={"document": "https://example.com/whitepaper.pdf"}, ) print(response.json()["status"]) ``` `file` (multipart) and `document` (JSON URL) are mutually exclusive: pass one or the other. ## What comes back The response includes the parsed Markdown, broken out **per page**, along with metadata about the original document: ```json theme={null} { "id": "parse_0196e4b2a3c14d5e8f7a9b2c1d0e3f4a", "status": "completed", "created_at": "2026-03-31T10:00:00+00:00", "completed_at": "2026-03-31T10:00:03+00:00", "processing_time_ms": 2840, "document": { "filename": "report.pdf", "page_count": 3, "file_size_bytes": 245120, "mime_type": "application/pdf" }, "result": { "pages": [ { "index": 1, "markdown": "# Invoice\n\nInvoice Number: INV-2026-001\nDate: March 15, 2026\n…" }, { "index": 2, "markdown": "## Line items\n\n| Item | Qty | Price |\n…" }, { "index": 3, "markdown": "## Totals\n\nSubtotal: …" } ] }, "usage": { "pages_processed": 3 } } ``` `result.pages` is an array, one entry per page, each with a 1-based `index` and the `markdown` for that page. Headings, lists, tables, and code blocks are preserved where the source document supports them. Concatenate the per-page `markdown` (in `index` order) to reconstruct the full document. ## Large documents: async mode Sync mode is capped at 20 MB / 15 pages. For larger documents, set `options.async = true`. The call returns **202** immediately with a `parse_` job id instead of blocking, and you poll [`GET /api/v3/parse/{id}`](/api-reference/parse/get-the-status-and-result-of-an-async-parse-job) until the job reaches a terminal status. ```python Python SDK theme={null} from lighton import ExecMode, LightOn with LightOn() as client: # wait=True polls for you and returns once the job is terminal job = client.parse( url="https://example.com/large-report.pdf", mode=ExecMode.ASYNC, wait=True, ) markdown = "\n\n".join(page.markdown for page in job.result.pages) print(markdown[:200]) ``` ```typescript TypeScript SDK theme={null} import { ExecMode, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // wait: true polls for you and resolves once the job is terminal const job = await client.parse({ url: "https://example.com/large-report.pdf", mode: ExecMode.async, wait: true, }) const markdown = (job.result?.pages ?? []).map((page) => page.markdown).join("\n\n") console.log(markdown.slice(0, 200)) ``` ```python Plain Python theme={null} import os import time import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Returns 202 immediately with a parse_ job ID instead of blocking response = requests.post( "https://api.lighton.ai/api/v3/parse", headers=headers, json={ "document": "https://example.com/large-report.pdf", "options": {"async": True}, }, ) job_id = response.json()["id"] # e.g. "parse_Kg" for _ in range(120): # give up after ~10 minutes response = requests.get( f"https://api.lighton.ai/api/v3/parse/{job_id}", headers=headers, ) job = response.json() if job["status"] in ("completed", "failed"): break time.sleep(5) pages = job["result"]["pages"] # null until status == "completed" markdown = "\n\n".join(page["markdown"] for page in pages) print(markdown[:200]) ``` For multipart uploads, pass `options` as a JSON-encoded form field: `-F 'options={"async":true}'`. The polling response uses the same envelope as the sync result: `result`, `usage`, `completed_at`, and `processing_time_ms` are `null` while the job is in flight and populated once `status` is `completed`. On terminal failure, `status` is `failed` and an `error` block describes the cause. Recommended polling cadence: every 1s for the first 10s, then every 5s, capped at 30s. Stop once `status` is `completed` or `failed`. The SDK handles this for you when you pass `wait=True`; drop it and call `job.poll()` yourself if you need to react to each status change. ## Supported formats and limits **Formats:** `.pdf`, `.png`, `.jpg`, `.jpeg`, `.pptx`, `.ppt`, `.odp`, `.docx`, `.odt`, `.doc`, `.html` | Mode | Max file size | Max pages | | - | - | - | | Sync (default) | 20 MB | 15 pages | | Async (`options.async = true`) | 100 MB | 1000 pages | To make a document **searchable** rather than just converting it once, ingest it with [Files](/tutorials/files) and retrieve relevant sections with [Search](/tutorials/search). ## Common errors | Status | Cause | | - | - | | `400` | Missing document, unsupported format, or page limit exceeded | | `401` | Missing or invalid API key | | `413` | File exceeds the size limit for the mode (20 MB sync / 100 MB async) | | `429` | Rate limit exceeded | | `503` | Parsing backend is overloaded. Retry later | # Searching documents Source: https://developers.lighton.ai/tutorials/search Find the most relevant passages in your documents using a natural-language query. Illustration of the Search use case. Search is how your application answers questions from documents. Send a query in plain language, get back ranked passages from across your corpus. No SQL, no keyword matching, no index tuning. Under the hood LightOn runs a hybrid pipeline: vector search for meaning, lexical search for exact terms, then a reranker that scores every candidate against the full query and returns the best results. This tutorial walks through [`POST /api/v3/search`](/api-reference/search/search-document-chunks). For the full schema and every parameter, see the [API reference](/api-reference/introduction). ## Your first search If you've already uploaded documents, this is all you need: ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # reads LIGHTON_API_KEY from the environment response = client.search("What is the JWT token expiry policy") for result in response.results: source = result.source print(result.content) print(f" → {source.filename}, p.{source.page_start}–{source.page_end}") print(f" → score {result.score:.2f}") ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment const response = await client.search("What is the JWT token expiry policy") for (const result of response.results) { const source = result.source console.log(result.content) console.log(` → ${source.filename}, p.${source.pageStart}–${source.pageEnd}`) console.log(` → score ${result.score.toFixed(2)}`) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json={"query": "What is the JWT token expiry policy"}, ) for result in response.json()["results"]: print(result["content"]) print(f" → {result['source']['filename']}, p.{result['source']['page_start']}–{result['source']['page_end']}") print(f" → score {result['score']:.2f}") ``` By default this searches every document your API key can reach and returns the top 10 results. That's usually a good starting point. ## Scoping to a subset of documents When you want to limit search to a specific team's workspace, a handful of files, or a tagged collection, use one of the three scoping parameters. `file_id` is mutually exclusive with `workspace_id` and `tag_id`, while `workspace_id` and `tag_id` can be combined. Best for multi-tenant products where each customer or team has their own workspace. ```python theme={null} json={ "query": "deployment runbook", "workspace_id": [12, 15], } ``` Best when you know exactly which documents are relevant, for example after the user selects files in your UI. ```python theme={null} json={ "query": "Q4 revenue forecast", "file_id": [101, 102], } ``` `file_id` cannot be combined with `workspace_id` or `tag_id`. Best for curated collections: tag documents at upload time, then search across the collection by tag. ```python theme={null} json={ "query": "GDPR data retention requirements", "tag_id": [7], } ``` ## Reading the response Each result contains: * **`content`**: the matched passage text. `null` for vision-mode chunks. * **`score`**: the relevance score you rank on. With scoring on (the default), it equals `scores.relevance` (0–1); with scoring off, it's the combined retrieval score (unbounded). * **`scores`**: the per-signal breakdown behind that score. See [Understanding the scores](#understanding-the-scores) below. * **`source`**: where the chunk came from: `file_id`, `filename`, `title`, `mime_type`, `size_bytes`, `page_start`/`page_end`, `total_pages`, `tags`, and `external_metadata` for connector-imported files. * **`workspace`**: the workspace the document belongs to. ```json theme={null} { "results": [ { "chunk_id": "550e8400-e29b-41d4-a716-446655440000", "content": "JWT tokens are signed using RS256 and expire after 1 hour.", "score": 0.95, "scores": { "text": 0.91, "vision": null, "keyword": 0.43, "multivector": 12.4, "relevance": 0.95 }, "source": { "file_id": 512, "filename": "auth-system.pdf", "title": "Authentication System Design", "mime_type": "pdf", "size_bytes": 482113, "page_start": 3, "page_end": 4, "total_pages": 12, "tags": [{"id": 7, "name": "security"}], "external_metadata": null }, "workspace": {"id": 42, "name": "Engineering Docs"} } ] } ``` ## Understanding the scores `score` is the single number you should rank and threshold on, and results come back ordered by it, descending. When relevance scoring runs (the default), `score` **is** the cross-encoder relevance score (`scores.relevance`, 0–1). When you turn scoring off with `relevance_scoring: none`, `score` falls back to the combined retrieval score (unbounded, higher is better). Use it directly unless you have a reason to inspect the parts. `scores` exposes those individual signals so you can debug *why* a chunk ranked where it did, or build your own re-ranking on top. Each signal is `null` when it didn't apply to that chunk. | Signal | What it measures | Range | When it's `null` | | - | - | - | - | | `text` | Dense text-embedding similarity between the query and the chunk (`1 − cosine distance`). The core semantic-match signal. | \~0–1, higher is closer | In vision mode (no text embedding is scored). | | `vision` | Visual page similarity from the vision embedding — matches layout, diagrams, and scanned content rather than extracted text. | \~0–1, higher is closer | When the document has no vision index. | | `keyword` | BM25 lexical score — rewards exact term and phrase overlap, the way classic keyword search does. Catches identifiers, codes, and rare terms that embeddings can blur. | ≥0, unbounded, higher is stronger | In vision mode (no text is scored). | | `multivector` | ColBERT multi-vector (MaxSim) score — a fine-grained token-level match that reranks candidates more precisely than a single embedding. | ≥0, unbounded, higher is stronger | When multi-vector reranking is disabled. | | `relevance` | Cross-encoder reranker confidence — the model reads the query and chunk together and scores how well the passage actually answers the query. The strongest single signal, and equal to the top-level `score` when present. | 0–1, higher is more relevant | When `relevance_scoring` is `none` or the reranker is unavailable. | A signal can also read `null` because it **failed**, not because it didn't apply. When that happens the result item carries a `warnings` array: each entry has a `code` naming the degraded signal (`relevance`, `multivector`, or `vision`) and a `reason` (`model_not_found`, `embed_failed`, `timeout`, `service_error`, or `unknown`). Results are still returned, in retrieval order when the reranker is the signal that degraded, but ranking quality is lower. A `null` with no matching warning just means the signal doesn't apply to that chunk, and `warnings` is absent entirely when every step succeeded. Only `text`, `keyword`, and `multivector` share a comparable footing within a single response; `relevance` is a calibrated probability and `vision` lives on its own scale. Don't compare raw signal values against each other or across queries — for ranking, always use the top-level `score`. ## Tuning result count and latency `max_results` (default 10, range 1–100) controls how many ranked chunks come back. For lower latency, set `relevance_scoring: "none"`. You lose the reranker's quality boost but the pipeline becomes a straight hybrid lookup: results come back in retrieval order, `score` is the combined retrieval score, and `scores.relevance` will be `null`. ```python Python SDK theme={null} from lighton import LightOn, RelevanceScoring with LightOn() as client: response = client.search( "incident response playbook", max_results=5, relevance_scoring=RelevanceScoring.none, ) print(len(response.results), "result(s)") ``` ```typescript TypeScript SDK theme={null} import { LightOn, RelevanceScoring } from "@lighton-ai/sdk" const client = new LightOn() const response = await client.search("incident response playbook", { maxResults: 5, relevanceScoring: RelevanceScoring.none, }) console.log(response.results.length, "result(s)") ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json={ "query": "incident response playbook", "max_results": 5, "relevance_scoring": "none", }, ) print(response.json()) ``` `relevance_scoring` also accepts `scoring_only` (score every candidate but return them all, without filtering below the quality threshold). Omit it for the default (`scoring_and_filtering`). The old `skip_rerank` flag is deprecated: `true` maps to `none`, `false` to `scoring_and_filtering`. ## Searching images and diagrams Switch to vision mode to search documents by their visual content, useful for scanned pages, slide decks, architecture diagrams, or any document where the meaning is in the layout rather than the words. ```python Python SDK theme={null} from lighton import LightOn, SearchMode with LightOn() as client: response = client.search( "network topology diagram showing DMZ", mode=SearchMode.vision, include_image=True, max_results=3, ) for result in response.results: print(result.source.filename, len(result.image.b64_content)) ``` ```typescript TypeScript SDK theme={null} import { LightOn, SearchMode } from "@lighton-ai/sdk" const client = new LightOn() const response = await client.search("network topology diagram showing DMZ", { mode: SearchMode.vision, includeImage: true, maxResults: 3, }) for (const result of response.results) { console.log(result.source.filename, result.image?.b64Content.length) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json={ "query": "network topology diagram showing DMZ", "mode": "vision", "include_image": True, "max_results": 3, }, ) print(response.json()) ``` Vision mode requires documents to have been indexed with vision embeddings (`status_vision: "embedded"`). With `include_image: true`, each result includes an `image.b64_content` field with the page rendered as a base64 image. In text mode, the image is fetched from the vision chunk covering the chunk's start page, or an empty string if no vision index exists for that page. ## Narrowing search with metadata filters If your documents are classified with [Facets](/tutorials/facets/overview), add `content_type` and `attribute` fields to the request body to scope results by metadata. See [Filtering by facets](/tutorials/facets/filter) for worked examples. ## Common errors | Status | Cause | | - | - | | `400` | Request body is not parsable JSON | | `403` | None of the provided filters resolve to authorized resources | | `422` | Validation error, e.g. `file_id` combined with `workspace_id`/`tag_id`, or `max_results` out of range | | `429` | Rate limit exceeded | | `500` | Unexpected server error | # Organizing documents with tags Source: https://developers.lighton.ai/tutorials/tags Group documents into collections with flat, reusable labels, then scope search and ask to a collection. Tags are flat, reusable labels you attach to documents to group them into collections: a project, a team, a topic, a customer. Unlike [Facets](/tutorials/facets/overview), there's no schema and no hierarchy to design first, and unlike [Workspaces](/tutorials/workspaces), a tag isn't tied to one container: the same tag can group files that live in **different workspaces**, so you can form cross-workspace collections. You create a tag, assign it to files, and immediately scope search, ask, and file listings to that collection. > *"Search only the Project Alpha documents."* > > *"Answer this question using just our compliance files."* That's what tags give you: a lightweight way to carve your corpus into named groups your app can filter on. This tutorial covers the tag lifecycle: [`POST /api/v3/tags`](/api-reference/tags/create-a-new-tag-for-the-company), [`GET /api/v3/tags`](/api-reference/tags/list-all-tags-for-the-authenticated-users-company), [`POST /api/v3/files/{id}/tags`](/api-reference/files/add-tags-to-a-file), and [`DELETE /api/v3/files/{id}/tags/{tag_id}`](/api-reference/files/remove-a-tag-from-a-file). Assigning tags at upload time is covered in [Uploading & managing files](/tutorials/files#organising-documents-with-tags-and-titles). ## Workspaces, tags, or facets? All three organise documents, but at different layers. They compose: a file lives in one [workspace](/tutorials/workspaces), can carry several tags, and can be classified with [facets](/tutorials/facets/overview). | | Workspaces | Tags | Facets | | - | - | - | - | | **What it is** | A container; every file lives in exactly one | Flat, reusable labels | Typed, hierarchical metadata with a schema | | **A file belongs to** | exactly one workspace | many tags, **across workspaces** | many content types, **across workspaces** | | **Best for** | Isolating teams, customers, tenants | Cross-cutting collections (project, topic) | Precise structured queries | | **Setup cost** | Create a workspace | Create a tag | Design a content-type tree | | **Scope a query with** | `workspace_id` | `tag_id` | `content_type` / `attribute` | | **Access control** | Yes: API keys can be scoped to a workspace with a per-key role | No: not a permission boundary | No: not a permission boundary | Only [workspaces](/tutorials/workspaces) are a permission boundary: API keys can be scoped to specific workspaces, so reach for workspaces (not tags) when different data needs different permission levels. Tags are the right tool when a collection spans workspaces, for example a `Project Alpha` tag on files that live in both the Engineering and Design workspaces. You can combine `tag_id` with `workspace_id` to scope to a tagged collection within a single workspace. The rest of this tutorial covers tags. ## Step 1: Create a tag A tag has a `name` and a `description`. Create it once at the company level, then reuse it across as many files as you like. ```python Python SDK theme={null} from lighton import LightOn, Tag with LightOn() as client: # reads LIGHTON_API_KEY from the environment tag = Tag( name="Project Alpha", description="Documents related to the development and release of Project Alpha", ).create(client) print(tag.id, tag.name) ``` ```typescript TypeScript SDK theme={null} import { LightOn, Tag } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment const tag = await new Tag({ name: "Project Alpha", description: "Documents related to the development and release of Project Alpha", }).create(client) console.log(tag.id, tag.name) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/tags", headers=headers, json={ "name": "Project Alpha", "description": "Documents related to the development and release of Project Alpha", }, ) print(response.json()) ``` A `201` returns the new tag with its `id`. Note it down: you'll use the ID everywhere else. ```json theme={null} { "id": 7, "name": "Project Alpha", "description": "Documents related to the development and release of Project Alpha", "auto_assign": true, "document_count": 0 } ``` **`auto_assign`** controls who can attach the tag. It defaults to `true`, meaning LightOn's AI can assign the tag to matching documents automatically (your `description` is the signal it matches on, so write it well). Set it to `false` for a manual-only tag that the system never touches. ## Step 2: Find a tag's ID Tags are company-scoped, so a teammate may already have created the one you need. List them, optionally filtering by name, to grab the ID. ```python Python SDK theme={null} from lighton import LightOn, Tag with LightOn() as client: # Find a tag by name (case-insensitive partial match) to get its ID. # Tag.list follows pagination to the end; omit `name` to list them all. for tag in Tag.list(client, name="alpha"): print(tag.id, tag.name, tag.document_count) ``` ```typescript TypeScript SDK theme={null} import { LightOn, Tag } from "@lighton-ai/sdk" const client = new LightOn() // Find a tag by name (case-insensitive partial match) to get its ID. // Tag.list follows pagination to the end; omit `filters` to list them all. for (const tag of await Tag.list(client, { filters: { name: "alpha" } })) { console.log(tag.id, tag.name, tag.documentCount) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Find a tag by name (case-insensitive partial match) to get its ID response = requests.get( "https://api.lighton.ai/api/v3/tags", headers=headers, params={"name": "alpha"}, ) print(response.json()) ``` The response is paginated. Each tag carries a `document_count` so you can see how populated a collection is. ```json theme={null} { "count": 1, "results": [ { "id": 7, "name": "Project Alpha", "auto_assign": true, "document_count": 12 } ] } ``` ## Step 3: Assign tags to files You have three ways to attach tags, depending on timing: * **At upload time:** pass `tags` in the upload payload. See [Uploading & managing files](/tutorials/files#organising-documents-with-tags-and-titles). * **Add to an existing file:** `POST /api/v3/files/{id}/tags`. Adds tags without disturbing the ones already there. * **Replace every tag:** [`PATCH /api/v3/files/{id}`](/api-reference/files/update-file-metadata-or-replace-its-file) with a new `tags` array. This overwrites *all* existing tags, manual and auto-assigned alike. For incremental tagging, adding is what you want. ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID with LightOn() as client: doc = File.get(client, file_id) # Adds these tags without touching tags already on the file. # tag() also takes Tag objects or names: doc.tag(["Project Alpha"]) doc.tag([3, 7]) # tag IDs from Tag.list(client) print(f"tagged file {doc.id}") ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const client = new LightOn() const doc = await File.get(client, fileId) // Adds these tags without touching tags already on the file. // tag() also takes Tag objects or names: doc.tag(["Project Alpha"]) await doc.tag([3, 7]) // tag IDs from Tag.list(client) console.log(`tagged file ${doc.id}`) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} file_id = 1234 # replace with your file ID # Adds these tags without touching tags already on the file response = requests.post( f"https://api.lighton.ai/api/v3/files/{file_id}/tags", headers=headers, json={"tags": [3, 7]}, # tag IDs from GET /api/v3/tags ) print(response.json()) ``` This returns the full file object with its updated `tags` array. Duplicate tags are ignored (no error), and newly added tags are marked `auto_assigned: false`. ## Step 4: List files in a collection Once files are tagged, filter the file listing by `tag_id` to retrieve a collection. ```python Python SDK theme={null} from lighton import File, LightOn with LightOn() as client: # All files carrying tag 7, most recent first for doc in File.list(client, tag_id=7, ordering="-created_at"): print(doc.id, doc.title, doc.status) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const client = new LightOn() // All files carrying tag 7, most recent first for (const doc of await File.list(client, { filters: { tag_id: "7", ordering: "-created_at" } })) { console.log(doc.id, doc.title, doc.status) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # All files carrying tag 7, most recent first response = requests.get( "https://api.lighton.ai/api/v3/files", headers=headers, params={"tag_id": "7", "ordering": "-created_at"}, ) print(response.json()) ``` You get back a paginated list of files carrying that tag. Combine `tag_id` with other filters (`extension`, `workspace_id`, `total_pages_min`) to narrow further. ## Step 5: Scope search and ask to a collection This is where tags earn their keep. Both [`POST /api/v3/search`](/tutorials/search) and [`POST /api/v3/ask`](/tutorials/ask) accept a `tag_id` array that restricts the query to documents in those collections. Pass several IDs to widen the scope to a union of collections. Search across a collection: ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # Search only within the collection tagged 7 (pass several IDs to widen it). # tags= also takes Tag objects or names: tags=["Project Alpha"] response = client.search("rollout timeline and milestones", tags=[7]) for result in response.results: print(result.score, result.source.filename, result.content) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // Search only within the collection tagged 7 (pass several IDs to widen it). // tags also takes Tag objects or names: tags: ["Project Alpha"] const response = await client.search("rollout timeline and milestones", { tags: [7] }) for (const result of response.results) { console.log(result.score, result.source.filename, result.content) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Search only within the collection tagged 7 (combine several IDs to widen it) response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json={ "query": "rollout timeline and milestones", "tag_id": [7], }, ) print(response.json()) ``` Ask a grounded question over a collection: ```python Python SDK theme={null} from lighton import LightOn with LightOn() as client: # Grounded answer over only the documents tagged 7 response = client.ask("What is the launch date for Project Alpha?", tags=[7]) print(response.answer) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const client = new LightOn() // Grounded answer over only the documents tagged 7 const response = await client.ask("What is the launch date for Project Alpha?", { tags: [7] }) console.log(response.answer) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Grounded answer over only the documents tagged 7 response = requests.post( "https://api.lighton.ai/api/v3/ask", headers=headers, json={ "query": "What is the launch date for Project Alpha?", "tag_id": [7], }, ) print(response.json()) ``` `tag_id` combines with `workspace_id` (both narrow the corpus together), but is mutually exclusive with `file_id`. ## Step 6: Remove a tag To take a single file out of a collection, delete that one association. The file and the tag both survive; only the link is removed. ```python Python SDK theme={null} from lighton import File, LightOn file_id = 1234 # replace with your file ID tag_id = 7 # replace with the tag to remove with LightOn() as client: doc = File.get(client, file_id) # Idempotent: no error if the tag was not on the file doc.untag([tag_id]) print(f"removed tag {tag_id} from file {doc.id}") ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const fileId = 1234 // replace with your file ID const tagId = 7 // replace with the tag to remove const client = new LightOn() const doc = await File.get(client, fileId) // Idempotent: no error if the tag was not on the file await doc.untag([tagId]) console.log(`removed tag ${tagId} from file ${doc.id}`) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} file_id = 1234 # replace with your file ID tag_id = 7 # replace with the tag to remove # Idempotent: returns 204 even if the tag was not on the file response = requests.delete( f"https://api.lighton.ai/api/v3/files/{file_id}/tags/{tag_id}", headers=headers, ) print(response.status_code) ``` A `204` confirms success. The call is idempotent: you get `204` even if the tag wasn't on the file. To retire a tag everywhere, [`DELETE /api/v3/tags/{id}`](/api-reference/tags/delete-a-company-tag). That removes the tag from every document it was attached to, so use it deliberately. ## Next steps Assign tags at upload time and manage file metadata Rank passages, then scope the search by tag Get grounded answers over a tagged collection Step up to typed, structured metadata with facets # Organizing documents with workspaces Source: https://developers.lighton.ai/tutorials/workspaces Partition your corpus into isolated containers per team, customer, or tenant, then scope search and ask to a workspace. A workspace is a container for documents. Every file lives in exactly one workspace, set at upload time. Workspaces are the top-level partition of your corpus: one per team, customer, or tenant. They're the natural fit for multi-tenant products, where each customer's documents must stay isolated from everyone else's. > *"Search only this customer's documents."* > > *"Answer using just the engineering team's files."* Where [tags](/tutorials/tags) and [facets](/tutorials/facets/overview) layer labels and metadata *across* your corpus, workspaces draw hard boundaries *through* it. The workspace endpoints are in **alpha** and may change. Creating and managing workspaces from the API requires the appropriate role; some companies provision workspaces through the console instead. You can always upload into and search existing workspaces by `workspace_id`. This tutorial covers [`POST /api/v3/workspaces`](/api-reference/workspaces/create-a-workspace), [`GET /api/v3/workspaces`](/api-reference/workspaces/list-workspaces), [`PATCH /api/v3/workspaces/{id}`](/api-reference/workspaces/update-a-workspace), and [`DELETE /api/v3/workspaces/{id}`](/api-reference/workspaces/delete-a-workspace). Uploading files into a workspace is covered in [Uploading & managing files](/tutorials/files). ## Workspaces, tags, or facets? All three organise documents, but at different layers. They compose: a file lives in one workspace, can carry several tags, and can be classified with facets. | | Workspaces | Tags | Facets | | - | - | - | - | | **What it is** | A container; every file lives in exactly one | Flat, reusable labels | Typed, hierarchical metadata with a schema | | **A file belongs to** | exactly one workspace | many tags, **across workspaces** | many content types, **across workspaces** | | **Best for** | Isolating teams, customers, tenants | Cross-cutting collections (project, topic) | Precise structured queries | | **Setup cost** | Create a workspace | Create a tag | Design a content-type tree | | **Scope a query with** | `workspace_id` | `tag_id` | `content_type` / `attribute` | | **Access control** | Yes: API keys can be scoped to a workspace with a per-key role | No: not a permission boundary | No: not a permission boundary | Only workspaces are a permission boundary: you can issue [API keys scoped to specific workspaces](#segmenting-access-with-scoped-api-keys), which makes them the right tool for segmenting data that needs different permission levels. `workspace_id` and `tag_id` can be combined in a search or ask to narrow within a workspace; `file_id` is mutually exclusive with both. The rest of this tutorial covers workspaces. ## Step 1: Create a workspace A workspace needs a `name`. The caller is automatically added as its owner. ```python Python SDK theme={null} from lighton import LightOn, Workspace with LightOn() as client: # reads LIGHTON_API_KEY from the environment workspace = Workspace( name="Engineering Team", description="Documents owned by the engineering org", ).create(client) print(workspace.id, workspace.name) ``` ```typescript TypeScript SDK theme={null} import { LightOn, Workspace } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment const workspace = await new Workspace({ name: "Engineering Team", description: "Documents owned by the engineering org", }).create(client) console.log(workspace.id, workspace.name) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.post( "https://api.lighton.ai/api/v3/workspaces", headers=headers, json={ "name": "Engineering Team", "description": "Documents owned by the engineering org", }, ) print(response.json()) ``` A `201` returns the new workspace with its `id`. You'll pass that ID when uploading files and when scoping queries. ```json theme={null} { "id": 42, "name": "Engineering Team", "workspace_type": "shared", "document_upload_method": "manual", "description": "Documents owned by the engineering org", "files_count": 0, "user_role": "owner" } ``` A `403` means workspace creation is disabled for your company and you're not an admin: create it from the console instead, or ask an admin. ## Step 2: Find a workspace's ID List the workspaces you're a member of to grab an ID. Filter by `name`, or by `workspace_type`, `user_role`, `datasource_type`, and `document_upload_method`. Results come back ordered by `name` ascending; pass `ordering` to sort by `created_at` or `updated_at` instead, prefixed with `-` for descending. ```python Python SDK theme={null} from lighton import LightOn, Workspace with LightOn() as client: # Workspaces you're a member of, optionally filtered by name. # Workspace.list follows pagination to the end. for workspace in Workspace.list(client, name="engineering"): print(workspace.id, workspace.name) ``` ```typescript TypeScript SDK theme={null} import { LightOn, Workspace } from "@lighton-ai/sdk" const client = new LightOn() // Workspaces you're a member of, optionally filtered by name. // Workspace.list follows pagination to the end. for (const workspace of await Workspace.list(client, { filters: { name: "engineering" } })) { console.log(workspace.id, workspace.name) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Workspaces you're a member of, optionally filtered by name response = requests.get( "https://api.lighton.ai/api/v3/workspaces", headers=headers, params={"name": "engineering"}, ) print(response.json()) ``` The response is paginated. Each workspace carries a `files_count` and your `user_role` (`owner`, `editor`, or `viewer`). ```json theme={null} { "count": 1, "results": [ { "id": 42, "name": "Engineering Team", "workspace_type": "shared", "files_count": 128, "user_role": "owner" } ] } ``` ## Step 3: Put files in a workspace A file is assigned to its workspace at upload time: pass `workspace_id` in the upload payload. A file's workspace is fixed; there's no "move file" operation. See [Uploading & managing files](/tutorials/files#upload-a-file) for the full upload flow. To pull back every file in a workspace, filter the file listing: ```python Python SDK theme={null} from lighton import File, LightOn workspace_id = 42 # replace with your workspace ID with LightOn() as client: # Every file in a workspace for doc in File.list(client, workspace_id=workspace_id): print(doc.id, doc.title, doc.status) ``` ```typescript TypeScript SDK theme={null} import { File, LightOn } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // Every file in a workspace for (const doc of await File.list(client, { workspaceId })) { console.log(doc.id, doc.title, doc.status) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Every file in a workspace response = requests.get( "https://api.lighton.ai/api/v3/files", headers=headers, params={"workspace_id": "42"}, ) print(response.json()) ``` To populate a workspace automatically from Google Drive, SharePoint, ServiceNow, an SMB file share, or a web scraper, convert it to a **synced** workspace with a `datasource` payload on [`PATCH /api/v3/workspaces/{id}`](/api-reference/workspaces/update-a-workspace). The workspace must be empty first. ## Step 4: Scope search and ask to a workspace Both [`POST /api/v3/search`](/tutorials/search) and [`POST /api/v3/ask`](/tutorials/ask) accept a `workspace_id` array that restricts the query to those workspaces. Pass several IDs to query across a set of them. Search within a workspace: ```python Python SDK theme={null} from lighton import LightOn workspace_id = 42 # replace with your workspace ID with LightOn() as client: # Search only within these workspaces (pass several IDs to widen the scope) response = client.search("deployment runbook", workspaces=[workspace_id]) for result in response.results: print(result.score, result.source.filename, result.content) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // Search only within these workspaces (pass several IDs to widen the scope) const response = await client.search("deployment runbook", { workspaces: [workspaceId] }) for (const result of response.results) { console.log(result.score, result.source.filename, result.content) } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Search only within these workspaces (pass several IDs to widen the scope) response = requests.post( "https://api.lighton.ai/api/v3/search", headers=headers, json={ "query": "deployment runbook", "workspace_id": [42], }, ) print(response.json()) ``` Ask a grounded question within a workspace: ```python Python SDK theme={null} from lighton import LightOn workspace_id = 42 # replace with your workspace ID with LightOn() as client: # Grounded answer over only the documents in these workspaces response = client.ask( "What is our incident escalation policy?", workspaces=[workspace_id] ) print(response.answer) ``` ```typescript TypeScript SDK theme={null} import { LightOn } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // Grounded answer over only the documents in these workspaces const response = await client.ask("What is our incident escalation policy?", { workspaces: [workspaceId], }) console.log(response.answer) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Grounded answer over only the documents in these workspaces response = requests.post( "https://api.lighton.ai/api/v3/ask", headers=headers, json={ "query": "What is our incident escalation policy?", "workspace_id": [42], }, ) print(response.json()) ``` Combine `workspace_id` with `tag_id` to scope to a tagged collection *within* a workspace. ## Step 5: Rename or delete a workspace Only **custom** workspaces can be updated, and only by their owner. Personal and company workspaces are managed by the platform. ```python Python SDK theme={null} from lighton import LightOn, Workspace workspace_id = 42 # replace with your workspace ID with LightOn() as client: # Only CUSTOM workspaces can be updated, and only by their owner workspace = Workspace.get(client, workspace_id) workspace.name = "Engineering (EMEA)" workspace.save() print(workspace.id, workspace.name) ``` ```typescript TypeScript SDK theme={null} import { LightOn, Workspace } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // Only CUSTOM workspaces can be updated, and only by their owner const workspace = await Workspace.get(client, workspaceId) workspace.name = "Engineering (EMEA)" await workspace.save() console.log(workspace.id, workspace.name) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} workspace_id = 42 # replace with your workspace ID # Only CUSTOM workspaces can be updated, and only by their owner response = requests.patch( f"https://api.lighton.ai/api/v3/workspaces/{workspace_id}", headers=headers, json={"name": "Engineering (EMEA)"}, ) print(response.json()) ``` Deleting a workspace removes it and every document inside it, so use it deliberately. ```python Python SDK theme={null} from lighton import LightOn, Workspace workspace_id = 42 # replace with your workspace ID with LightOn() as client: # Deletes the workspace and every document it contains Workspace.get(client, workspace_id).delete() print(f"deleted workspace {workspace_id}") ``` ```typescript TypeScript SDK theme={null} import { LightOn, Workspace } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // Deletes the workspace and every document it contains const workspace = await Workspace.get(client, workspaceId) await workspace.delete() console.log(`deleted workspace ${workspaceId}`) ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} workspace_id = 42 # replace with your workspace ID # Deletes the workspace and every document it contains response = requests.delete( f"https://api.lighton.ai/api/v3/workspaces/{workspace_id}", headers=headers, ) print(response.status_code) ``` A `204` confirms the workspace and its contents are gone. ## Checking classification coverage If you classify documents with [facets](/tutorials/facets/overview), the workspace listing tells you how much of each workspace is actually classified, without listing a single file. Every workspace carries a `taxonomy` object with a `classified_files_rate` (0 to 1) and a per-root document count: ```python Python SDK theme={null} from lighton import LightOn, Workspace with LightOn() as client: # reads LIGHTON_API_KEY from the environment # Only list() carries taxonomy; the detail endpoint omits it. for workspace in Workspace.list(client): if workspace.taxonomy is None: continue # workspace has no documents yet rate = workspace.taxonomy.classified_files_rate print(f"{workspace.name}: {rate:.0%} classified") for root in workspace.taxonomy.root_content_types: print(f" {root.label}: {root.count} documents") ``` ```typescript TypeScript SDK theme={null} import { LightOn, Workspace } from "@lighton-ai/sdk" const client = new LightOn() // reads LIGHTON_API_KEY from the environment // Only list() carries taxonomy; the detail endpoint omits it. for (const workspace of await Workspace.list(client)) { if (!workspace.taxonomy) continue // workspace has no documents yet const rate = workspace.taxonomy.classifiedFilesRate console.log(`${workspace.name}: ${Math.round(rate * 100)}% classified`) for (const root of workspace.taxonomy.rootContentTypes) { console.log(` ${root.label}: ${root.count} documents`) } } ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} response = requests.get( "https://api.lighton.ai/api/v3/workspaces", headers=headers, ) for workspace in response.json()["results"]: taxonomy = workspace.get("taxonomy") if not taxonomy: continue # workspace has no documents yet print(f"{workspace['name']}: {taxonomy['classified_files_rate']:.0%} classified") for root in taxonomy["root_content_types"]: print(f" {root['label']}: {root['count']} documents") ``` ```json theme={null} { "id": 42, "name": "Engineering Team", "files_count": 128, "taxonomy": { "classified_files_rate": 0.87, "root_content_types": [ {"path": "legal", "label": "Legal", "count": 74}, {"path": "finance", "label": "Finance", "count": 38} ] } } ``` A workspace whose documents are all unclassified reports `0.0` with an empty `root_content_types`. `taxonomy` is `null` only when the workspace has no documents at all. Only the list endpoint returns `taxonomy`. [`GET /api/v3/workspaces/{id}`](/api-reference/workspaces/retrieve-a-workspace) omits the key, so fetch a single workspace and you won't see it. ## Segmenting access with scoped API keys Workspaces aren't only an organisational boundary, they're a **permission boundary**. You can mint an API key that is scoped to specific workspaces, so the key can only read or write the documents in those workspaces and nothing else. This is what makes workspaces the right tool when different data needs different permission levels. Pass a `scopes` list to [`POST /api/v3/keys`](/api-reference/api-keys/create-api-key). Each entry pairs a `workspace_id` with a `role`, one of `viewer`, `editor`, or `owner`. The requested role on a workspace is capped at the role you currently hold there, so you can't hand out more access than you have. ```python Python SDK theme={null} from lighton import ApiKey, ApiKeyScope, LightOn, Role workspace_id = 42 # replace with your workspace ID with LightOn() as client: # Scope the key to a workspace with a per-workspace role (viewer | editor | owner). # The requested role is capped at the role you hold on that workspace. # Omit `scopes` for an unscoped key, or set `expires_at` to auto-expire it. key = ApiKey( name="Acme tenant key (read-only)", scopes=[ApiKeyScope(workspace_id=workspace_id, role=Role.viewer)], ).create(client) print(key.key.get_secret_value()) # the secret is returned only once ``` ```typescript TypeScript SDK theme={null} import { ApiKey, LightOn, Role } from "@lighton-ai/sdk" const workspaceId = 42 // replace with your workspace ID const client = new LightOn() // Scope the key to a workspace with a per-workspace role (viewer | editor | owner). // The requested role is capped at the role you hold on that workspace. // Omit `scopes` for an unscoped key, or set `expiresAt` to auto-expire it. const key = await new ApiKey({ name: "Acme tenant key (read-only)", scopes: [{ workspaceId, role: Role.viewer }], }).create(client) console.log(key.key) // the secret is returned only once ``` ```python Plain Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"} # Scope the key to a workspace with a per-workspace role (viewer | editor | owner). # The requested role is capped at the role you hold on that workspace. response = requests.post( "https://api.lighton.ai/api/v3/keys", headers=headers, json={ "name": "Acme tenant key (read-only)", "expires_at": None, # or an ISO 8601 datetime to auto-expire "scopes": [{"workspace_id": 42, "role": "viewer"}], }, ) print(response.json()) # the full `key` value is returned only once ``` The `key` value is returned **only once**, on creation, so store it immediately. A few patterns this unlocks: * **Multi-tenant isolation:** give each customer a key scoped to their own workspace. A request made with that key can never reach another tenant's documents, even if your application code has a bug. * **Read-only integrations:** issue a `viewer`-scoped key to a dashboard or analytics job that should search and ask but never upload or delete. * **Per-environment separation:** scope staging and production keys to different workspaces so a misconfigured job can't touch the wrong corpus. Because a single key can list several scopes with **different roles per workspace**, you can grant, say, `editor` on one workspace and `viewer` on another with one key. An unscoped key, by contrast, inherits the full permissions of its owner across every workspace, so prefer scoped keys whenever a credential is handed to a narrower-trust consumer. Tags and facets don't carry their own permissions: a key that can see a workspace can see every tag and facet on the files inside it. When access control is the goal, segment with workspaces. ## Next steps Upload documents into a workspace and manage file metadata Group files into collections that cut across workspaces Rank passages, then scope the search by workspace Get grounded answers over a workspace # Authentication Source: https://developers.lighton.ai/untitled-page Step-by-step guides for configuring different SSO authentication protocols Different SSO protocols are configurable in the administration. The currently supported ones are: Upcoming guide Upcoming guide # Use Cases Source: https://developers.lighton.ai/use-cases/index Real-world examples and patterns built on the LightOn API. Coming soon.