# SunTrace3D API Reference

> Machine-readable API documentation for the SunTrace3D Partner API (v1).
> Interactive version: [https://suntrace3d.com/docs/api](https://suntrace3d.com/docs/api)

> **AI Agents**: For a concise product overview, see [/llms.txt](https://suntrace3d.com/llms.txt). For agent integration (session API, parameters, rate limits), see [/llms-agent.txt](https://suntrace3d.com/llms-agent.txt). For the full documentation (user guide + API reference), see [/llms-full.txt](https://suntrace3d.com/llms-full.txt). For AI agent session integration details, jump to [AI Agent Integration](#ai-agent-integration).

---

## Overview

The SunTrace3D Partner API is a RESTful service for programmatic access to 3D city model generation and solar energy calculations.

- **Base URL**: `https://suntrace3d.com/api/v1`
- **Authentication**: Bearer token (API key)
- **Format**: JSON request/response
- **All generated models are HD quality**

### Endpoints

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `POST` | `/api/v1/models` | Required | Generate a new HD 3D model |
| `GET` | `/api/v1/models/:id` | Required | Check model generation status |
| `POST` | `/api/solar/calculate` | None | Calculate solar energy yield |
| `GET` | `/embed` | API key in query | Embeddable 3D viewer (iframe) |
| `GET` | `/api/health` | None | Health check |

---

## Authentication

All API requests (except `/api/solar/calculate` and `/api/health`) require a Bearer token in the `Authorization` header.

```
Authorization: Bearer st_live_abc123def456...
```

### Getting an API Key

1. Create an account at `/auth/signup`
2. Upgrade to a Business subscription (€99/month)
3. Visit the Partner Portal at `/partner`
4. Click "Create Key" — copy the key immediately (it won't be shown again)

> **Warning:** Keep your API key secret. Do not expose it in client-side code. Use server-side requests or environment variables.

---

## POST /api/v1/models

Generate an HD 3D city model for a geographic location. Models are generated asynchronously — poll the status endpoint to check when ready.

### Request

```http
POST /api/v1/models
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
  "latitude": 44.8699,
  "longitude": 13.8420,
  "radiusKm": 0.3
}
```

### Request Body Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `latitude` | number | Yes | Latitude of center point (-90 to 90) |
| `longitude` | number | Yes | Longitude of center point (-180 to 180) |
| `radiusKm` | number | No | Radius of area to model in km (default: 0.3) |

### Response (new generation)

```json
{
  "id": "hd_44.8699_13.8420_0.3",
  "status": "pending"
}
```

### Response (cache hit)

If a model for the same location and radius already exists, the API returns the cached result immediately:

```json
{
  "id": "hd_44.8699_13.8420_0.3",
  "status": "ready",
  "modelUrl": "https://s3.eu-central-1.amazonaws.com/suntrace-models/hd_44.8699_13.8420_0.3/scene.glb"
}
```

> **Tip:** Always check the `status` field in the POST response. If it is `"ready"`, you can skip polling entirely.

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique model identifier for status polling |
| `status` | string | `"pending"` \| `"processing"` \| `"ready"` \| `"failed"` |
| `modelUrl` | string \| null | URL to the GLB model file (non-null when status is `"ready"`) |
| `progress` | number \| null | Generation progress 0-100 (non-null when status is `"processing"` or `"ready"`) |
| `step` | string \| null | Current generation step description |

### Example (curl)

```bash
curl -X POST https://suntrace3d.com/api/v1/models \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"latitude": 44.8699, "longitude": 13.8420, "radiusKm": 0.3}'
```

---

## GET /api/v1/models/:id

Check the status of a model generation request.

### Request

```http
GET /api/v1/models/hd_44.8699_13.8420_0.3
Authorization: Bearer YOUR_API_KEY
```

### Response (processing)

```json
{
  "id": "hd_44.8699_13.8420_0.3",
  "status": "processing",
  "progress": 65,
  "step": "Generating textures..."
}
```

### Response (ready)

```json
{
  "id": "hd_44.8699_13.8420_0.3",
  "status": "ready",
  "modelUrl": "https://s3.eu-central-1.amazonaws.com/suntrace-models/hd_44.8699_13.8420_0.3/scene.glb",
  "progress": 100,
  "step": null
}
```

### Status Values

| Status | Description |
|--------|-------------|
| `pending` | Job is queued, waiting for a worker |
| `processing` | Model is being generated (check `progress` for %) |
| `ready` | Model is ready — `modelUrl` contains the download URL |
| `failed` | Generation failed — retry by creating a new request |

### Polling Recommendation

Poll every 5-10 seconds. Typical generation times are 30-120 seconds.

---

## POST /api/solar/calculate

Calculate annual solar energy yield for a panel configuration and location. Uses PVGIS satellite irradiance data.

**No authentication required.**

### Request

```http
POST /api/solar/calculate
Content-Type: application/json

{
  "latitude": 44.8699,
  "longitude": 13.8420,
  "tiltDeg": 35,
  "azimuthDeg": 180,
  "panelAreaM2": 20,
  "panelEfficiency": 0.20,
  "shadingLossFraction": 0.05
}
```

### Request Body Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `latitude` | number | Yes | Location latitude |
| `longitude` | number | Yes | Location longitude |
| `tiltDeg` | number | Yes | Panel tilt angle in degrees (0-90) |
| `azimuthDeg` | number | Yes | Panel azimuth (0=North, 180=South) |
| `panelAreaM2` | number | Yes | Total panel area in m² |
| `panelEfficiency` | number | Yes | Efficiency (0.0-1.0, typically 0.18-0.22) |
| `shadingLossFraction` | number | No | Shading loss (0.0-1.0, default: 0) |

### Response

```json
{
  "annualYieldKwh": 4982,
  "peakPowerKw": 4.0,
  "specificYield": 1246,
  "monthlyKwh": [248, 305, 412, 465, 522, 548, 562, 530, 445, 368, 280, 232],
  "source": "pvgis"
}
```

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `annualYieldKwh` | number | Estimated annual energy yield in kWh |
| `peakPowerKw` | number | Peak power output in kW |
| `specificYield` | number | Specific yield in kWh/kWp |
| `monthlyKwh` | number[] | Array of 12 monthly kWh values (Jan-Dec) |
| `source` | string | Data source identifier (`"pvgis"`) |

### Calculation Method

```
peakPowerKw = (panelAreaM2 * panelEfficiency * 1000) / 1000
annualYieldKwh = peakPowerKw * annualGTI * (1 - shadingLossFraction) * (1 - systemLosses)
```

Where `systemLosses = 0.14` (14% for inverter + wiring) and `annualGTI` is the annual Global Tilted Irradiation from PVGIS.

---

## Embed Viewer

Embed an interactive 3D solar viewer on your website using an iframe.

### URL Format

```
https://suntrace3d.com/embed?lat={LATITUDE}&lng={LONGITUDE}&key={API_KEY}
```

### URL Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `lat` | number | Yes | Latitude of location to display |
| `lng` | number | Yes | Longitude of location to display |
| `key` | string | Yes | Your API key |

### Basic Embed Code

```html
<iframe
  src="https://suntrace3d.com/embed?lat=44.8699&lng=13.8420&key=YOUR_API_KEY"
  width="100%"
  height="500"
  frameborder="0"
  allow="fullscreen"
  style="border-radius: 12px; border: 1px solid #e5e7eb;">
</iframe>
```

### Responsive Embed (recommended)

```html
<div style="position: relative; width: 100%; padding-bottom: 56.25%; overflow: hidden; border-radius: 12px;">
  <iframe
    src="https://suntrace3d.com/embed?lat=44.8699&lng=13.8420&key=YOUR_API_KEY"
    style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
    allow="fullscreen">
  </iframe>
</div>
```

### Embed Features

- Interactive 3D orbit, pan, and zoom
- Time slider for shadow simulation
- Responsive — works on mobile
- Google 3D Tiles photorealistic models
- No additional JavaScript required
- Fullscreen support

### Lead Capture in Embeds

If lead capture is enabled in your Partner Portal profile, the embedded viewer will display a lead capture form to visitors. When a visitor submits the form:

1. The lead is saved to your Partner Portal dashboard
2. You receive a notification email with the contact details and system configuration
3. The homeowner receives a confirmation email

Enable lead capture in the Partner Portal at `/partner` by toggling "Lead Capture" on and setting a notification email.

---

## Rate Limits

| Endpoint | Limit |
|----------|-------|
| `POST /api/v1/models` | 10 requests per hour per API key |
| `GET /api/v1/models/:id` | No limit |
| `POST /api/solar/calculate` | No limit (public) |
| `GET /embed` | No limit |

### Rate Limit Exceeded Response (429)

```json
{
  "error": "Rate limit exceeded. Maximum 10 generations per hour."
}
```

---

## Error Handling

All error responses include a JSON body with an `error` field.

### HTTP Status Codes

| Code | Description |
|------|-------------|
| `200` | Success |
| `400` | Bad Request — missing or invalid parameters |
| `401` | Unauthorized — missing or invalid API key |
| `404` | Not Found — model ID does not exist |
| `429` | Too Many Requests — rate limit exceeded |
| `503` | Service Unavailable — PVGIS or external service is down (response includes `useLocalFallback: true` when client-side fallback is available) |

### Error Response Examples

```json
{"error": "latitude and longitude are required"}
```

```json
{"error": "Missing Authorization header"}
```

```json
{"error": "Invalid or revoked API key"}
```

```json
{"error": "Rate limit exceeded. Maximum 10 generations per hour."}
```

```json
{"error": "Model not found"}
```

---

## Webhooks

> **Coming Soon** — Webhook support is planned for a future release. Webhooks will allow you to receive HTTP POST notifications when model generation completes, eliminating the need to poll for status updates.

---

## Full Examples

### Bash — Generate model and wait for completion

```bash
#!/bin/bash
API_KEY="YOUR_API_KEY"
BASE_URL="https://suntrace3d.com"

# 1. Request model generation
echo "Requesting model generation..."
RESPONSE=$(curl -s -X POST "$BASE_URL/api/v1/models" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"latitude": 44.8699, "longitude": 13.8420, "radiusKm": 0.3}')

MODEL_ID=$(echo $RESPONSE | jq -r '.id')
STATUS=$(echo $RESPONSE | jq -r '.status')
echo "Model ID: $MODEL_ID (status: $STATUS)"

# 2. Poll until ready
while [ "$STATUS" != "ready" ] && [ "$STATUS" != "failed" ]; do
  sleep 5
  RESPONSE=$(curl -s "$BASE_URL/api/v1/models/$MODEL_ID" \
    -H "Authorization: Bearer $API_KEY")
  STATUS=$(echo $RESPONSE | jq -r '.status')
  PROGRESS=$(echo $RESPONSE | jq -r '.progress // 0')
  echo "Status: $STATUS ($PROGRESS%)"
done

# 3. Get the model URL
if [ "$STATUS" = "ready" ]; then
  MODEL_URL=$(echo $RESPONSE | jq -r '.modelUrl')
  echo "Model ready: $MODEL_URL"
else
  echo "Generation failed"
fi
```

### JavaScript / Node.js

```javascript
const API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://suntrace3d.com';

async function generateModel(lat, lng) {
  // 1. Request generation
  const res = await fetch(`${BASE_URL}/api/v1/models`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ latitude: lat, longitude: lng }),
  });

  const { id, status, modelUrl } = await res.json();

  // If already cached, return immediately
  if (status === 'ready') return { id, modelUrl };

  // 2. Poll until ready
  return pollStatus(id);
}

async function pollStatus(modelId) {
  while (true) {
    await new Promise(r => setTimeout(r, 5000)); // Wait 5s

    const res = await fetch(`${BASE_URL}/api/v1/models/${modelId}`, {
      headers: { 'Authorization': `Bearer ${API_KEY}` },
    });

    const data = await res.json();
    console.log(`Status: ${data.status} (${data.progress || 0}%)`);

    if (data.status === 'ready') return data;
    if (data.status === 'failed') throw new Error('Generation failed');
  }
}

// Usage
generateModel(44.8699, 13.8420)
  .then(data => console.log('Model URL:', data.modelUrl))
  .catch(err => console.error(err));
```

### Python

```python
import requests
import time

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://suntrace3d.com"

def generate_model(lat: float, lng: float) -> dict:
    """Generate an HD 3D model and wait for completion."""
    # Request generation
    res = requests.post(
        f"{BASE_URL}/api/v1/models",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"latitude": lat, "longitude": lng, "radiusKm": 0.3},
    )
    data = res.json()

    if data["status"] == "ready":
        return data

    # Poll until ready
    model_id = data["id"]
    while True:
        time.sleep(5)
        res = requests.get(
            f"{BASE_URL}/api/v1/models/{model_id}",
            headers={"Authorization": f"Bearer {API_KEY}"},
        )
        data = res.json()
        print(f"Status: {data['status']} ({data.get('progress', 0)}%)")

        if data["status"] == "ready":
            return data
        if data["status"] == "failed":
            raise Exception("Generation failed")

def calculate_solar(lat: float, lng: float, tilt: float = 35, azimuth: float = 180) -> dict:
    """Calculate solar energy yield for a panel configuration."""
    res = requests.post(
        f"{BASE_URL}/api/solar/calculate",
        json={
            "latitude": lat,
            "longitude": lng,
            "tiltDeg": tilt,
            "azimuthDeg": azimuth,
            "panelAreaM2": 20,
            "panelEfficiency": 0.20,
        },
    )
    return res.json()

# Usage
model = generate_model(44.8699, 13.8420)
print(f"Model URL: {model['modelUrl']}")

solar = calculate_solar(44.8699, 13.8420)
print(f"Annual yield: {solar['annualYieldKwh']} kWh")
print(f"Monthly: {solar['monthlyKwh']}")
```

---

## AI Agent Integration

AI agents (chatbots, virtual assistants) can create sessions to redirect users to SunTrace3D with pre-configured Solar Wizard settings and optional automatic roof detection. No API key is required — sessions use the public MCP Sessions API.

Agent sessions do not pre-generate 3D models. When the user clicks the viewer URL, the SD model generates on demand (about 1-2 minutes depending on location — the user sees a loading animation). If the user has a Pro subscription, the viewer automatically generates an HD model instead.

### Endpoints

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `POST` | `/api/mcp-sessions` | None | Create an agent session with wizard parameters |
| `GET` | `/api/mcp-sessions/:id` | None | Check session status |

### POST /api/mcp-sessions

Create a session that pre-configures the Solar Wizard for the user.

#### Request

```http
POST /api/mcp-sessions
Content-Type: application/json

{
  "latitude": 44.8699,
  "longitude": 13.8420,
  "agentName": "ChatGPT",
  "wizardGoal": "cover_bill",
  "wizardKwh": 350,
  "roofLat": 44.8701,
  "roofLng": 13.8418
}
```

#### Request Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `latitude` | number | Yes | Location center latitude |
| `longitude` | number | Yes | Location center longitude |
| `agentName` | string | Yes | Agent identifier (e.g., "ChatGPT", "Claude", "MyBot"). Max 100 chars. |
| `radiusKm` | number | No | Radius in km (default: 0.3) |
| `wizardGoal` | string | No | `maximize_energy`, `cover_bill`, `maximize_roi`, `environmental` |
| `wizardKwh` | number | No | Monthly kWh target (required when wizardGoal is `cover_bill`) |
| `roofLat` | number | No | Precise roof latitude for auto-click (requires `roofLng`) |
| `roofLng` | number | No | Precise roof longitude for auto-click (requires `roofLat`) |

#### Response

```json
{
  "sessionId": "clxyz...",
  "viewerUrl": "https://suntrace3d.com/viewer?session=clxyz...&lat=44.869900&lng=13.842000&wizard=cover_bill&wizardKwh=350&roofLat=44.8701000&roofLng=13.8418000",
  "status": "created",
  "modelStatus": "not_generated",
  "agentName": "ChatGPT"
}
```

### GET /api/mcp-sessions/:id

Check session status and retrieve session details. The `modelStatus` reflects whether an SD model has been previously generated for this location (by any user visiting it). For new locations, this will be `"not_generated"` until a user opens the viewer link.

#### Response

```json
{
  "sessionId": "clxyz...",
  "status": "created",
  "modelStatus": "not_generated",
  "latitude": 44.8699,
  "longitude": 13.8420,
  "quality": "sd",
  "agentName": "ChatGPT",
  "wizardGoal": "cover_bill",
  "wizardKwh": 350,
  "roofLat": 44.8701,
  "roofLng": 13.8418,
  "createdAt": "2025-01-15T10:30:00Z",
  "expiresAt": "2025-01-17T10:30:00Z"
}
```

### Recommended Workflows

#### Workflow 1: With Roof Coordinates (Best UX)

Provide precise roof coordinates for automatic panel placement.

```
1. POST /api/mcp-sessions
   { agentName, latitude, longitude, wizardGoal, wizardKwh, roofLat, roofLng }
2. Send viewerUrl to user
3. User clicks link → model generates (~1-2 min, loading animation shown) →
   wizard opens → auto-clicks on roof → panels placed → results shown
4. If auto-click misses: user sees "Click on roof" and clicks manually
```

#### Workflow 2: Manual Roof Selection

Let the user pick the exact roof location.

```
1. POST /api/mcp-sessions
   { agentName, latitude, longitude, wizardGoal }
2. Send viewerUrl to user
3. User clicks link → model generates (~1-2 min) → wizard opens → user clicks roof
```

#### Workflow 3: Direct URL (No API)

Simplest integration. Construct the viewer URL directly.

```
https://suntrace3d.com/viewer?lat=44.87&lng=13.84&wizard=maximize_energy
```

No session tracking, no agent identification. Model generates on-the-fly when the user arrives.

### URL Parameters (viewer)

These parameters are used in the viewer URL (included automatically in `viewerUrl` from the API, or constructed manually for Workflow 3).

| Parameter | Type | Description |
|-----------|------|-------------|
| `wizard` | string | Pre-select wizard goal (`maximize_energy`, `cover_bill`, `maximize_roi`, `environmental`) |
| `wizardKwh` | number | Monthly kWh for `cover_bill` goal |
| `roofLat` | number | Auto-click roof latitude |
| `roofLng` | number | Auto-click roof longitude |
| `session` | string | MCP session ID (auto-included by API) |
| `lat` | number | Location latitude |
| `lng` | number | Location longitude |

### Important Notes

- **Agent identification**: `agentName` is required. Every session is tagged with the agent name for analytics.
- **No pre-generation**: Agent sessions do not trigger model generation. The 3D model generates when the user opens the viewer link (~1-2 minutes, loading animation shown). Pro users automatically get HD quality; free/non-Pro users get SD.
- **Auto-click precision**: `roofLat`/`roofLng` should point to the CENTER of the desired roof surface. The system raycasts downward at that GPS point. If it hits a roof, placement is automatic. If it misses (ground, wrong building), the user clicks manually.
- **User can always redo**: If auto-placed panels are on the wrong surface, the user clicks "Start Over" to remove them and pick a different roof.
- **Pro tier required**: The Solar Wizard requires a Pro subscription (€19/mo). Free users see a sign-up/upgrade prompt.
- **Session expiry**: Sessions expire after 48 hours.
- **Rate limit**: 10 sessions per hour per IP address.

### Full Example (Python)

```python
import requests

def create_solar_link(lat, lng, roof_lat=None, roof_lng=None, monthly_kwh=None):
    """Create an agent session and return the viewer URL to send to the user."""
    payload = {
        "agentName": "MySolarBot",
        "latitude": lat,
        "longitude": lng,
        "wizardGoal": "cover_bill" if monthly_kwh else "maximize_energy",
    }
    if monthly_kwh:
        payload["wizardKwh"] = monthly_kwh
    if roof_lat is not None and roof_lng is not None:
        payload["roofLat"] = roof_lat
        payload["roofLng"] = roof_lng

    res = requests.post("https://suntrace3d.com/api/mcp-sessions", json=payload)
    res.raise_for_status()
    return res.json()["viewerUrl"]

# Usage — send this link to the user
url = create_solar_link(
    lat=44.8699, lng=13.8420,
    roof_lat=44.8701, roof_lng=13.8418,
    monthly_kwh=350,
)
print(f"Send this to your user: {url}")
# User clicks → sees loading animation (~1-2 min) → wizard auto-opens → panels placed
```

---

## Links

- **Interactive User Guide**: [/docs](https://suntrace3d.com/docs)
- **Interactive API Docs**: [/docs/api](https://suntrace3d.com/docs/api)
- **User Guide (Markdown)**: [/docs/user-guide.md](https://suntrace3d.com/docs/user-guide.md)
- **LLM Product Overview (concise)**: [/llms.txt](https://suntrace3d.com/llms.txt)
- **LLM Agent Integration Guide**: [/llms-agent.txt](https://suntrace3d.com/llms-agent.txt)
- **LLM Documentation (full)**: [/llms-full.txt](https://suntrace3d.com/llms-full.txt)
- **3D Viewer**: [/viewer](https://suntrace3d.com/viewer)
- **Partner Portal**: [/partner](https://suntrace3d.com/partner)
- **Health Check**: [/api/health](https://suntrace3d.com/api/health)
