Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.stru.ai/llms.txt

Use this file to discover all available pages before exploring further.

Analyze construction drawings with AI. Extract annotations, build knowledge graphs, and search across entire drawing sets.
from struai import StruAI

client = StruAI(api_key="YOUR_API_KEY")
result = client.drawings.analyze("structural.pdf", page=4)
print(result.annotations.leaders)
Built for: AEC firms, software integrators, document management platforms, and startups building on drawing intelligence.

Get Started

1

Install the SDK

pip install struai
Requires Python 3.9+
2

Set your API key

Get an API key from app.stru.ai, then set it as an environment variable:
export STRUAI_API_KEY="YOUR_API_KEY"
3

Analyze your first drawing

import os
from struai import StruAI

client = StruAI(api_key=os.environ["STRUAI_API_KEY"])
result = client.drawings.analyze("drawing.pdf", page=1)

# Access detected annotations
for leader in result.annotations.leaders:
    print(f"Found: {leader.texts_inside}")

Capabilities

Fast geometric detection returning results in 1-2 seconds. No LLM processing, no graph storage.Detects: Leaders, Section Tags, Detail Tags, Revision Triangles, Revision Clouds, Title Block bounds.
result = client.drawings.analyze("structural.pdf", page=4)

# File hash caching skips re-uploads
from struai import compute_file_hash
file_hash = compute_file_hash("structural.pdf")

# Check cache before uploading
cached = client.drawings.check_cache(file_hash)
Price: $0.02/page

Use Cases

Cross-Reference Indexing

Automatically link section tags, detail callouts, and sheet references across a drawing set

QA/QC Automation

Validate drawing consistency, detect missing references, flag revision conflicts

Quantity Takeoffs

Extract and count components, connections, and annotations

Analytical Model Generation

Build structured data for BIM/analysis workflows

API Reference

Authentication

All requests require a Bearer token:
from struai import StruAI
client = StruAI(api_key="YOUR_API_KEY")

# Or use environment variable
client = StruAI()  # Uses STRUAI_API_KEY

Tier 1: Raw Detection

Fast geometric detection. No LLM, no graph storage. Returns annotations in 1-2 seconds. Price: $0.02/page

GET /v1/drawings/cache/

Check whether a PDF file hash exists in the drawing cache.
curl https://api.stru.ai/v1/drawings/cache/{file_hash} \
  -H "Authorization: Bearer $STRUAI_API_KEY"
Returns cache metadata when the file hash is found.

POST /v1/drawings

Submit a PDF page for annotation detection.
FieldTypeRequiredDescription
filefileOne of file or file_hashPDF file (max 50MB)
file_hashstringOne of file or file_hashHash of a previously uploaded PDF
pageintegerYesPage number (1-indexed)
result = client.drawings.analyze("structural.pdf", page=4)

# Access annotations
print(result.annotations.leaders)
print(result.annotations.section_tags)
print(result.annotations.detail_tags)
{
  "id": "drw_7f8a9b2c",
  "page": 4,
  "dimensions": {"width": 2592, "height": 1728},
  "processing_ms": 1250,
  "annotations": {
    "leaders": [
      {
        "id": "ldr_001",
        "bbox": [1200, 450, 1400, 520],
        "arrow_tip": [1200, 485],
        "text_bbox": [1280, 450, 1400, 520],
        "texts_inside": [{"id": 45, "text": "W12x26"}]
      }
    ],
    "section_tags": [
      {
        "id": "sec_001",
        "bbox": [800, 600, 850, 650],
        "circle": {"center": [825, 625], "radius": 22},
        "direction": "right",
        "texts_inside": [{"id": 12, "text": "A"}, {"id": 13, "text": "S1.5"}],
        "section_line": {"start": [200, 625], "end": [800, 625]}
      }
    ],
    "detail_tags": [
      {
        "id": "det_001",
        "bbox": [1500, 900, 1550, 950],
        "circle": {"center": [1525, 925], "radius": 22},
        "texts_inside": [{"id": 78, "text": "3"}, {"id": 79, "text": "S1.6"}],
        "has_dashed_bbox": true
      }
    ],
    "revision_triangles": [
      {
        "id": "tri_001",
        "bbox": [600, 300, 620, 330],
        "vertices": [[610, 300], [600, 330], [620, 330]],
        "text": "B"
      }
    ],
    "revision_clouds": [
      {
        "id": "cld_001",
        "bbox": [400, 200, 700, 450]
      }
    ]
  },
  "titleblock": {
    "bounds": [2100, 50, 2550, 1700],
    "viewport": [50, 50, 2100, 1700]
  }
}

GET /v1/drawings/

Retrieve a previously processed drawing.
result = client.drawings.get("drw_7f8a9b2c")

DELETE /v1/drawings/

Delete a drawing result.
client.drawings.delete("drw_7f8a9b2c")

Full pipeline: detection → LLM enrichment → knowledge graph → semantic search. What you get:
  • Entities with semantic descriptions
  • Relationships between entities
  • Cross-sheet reference linking
  • Natural language search
OperationPrice
Graph Ingestion$0.15/page
Search$0.005/query

POST /v1/projects

Create a project to group related sheets.
project = client.projects.create(
    name="Building A Structural",
    description="96-page structural drawing set"
)
{
  "id": "proj_abc123",
  "name": "Building A Structural",
  "description": "96-page structural drawing set",
  "created_at": "2026-01-29T10:00:00Z"
}

GET /v1/projects

List all projects.
{
  "projects": [
    {
      "id": "proj_abc123",
      "name": "Building A Structural",
      "description": "96-page structural drawing set",
      "created_at": "2026-01-29T10:00:00Z"
    }
  ]
}

GET /v1/projects/

Get project details and aggregate stats.
{
  "id": "proj_abc123",
  "name": "Building A Structural",
  "description": "96-page structural drawing set",
  "created_at": "2026-01-29T10:00:00Z",
  "sheet_count": 12,
  "entity_count": 847,
  "rel_count": 392,
  "community_count": 8
}

DELETE /v1/projects/

Delete project and all associated graph data.
{"deleted": true, "id": "proj_abc123"}

Sheets

POST /v1/projects//sheets

Ingest one or more PDF pages into the knowledge graph. Returns immediately with job IDs for polling.
FieldTypeRequiredDescription
filefileOne of file or file_hashPDF file
file_hashstringOne of file or file_hashHash of a previously uploaded PDF
pagestringYesPage selector (see formats below)
source_descriptionstringNoDescription of the source document
on_sheet_existsstringNoBehavior when sheet already exists: error, skip (default), or rebuild
community_update_modestringNoincremental or rebuild
semantic_index_update_modestringNoincremental or rebuild
Page selector formats:
FormatExampleDescription
Single page12One specific page
Range1-5Inclusive page range
List / mixed1,3,8-10Comma-separated pages and ranges
All pagesallEvery page in the PDF
# Single page
job = project.sheets.add("structural.pdf", page="1")

# Range of pages
jobs = project.sheets.add("structural.pdf", page="1-10")

# Mixed selection
jobs = project.sheets.add("structural.pdf", page="1,3,8-10")

# All pages
jobs = project.sheets.add("structural.pdf", page="all")

# With options
jobs = project.sheets.add(
    "structural.pdf",
    page="1-5",
    on_sheet_exists="rebuild"
)
One job is created per page:
{
  "jobs": [
    {"job_id": "job_abc123def4", "page": 1},
    {"job_id": "job_abc123def5", "page": 2},
    {"job_id": "job_abc123def6", "page": 3}
  ]
}

GET /v1/projects//jobs/

Poll job status for async sheet ingestion. Each job progresses through a 5-step pipeline. Pipeline steps:
StepKeyDescription
1/5detect_annotationsDetect Annotations
2/5enrich_annotationsEnrich Annotations
3/5synthesize_remaining_textSynthesize Remaining Text
4/5resolve_entities_and_factsResolve Entities and Facts
5/5load_graph_and_indexLoad Graph and Index
Job statuses: queuedrunningcomplete | failed Timeout rules:
  • Queued timeout: 30 minutes from enqueue
  • Running timeout: 10 minutes from first start
{
  "job_id": "job_abc123def4",
  "status": "running",
  "created_at_utc": "2026-02-08T19:40:10.112Z",
  "started_at_utc": "2026-02-08T19:40:10.220Z",
  "completed_at_utc": null,
  "status_log": [
    {
      "seq": 1,
      "event": "queued",
      "status": "queued",
      "at_utc": "2026-02-08T19:40:10.112Z",
      "message": "Queued"
    },
    {
      "seq": 2,
      "event": "step_started",
      "status": "running",
      "at_utc": "2026-02-08T19:40:10.220Z",
      "step": {
        "key": "detect_annotations",
        "index": 1,
        "total": 5,
        "label": "Step 1/5: Detect Annotations"
      },
      "message": "Step 1/5: Detect Annotations started"
    },
    {
      "seq": 3,
      "event": "step_completed",
      "status": "running",
      "at_utc": "2026-02-08T19:40:14.031Z",
      "step": {
        "key": "detect_annotations",
        "index": 1,
        "total": 5,
        "label": "Step 1/5: Detect Annotations"
      },
      "message": "Step 1/5: Detect Annotations completed"
    },
    {
      "seq": 4,
      "event": "step_started",
      "status": "running",
      "at_utc": "2026-02-08T19:40:14.033Z",
      "step": {
        "key": "enrich_annotations",
        "index": 2,
        "total": 5,
        "label": "Step 2/5: Enrich Annotations"
      },
      "message": "Step 2/5: Enrich Annotations started"
    }
  ]
}
{
  "job_id": "job_abc123def4",
  "status": "complete",
  "created_at_utc": "2026-02-08T19:40:10.112Z",
  "started_at_utc": "2026-02-08T19:40:10.220Z",
  "completed_at_utc": "2026-02-08T19:41:45.800Z",
  "status_log": ["..."],
  "result": {
    "sheet_id": "S1.4",
    "entities_created": 87,
    "relationships_created": 42,
    "skipped": false,
    "sheet_exists_mode": "skip",
    "community_mode": "incremental",
    "semantic_index_mode": "incremental"
  }
}
{
  "job_id": "job_abc123def4",
  "status": "failed",
  "created_at_utc": "2026-02-08T19:40:10.112Z",
  "started_at_utc": "2026-02-08T19:40:10.220Z",
  "completed_at_utc": "2026-02-08T19:40:22.500Z",
  "status_log": ["..."],
  "error": {
    "code": "step_failed",
    "message": "Enrichment timed out"
  }
}

GET /v1/projects//sheets

List all ingested sheets in a project.
{
  "project_id": "proj_abc123",
  "sheets": [
    {
      "id": "S1.4",
      "sheet_uuid": "uuid_xxx",
      "title": "LEVEL 1 FOUNDATION PLAN",
      "revision": "A",
      "page": 12,
      "width": 2592,
      "height": 1728,
      "mention_count": 45,
      "component_instance_count": 23,
      "region_count": 6,
      "created_at": "2026-02-08T19:41:45.800Z"
    }
  ]
}

GET /v1/projects//sheets/

Get full sheet graph slice including regions, mentions, component instances, and references.
{
  "id": "S1.4",
  "sheet_uuid": "uuid_xxx",
  "title": "LEVEL 1 FOUNDATION PLAN",
  "regions": ["..."],
  "mentions": ["..."],
  "component_instances": ["..."],
  "references": ["..."]
}

GET /v1/projects//sheets//annotations

Get the raw annotation geometry from cached detection results for a processed sheet.
curl https://api.stru.ai/v1/projects/proj_xxx/sheets/S1.4/annotations \
  -H "Authorization: Bearer $STRUAI_API_KEY"
{
  "sheet_id": "S1.4",
  "page": 12,
  "dimensions": {"width": 2592, "height": 1728},
  "annotations": {
    "leaders": ["..."],
    "section_tags": ["..."],
    "detail_tags": ["..."],
    "revision_triangles": ["..."],
    "revision_clouds": ["..."]
  },
  "titleblock": {
    "bounds": [2100, 50, 2550, 1700],
    "viewport": [50, 50, 2100, 1700]
  }
}

DELETE /v1/projects//sheets/

Remove a sheet from the graph and run maintenance rebuilds.
{
  "deleted": true,
  "sheet_id": "S1.4",
  "cleanup": {
    "deleted_nodes": 87,
    "deleted_facts": 42,
    "deleted_references": 12
  },
  "maintenance": {
    "communities_rebuilt": 3,
    "index_updated": true
  }
}

POST /v1/projects//search

Hybrid retrieval combining Qdrant vector search, BM25 fulltext, and optional Neo4j graph context. $0.005/query
FieldTypeRequiredDescription
querystringYesSearch query
limitintegerNoResults per channel (1-100, default 10)
channelsarrayNoSubset of entities, facts, communities
include_graph_contextbooleanNoInclude graph neighborhood (default true)
/search returns relevance-ranked results, not exhaustive traversal. For deterministic full traversal, use the /entities and /relationships endpoints instead.
results = project.search(
    "W12x26 beam connections at grid A",
    limit=10,
    channels=["entities"],
    include_graph_context=True
)

for entity in results.entities:
    print(f"{entity.label}: {entity.score}")
{
  "entities": [
    {
      "id": "ent_abc123",
      "type": "component_instance",
      "label": "W12x26 Steel Beam",
      "description": "Wide flange beam spanning grid A to C",
      "sheet_id": "S1.4",
      "bbox": [450, 320, 1200, 380],
      "score": 0.94,
      "attributes": {},
      "graph_context": {
        "connected_entities": [
          {"id": "ent_def456", "type": "mention", "label": "Bolted Connection"},
          {"id": "ent_ghi789", "type": "mention", "label": "Grid A"}
        ],
        "relationships": [
          {"type": "CONNECTS_TO", "fact": "W12x26 beam connects to column at grid A"}
        ]
      }
    }
  ],
  "facts": [],
  "communities": [],
  "search_ms": 245
}

Entities & Relationships

GET /v1/projects//entities

Deterministic entity listing for full traversal and export.
FilterTypeDescription
sheet_idstringFilter by sheet
typestringEntity type (see below)
familystringComponent family
normalized_specstringNormalized specification
region_uuidstringFilter by region
region_labelstringFilter by region label
note_numberstringFilter by note number
limitintegerMax results (default 200)
Supported types: mention, component_instance, component_type, region, community The type filter also accepts mention subtypes (e.g., callout) via mention_type. For bbox-based traversal, the primary types are mention, component_instance, and region.
# All entities for a sheet
entities = project.entities.list(sheet_id="S1.4", limit=1000)

# Filter by type
mentions = project.entities.list(
    sheet_id="S1.4",
    type="mention",
    limit=1000
)

components = project.entities.list(
    sheet_id="S1.4",
    type="component_instance",
    limit=1000
)
{
  "project_id": "proj_abc123",
  "entities": [
    {
      "id": "ent_abc123",
      "label": "W12x26 Steel Beam",
      "type": "component_instance",
      "description": "Wide flange beam spanning grid A to C",
      "sheet_id": "S1.4",
      "bbox": [450, 320, 1200, 380],
      "attributes": {}
    }
  ]
}

GET /v1/projects//entities/

Get entity detail with relationships and location info.
ParameterTypeDescription
include_invalidbooleanInclude invalidated relationships (default false)
expand_targetbooleanExpand REFERENCES target sheet summaries (default false)
Returns entity record with attributes, provenance, outgoing relationships, incoming relationships, and locations.

GET /v1/projects//relationships

List facts and references as relationship rows.
FilterTypeDescription
sheet_idstringFilter by sheet
source_idstringFilter by source entity
target_idstringFilter by target entity
typestringRelationship type
include_invalidbooleanInclude invalidated (default false)
invalid_onlybooleanOnly invalidated relationships
orphan_onlybooleanOnly orphaned references
limitintegerMax results (default 200)
{
  "project_id": "proj_abc123",
  "relationships": [
    {
      "id": "rel_xxx",
      "type": "CONNECTS_TO",
      "fact": "W12x26 beam connects to column at grid A",
      "source_id": "ent_abc123",
      "target_id": "ent_def456",
      "sheet_id": "S1.4",
      "valid_at": "2026-02-08T19:41:45.800Z",
      "invalid_at": null,
      "target_sheet_id": null,
      "target_unresolved": false
    }
  ]
}

Traverse Guide

Use this sequence for complete JSON traversal (not ranked results):
1

List projects

GET /v1/projects
2

List sheets in a project

GET /v1/projects/{project_id}/sheets
3

Traverse entities by sheet

GET /v1/projects/{project_id}/entities?sheet_id={sheet_id}&limit=...
4

Split by type as needed

Filter by mention, component_instance, region, etc.
5

Get adjacency for a specific node

GET /v1/projects/{project_id}/entities/{entity_id}
6

Traverse edges directly

GET /v1/projects/{project_id}/relationships?...
Use /search for relevance-ranked retrieval. Use /entities and /relationships for deterministic traversal and export.

Reference

TypeDescription
mentionDetected annotation or text reference (subtypes: callout, etc.)
component_instanceSpecific instance of a structural/MEP component
component_typeComponent type definition (e.g., W12x26)
regionSpatial region on a sheet (view, zone)
communityGraph community grouping related entities

Error Handling

The API returns errors in two formats: Validation / business errors:
{"error": {"code": "invalid_page", "message": "Page 15 does not exist (total: 10)"}}
{"error": {"code": "rate_limited", "message": "Too many requests", "retry_after": 30}}
FastAPI exceptions:
{"detail": "Project not found"}
The SDK provides specific exception classes:
from struai import StruAI, AuthenticationError, RateLimitError, NotFoundError

try:
    result = client.drawings.analyze("drawing.pdf", page=15)
except AuthenticationError:
    print("Invalid API key")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds")
except NotFoundError:
    print("Drawing or page not found")

Pricing

CapabilityEndpointPrice
Raw DetectionPOST /v1/drawings$0.02/page
Graph IngestionPOST /v1/projects/{id}/sheets$0.15/page
SearchPOST /v1/projects/{id}/search$0.005/query
NeedUseCost Example
Fast annotation detection, no storageRaw Detection100 pages = $2
Searchable knowledge graph across sheetsGraph Ingestion100 pages = $15
Find specific entities or componentsSearch200 queries = $1
Full example: A 96-page structural set with Graph Ingestion + 100 searches = $15.50