API Documentation

Access Immopix features programmatically. Edit images, manage properties and integrate AI enhancements into your workflow.

Want to use Immopix in an AI assistant without writing code? That is what the Immopix connector is for.

Note

The API is included in every package. Each API call uses credits, the same way an edit in the web interface does.

Authentication

The Immopix API uses token-based authentication. You can generate and revoke your API token in your account settings.

Send your token in the Authorization header with every request:

Authorization: Token your_api_token

Safe retries

If your connection drops, send the same request again with the same Idempotency-Key. Immopix then does not create the object or the edit job twice.

The optional header applies to POST /properties, POST /photos/upload and POST /photos/edit.

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
  • Use a new key for every operation you want to happen.
  • The key may contain 1 to 128 visible ASCII characters without spaces. A UUID works well.
  • A retry with the same data returns the stored response. The response header Idempotency-Replayed: true marks that response.
  • If you change the data, use a new key. Otherwise the API answers with 409 idempotency_conflict.
  • The API also stores business errors for that key. After you fix the cause, for example after buying credits, use a new key.

Error handling

On an error the API returns a JSON object with an error field:

{
  "error": {
    "code": "insufficient_credits",
    "message": "Keine Credits verfügbar"
  }
}

Error codes

Code Status Description
invalid_token401Token is missing or invalid
insufficient_credits402No credits available
invalid_preset400Invalid preset key
image_too_large400Image exceeds 20MB
image_required400Neither image_base64 nor image_url was provided
rate_limit_exceeded429Rate limit reached
invalid_idempotency_key400Key is empty, too long or contains spaces
idempotency_conflict409The same key was used with different data
idempotency_in_progress409The first call is still running. Retry the request after the time in the Retry-After header
conflict400presets and custom_prompt were sent together
not_found404Resource not found

Pagination

List endpoints support cursor-based pagination with the limit (default: 20, max: 100) and cursor parameters.

{
  "data": [...],
  "pagination": {
    "has_more": true,
    "next_cursor": "abc123"
  }
}

Rate limits

60 requests per minute per token. Above that: 429 status with a Retry-After header.

Image limits: max 20MB, formats JPEG/PNG/WebP, max 8192x8192 pixels


Tutorial: edit an image

The usual workflow: upload the image, wait for the job, fetch the result.

Python

import base64
import time
import uuid

import requests

API_TOKEN = "your_api_token"
headers = {"Authorization": f"Token {API_TOKEN}"}
edit_headers = {**headers, "Idempotency-Key": str(uuid.uuid4())}

# 1. Upload the image
with open("photo.jpg", "rb") as f:
    image_base64 = base64.b64encode(f.read()).decode()

response = requests.post(
    "https://immopix.ai/api/v1/photos/edit",
    headers=edit_headers,
    json={
        "image_base64": image_base64,
        "presets": "auto"  # or: ["sunshine", "optimize"]
    }
)
job = response.json()
job_id = job["job_id"]
print(f"Job started: {job_id}")

# 2. Poll the job status
while True:
    response = requests.get(
        f"https://immopix.ai/api/v1/jobs/{job_id}",
        headers=headers
    )
    job = response.json()

    if job["status"] == "completed":
        print(f"Done: {job['result_url']}")
        break
    elif job["status"] == "failed":
        print(f"Error: {job['error_message']}")
        break

    time.sleep(2)

cURL

# 1. Upload the image
curl -X POST "https://immopix.ai/api/v1/photos/edit" \
  -H "Authorization: Token your_api_token" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://example.com/photo.jpg",
    "presets": "auto"
  }'

# Response: {"job_id": "job_abc123", "status": "pending", ...}

# 2. Check the job status
curl "https://immopix.ai/api/v1/jobs/job_abc123" \
  -H "Authorization: Token your_api_token"

Your own instructions (custom_prompt)

Instead of presets you can also pass your own instructions:

curl -X POST "https://immopix.ai/api/v1/photos/edit" \
  -H "Authorization: Token your_api_token" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://example.com/photo.jpg",
    "custom_prompt": "Make the lawn green, the sky blue, remove the bins"
  }'

Tip: use webhooks instead of polling to get notified when a job finishes.


Properties

Properties are containers for the photos of one property.

GET /api/v1/properties

Lists all properties.

Query Parameters

queryFilters by property name. Any part of the name matches and upper and lower case are the same.
{
  "data": [
    {
      "id": "prp_abc123",
      "name": "Musterstraße 42",
      "created_at": "2024-01-15T10:30:00Z",
      "photo_count": 12
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
POST /api/v1/properties

Creates a new property.

Request Body

name *Name of the property
{
  "id": "prp_abc123",
  "name": "Musterstraße 42",
  "created_at": "2024-01-15T10:30:00Z",
  "photo_count": 0
}
GET /api/v1/properties/{id}

Property details with a nested photo list.

{
  "id": "prp_abc123",
  "name": "Musterstraße 42",
  "created_at": "2024-01-15T10:30:00Z",
  "photo_count": 3,
  "photos": [
    {
      "id": "pht_xyz789",
      "title": "Wohnzimmer",
      "image_type": "indoor",
      "thumbnail_url": "https://..."
    }
  ]
}
PATCH /api/v1/properties/{id}

Updates a property. Parameter: name

DELETE /api/v1/properties/{id}

Deletes the property and all photos that belong to it. This cannot be undone.

Photos

GET /api/v1/properties/{id}/photos

Lists all photos of a property.

Query Parameters

image_typeFilters by image type. Values: indoor, outdoor, floorplan, other. Any other value returns 400 validation_error.
{
  "data": [
    {
      "id": "pht_xyz789",
      "title": "Wohnzimmer",
      "image_type": "indoor",
      "current_url": "https://...",
      "thumbnail_url": "https://...",
      "created_at": "2024-01-15T10:30:00Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
GET /api/v1/photos/{id}

Photo details with revision history and URLs.

{
  "id": "pht_xyz789",
  "title": "Wohnzimmer",
  "image_type": "indoor",
  "current_url": "https://...",
  "thumbnail_url": "https://...",
  "revisions": [
    {
      "id": "rev_abc123",
      "version": 1,
      "is_original": false,
      "url": "https://...",
      "created_at": "2024-01-15T10:35:00Z"
    }
  ],
  "created_at": "2024-01-15T10:30:00Z"
}
Note: image URLs are signed and expire after 60 minutes.
POST /api/v1/photos/edit

Sends an image for AI enhancement.

Request Body

image_base64Base64-encoded image data
image_urlURL of the image (publicly reachable)
photo_idEdit an existing photo again
presetsArray of preset keys or "auto". GET /presets returns all keys.
custom_promptYour own instructions (max. 1000 characters). Core rules are applied automatically.
property_idLink the photo to a property
edit_mode"outdoor" (default), "indoor" or "floorplan"
lock_camera_angleKeep the camera angle (boolean)
room_typeRoom type for furnishing. Values: living_room, bedroom, kitchen, bathroom, kids_room, dining_room, hallway, office, garage, basement, dressing_room. Detected automatically when you leave it out.
furniture_styleFurniture style for staging. Values: modern, skandinavisch, klassisch, landhausstil, industriell. Detected automatically when you leave it out.
intensity"normal" (default) or "subtle" for a more restrained edit.
resolution"2K" (default) or "4K". 4K uses 2 credits.

You must provide image_base64, image_url or photo_id.

presets and custom_prompt are mutually exclusive, so use one of the two. Without either, the default presets apply.

The current values for presets, room_type and furniture_style come from GET /presets. Note that furniture_style keys are German words.

Available presets

Use "auto" for automatic detection, or pick specific presets. GET /presets returns the complete, current list.

Response

{
  "job_id": "job_abc123",
  "photo_id": "pht_xyz789",
  "status": "pending"
}
POST /api/v1/photos/upload

Uploads a photo without starting an edit. No credits are used. The photo is stored, categorised and ready to edit in the dashboard.

Request Body

image_base64Base64-encoded image data
image_urlURL of the image (publicly reachable)
property_idLink the photo to a property (optional, default: "API Uploads")

You must provide image_base64 or image_url (exactly one).

Response (201)

{
  "photo_id": "pht_xyz789",
  "property_id": "prp_abc123",
  "property_url": "https://immopix.ai/app/property/prp_abc123/"
}
DELETE /api/v1/photos/{id}

Deletes the photo and all its revisions.

Jobs

GET /api/v1/jobs/{job_id}

Check the job status. Status: pending | processing | completed | failed

{
  "job_id": "job_abc123",
  "photo_id": "pht_xyz789",
  "status": "completed",
  "presets": ["sunshine", "optimize"],
  "result_url": "https://...",
  "width": 1920,
  "height": 1080,
  "error_message": null,
  "created_at": "2024-01-15T10:30:00Z"
}
result_url, width, height only with status: "completed". error_message only with status: "failed".

Presets

This endpoint returns every option an edit accepts. That keeps your integration current, without hard-coding the values.

GET /api/v1/presets

Lists all presets, furniture styles and room types.

The key values are stable identifiers, while label and description are returned in German.

Response

{
  "presets": [
    {
      "key": "optimize",
      "edit_mode": "outdoor",
      "label": "Belichtung optimieren",
      "description": "Helligkeit, Farben & Kontrast verbessern"
    },
    {
      "key": "staging",
      "edit_mode": "indoor",
      "label": "Virtuell möblieren",
      "description": "Leere Räume mit passenden Möbeln einrichten"
    },
    {
      "key": "floorplan_3d",
      "edit_mode": "floorplan",
      "label": "3D Grundriss",
      "description": "2D-Grundriss in eine 3D-Ansicht umwandeln"
    }
    ...
  ],
  "furniture_styles": ["modern", "skandinavisch", ...],
  "room_types": [
    {"key": "living_room", "label": "Wohnzimmer"},
    {"key": "bedroom", "label": "Schlafzimmer"}
    ...
  ]
}

You put the key values straight into POST /api/v1/photos/edit: presets from presets, room_type from room_types and furniture_style from furniture_styles.

edit_mode assigns each preset to one mode: outdoor, indoor or floorplan. Use only presets from the mode you send in the request.

Without presets, or with "auto", Immopix picks suitable presets for the image.


Webhooks

You configure webhook URLs in your account settings. Events: job.completed, job.failed

{
  "event": "job.completed",
  "job": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "photo_id": "pht_xyz789",
    "status": "completed",
    "presets": ["sunshine", "optimize"],
    "result_url": "https://...",
    "width": 1920,
    "height": 1080,
    "created_at": "2024-01-15T10:30:00Z"
  }
}
Retry policy: 3 attempts with exponential backoff (1min, 5min, 30min). After 3 failures the webhook is disabled.

Signature verification

Every webhook carries an X-Immopix-Signature header (HMAC-SHA256).

Python
import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# In your webhook handler:
signature = request.headers.get("X-Immopix-Signature")
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
    return "Invalid signature", 401
Node.js
const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Credits

The API uses the same credit system as the web interface. You find the details on our pricing page. An insufficient_credits error means your credits are used up.