{
  "help": {
    "id": "pov",
    "name": "POV",
    "what_it_does": "Re-shoot the room from another camera angle.",
    "about": "Shows the same room from another camera angle: turn left or right, tilt up or down, zoom in or out.",
    "the_photo": "A room photo.",
    "credits": 2,
    "credits_label": "2 credits per run",
    "pricing": "2 credits per run, refunded if the run fails.",
    "output_kind": "image",
    "output": "One image, saved to your Library. The file is at job.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 photo",
        "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.jpg"
      },
      {
        "title": "See the choices (optional)",
        "detail": "The same list the website shows for this tool.",
        "command": "curl -s https://app.housekraft.co/api/v1/tools/pov/options"
      },
      {
        "title": "Run POV",
        "detail": "Answers straight away with the job, which keeps running.",
        "command": "curl -s -X POST https://app.housekraft.co/api/v1/tools/pov/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"image\": \"<asset id from POST /api/v1/uploads>\",\n  \"azimuth\": -45,\n  \"elevation\": 0,\n  \"zoom\": 5\n}'"
      },
      {
        "title": "Get the result",
        "detail": "Fetch the job every few seconds until status is \"done\"; the file is at job.output.url.",
        "command": "curl -s https://app.housekraft.co/api/v1/jobs/<job id> \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\""
      }
    ],
    "inputs": [
      {
        "name": "image",
        "type": "image",
        "required": true,
        "description": "The room photo to work on.",
        "help": "An asset id (from POST /api/v1/uploads or your Library) or an https:// image URL."
      },
      {
        "name": "azimuth",
        "type": "number",
        "required": false,
        "description": "Move the camera around the room, in degrees: negative to the left, positive to the right (90 = the right side, 180 = from behind).",
        "min": -180,
        "max": 180,
        "default": 0
      },
      {
        "name": "elevation",
        "type": "number",
        "required": false,
        "description": "Camera height, in degrees: positive raises the camera to look down on the room (60 = bird's eye, 90 = straight down); negative lowers it to look up.",
        "min": -30,
        "max": 90,
        "default": 0
      },
      {
        "name": "zoom",
        "type": "number",
        "required": false,
        "description": "Zoom, 0 (wide) to 10 (close).",
        "min": 0,
        "max": 10,
        "default": 5
      }
    ],
    "options": "GET /api/v1/tools/pov/options",
    "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": "Turn the camera 45° to the left",
      "method": "POST",
      "url": "https://app.housekraft.co/api/v1/tools/pov/run",
      "body": {
        "image": "<asset id from POST /api/v1/uploads>",
        "azimuth": -45,
        "elevation": 0,
        "zoom": 5
      }
    },
    "example_curl": "curl -s -X POST https://app.housekraft.co/api/v1/tools/pov/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"image\": \"<asset id from POST /api/v1/uploads>\",\n  \"azimuth\": -45,\n  \"elevation\": 0,\n  \"zoom\": 5\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 photo\nphoto = upload(\"room.jpg\")\n\n# 2. Run POV\njob = call(\"POST\", \"/api/v1/tools/pov/run\", json={\n    \"image\": photo,\n    \"azimuth\": -45,\n    \"elevation\": 0,\n    \"zoom\": 5,\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    print(\"Result:\", job[\"output\"][\"url\"])\nelse:\n    print(\"Failed:\", job[\"error\"][\"message\"], \"| refunded:\", job[\"credits\"][\"refunded\"])\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 photo\nconst photo = await upload(\"room.jpg\");\n\n// 2. Run POV\nlet { job } = await call(\"POST\", \"/api/v1/tools/pov/run\", {\n  \"image\": photo,\n  \"azimuth\": -45,\n  \"elevation\": 0,\n  \"zoom\": 5,\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\") console.log(\"Result:\", job.output.url);\nelse console.log(\"Failed:\", job.error.message, \"| refunded:\", job.credits.refunded);\n",
    "more_examples": [
      {
        "title": "Look down from above",
        "body": {
          "image": "<asset id from POST /api/v1/uploads>",
          "azimuth": 0,
          "elevation": 45,
          "zoom": 3
        },
        "curl": "curl -s -X POST https://app.housekraft.co/api/v1/tools/pov/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"image\": \"<asset id from POST /api/v1/uploads>\",\n  \"azimuth\": 0,\n  \"elevation\": 45,\n  \"zoom\": 3\n}'"
      },
      {
        "title": "Move in close",
        "body": {
          "image": "<asset id from POST /api/v1/uploads>",
          "azimuth": 0,
          "elevation": 0,
          "zoom": 8
        },
        "curl": "curl -s -X POST https://app.housekraft.co/api/v1/tools/pov/run \\\n  -H \"Authorization: Bearer $HOUSEKRAFT_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"image\": \"<asset id from POST /api/v1/uploads>\",\n  \"azimuth\": 0,\n  \"elevation\": 0,\n  \"zoom\": 8\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": "pov",
            "status": "running",
            "created_at": 1793700000,
            "finished_at": null,
            "credits": {
              "charged": 2,
              "refunded": false
            },
            "input": {
              "image": "<asset id from POST /api/v1/uploads>",
              "azimuth": -45,
              "elevation": 0,
              "zoom": 5
            },
            "warnings": [],
            "check_overridden": false,
            "output": null,
            "error": null,
            "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": "pov",
            "status": "done",
            "created_at": 1793700000,
            "finished_at": 1793700025,
            "credits": {
              "charged": 2,
              "refunded": false
            },
            "input": {
              "image": "<asset id from POST /api/v1/uploads>",
              "azimuth": -45,
              "elevation": 0,
              "zoom": 5
            },
            "warnings": [],
            "check_overridden": false,
            "output": {
              "id": "a3f0c6d1-8b27-4c5e-9f14-6e2b7d90c3b5",
              "type": "generated",
              "tool": "pov",
              "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
          }
        }
      },
      {
        "title": "Failed",
        "status": 200,
        "when": "GET /api/v1/jobs/<id> answers like this if the model could not finish. The credits are refunded automatically.",
        "body": {
          "job": {
            "id": "7c1e9b02-5d4a-4e8f-b6a3-2f90d1c4e5a8",
            "tool": "pov",
            "status": "failed",
            "created_at": 1793700000,
            "finished_at": 1793700025,
            "credits": {
              "charged": 2,
              "refunded": true
            },
            "input": {
              "image": "<asset id from POST /api/v1/uploads>",
              "azimuth": -45,
              "elevation": 0,
              "zoom": 5
            },
            "warnings": [],
            "check_overridden": false,
            "output": null,
            "error": {
              "message": "The image model could not finish this request."
            }
          }
        }
      }
    ],
    "checks": [
      "Image check: the website's own check of whether this photo suits this tool. If not, you get 422 image_check_failed with the reason and a fix, and no credits are used. Send the same request with \"override_check\": true to run it anyway.",
      "Misuse check: the photo must show a room, building or plan, 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": 402,
        "code": "insufficient_credits",
        "meaning": "Not enough credits for this run.",
        "what_to_do": "Buy credits or upgrade your plan, then try again."
      },
      {
        "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": [
      "azimuth turns the camera (-180 to 180), elevation tilts it (-30 to 90), zoom 0 is wide and 10 is close.",
      "See GET /api/v1/tools/pov/options for the website's named angles."
    ],
    "help_url": "https://app.housekraft.co/api/v1/tools/pov/help"
  }
}
