> ## 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.

# Drawing Analysis API

> AI-powered construction drawing analysis with structured data extraction and semantic search

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

<CodeGroup>
  ```python Python theme={null}
  from struai import StruAI

  client = StruAI(api_key="YOUR_API_KEY")
  result = client.drawings.analyze("structural.pdf", page=4)
  print(result.annotations.leaders)
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.stru.ai/v1/drawings \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -F "file=@structural.pdf" \
    -F "page=4"
  ```
</CodeGroup>

**Built for:** AEC firms, software integrators, document management platforms, and startups building on drawing intelligence.

***

## Get Started

<Steps>
  <Step title="Install the SDK">
    ```bash theme={null}
    pip install struai
    ```

    Requires Python 3.9+
  </Step>

  <Step title="Set your API key">
    Get an API key from [app.stru.ai](https://app.stru.ai), then set it as an environment variable:

    ```bash theme={null}
    export STRUAI_API_KEY="YOUR_API_KEY"
    ```
  </Step>

  <Step title="Analyze your first drawing">
    <CodeGroup>
      ```python Python theme={null}
      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}")
      ```

      ```bash cURL theme={null}
      curl -X POST https://api.stru.ai/v1/drawings \
        -H "Authorization: Bearer $STRUAI_API_KEY" \
        -F "file=@drawing.pdf" \
        -F "page=1"
      ```
    </CodeGroup>
  </Step>
</Steps>

***

## Capabilities

<Tabs>
  <Tab title="Raw Detection">
    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.

    <CodeGroup>
      ```python Python theme={null}
      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)
      ```

      ```bash cURL theme={null}
      # Analyze with file upload
      curl -X POST https://api.stru.ai/v1/drawings \
        -H "Authorization: Bearer $STRUAI_API_KEY" \
        -F "file=@structural.pdf" \
        -F "page=4"

      # Or check cache by file hash
      curl https://api.stru.ai/v1/drawings/cache/{file_hash} \
        -H "Authorization: Bearer $STRUAI_API_KEY"
      ```
    </CodeGroup>

    **Price:** \$0.02/page
  </Tab>

  <Tab title="Graph + Search">
    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

    <CodeGroup>
      ```python Python theme={null}
      # Create a project
      project = client.projects.create(name="Building A Structural")

      # Ingest multiple pages at once
      jobs = project.sheets.add("structural.pdf", page="1-10")

      # Search with natural language
      results = project.search("W12x26 beam connections")
      ```

      ```bash cURL theme={null}
      # Create project
      curl -X POST https://api.stru.ai/v1/projects \
        -H "Authorization: Bearer $STRUAI_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"name": "Building A Structural"}'

      # Ingest multiple pages
      curl -X POST https://api.stru.ai/v1/projects/proj_xxx/sheets \
        -H "Authorization: Bearer $STRUAI_API_KEY" \
        -F "file=@structural.pdf" \
        -F "page=1-10"
      ```
    </CodeGroup>

    | Operation       | Price         |
    | --------------- | ------------- |
    | Graph Ingestion | \$0.15/page   |
    | Search          | \$0.005/query |
  </Tab>

  <Tab title="Async Support">
    Use `AsyncStruAI` for non-blocking operations in async applications.

    ```python theme={null}
    import asyncio
    from struai import AsyncStruAI

    async def analyze_drawings():
        client = AsyncStruAI(api_key="YOUR_API_KEY")
        result = await client.drawings.analyze("drawing.pdf", page=1)
        return result

    asyncio.run(analyze_drawings())
    ```
  </Tab>
</Tabs>

***

## Use Cases

<CardGroup cols={2}>
  <Card title="Cross-Reference Indexing" icon="link">
    Automatically link section tags, detail callouts, and sheet references across a drawing set
  </Card>

  <Card title="QA/QC Automation" icon="check-double">
    Validate drawing consistency, detect missing references, flag revision conflicts
  </Card>

  <Card title="Quantity Takeoffs" icon="calculator">
    Extract and count components, connections, and annotations
  </Card>

  <Card title="Analytical Model Generation" icon="cube">
    Build structured data for BIM/analysis workflows
  </Card>
</CardGroup>

***

## API Reference

### Authentication

All requests require a Bearer token:

<CodeGroup>
  ```python Python theme={null}
  from struai import StruAI
  client = StruAI(api_key="YOUR_API_KEY")

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

  ```bash cURL theme={null}
  Authorization: Bearer <your-api-key>
  ```
</CodeGroup>

***

### 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/{file_hash}

Check whether a PDF file hash exists in the drawing cache.

```bash theme={null}
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.

| Field       | Type    | Required                     | Description                       |
| ----------- | ------- | ---------------------------- | --------------------------------- |
| `file`      | file    | One of `file` or `file_hash` | PDF file (max 50MB)               |
| `file_hash` | string  | One of `file` or `file_hash` | Hash of a previously uploaded PDF |
| `page`      | integer | Yes                          | Page number (1-indexed)           |

<CodeGroup>
  ```python Python theme={null}
  result = client.drawings.analyze("structural.pdf", page=4)

  # Access annotations
  print(result.annotations.leaders)
  print(result.annotations.section_tags)
  print(result.annotations.detail_tags)
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.stru.ai/v1/drawings \
    -H "Authorization: Bearer $STRUAI_API_KEY" \
    -F "file=@structural.pdf" \
    -F "page=4"
  ```
</CodeGroup>

<Accordion title="Response">
  ```json theme={null}
  {
    "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]
    }
  }
  ```
</Accordion>

#### GET /v1/drawings/{id}

Retrieve a previously processed drawing.

```python theme={null}
result = client.drawings.get("drw_7f8a9b2c")
```

#### DELETE /v1/drawings/{id}

Delete a drawing result.

```python theme={null}
client.drawings.delete("drw_7f8a9b2c")
```

***

### Tier 2: Graph + Search

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

| Operation       | Price         |
| --------------- | ------------- |
| Graph Ingestion | \$0.15/page   |
| Search          | \$0.005/query |

#### POST /v1/projects

Create a project to group related sheets.

<CodeGroup>
  ```python Python theme={null}
  project = client.projects.create(
      name="Building A Structural",
      description="96-page structural drawing set"
  )
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.stru.ai/v1/projects \
    -H "Authorization: Bearer $STRUAI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name": "Building A Structural"}'
  ```
</CodeGroup>

<Accordion title="Response">
  ```json theme={null}
  {
    "id": "proj_abc123",
    "name": "Building A Structural",
    "description": "96-page structural drawing set",
    "created_at": "2026-01-29T10:00:00Z"
  }
  ```
</Accordion>

#### GET /v1/projects

List all projects.

<Accordion title="Response">
  ```json theme={null}
  {
    "projects": [
      {
        "id": "proj_abc123",
        "name": "Building A Structural",
        "description": "96-page structural drawing set",
        "created_at": "2026-01-29T10:00:00Z"
      }
    ]
  }
  ```
</Accordion>

#### GET /v1/projects/{id}

Get project details and aggregate stats.

<Accordion title="Response">
  ```json theme={null}
  {
    "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
  }
  ```
</Accordion>

#### DELETE /v1/projects/{id}

Delete project and all associated graph data.

```json theme={null}
{"deleted": true, "id": "proj_abc123"}
```

***

### Sheets

#### POST /v1/projects/{project_id}/sheets

Ingest one or more PDF pages into the knowledge graph. Returns immediately with job IDs for polling.

| Field                        | Type   | Required                     | Description                                                                 |
| ---------------------------- | ------ | ---------------------------- | --------------------------------------------------------------------------- |
| `file`                       | file   | One of `file` or `file_hash` | PDF file                                                                    |
| `file_hash`                  | string | One of `file` or `file_hash` | Hash of a previously uploaded PDF                                           |
| `page`                       | string | Yes                          | Page selector (see formats below)                                           |
| `source_description`         | string | No                           | Description of the source document                                          |
| `on_sheet_exists`            | string | No                           | Behavior when sheet already exists: `error`, `skip` (default), or `rebuild` |
| `community_update_mode`      | string | No                           | `incremental` or `rebuild`                                                  |
| `semantic_index_update_mode` | string | No                           | `incremental` or `rebuild`                                                  |

**Page selector formats:**

| Format       | Example    | Description                      |
| ------------ | ---------- | -------------------------------- |
| Single page  | `12`       | One specific page                |
| Range        | `1-5`      | Inclusive page range             |
| List / mixed | `1,3,8-10` | Comma-separated pages and ranges |
| All pages    | `all`      | Every page in the PDF            |

<CodeGroup>
  ```python Python theme={null}
  # 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"
  )
  ```

  ```bash cURL theme={null}
  # Single page
  curl -X POST https://api.stru.ai/v1/projects/proj_xxx/sheets \
    -H "Authorization: Bearer $STRUAI_API_KEY" \
    -F "file=@structural.pdf" \
    -F "page=1"

  # Range of pages
  curl -X POST https://api.stru.ai/v1/projects/proj_xxx/sheets \
    -H "Authorization: Bearer $STRUAI_API_KEY" \
    -F "file=@structural.pdf" \
    -F "page=1-10"

  # With options
  curl -X POST https://api.stru.ai/v1/projects/proj_xxx/sheets \
    -H "Authorization: Bearer $STRUAI_API_KEY" \
    -F "file=@structural.pdf" \
    -F "page=all" \
    -F "on_sheet_exists=skip"
  ```
</CodeGroup>

<Accordion title="Response (202 Accepted)">
  One job is created per page:

  ```json theme={null}
  {
    "jobs": [
      {"job_id": "job_abc123def4", "page": 1},
      {"job_id": "job_abc123def5", "page": 2},
      {"job_id": "job_abc123def6", "page": 3}
    ]
  }
  ```
</Accordion>

#### GET /v1/projects/{project_id}/jobs/{job_id}

Poll job status for async sheet ingestion. Each job progresses through a 5-step pipeline.

**Pipeline steps:**

| Step | Key                          | Description                |
| ---- | ---------------------------- | -------------------------- |
| 1/5  | `detect_annotations`         | Detect Annotations         |
| 2/5  | `enrich_annotations`         | Enrich Annotations         |
| 3/5  | `synthesize_remaining_text`  | Synthesize Remaining Text  |
| 4/5  | `resolve_entities_and_facts` | Resolve Entities and Facts |
| 5/5  | `load_graph_and_index`       | Load Graph and Index       |

**Job statuses:** `queued` → `running` → `complete` | `failed`

**Timeout rules:**

* Queued timeout: 30 minutes from enqueue
* Running timeout: 10 minutes from first start

<Accordion title="Response (in-progress)">
  ```json theme={null}
  {
    "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"
      }
    ]
  }
  ```
</Accordion>

<Accordion title="Response (complete)">
  ```json theme={null}
  {
    "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"
    }
  }
  ```
</Accordion>

<Accordion title="Response (failed)">
  ```json theme={null}
  {
    "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"
    }
  }
  ```
</Accordion>

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

List all ingested sheets in a project.

<Accordion title="Response">
  ```json theme={null}
  {
    "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"
      }
    ]
  }
  ```
</Accordion>

#### GET /v1/projects/{project_id}/sheets/{sheet_id}

Get full sheet graph slice including regions, mentions, component instances, and references.

<Accordion title="Response">
  ```json theme={null}
  {
    "id": "S1.4",
    "sheet_uuid": "uuid_xxx",
    "title": "LEVEL 1 FOUNDATION PLAN",
    "regions": ["..."],
    "mentions": ["..."],
    "component_instances": ["..."],
    "references": ["..."]
  }
  ```
</Accordion>

#### GET /v1/projects/{project_id}/sheets/{sheet_id}/annotations

Get the raw annotation geometry from cached detection results for a processed sheet.

```bash theme={null}
curl https://api.stru.ai/v1/projects/proj_xxx/sheets/S1.4/annotations \
  -H "Authorization: Bearer $STRUAI_API_KEY"
```

<Accordion title="Response">
  ```json theme={null}
  {
    "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]
    }
  }
  ```
</Accordion>

#### DELETE /v1/projects/{project_id}/sheets/{sheet_id}

Remove a sheet from the graph and run maintenance rebuilds.

<Accordion title="Response">
  ```json theme={null}
  {
    "deleted": true,
    "sheet_id": "S1.4",
    "cleanup": {
      "deleted_nodes": 87,
      "deleted_facts": 42,
      "deleted_references": 12
    },
    "maintenance": {
      "communities_rebuilt": 3,
      "index_updated": true
    }
  }
  ```
</Accordion>

***

### Search

#### POST /v1/projects/{project_id}/search

Hybrid retrieval combining Qdrant vector search, BM25 fulltext, and optional Neo4j graph context. **\$0.005/query**

| Field                   | Type    | Required | Description                                  |
| ----------------------- | ------- | -------- | -------------------------------------------- |
| `query`                 | string  | Yes      | Search query                                 |
| `limit`                 | integer | No       | Results per channel (1-100, default 10)      |
| `channels`              | array   | No       | Subset of `entities`, `facts`, `communities` |
| `include_graph_context` | boolean | No       | Include graph neighborhood (default `true`)  |

<Info>
  `/search` returns relevance-ranked results, not exhaustive traversal. For deterministic full traversal, use the `/entities` and `/relationships` endpoints instead.
</Info>

<CodeGroup>
  ```python Python theme={null}
  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}")
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.stru.ai/v1/projects/proj_xxx/search \
    -H "Authorization: Bearer $STRUAI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "W12x26 beam connections at grid A",
      "limit": 10,
      "channels": ["entities"],
      "include_graph_context": true
    }'
  ```
</CodeGroup>

<Accordion title="Response">
  ```json theme={null}
  {
    "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
  }
  ```
</Accordion>

***

### Entities & Relationships

#### GET /v1/projects/{project_id}/entities

Deterministic entity listing for full traversal and export.

| Filter            | Type    | Description               |
| ----------------- | ------- | ------------------------- |
| `sheet_id`        | string  | Filter by sheet           |
| `type`            | string  | Entity type (see below)   |
| `family`          | string  | Component family          |
| `normalized_spec` | string  | Normalized specification  |
| `region_uuid`     | string  | Filter by region          |
| `region_label`    | string  | Filter by region label    |
| `note_number`     | string  | Filter by note number     |
| `limit`           | integer | Max 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`.

<CodeGroup>
  ```python Python theme={null}
  # 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
  )
  ```

  ```bash cURL theme={null}
  # All entities for a sheet
  curl 'https://api.stru.ai/v1/projects/proj_xxx/entities?sheet_id=S1.4&limit=1000' \
    -H "Authorization: Bearer $STRUAI_API_KEY"

  # Mentions only
  curl 'https://api.stru.ai/v1/projects/proj_xxx/entities?sheet_id=S1.4&type=mention&limit=1000' \
    -H "Authorization: Bearer $STRUAI_API_KEY"

  # Component instances only
  curl 'https://api.stru.ai/v1/projects/proj_xxx/entities?sheet_id=S1.4&type=component_instance&limit=1000' \
    -H "Authorization: Bearer $STRUAI_API_KEY"
  ```
</CodeGroup>

<Accordion title="Response">
  ```json theme={null}
  {
    "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": {}
      }
    ]
  }
  ```
</Accordion>

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

Get entity detail with relationships and location info.

| Parameter         | Type    | Description                                                  |
| ----------------- | ------- | ------------------------------------------------------------ |
| `include_invalid` | boolean | Include invalidated relationships (default `false`)          |
| `expand_target`   | boolean | Expand `REFERENCES` target sheet summaries (default `false`) |

Returns entity record with `attributes`, `provenance`, `outgoing` relationships, `incoming` relationships, and `locations`.

#### GET /v1/projects/{project_id}/relationships

List facts and references as relationship rows.

| Filter            | Type    | Description                           |
| ----------------- | ------- | ------------------------------------- |
| `sheet_id`        | string  | Filter by sheet                       |
| `source_id`       | string  | Filter by source entity               |
| `target_id`       | string  | Filter by target entity               |
| `type`            | string  | Relationship type                     |
| `include_invalid` | boolean | Include invalidated (default `false`) |
| `invalid_only`    | boolean | Only invalidated relationships        |
| `orphan_only`     | boolean | Only orphaned references              |
| `limit`           | integer | Max results (default 200)             |

<Accordion title="Response">
  ```json theme={null}
  {
    "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
      }
    ]
  }
  ```
</Accordion>

***

### Traverse Guide

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

<Steps>
  <Step title="List projects">
    `GET /v1/projects`
  </Step>

  <Step title="List sheets in a project">
    `GET /v1/projects/{project_id}/sheets`
  </Step>

  <Step title="Traverse entities by sheet">
    `GET /v1/projects/{project_id}/entities?sheet_id={sheet_id}&limit=...`
  </Step>

  <Step title="Split by type as needed">
    Filter by `mention`, `component_instance`, `region`, etc.
  </Step>

  <Step title="Get adjacency for a specific node">
    `GET /v1/projects/{project_id}/entities/{entity_id}`
  </Step>

  <Step title="Traverse edges directly">
    `GET /v1/projects/{project_id}/relationships?...`
  </Step>
</Steps>

<Info>
  Use `/search` for relevance-ranked retrieval. Use `/entities` and `/relationships` for deterministic traversal and export.
</Info>

***

## Reference

<Tabs>
  <Tab title="Entity Types">
    | Type                 | Description                                                       |
    | -------------------- | ----------------------------------------------------------------- |
    | `mention`            | Detected annotation or text reference (subtypes: `callout`, etc.) |
    | `component_instance` | Specific instance of a structural/MEP component                   |
    | `component_type`     | Component type definition (e.g., W12x26)                          |
    | `region`             | Spatial region on a sheet (view, zone)                            |
    | `community`          | Graph community grouping related entities                         |
  </Tab>

  <Tab title="Relationship Types">
    | Type              | Description            |
    | ----------------- | ---------------------- |
    | `CONNECTS_TO`     | Physical interface     |
    | `SUPPORTS`        | Structural load path   |
    | `PART_OF`         | Assembly hierarchy     |
    | `LOCATED_AT_GRID` | At grid intersection   |
    | `REFERENCES`      | Points to sheet/detail |
    | `DESCRIBES`       | Annotates              |
    | `REVISES`         | Marks revision         |
  </Tab>

  <Tab title="Rate Limits">
    | Plan       | Detection  | Graph Ingestion | Search     |
    | ---------- | ---------- | --------------- | ---------- |
    | Free       | 50/month   | 10/month        | 100/month  |
    | Pro        | 1000/month | 500/month       | 5000/month |
    | Enterprise | Unlimited  | Unlimited       | Unlimited  |
  </Tab>
</Tabs>

***

## Error Handling

The API returns errors in two formats:

**Validation / business errors:**

```json theme={null}
{"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:**

```json theme={null}
{"detail": "Project not found"}
```

The SDK provides specific exception classes:

```python theme={null}
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

| Capability          | Endpoint                        | Price         |
| ------------------- | ------------------------------- | ------------- |
| **Raw Detection**   | `POST /v1/drawings`             | \$0.02/page   |
| **Graph Ingestion** | `POST /v1/projects/{id}/sheets` | \$0.15/page   |
| **Search**          | `POST /v1/projects/{id}/search` | \$0.005/query |

<Accordion title="Pricing Examples">
  | Need                                     | Use             | Cost Example      |
  | ---------------------------------------- | --------------- | ----------------- |
  | Fast annotation detection, no storage    | Raw Detection   | 100 pages = \$2   |
  | Searchable knowledge graph across sheets | Graph Ingestion | 100 pages = \$15  |
  | Find specific entities or components     | Search          | 200 queries = \$1 |

  **Full example:** A 96-page structural set with Graph Ingestion + 100 searches = **\$15.50**
</Accordion>
