Skip to main content
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