Skip to main content
This tutorial uses POST /api/v3/content-types and GET /api/v3/content-types. The full schema lives in the API reference.
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.
browse_templates.py
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:
adopt_templates.py
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.
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: 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.
define_root_content_type.py
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.
define_child_content_types.py
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

read_tree.py
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 for naming restrictions, type immutability, and other validation rules.

Shared attributes on contract (inherited by all subtypes)

define_shared_attributes.py
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)

define_nda_attributes.py
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

define_sa_attributes.py
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.

Full script: build the contract classification tree in one batch call

All schema operations can be batched in a single request by sending a JSON array. LightOn processes them in order and fails fast on the first error. This is the recommended approach for onboarding automations.
batch_tree.py
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"},
        {"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 list 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

verify_tree.py
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

ActionWhat it does
define_content_typeCreate or update a content type node (idempotent)
undefine_content_typeDelete a node, its subtree, and all file classifications + values under it
define_attributeCreate or update an attribute on a content type (idempotent)
undefine_attributeDelete an attribute and all its stored values across all files