{
  "help": {
    "id": "bulk_stage",
    "name": "Bulk Staging",
    "what_it_does": "Stage up to 50 empty rooms in one job, in one style.",
    "about": "Stages a whole listing at once: up to 50 empty rooms in one job, every room in the same style. The photos are worked on a few at a time, and each result is ready as soon as its photo is done.",
    "the_photo": "Photos of empty rooms. For furnished rooms, run bulk_unstage first.",
    "credits": 2,
    "credits_label": "2 credits per photo",
    "pricing": "2 credits per photo, each refunded if that photo fails.",
    "output_kind": "images",
    "output": "One image for each photo (or each reference), saved to your Library. They are in job.outputs, each with its own status; each file is at outputs[].output.url.",
    "response": "Answers 202 at once with the job id; fetch GET /api/v1/jobs/{id} until status is \"done\". Send \"wait\": true to wait up to 50 s for it instead.",
    "waits_for_result": false,
    "how_to_use": [
      {
        "title": "Keep your key in an environment variable",
        "detail": "Make a key on the Developers page. Every request sends it in the Authorization header.",
        "command": "export HOUSEKRAFT_KEY=hk_live_…"
      },
      {
        "title": "Upload your photos, one request each",
        "detail": "The answer has asset.id: use it in the run. You can also skip this step and put an https:// image URL straight into the run.",
        "command": "curl -s -X POST https://app.housekraft.co/api/v1/uploads \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -F image=@room-1.jpg"
      },
      {
        "title": "Run Bulk Staging",
        "detail": "Answers straight away with the job, which keeps running.",
        "command": "curl -s -X POST https://app.housekraft.co/api/v1/tools/bulk_stage/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"images\": [\"<photo 1>\", \"<photo 2>\", \"<photo 3>\"],\n  \"prompt\": \"Scandinavian, light oak and linen\"\n}'"
      },
      {
        "title": "Get the results",
        "detail": "Fetch the job every few seconds until status is \"done\". Each result is in job.outputs, in order, with its own status; its file is at output.url. job.progress counts them.",
        "command": "curl -s https://app.housekraft.co/api/v1/jobs/<job id> \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\""
      }
    ],
    "inputs": [
      {
        "name": "images",
        "type": "images",
        "required": true,
        "description": "1–50 photos of empty rooms. Each gets its own result.",
        "help": "An asset id (from POST /api/v1/uploads or your Library) or an https:// image URL.",
        "min_items": 1,
        "max_items": 50
      },
      {
        "name": "prompt",
        "type": "text",
        "required": false,
        "description": "Optional. The style for every room, e.g. \"Scandinavian, light oak\"."
      },
      {
        "name": "reference_image",
        "type": "image",
        "required": false,
        "description": "Optional. A theme or style photo: every room is staged in its style.",
        "help": "An asset id (from POST /api/v1/uploads or your Library) or an https:// image URL."
      },
      {
        "name": "theme",
        "type": "catalog",
        "required": false,
        "description": "Or a theme from HouseKraft's catalogue, by name or id (GET /api/v1/catalog/themes): every room is staged in it.",
        "catalog": "themes",
        "help": "A name or id from the themes catalogue (GET /api/v1/catalog/themes). Several matches → the answer lists them to choose from."
      },
      {
        "name": "theme_category",
        "type": "catalog_group",
        "required": false,
        "description": "Or a theme category (GET /api/v1/catalog/themes/groups), e.g. \"Japandi\": one theme from it is used, the first, or a random one with random_theme.",
        "catalog": "themes",
        "help": "A category name from GET /api/v1/catalog/themes/groups."
      },
      {
        "name": "random_theme",
        "type": "boolean",
        "required": false,
        "description": "With theme_category: pick the theme at random instead of the first."
      },
      {
        "name": "sku",
        "type": "text",
        "required": false,
        "description": "Optional. A listing id saved on every result, to find them later (letters, numbers, - _ .). One is made up if you leave it out."
      }
    ],
    "run_options": {
      "wait": "true or false. Whether the run request waits for the result (up to 50 s). Default for this tool: false.",
      "override_check": "true to run even if the image check rejects the photo. Charged as usual."
    },
    "images": "Each image field takes an asset id (from POST /api/v1/uploads, or an earlier result's output.id) or an https:// URL. JPG, PNG or WEBP, up to 16 MB.",
    "same_as_website": "This tool runs the website's own code: the same prompt, model, settings, price and Library record as the website.",
    "example_request": {
      "title": "Stage three empty rooms in one style",
      "method": "POST",
      "url": "https://app.housekraft.co/api/v1/tools/bulk_stage/run",
      "body": {
        "images": [
          "<photo 1>",
          "<photo 2>",
          "<photo 3>"
        ],
        "prompt": "Scandinavian, light oak and linen"
      }
    },
    "example_curl": "curl -s -X POST https://app.housekraft.co/api/v1/tools/bulk_stage/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"images\": [\"<photo 1>\", \"<photo 2>\", \"<photo 3>\"],\n  \"prompt\": \"Scandinavian, light oak and linen\"\n}'",
    "example_python": "# pip install requests\nimport os\nimport time\n\nimport requests\n\nAPI = \"https://app.housekraft.co\"\nHEADERS = {\"Authorization\": f\"Bearer {os.environ['HOUSEKRAFT_KEY']}\"}\n\n\ndef call(method, path, **kwargs):\n    response = requests.request(method, API + path, headers=HEADERS, timeout=90, **kwargs)\n    data = response.json()\n    if response.status_code >= 400:\n        raise RuntimeError(f\"{data['error']['code']}: {data['error']['message']}\")\n    return data\n\n\ndef upload(path):\n    with open(path, \"rb\") as f:\n        return call(\"POST\", \"/api/v1/uploads\", files={\"image\": f})[\"asset\"][\"id\"]\n\n\n# 1. Upload the photos\nphotos = [upload(path) for path in [\"room-1.jpg\", \"room-2.jpg\", \"room-3.jpg\"]]\n\n# 2. Run Bulk Staging\njob = call(\"POST\", \"/api/v1/tools/bulk_stage/run\", json={\n    \"images\": photos,\n    \"prompt\": \"Scandinavian, light oak and linen\",\n})[\"job\"]\n\n# 3. If it is still running, check again until it finishes\nwhile job[\"status\"] == \"running\":\n    time.sleep(job.get(\"poll_after_seconds\", 3))\n    job = call(\"GET\", f\"/api/v1/jobs/{job['id']}\")[\"job\"]\n\nif job[\"status\"] == \"done\":\n    for item in job[\"outputs\"]:          # one per photo or reference, in order\n        print(item[\"index\"], item[\"status\"], item[\"output\"][\"url\"] if item[\"output\"] else item[\"error\"])\nelse:\n    print(\"Failed:\", job[\"error\"][\"message\"])\n",
    "example_javascript": "// Node 18 or newer. Save as run.mjs, then: node run.mjs\nimport { readFile } from \"node:fs/promises\";\n\nconst API = \"https://app.housekraft.co\";\nconst HEADERS = { Authorization: `Bearer ${process.env.HOUSEKRAFT_KEY}` };\n\nasync function call(method, path, body) {\n  const options = { method, headers: { ...HEADERS } };\n  if (body instanceof FormData) {\n    options.body = body;\n  } else if (body) {\n    options.headers[\"Content-Type\"] = \"application/json\";\n    options.body = JSON.stringify(body);\n  }\n  const response = await fetch(API + path, options);\n  const data = await response.json();\n  if (!response.ok) throw new Error(`${data.error.code}: ${data.error.message}`);\n  return data;\n}\n\nasync function upload(path) {\n  const form = new FormData();\n  form.append(\"image\", new Blob([await readFile(path)]), path);\n  return (await call(\"POST\", \"/api/v1/uploads\", form)).asset.id;\n}\n\n// 1. Upload the photos\nconst photos = await Promise.all([\"room-1.jpg\", \"room-2.jpg\", \"room-3.jpg\"].map(upload));\n\n// 2. Run Bulk Staging\nlet { job } = await call(\"POST\", \"/api/v1/tools/bulk_stage/run\", {\n  \"images\": photos,\n  \"prompt\": \"Scandinavian, light oak and linen\",\n});\n\n// 3. If it is still running, check again until it finishes\nwhile (job.status === \"running\") {\n  await new Promise((resolve) => setTimeout(resolve, (job.poll_after_seconds || 3) * 1000));\n  ({ job } = await call(\"GET\", `/api/v1/jobs/${job.id}`));\n}\n\nif (job.status === \"done\") {\n  // one per photo or reference, in order\n  for (const item of job.outputs) console.log(item.index, item.status, item.output ? item.output.url : item.error);\n} else {\n  console.log(\"Failed:\", job.error.message);\n}\n",
    "more_examples": [
      {
        "title": "Every room in a catalogue theme",
        "body": {
          "images": [
            "<photo 1>",
            "<photo 2>",
            "<photo 3>"
          ],
          "theme": "Japandi Calm"
        },
        "curl": "curl -s -X POST https://app.housekraft.co/api/v1/tools/bulk_stage/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"images\": [\"<photo 1>\", \"<photo 2>\", \"<photo 3>\"],\n  \"theme\": \"Japandi Calm\"\n}'"
      },
      {
        "title": "Every room in a theme from a category",
        "body": {
          "images": [
            "<photo 1>",
            "<photo 2>"
          ],
          "theme_category": "Scandinavian"
        },
        "curl": "curl -s -X POST https://app.housekraft.co/api/v1/tools/bulk_stage/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"images\": [\"<photo 1>\", \"<photo 2>\"], \"theme_category\": \"Scandinavian\"}'"
      },
      {
        "title": "Stage every room in the style of a theme photo",
        "body": {
          "images": [
            "<photo 1>",
            "<photo 2>"
          ],
          "reference_image": "<asset id of a theme photo>"
        },
        "curl": "curl -s -X POST https://app.housekraft.co/api/v1/tools/bulk_stage/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"images\": [\"<photo 1>\", \"<photo 2>\"],\n  \"reference_image\": \"<asset id of a theme photo>\"\n}'"
      },
      {
        "title": "Tag the results with a listing id",
        "body": {
          "images": [
            "<photo 1>",
            "<photo 2>"
          ],
          "prompt": "modern farmhouse",
          "sku": "MLS-2291"
        },
        "curl": "curl -s -X POST https://app.housekraft.co/api/v1/tools/bulk_stage/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"images\": [\"<photo 1>\", \"<photo 2>\"],\n  \"prompt\": \"modern farmhouse\",\n  \"sku\": \"MLS-2291\"\n}'"
      }
    ],
    "responses": [
      {
        "title": "Started",
        "status": 202,
        "when": "The run answers like this straight away. Fetch check.url every few seconds until status is \"done\".",
        "body": {
          "job": {
            "id": "7c1e9b02-5d4a-4e8f-b6a3-2f90d1c4e5a8",
            "tool": "bulk_stage",
            "status": "running",
            "created_at": 1793700000,
            "finished_at": null,
            "credits": {
              "charged": 4,
              "refunded": false,
              "refunded_credits": 0
            },
            "input": {
              "images": [
                "<photo 1>",
                "<photo 2>",
                "<photo 3>"
              ],
              "prompt": "Scandinavian, light oak and linen"
            },
            "warnings": [],
            "check_overridden": false,
            "output": null,
            "error": null,
            "progress": {
              "total": 3,
              "done": 1,
              "failed": 0
            },
            "outputs": [
              {
                "index": 0,
                "status": "done",
                "output": {
                  "id": "a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b5",
                  "type": "generated",
                  "tool": "stage",
                  "url": "https://…/generated/a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b5.jpg",
                  "thumbnail_url": "https://…/thumbnails/a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b5.jpg",
                  "width": 1024,
                  "height": 1024,
                  "created_at": 1793700025,
                  "kind": "image"
                },
                "error": null
              },
              {
                "index": 1,
                "status": "running",
                "output": null,
                "error": null
              },
              {
                "index": 2,
                "status": "waiting",
                "output": null,
                "error": null
              }
            ],
            "sku": "HK-3F9A1C2B",
            "poll_after_seconds": 3
          },
          "check": {
            "message": "Started. The job carries on; get the result from this URL. Nothing is lost if this connection closes, and GET /api/v1/jobs lists your recent jobs.",
            "url": "https://app.housekraft.co/api/v1/jobs/7c1e9b02-5d4a-4e8f-b6a3-2f90d1c4e5a8",
            "command": "curl -s https://app.housekraft.co/api/v1/jobs/7c1e9b02-5d4a-4e8f-b6a3-2f90d1c4e5a8 -H \"Authorization: Bearer $HOUSEKRAFT_KEY\""
          }
        }
      },
      {
        "title": "Finished",
        "status": 200,
        "when": "GET /api/v1/jobs/<id> answers like this once the job is done.",
        "body": {
          "job": {
            "id": "7c1e9b02-5d4a-4e8f-b6a3-2f90d1c4e5a8",
            "tool": "bulk_stage",
            "status": "done",
            "created_at": 1793700000,
            "finished_at": 1793700025,
            "credits": {
              "charged": 6,
              "refunded": false,
              "refunded_credits": 0
            },
            "input": {
              "images": [
                "<photo 1>",
                "<photo 2>",
                "<photo 3>"
              ],
              "prompt": "Scandinavian, light oak and linen"
            },
            "warnings": [],
            "check_overridden": false,
            "output": null,
            "error": null,
            "progress": {
              "total": 3,
              "done": 3,
              "failed": 0
            },
            "outputs": [
              {
                "index": 0,
                "status": "done",
                "output": {
                  "id": "a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b5",
                  "type": "generated",
                  "tool": "stage",
                  "url": "https://…/generated/a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b5.jpg",
                  "thumbnail_url": "https://…/thumbnails/a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b5.jpg",
                  "width": 1024,
                  "height": 1024,
                  "created_at": 1793700025,
                  "kind": "image"
                },
                "error": null
              },
              {
                "index": 1,
                "status": "done",
                "output": {
                  "id": "a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b6",
                  "type": "generated",
                  "tool": "stage",
                  "url": "https://…/generated/a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b6.jpg",
                  "thumbnail_url": "https://…/thumbnails/a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b6.jpg",
                  "width": 1024,
                  "height": 1024,
                  "created_at": 1793700025,
                  "kind": "image"
                },
                "error": null
              },
              {
                "index": 2,
                "status": "done",
                "output": {
                  "id": "a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b7",
                  "type": "generated",
                  "tool": "stage",
                  "url": "https://…/generated/a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b7.jpg",
                  "thumbnail_url": "https://…/thumbnails/a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b7.jpg",
                  "width": 1024,
                  "height": 1024,
                  "created_at": 1793700025,
                  "kind": "image"
                },
                "error": null
              }
            ],
            "sku": "HK-3F9A1C2B"
          }
        }
      },
      {
        "title": "Failed",
        "status": 200,
        "when": "GET /api/v1/jobs/<id> answers like this if no photo could be finished. Each photo's credits are refunded automatically; a job where only some failed is \"done\", with those photos marked failed in outputs.",
        "body": {
          "job": {
            "id": "7c1e9b02-5d4a-4e8f-b6a3-2f90d1c4e5a8",
            "tool": "bulk_stage",
            "status": "failed",
            "created_at": 1793700000,
            "finished_at": 1793700025,
            "credits": {
              "charged": 0,
              "refunded": true,
              "refunded_credits": 6
            },
            "input": {
              "images": [
                "<photo 1>",
                "<photo 2>",
                "<photo 3>"
              ],
              "prompt": "Scandinavian, light oak and linen"
            },
            "warnings": [],
            "check_overridden": false,
            "output": null,
            "error": {
              "message": "None of the photos could be finished. Their credits have been refunded."
            },
            "progress": {
              "total": 3,
              "done": 0,
              "failed": 3
            },
            "outputs": [
              {
                "index": 0,
                "status": "failed",
                "output": null,
                "error": "The image model could not finish this photo."
              },
              {
                "index": 1,
                "status": "failed",
                "output": null,
                "error": "The image model could not finish this photo."
              },
              {
                "index": 2,
                "status": "failed",
                "output": null,
                "error": "The image model could not finish this photo."
              }
            ],
            "sku": "HK-3F9A1C2B"
          }
        }
      }
    ],
    "checks": [
      "Image check: the website's own check of whether each room photo suits this tool. If any does not, you get 422 image_check_failed listing each photo (photos[]) with the reason and a fix, and nothing runs or is charged. Send the same request with \"override_check\": true to run them anyway.",
      "Misuse check: every photo must show a room, building or plan (references may also be materials or swatches), and the request must be about the space, not people. A refusal is 422 request_not_allowed and cannot be overridden."
    ],
    "errors": [
      {
        "status": 400,
        "code": "invalid_request",
        "meaning": "A field is missing, unknown, or the wrong type. The message names the field.",
        "what_to_do": "Fix the field and send again. Nothing was charged."
      },
      {
        "status": 400,
        "code": "needs_more",
        "meaning": "The run is missing what the tool needs to be worth running (a style, a colour, a camera move, a video length…). message is the question to ask; suggestions lists good answers.",
        "what_to_do": "Ask the user, then send the run again with the answer. Nothing ran and nothing was charged."
      },
      {
        "status": 400,
        "code": "choose_one",
        "meaning": "A catalogue name matches several items. options lists them (id, name, details).",
        "what_to_do": "Ask which one, then send its name or id. Nothing was charged."
      },
      {
        "status": 400,
        "code": "not_in_catalog",
        "meaning": "No catalogue item (or category) matches that name.",
        "what_to_do": "Search the catalogue (GET /api/v1/catalog/{catalog}?search=…) and use a name or id from it."
      },
      {
        "status": 402,
        "code": "insufficient_credits",
        "meaning": "Not enough credits for this run.",
        "what_to_do": "Buy credits or upgrade your plan, then try again."
      },
      {
        "status": 409,
        "code": "bulk_job_running",
        "meaning": "You already have a bulk job running (on the website or the API).",
        "what_to_do": "Wait for it to finish, then start the next one."
      },
      {
        "status": 404,
        "code": "image_not_found",
        "meaning": "An asset id is not in your account, or it has expired.",
        "what_to_do": "Upload the photo again with POST /api/v1/uploads."
      },
      {
        "status": 422,
        "code": "image_check_failed",
        "meaning": "The image check thinks this photo does not suit this tool. The answer has the reason (message, reasons) and a suggested fix.",
        "what_to_do": "Use a better photo, or send the same request with \"override_check\": true to run it anyway. Nothing was charged."
      },
      {
        "status": 422,
        "code": "request_not_allowed",
        "meaning": "The photo is not of a room, building or plan, or the request is about people rather than the space.",
        "what_to_do": "This cannot be overridden. Nothing was charged."
      },
      {
        "status": 429,
        "code": "too_many_running_jobs",
        "meaning": "You already have 3 jobs running.",
        "what_to_do": "Wait for one to finish, then start the next."
      },
      {
        "status": 502,
        "code": "provider_error",
        "meaning": "The image model could not be reached.",
        "what_to_do": "Try again. If credits were taken, they were refunded (the message says so)."
      }
    ],
    "tips": [
      "Have the photos in a .zip? Upload it to POST /api/v1/uploads and send the asset ids it answers with (assets[].id).",
      "Each photo is charged when it starts and refunded if it fails: job.credits shows the total.",
      "job.outputs lists every photo in the order you sent them, each with its own status and result.",
      "One bulk job at a time per account, on the website and the API together.",
      "The batch also shows in Bulk on the website."
    ],
    "help_url": "https://app.housekraft.co/api/v1/tools/bulk_stage/help"
  }
}
