API Reference¶
Base URL¶
- Development:
http://localhost:8000/api - Production:
https://your-domain.com/api
Authentication¶
Authentication is handled by Better Auth. The frontend manages sign-in/sign-up via /api/auth/[...all] API routes, which set an HTTP-only session cookie (better-auth.session_token).
Backend endpoints validate requests by reading this session cookie:
curl -X GET http://localhost:8000/api/properties \
-H "Cookie: better-auth.session_token=SESSION_TOKEN"
The backend also supports legacy JWT Bearer tokens during migration (via get_current_user_hybrid).
Endpoints¶
Authentication¶
Register User¶
Request Body:
Response 201 Created:
{
"id": 1,
"email": "user@example.com",
"full_name": "John Doe",
"created_at": "2025-01-30T10:00:00Z"
}
Login¶
Authentication is handled client-side via the Better Auth SDK. The frontend calls authClient.signIn.email() which hits the Next.js /api/auth/sign-in/email route, setting a session cookie on success.
For the legacy endpoint (kept for backward compatibility):
Request Body:
Response 200 OK: Sets better-auth.session_token cookie.
Properties¶
List Properties¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
skip |
int | Pagination offset (default: 0) |
limit |
int | Results per page (default: 100) |
Response 200 OK:
[
{
"id": 1,
"address": "56 Rue Notre-Dame des Champs",
"postal_code": "75006",
"city": "Paris",
"asking_price": 850000,
"surface_area": 65.5,
"created_at": "2025-01-30T10:00:00Z"
}
]
Create Property¶
Request Body:
{
"address": "56 Rue Notre-Dame des Champs",
"postal_code": "75006",
"city": "Paris",
"asking_price": 850000,
"surface_area": 65.5,
"property_type": "apartment",
"rooms": 3
}
Get Property¶
List Properties with Synthesis¶
Returns all properties enriched with synthesis preview data (risk level, costs, document/redesign counts).
Response 200 OK:
[
{
"id": 1,
"address": "56 Rue Notre-Dame des Champs",
"postal_code": "75006",
"city": "Paris",
"asking_price": 850000,
"surface_area": 65.5,
"synthesis": {
"risk_level": "medium",
"total_annual_cost": 3500.0,
"total_one_time_cost": 15000.0,
"key_findings": "Property analysis summary...",
"document_count": 5,
"redesign_count": 2
}
}
]
Update Property¶
Request Body (all fields optional):
{
"address": "56 Rue Notre-Dame des Champs",
"postal_code": "75006",
"city": "Paris",
"department": "75",
"property_type": "apartment",
"asking_price": 840000,
"surface_area": 65.5,
"rooms": 3,
"floor": 4,
"building_floors": 7,
"building_year": 1920
}
Delete Property¶
Documents¶
Upload Single Document¶
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | PDF or image file |
property_id |
int | Yes | Associated property |
document_type |
string | No | Type (auto-classified if omitted) |
Response 201 Created:
{
"id": 1,
"filename": "pv_ag_2024.pdf",
"document_type": "pv_ag",
"status": "processing",
"property_id": 1,
"created_at": "2025-01-30T10:00:00Z"
}
Bulk Upload Documents¶
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
files |
file[] | Yes | Multiple PDF/image files |
property_id |
int | Yes | Associated property |
Response 202 Accepted:
{
"status": "processing",
"workflow_id": "bulk-processing-1-1706612400",
"document_ids": [1, 2, 3],
"total_files": 3,
"message": "Successfully uploaded 3 documents. Processing..."
}
Get Bulk Processing Status¶
Response 200 OK:
{
"workflow_id": "bulk-processing-1-1706612400",
"property_id": 1,
"status": "completed",
"progress": {
"total": 3,
"completed": 3,
"failed": 0,
"percentage": 100
},
"documents": [
{
"id": 1,
"filename": "pv_ag_2024.pdf",
"document_type": "pv_ag",
"status": "analyzed"
}
],
"synthesis": {
"summary": "Property analysis complete...",
"total_annual_cost": 3500.0,
"total_one_time_cost": 15000.0,
"risk_level": "medium",
"recommendations": ["..."]
}
}
List Documents¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
property_id |
int | Filter by property |
document_type |
string | Filter by type |
Get Document¶
Delete Document¶
Deleting a document automatically triggers synthesis regeneration for the property.
Bulk Delete Documents¶
Request Body:
Response 200 OK:
Automatically triggers synthesis regeneration after deletion.
Rename Document¶
Request Body:
The original file extension is preserved automatically.
Save Synthesis Overrides¶
Saves user-defined overrides into the synthesis data (e.g., tantiemes values, cost adjustments). Overrides are preserved across synthesis regenerations.
Request Body:
Regenerate Overall Synthesis¶
Manually triggers regeneration of the cross-document synthesis from all analyzed documents. User overrides are preserved.
Response 200 OK:
{
"status": "success",
"synthesis": {
"summary": "Updated synthesis...",
"total_annual_cost": 3500.0,
"total_one_time_cost": 15000.0,
"risk_level": "medium",
"synthesis_data": { ... }
}
}
Analysis¶
Get Price Analysis¶
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
address |
string | Yes | Property address |
postal_code |
string | Yes | Postal code |
property_type |
string | No | apartment/house |
Response 200 OK:
{
"address": "56 Rue Notre-Dame des Champs",
"postal_code": "75006",
"simple_analysis": {
"sales_count": 3,
"avg_price_per_sqm": 12500,
"min_price": 600000,
"max_price": 1350000,
"sales": [...]
},
"trend_analysis": {
"price_trend": 2.5,
"projected_price_per_sqm": 12812,
"confidence": "high",
"data_points": 45
},
"recommendation": {
"fair_price_range": [800000, 900000],
"market_position": "slightly_above_market",
"negotiation_suggestion": "Consider offering 5-8% below asking"
}
}
Get Market Trends¶
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
postal_code |
string | Yes | Area postal code |
years |
int | No | Years of history (default: 5) |
Response 200 OK:
{
"postal_code": "75006",
"trends": [
{"year": 2021, "avg_price_per_sqm": 11200, "transactions": 245},
{"year": 2022, "avg_price_per_sqm": 11800, "transactions": 231},
{"year": 2023, "avg_price_per_sqm": 12100, "transactions": 198},
{"year": 2024, "avg_price_per_sqm": 12400, "transactions": 167},
{"year": 2025, "avg_price_per_sqm": 12650, "transactions": 89}
],
"yoy_change": 2.0,
"five_year_change": 12.9
}
Photos¶
Upload Photo¶
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | JPG/PNG image |
property_id |
int | Yes | Associated property |
room_type |
string | No | living_room, bedroom, etc. |
Request Redesign¶
Request Body:
{
"style": "modern",
"preferences": {
"color_scheme": "neutral",
"furniture_style": "minimalist"
}
}
Response 202 Accepted:
Get Redesign Status¶
Promote Redesign¶
Sets a redesign as the promoted (featured) redesign for a photo. The promoted redesign is displayed on the property overview page.
Response 200 OK:
{
"id": 1,
"filename": "living_room.jpg",
"room_type": "living_room",
"promoted_redesign": {
"id": 5,
"redesign_uuid": "abc-123",
"style_preset": "modern",
"prompt": "Modern Norwegian style...",
"presigned_url": "https://...",
"created_at": "2026-02-08T10:00:00Z"
}
}
Remove Promoted Redesign¶
Clears the promoted redesign from a photo.
Webhooks¶
MinIO Event Webhook¶
Internal endpoint for MinIO event notifications.
Error Responses¶
All endpoints return consistent error format:
| Status Code | Description |
|---|---|
| 400 | Bad Request - Invalid input |
| 401 | Unauthorized - Missing/invalid token |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found - Resource doesn't exist |
| 422 | Validation Error - Schema mismatch |
| 500 | Internal Server Error |
Rate Limiting¶
API requests are rate-limited per user:
- Standard: 100 requests/minute
- Bulk operations: 10 requests/minute
Exceeded limits return 429 Too Many Requests.