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

---

## 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 Pro subscription ($9/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']}")
```

---

## 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)
- **3D Viewer**: [/viewer](https://suntrace3d.com/viewer)
- **Partner Portal**: [/partner](https://suntrace3d.com/partner)
- **Health Check**: [/api/health](https://suntrace3d.com/api/health)
