Skip to main content

Document Index

This workflow shows how to navigate a document's structure and retrieve verbatim text from specific provisions — without pulling the entire document.

Overview

1. Get a document → POST /documents/list (or upload one first)
2. Find relevant nodes → POST /clauses/search
3. Inspect the index → GET /documents/{id}/index?mode=structure
4. Pull verbatim text → POST /clauses/batch

All requests require x-user-api-key, x-workspace-id, and x-app-name headers. See Authentication.


Step 1 — Get a document

If you haven't uploaded a document yet, follow the Document Management workflow first.

To list documents already in your workspace:

API Reference: POST /documents/list

POST /documents/list
Content-Type: application/json
{
"limit": 25,
"offset": 0,
"orderBy": [{ "field": "created", "direction": "DESC" }]
}

Pick the documentId of the document you want to index.


Use clause search to semantically search for clauses across your workspace. This is useful for identifying the clauseId values — which correspond to node IDs in the index — before fetching full text.

API Reference: POST /clauses/search

POST /clauses/search
Content-Type: application/json

Request body:

This example searches for clauses about "funds", restricted to the top level of a document (depth ≤ 1):

{
"filter": {
"conditions": {
"conjunction": "AND",
"nodes": [
{ "rule": { "field": "primaryText", "operator": "EQUAL", "value": ["funds"] } },
{ "rule": { "field": "depth", "operator": "LESS_THAN_OR_EQUAL", "value": 1 } }
]
},
"disableExternalTextSearch": false,
"limit": 25,
"offset": 0,
"returnDepth": true
}
}
FieldRequiredDescription
filter.conditionsNoRule tree used to filter clauses. Each node is { "rule": { field, operator, value } }, combined with conjunction: "AND" | "OR". Omit to search across all clauses.
filter.disableExternalTextSearchNoWhen false (default), primaryText rules run a semantic search via an external search service; when true, matching falls back to Postgres text similarity.
filter.limitNoMax clauses to return. Defaults to 10.
filter.offsetNoNumber of clauses to skip, for pagination. Defaults to 0.
filter.returnDepthNoWhen true, includes each clause's nesting depth in the response.

primaryText is special: the operator is ignored — it always runs a text/semantic search rather than a literal comparison — and value accepts a single string or an array of strings. See the API reference for the full list of filterable fields and operators.

Response (200):

{
"count": 3,
"clauses": [
{
"clauseId": "98001",
"documentId": "10567",
"title": "Use of Funds",
"text": "The borrower shall apply the funds solely for...",
"depth": 1,
"similarity": 0.87,
"textBlockId": "70318",
"date": "2024-03-01",
"endorsed": false
}
]
}

Key fields to note:

FieldDescription
clauseIdThe node ID used in document index requests
documentIdWhich document this clause belongs to
titleClause heading
textClause text as extracted
depthNesting depth (1 = top-level section); only present when filter.returnDepth is true
similarityRelevance score for the primaryText search query

Step 3 — Inspect the document index

The document index endpoint has two modes. Use structure first to get a full navigation map of a document without fetching any text.

API Reference: GET /documents/{id}/index

structure mode — navigation map

Returns the section tree: node IDs, titles, clause references, snippets, keywords, and cross-references. No full verbatim text is included.

GET /documents/{id}/index?mode=structure

Optional query parameters:

ParameterDescription
depthLimit results to nodes at this depth or shallower (integer > 0)
includeDocumentMetadataWhen true, adds a documentTags object (tags grouped by category)

Response (200):

{
"spec": "syntheia/v1-draft",
"documentId": "10567",
"documentTitle": "Acme NDA 2024",
"extraction_mode": "acceptedOnly",
"documentIndex": [
{
"nodeId": "98001",
"title": "Payment Terms",
"clauseReference": "4",
"snippet": "Payment shall be made within 30 days...",
"crossReferencedIds": ["98045"],
"keywords": ["Payment"],
"children": [
{
"nodeId": "98002",
"title": "Late Payment",
"clauseReference": "4.1",
"snippet": "Any payment not received within the due date...",
"crossReferencedIds": [],
"keywords": [],
"children": []
}
]
}
]
}

Use the nodeId values from documentIndex to target specific nodes in the next step.

xrefs mode — cross-references

Returns cross-reference IDs for specified nodes — useful for tracing how provisions refer to each other — without fetching their text.

GET /documents/{id}/index?mode=xrefs&clauseIds=98001,98002

Response (200):

{
"spec": "syntheia/v1-draft",
"documentId": "10567",
"documentTitle": "Acme NDA 2024",
"extraction_mode": "acceptedOnly",
"nodes": [
{ "nodeId": "98001", "crossReferencedIds": ["98045", "98067"] }
]
}

Step 4 — Pull verbatim text

Once you have the clauseId values you need, call POST /clauses/batch to fetch verbatim text for up to 500 clauses at once — across any documents in the workspace.

API Reference: POST /clauses/batch

POST /clauses/batch
Content-Type: application/json

Request body:

{
"clauseIds": ["98001", "98002"]
}
FieldRequiredDescription
clauseIdsYesArray of clause IDs to fetch text for (max 500)

Response (200):

{
"clauses": [
{
"clauseId": "98001",
"text": "Payment shall be made within 30 days of the invoice date..."
},
{
"clauseId": "98002",
"text": "Any payment not received within the due date shall incur..."
}
]
}

Returns one entry per requested clauseId. When a clause has no direct text, its descendants' text is joined together. text is null when neither the clause nor any descendant has text.


End-to-end example (TypeScript)

Map a document with structure, then pull verbatim text for the top-level nodes with POST /clauses/batch.

const BASE = 'https://api.your-domain.com';
const HEADERS = {
'x-user-api-key': process.env.SYNTHEIA_API_KEY!,
'x-workspace-id': process.env.SYNTHEIA_WORKSPACE_ID!,
'x-app-name': 'my-app',
'Content-Type': 'application/json',
};

type IndexNode = { nodeId: string; title: string | null; children: IndexNode[] };

async function getStructure(docId: string, depth = 3): Promise<{ documentIndex: IndexNode[] }> {
const res = await fetch(`${BASE}/documents/${docId}/index?mode=structure&depth=${depth}`, {
headers: HEADERS,
});
return res.json();
}

async function batchFetch(clauseIds: string[]): Promise<{ clauses: { clauseId: string; text: string | null }[] }> {
const res = await fetch(`${BASE}/clauses/batch`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ clauseIds }),
});
return res.json();
}

// Map the document, then fetch text for the first 5 top-level nodes
const { documentIndex } = await getStructure('10567');
const topClauseIds = documentIndex.slice(0, 5).map((n) => n.nodeId);
const { clauses } = await batchFetch(topClauseIds);
console.log(clauses);

Next steps