Examples

Complete integration examples using curl, JavaScript, and Python.

This page walks through a complete photo shoot generation workflow in three languages: curl, JavaScript (fetch), and Python (requests). Each example follows the same steps:

  1. List your products to pick one for the shoot.
  2. List approved AI models and sceneries.
  3. Register a photo shoot generation combining a product, model, and scenery.
  4. Poll the image-picker endpoint until results are ready.
  5. Review and approve the generated images.

Common Setup

Every request requires two things:

  • Base URLhttps://app.qamera.ai (replace with your own URL for self-hosted deployments).
  • Authentication — Pass your API key in the X-Api-Key header.
X-Api-Key: mk_live_abc123.secret456

You can generate an API key from Settings > API Keys in your team workspace. The key is shown only once, so copy it immediately.

GET endpoints are free and never consume credits. POST endpoints that trigger content generation reserve credits up front and consume them after processing completes.


curl Examples

Step 1 — List Products

Retrieve all products uploaded to your account.

curl -s -X GET "https://app.qamera.ai/api/external/products" \
  -H "X-Api-Key: mk_live_abc123.secret456"

Response:

{
  "products": [
    {
      "id": "rec_product_001",
      "name": "White T-Shirt Front",
      "thumbnail": "https://cdn.example.com/products/thumb_001.jpg",
      "status": "UPLOADED",
      "createdAt": "2026-03-20T14:30:00.000Z"
    },
    {
      "id": "rec_product_002",
      "name": "Leather Handbag",
      "thumbnail": "https://cdn.example.com/products/thumb_002.jpg",
      "status": "UPLOADED",
      "createdAt": "2026-03-21T09:15:00.000Z"
    }
  ],
  "count": 2
}

Pick a product ID to use in the photo shoot. In these examples we will use rec_product_001.

Step 2 — List Approved Models

Fetch only models that have been approved for use.

curl -s -X GET "https://app.qamera.ai/api/external/models?statusFilter=approved" \
  -H "X-Api-Key: mk_live_abc123.secret456"

Response:

{
  "models": [
    {
      "id": "rec_model_101",
      "name": "Sophie — Casual",
      "thumbnail": "https://cdn.example.com/models/thumb_101.jpg",
      "status": "DONE",
      "voting": "APPROVED",
      "createdAt": "2026-03-18T10:00:00.000Z"
    }
  ],
  "count": 1
}

Step 3 — List Approved Sceneries

curl -s -X GET "https://app.qamera.ai/api/external/sceneries?statusFilter=approved" \
  -H "X-Api-Key: mk_live_abc123.secret456"

Response:

{
  "sceneries": [
    {
      "id": "rec_scenery_201",
      "name": "Bright Studio",
      "thumbnail": "https://cdn.example.com/sceneries/thumb_201.jpg",
      "status": "DONE",
      "voting": "APPROVED",
      "createdAt": "2026-03-19T11:45:00.000Z"
    }
  ],
  "count": 1
}

Step 4 — Register a Photo Shoot

Combine the product, model, and scenery to start a generation. This endpoint reserves credits.

curl -s -X POST "https://app.qamera.ai/api/external/image-picker/register-ideas" \
  -H "X-Api-Key: mk_live_abc123.secret456" \
  -H "Content-Type: application/json" \
  -d '{
    "config": {
      "product": { "id": "rec_product_001" },
      "model": { "id": "rec_model_101" },
      "scenery": { "id": "rec_scenery_201" },
      "industry": "fashion",
      "suggestions": "bright lighting, clean composition"
    },
    "count": 4
  }'

Response:

{
  "createdRecordIds": ["rec_gen_301", "rec_gen_302", "rec_gen_303", "rec_gen_304"],
  "batchId": "batch_abc123",
  "orderId": "ord_xyz789",
  "reservationId": "res_def456",
  "creditsReserved": 120
}

Save the createdRecordIds — you will use them to check progress.

Step 5 — Poll for Results

Generation is asynchronous. Poll the image-picker endpoint, filtering by your product, until images move to the DONE status.

curl -s -X GET "https://app.qamera.ai/api/external/image-picker?productId=rec_product_001" \
  -H "X-Api-Key: mk_live_abc123.secret456"

Response (while still processing):

{
  "images": [
    {
      "id": "rec_gen_301",
      "name": "White T-Shirt — Variant 1",
      "thumbnail": "",
      "status": "INPROGRESS",
      "voting": null,
      "createdAt": "2026-03-27T12:00:00.000Z"
    }
  ],
  "count": 1
}

Wait a minute and call the same endpoint again. Once status is DONE, the thumbnail field contains the image URL.

Step 6 — Approve or Reject Results

You can approve images using the packshot update endpoint. For photo shoot images that you want to keep, approve them so they appear in filtered views.

# Approve an image
curl -s -X POST "https://app.qamera.ai/api/external/packshots/update-packshot" \
  -H "X-Api-Key: mk_live_abc123.secret456" \
  -H "Content-Type: application/json" \
  -d '{
    "recordId": "rec_gen_301",
    "Voting": "APPROVED"
  }'

Response:

{
  "success": true,
  "message": "Packshot voting updated successfully",
  "recordId": "rec_gen_301"
}

To reject an image (which archives it):

curl -s -X POST "https://app.qamera.ai/api/external/packshots/update-packshot" \
  -H "X-Api-Key: mk_live_abc123.secret456" \
  -H "Content-Type: application/json" \
  -d '{
    "recordId": "rec_gen_302",
    "Voting": "REJECTED"
  }'

JavaScript (fetch) Example

The following example uses the browser/Node.js fetch API to perform the same workflow.

Helper Function

const BASE_URL = "https://app.qamera.ai";
const API_KEY = "mk_live_abc123.secret456";

async function qameraFetch(path, options = {}) {
  const url = new URL(path, BASE_URL);

  if (options.params) {
    Object.entries(options.params).forEach(([key, value]) =>
      url.searchParams.set(key, value)
    );
  }

  const response = await fetch(url.toString(), {
    method: options.method || "GET",
    headers: {
      "X-Api-Key": API_KEY,
      ...(options.body ? { "Content-Type": "application/json" } : {}),
    },
    ...(options.body ? { body: JSON.stringify(options.body) } : {}),
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(
      `API error ${response.status}: ${error.error || response.statusText}`
    );
  }

  return response.json();
}

Full Workflow

async function runPhotoShootWorkflow() {
  // 1. List products
  const { products } = await qameraFetch("/api/external/products");
  console.log(`Found ${products.length} products`);

  const product = products[0];
  if (!product) throw new Error("No products found. Upload one first.");
  console.log(`Using product: ${product.name} (${product.id})`);

  // 2. List approved models
  const { models } = await qameraFetch("/api/external/models", {
    params: { statusFilter: "approved" },
  });
  console.log(`Found ${models.length} approved models`);

  // 3. List approved sceneries
  const { sceneries } = await qameraFetch("/api/external/sceneries", {
    params: { statusFilter: "approved" },
  });
  console.log(`Found ${sceneries.length} approved sceneries`);

  // 4. Register photo shoot generation
  const registration = await qameraFetch(
    "/api/external/image-picker/register-ideas",
    {
      method: "POST",
      body: {
        config: {
          product: { id: product.id },
          model: models[0] ? { id: models[0].id } : undefined,
          scenery: sceneries[0] ? { id: sceneries[0].id } : undefined,
          industry: "fashion",
          suggestions: "bright lighting, clean composition",
        },
        count: 4,
      },
    }
  );

  console.log(`Generation started — order ${registration.orderId}`);
  console.log(`Credits reserved: ${registration.creditsReserved}`);

  // 5. Poll until results are ready
  const generatedIds = new Set(registration.createdRecordIds);
  let attempts = 0;
  const maxAttempts = 30;

  while (attempts < maxAttempts) {
    await new Promise((resolve) => setTimeout(resolve, 10_000));
    attempts++;

    const { images } = await qameraFetch("/api/external/image-picker", {
      params: { productId: product.id },
    });

    const ours = images.filter((img) => generatedIds.has(img.id));
    const done = ours.filter((img) => img.status === "DONE");

    console.log(`Poll ${attempts}: ${done.length}/${ours.length} done`);

    if (done.length === ours.length && done.length > 0) {
      console.log("All images ready!");

      // 6. Approve the first result
      await qameraFetch("/api/external/packshots/update-packshot", {
        method: "POST",
        body: {
          recordId: done[0].id,
          Voting: "APPROVED",
        },
      });
      console.log(`Approved image ${done[0].id}`);

      return done;
    }
  }

  throw new Error("Generation timed out after 5 minutes");
}

runPhotoShootWorkflow()
  .then((images) => console.log("Completed:", images))
  .catch((err) => console.error("Failed:", err.message));

Python (requests) Example

The following example uses the requests library (pip install requests).

Session Setup

import time
import requests

BASE_URL = "https://app.qamera.ai"
API_KEY = "mk_live_abc123.secret456"

session = requests.Session()
session.headers.update({"X-Api-Key": API_KEY})

Full Workflow

def run_photo_shoot_workflow():
    # 1. List products
    resp = session.get(f"{BASE_URL}/api/external/products")
    resp.raise_for_status()
    products = resp.json()["products"]
    print(f"Found {len(products)} products")

    if not products:
        raise RuntimeError("No products found. Upload one first.")

    product = products[0]
    print(f"Using product: {product['name']} ({product['id']})")

    # 2. List approved models
    resp = session.get(
        f"{BASE_URL}/api/external/models",
        params={"statusFilter": "approved"},
    )
    resp.raise_for_status()
    models = resp.json()["models"]
    print(f"Found {len(models)} approved models")

    # 3. List approved sceneries
    resp = session.get(
        f"{BASE_URL}/api/external/sceneries",
        params={"statusFilter": "approved"},
    )
    resp.raise_for_status()
    sceneries = resp.json()["sceneries"]
    print(f"Found {len(sceneries)} approved sceneries")

    # 4. Register photo shoot generation
    config = {
        "product": {"id": product["id"]},
        "industry": "fashion",
        "suggestions": "bright lighting, clean composition",
    }
    if models:
        config["model"] = {"id": models[0]["id"]}
    if sceneries:
        config["scenery"] = {"id": sceneries[0]["id"]}

    resp = session.post(
        f"{BASE_URL}/api/external/image-picker/register-ideas",
        json={"config": config, "count": 4},
    )
    resp.raise_for_status()
    registration = resp.json()

    print(f"Generation started — order {registration['orderId']}")
    print(f"Credits reserved: {registration['creditsReserved']}")

    # 5. Poll until results are ready
    generated_ids = set(registration["createdRecordIds"])
    max_attempts = 30

    for attempt in range(1, max_attempts + 1):
        time.sleep(10)

        resp = session.get(
            f"{BASE_URL}/api/external/image-picker",
            params={"productId": product["id"]},
        )
        resp.raise_for_status()
        images = resp.json()["images"]

        ours = [img for img in images if img["id"] in generated_ids]
        done = [img for img in ours if img["status"] == "DONE"]

        print(f"Poll {attempt}: {len(done)}/{len(ours)} done")

        if len(done) == len(ours) and done:
            print("All images ready!")

            # 6. Approve the first result
            resp = session.post(
                f"{BASE_URL}/api/external/packshots/update-packshot",
                json={
                    "recordId": done[0]["id"],
                    "Voting": "APPROVED",
                },
            )
            resp.raise_for_status()
            print(f"Approved image {done[0]['id']}")

            return done

    raise TimeoutError("Generation timed out after 5 minutes")


if __name__ == "__main__":
    try:
        results = run_photo_shoot_workflow()
        for img in results:
            print(f"  {img['name']}: {img['thumbnail']}")
    except Exception as e:
        print(f"Error: {e}")