{"openapi":"3.0.3","info":{"title":"RealtyEdit API","version":"1.10.0","description":"<div class=\"api-docs-endpoint\" aria-label=\"Endpoint: POST /api/v1/process-listing\">\n  <span>POST</span><code>/api/v1/process-listing</code>\n</div>\n<div class=\"api-docs-endpoint\" aria-label=\"Endpoint: GET /api/v1/batch-status/{batchId}\">\n  <span>GET</span><code>/api/v1/batch-status/{batchId}</code>\n</div>\n<div class=\"api-docs-endpoint\" aria-label=\"Endpoint: GET /api/v1/credits\">\n  <span>GET</span><code>/api/v1/credits</code>\n</div>\n\n<h2 class=\"api-docs-how-heading\">How it works</h2>\n\nSubmit a listing for asynchronous photo processing. Here is the complete `hdrMode: 3` flow at a glance.\n\n<div class=\"api-docs-workflow\">\n  <section class=\"api-docs-stream-section\" aria-label=\"Six input photos\">\n    <h3>Input · 6 photos</h3>\n    <p>Photos are sorted by filename first, then grouped according to <code>settings.hdrMode</code>. This example uses <code>hdrMode: 3</code>, so every three consecutive photos form one bracket.</p>\n    <div class=\"api-docs-photo-stream api-docs-input-stream\">\n      <figure><img src=\"https://realtyedit.app/api-docs/brackets/IMG_0001.jpg\" alt=\"Bracket set 1 underexposed input\" loading=\"lazy\" /><figcaption>IMG_0001.jpg</figcaption></figure>\n      <figure><img src=\"https://realtyedit.app/api-docs/brackets/IMG_0002.jpg\" alt=\"Bracket set 1 normal exposure input\" loading=\"lazy\" /><figcaption>IMG_0002.jpg</figcaption></figure>\n      <figure><img src=\"https://realtyedit.app/api-docs/brackets/IMG_0003.jpg\" alt=\"Bracket set 1 overexposed input\" loading=\"lazy\" /><figcaption>IMG_0003.jpg</figcaption></figure>\n      <figure><img src=\"https://realtyedit.app/api-docs/brackets/IMG_0004.jpg\" alt=\"Bracket set 2 underexposed input\" loading=\"lazy\" /><figcaption>IMG_0004.jpg</figcaption></figure>\n      <figure><img src=\"https://realtyedit.app/api-docs/brackets/IMG_0005.jpg\" alt=\"Bracket set 2 normal exposure input\" loading=\"lazy\" /><figcaption>IMG_0005.jpg</figcaption></figure>\n      <figure><img src=\"https://realtyedit.app/api-docs/brackets/IMG_0006.jpg\" alt=\"Bracket set 2 overexposed input\" loading=\"lazy\" /><figcaption>IMG_0006.jpg</figcaption></figure>\n    </div>\n  </section>\n  <div class=\"api-docs-flow-arrow\" aria-label=\"The six inputs are processed into two original photos\">\n    <span aria-hidden=\"true\">↓</span>\n    Two 3-photo brackets are processed\n  </div>\n  <section class=\"api-docs-stream-section\" aria-label=\"Two original output photos\">\n    <h3>Output · 2 original photos</h3>\n    <p>Automatic removal can add extra versions. The callback example below includes one removed version, for 3 downloadable photos in total.</p>\n    <div class=\"api-docs-photo-stream api-docs-output-stream\">\n      <figure><img src=\"https://realtyedit.app/api-docs/brackets/out-1.jpg\" alt=\"Finished RealtyEdit photo produced from bracket set 1\" loading=\"lazy\" /><figcaption>IMG_0001.jpg</figcaption></figure>\n      <figure><img src=\"https://realtyedit.app/api-docs/brackets/out-2.jpg\" alt=\"Finished RealtyEdit photo produced from bracket set 2\" loading=\"lazy\" /><figcaption>IMG_0004.jpg</figcaption></figure>\n    </div>\n  </section>\n</div>\n\n### Filename ordering and bracket grouping\n\nBefore processing, RealtyEdit sorts `photoUrls` by the filenames derived from their URL paths. **Submitted array order is not preserved.** Name files so filename sorting keeps each bracket's exposures together; changing only the URL array order does not select a different bracket sequence.\n\nAfter sorting, `settings.hdrMode` determines the fixed number of consecutive photos per output:\n\n- `1`: process each photo individually.\n- `3` or `5` or `7`: merge each consecutive set of that many photos into one output.\n\nFor example, with `hdrMode: 3`, sequential filenames `IMG_0001.jpg` through `IMG_0006.jpg` form two brackets: 0001/0002/0003 and 0004/0005/0006, regardless of the submitted URL order. The photo count must be a multiple of `hdrMode`.\n\nThe API uses **fixed-size grouping, not image-based automatic grouping**. It does not detect scene changes to choose bracket boundaries.\n\n### Input URLs and ingestion\n\nSubmit between 1 and 1000 source URLs per request. Each photo must be at most 200 MiB (209715200 bytes).\nURLs must return the image directly without additional authentication headers or cookies. Signed URLs are supported, but must remain valid until ingestion finishes.\n\nSource downloads happen asynchronously. A `202` response means the batch was accepted, not that the photos have already been downloaded or successfully processed. A download failure marks the affected processing group as failed and is reflected in polling results and the completion callback.\n\n### Idempotent submissions\n\nTo retry a submission without creating another batch, reuse the same `clientRequestId` and `x-api-client-id` header. The optional header is trimmed and truncated to 100 characters; omitted or empty values use `default`.\nReusing both values returns the existing batch unchanged, even if you submit different photos, settings, or a different `webhookUrl`. Use a new `clientRequestId` for new work. Omitting `clientRequestId` creates a new batch on every submission.\n\n### 1. Submit the six inputs\n\nThe request returns immediately with the queued batch and request IDs.\n\n```bash\ncurl -X POST 'https://realtyedit.app/api/v1/process-listing' \\\n  -H 'Content-Type: application/json' \\\n  -H 'x-api-key: YOUR_API_KEY' \\\n  -d '{\n  \"listingName\": \"123 Main St, San Diego, CA\",\n  \"photoUrls\": [\n    \"https://realtyedit.app/api-docs/brackets/IMG_0001.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0002.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0003.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0004.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0005.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0006.jpg\"\n  ],\n  \"settings\": {\n    \"hdrMode\": 3,\n    \"skyOption\": \"smart-sky\",\n    \"applyVerticalCorrection\": true,\n    \"applyLensCorrection\": true\n  },\n  \"webhookUrl\": \"https://partner.example.com/webhooks/realtyedit\",\n  \"clientRequestId\": \"crm-listing-123-main-st-v1\"\n}'\n```\n\n### 2. Receive originals and automatic-removal versions\n\nWhen processing finishes, RealtyEdit sends one callback to your `webhookUrl`. In this example, each result maps its three source exposures to one original processed photo, with an additional removed version for the first result.\nIf your account enables Automatic removal,\ncompletion waits for detection and any removal to finish. Each result's `outputUrl`\nalways remains the original processed image. Successful removal adds a `variants`\nentry with `type: \"auto_removed\"`, its own request ID, a stable `_r.jpg`\nfilename, and a downloadable URL. Clean detections and disabled/skipped removal\nreturn an empty `variants` array. Removal failures are retried with bounded\nattempts and silently retain the original, with `autoRemoveStatus: \"skipped\"`\nand `autoRemoveError: null`; they never turn a successful original into a failed processing group.\nAutomatic fills must pass an explicit clean quality check. If none passes, removal\nfinishes with `autoRemoveStatus: \"skipped\"`, a human-readable\n`autoRemoveSkipReason`, and an empty `variants` array; the normal processed\nphoto is retained and the callback is not blocked. Provider/runtime failure\ndetails remain internal rather than being exposed to customers.\n\n`outputUrls` is the **full flattened download list**, in result order: each\noriginal immediately followed by its variants. `total` and `doneCount` count\noriginal processing groups, not image files. **Compatibility note (v1.10):\n`outputUrls.length` can exceed `doneCount`; do not assume one URL per group.**\nManual Remove and AI Prompt Edits created later are not added to the API batch.\n\n```json\n{\n  \"batchId\": \"b_3b2d9e4f8f2745f7a55f\",\n  \"listingId\": \"l_5f5bf8d6\",\n  \"listingName\": \"123 Main St, San Diego, CA\",\n  \"status\": \"done\",\n  \"total\": 2,\n  \"doneCount\": 2,\n  \"failedCount\": 0,\n  \"results\": [\n    {\n      \"requestId\": \"req_a\",\n      \"status\": \"done\",\n      \"inputUrls\": [\n        \"https://realtyedit.app/api-docs/brackets/IMG_0001.jpg\",\n        \"https://realtyedit.app/api-docs/brackets/IMG_0002.jpg\",\n        \"https://realtyedit.app/api-docs/brackets/IMG_0003.jpg\"\n      ],\n      \"outputUrl\": \"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg\",\n      \"autoRemoveStatus\": \"complete\",\n      \"autoRemoveError\": null,\n      \"variants\": [\n        {\n          \"requestId\": \"remove_auto_example\",\n          \"type\": \"auto_removed\",\n          \"filename\": \"IMG_0001_r.jpg\",\n          \"outputUrl\": \"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001_r.jpg\"\n        }\n      ]\n    },\n    {\n      \"requestId\": \"req_b\",\n      \"status\": \"done\",\n      \"inputUrls\": [\n        \"https://realtyedit.app/api-docs/brackets/IMG_0004.jpg\",\n        \"https://realtyedit.app/api-docs/brackets/IMG_0005.jpg\",\n        \"https://realtyedit.app/api-docs/brackets/IMG_0006.jpg\"\n      ],\n      \"outputUrl\": \"https://realtyedit.app/download/l_5f5bf8d6/IMG_0004.jpg\",\n      \"autoRemoveStatus\": \"clean\",\n      \"autoRemoveError\": null,\n      \"variants\": []\n    }\n  ],\n  \"outputUrls\": [\n    \"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg\",\n    \"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001_r.jpg\",\n    \"https://realtyedit.app/download/l_5f5bf8d6/IMG_0004.jpg\"\n  ],\n  \"requestIds\": [\n    \"req_a\",\n    \"req_b\"\n  ],\n  \"completedAt\": \"2026-09-07T18:42:11.129Z\"\n}\n```\n\n#### Verify signed callbacks\n\nEnable webhook signing in your RealtyEdit account's API settings and store the one-time `whsec_...` secret securely. Signed callbacks include:\n\n`X-RealtyEdit-Signature: t=1789632000,v1=...`\n\nCompute HMAC-SHA256 over `<timestamp>.<exact raw request body>`. Verify the request before parsing JSON, use a constant-time digest comparison, and reject timestamps more than five minutes from your server clock. During the 24-hour rotation window, the header contains two `v1` values; accept either one. Use `batchId` to make callback handling idempotent.\n\nExisting integrations remain unsigned until webhook signing is enabled for their account.\n\n```javascript\nconst crypto = require(\"node:crypto\");\n\nfunction verifyRealtyEditWebhook(rawBody, signatureHeader, signingSecret) {\n  if (typeof signatureHeader !== \"string\" ||\n      typeof signingSecret !== \"string\" || !signingSecret ||\n      !(typeof rawBody === \"string\" || Buffer.isBuffer(rawBody))) return false;\n  const parts = signatureHeader.split(\",\").map((part) => part.trim());\n  const timestamp = Number(parts.find((part) => part.startsWith(\"t=\"))?.slice(2));\n  const signatures = parts.filter((part) => part.startsWith(\"v1=\")).map((part) => part.slice(3));\n  if (!Number.isSafeInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;\n  const expected = crypto.createHmac(\"sha256\", signingSecret)\n    .update(String(timestamp) + \".\")\n    .update(rawBody)\n    .digest(\"hex\");\n  return signatures.some((signature) =>\n    /^[a-f0-9]{64}$/.test(signature) &&\n    crypto.timingSafeEqual(Buffer.from(expected, \"hex\"), Buffer.from(signature, \"hex\"))\n  );\n}\n```\n\n```python\nimport hashlib\nimport hmac\nimport re\nimport time\n\ndef verify_realtyedit_webhook(raw_body, signature_header, signing_secret):\n    if (not isinstance(raw_body, bytes) or\n            not isinstance(signature_header, str) or\n            not isinstance(signing_secret, str) or not signing_secret):\n        return False\n    parts = [part.strip() for part in signature_header.split(\",\")]\n    timestamp_text = next((part[2:] for part in parts if part.startswith(\"t=\")), \"\")\n    try:\n        timestamp = int(timestamp_text)\n    except ValueError:\n        return False\n    signatures = [part[3:] for part in parts if part.startswith(\"v1=\")]\n    if abs(int(time.time()) - timestamp) > 300:\n        return False\n    signed = str(timestamp).encode() + b\".\" + raw_body\n    expected = hmac.new(signing_secret.encode(), signed, hashlib.sha256).hexdigest()\n    return any(re.fullmatch(r\"[a-f0-9]{64}\", signature) is not None and\n               hmac.compare_digest(expected, signature) for signature in signatures)\n```\n\n### 3. Poll status or recover results\n\nUse the `batchId` returned by the submission request to check live progress. This is also the fallback for retrieving results when a webhook is delayed or cannot be delivered.\n\n```bash\ncurl 'https://realtyedit.app/api/v1/batch-status/batch_03a12e3d-20a1-4ca5-90cd-d21fa36553ce' \\\n  -H 'x-api-key: YOUR_API_KEY'\n```\n\nStop polling when `status` is `done`, `partial_failed`, or `failed`. While processing, completed outputs are included alongside requests whose status is still `processing`.\nPolling uses the same result/variant shape and download order as the webhook.\nAn original may already have a usable `outputUrl` while its result remains\n`processing` during automatic removal. The batch only becomes terminal after\nevery original's removal outcome is known. Retries keep the same filenames and\nURLs; use the parent and variant request IDs for idempotent saving.\n\n### 4. Check available credits\n\nUse the same API key to read its owner's current balance. Each processed output group uses one credit, regardless of HDR bracket size.\n\n```bash\ncurl 'https://realtyedit.app/api/v1/credits' \\\n  -H 'x-api-key: YOUR_API_KEY'\n```\n\nThe balance is a snapshot and can change before a later submission. Listing submission checks the balance again but does not reserve credits for the whole batch; each group's credit is reserved later when processing is dispatched."},"servers":[{"url":"https://realtyedit.app","description":"Production"}],"components":{"schemas":{"ProcessListingRequest":{"type":"object","properties":{"listingName":{"type":"string","minLength":1,"maxLength":200,"description":"Listing display name. This is used as the listing name in RealtyEdit listings.","example":"123 Main St, San Diego, CA"},"photoUrls":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":1000,"description":"Source image URLs to ingest and process for this listing (1–1000 URLs, at most 200 MiB per photo). URLs must be directly fetchable without additional authentication headers or cookies and remain valid until ingestion finishes. Downloads happen asynchronously after acceptance; ingestion failures appear in batch results. URLs are sorted by filename derived from each URL path before processing; submitted array order is not preserved. Name files so filename sorting keeps each bracket's exposures together. In bracket modes, the number of URLs must be a multiple of settings.hdrMode."},"settings":{"type":"object","properties":{"hdrMode":{"type":"integer","enum":[1,3,5,7],"default":1,"description":"Fixed number of consecutive photos per output, after filename sorting. Use 1, 3, 5, or 7. Use 1 to process each photo individually. Bracket modes use fixed-size groups, not image-based automatic grouping.","example":1},"skyOption":{"type":"string","enum":["original-sky","smart-sky","fluffy-clouds","wispy-clouds","clear-sky"],"default":"smart-sky","description":"Sky replacement mode. Supported values: original-sky, smart-sky, fluffy-clouds, wispy-clouds, clear-sky.","examples":["original-sky","smart-sky","fluffy-clouds","wispy-clouds","clear-sky"]},"applyVerticalCorrection":{"type":"boolean","default":true,"description":"Applies perspective/vertical alignment correction to reduce converging vertical lines."},"applyLensCorrection":{"type":"boolean","default":true,"description":"Applies lens profile correction for distortion and optics-related artifacts."}},"default":{"hdrMode":1,"skyOption":"smart-sky","applyVerticalCorrection":true,"applyLensCorrection":true}},"webhookUrl":{"type":"string","format":"uri","description":"Optional HTTPS endpoint that receives one completion callback per batch. RealtyEdit treats any 2xx response as success. Accounts with webhook signing enabled receive an X-RealtyEdit-Signature header.","example":"https://example.com/realtyedit-webhook"},"clientRequestId":{"type":"string","minLength":1,"maxLength":120,"description":"Optional idempotency key from your system, scoped by x-api-client-id (default: default). Reusing both values returns the existing batch unchanged, even if photos, settings, or webhookUrl differ. Use a new key for new work. Omitting this field creates a new batch on every submission.","example":"crm-listing-123-main-st-v1"}},"required":["listingName","photoUrls"]},"ProcessListingResponse":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true],"description":"Indicates the request was accepted."},"idempotent":{"type":"boolean","description":"True when this response reuses an existing batch for the same clientRequestId."},"batchId":{"type":"string","description":"Unique ID for the processing batch."},"listingId":{"type":"string","description":"RealtyEdit listing ID associated with the batch."},"queuedCount":{"type":"integer","minimum":0,"description":"Number of processing requests queued from the submitted photos."},"requestIds":{"type":"array","items":{"type":"string"},"description":"Trace IDs for queued processing requests (one per grouped input set)."}},"required":["ok","idempotent","batchId","listingId","queuedCount","requestIds"]},"ApiError":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]},"ProcessListingWebhookPayload":{"type":"object","properties":{"batchId":{"type":"string","description":"Unique ID for the submitted batch."},"listingId":{"type":"string","nullable":true,"description":"RealtyEdit listing ID created for this batch (can be null)."},"listingName":{"type":"string","nullable":true,"description":"Listing name associated with this batch (can be null)."},"status":{"type":"string","enum":["done","partial_failed","failed"],"description":"Final aggregate outcome for the batch."},"total":{"type":"integer","minimum":0,"description":"Total original processing groups in the batch. Variants do not increase this count."},"doneCount":{"type":"integer","minimum":0,"description":"Original processing groups completed successfully, including their automatic-removal outcome. Variants do not increase this count."},"failedCount":{"type":"integer","minimum":0,"description":"How many requests ended in failure."},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string","description":"Trace ID for this processing request."},"status":{"type":"string","enum":["done","failed"],"description":"Terminal status for this request."},"inputUrls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Input source URLs for this result (single: 1 URL, bracket modes: 3, 5, or 7 URLs)."},"outputUrl":{"type":"string","nullable":true,"format":"uri","description":"Original processed image URL for successful requests, or null when the original failed. Automatic removal never replaces this URL."},"autoRemoveStatus":{"type":"string","nullable":true,"enum":["pending","detecting","removing","clean","complete","skipped","error"],"description":"Automatic removal state, or null for legacy photos without this stage. Clean/skipped means no extra image; unsuccessful removal silently retains the successful original."},"autoRemoveError":{"type":"string","nullable":true,"description":"Retained for compatibility; null. Automatic-removal diagnostics are internal only."},"autoRemoveSkipReason":{"type":"string","nullable":true,"description":"Why automatic removal was skipped, such as no fill passing quality checks. The successful original remains available; null when not applicable."},"variants":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string","description":"Trace ID for the automatic Remove result."},"type":{"type":"string","enum":["auto_removed"]},"filename":{"type":"string","description":"Stable original-based JPEG filename with an _r suffix."},"outputUrl":{"type":"string","format":"uri","description":"Download URL for the auto-removed version."}},"required":["requestId","type","filename","outputUrl"]},"description":"Available automatic-removal versions of this original. Empty when none was produced. Manual Remove and AI Prompt Edits are not included."}}},"description":"Canonical per-request terminal results in filename-sorted bracket order."},"outputUrls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Full flattened download list in result order: each original followed by its variants. Its length can exceed doneCount; do not assume one output per processing group."},"requestIds":{"type":"array","items":{"type":"string"},"description":"Trace IDs for per-request correlation and support."},"completedAt":{"type":"string","format":"date-time","description":"ISO timestamp when webhook payload was emitted."}},"description":"Outbound callback payload sent by RealtyEdit. In this webhook schema, fields marked required are always included in the payload. Nullable fields may be null."},"BatchStatusResponse":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true],"description":"Indicates that the batch was found."},"batchId":{"type":"string","description":"Unique ID for the submitted batch."},"listingId":{"type":"string","nullable":true,"description":"RealtyEdit listing ID associated with the batch."},"listingName":{"type":"string","nullable":true,"description":"Listing name associated with the batch."},"status":{"type":"string","enum":["processing","done","partial_failed","failed"],"description":"Current aggregate status. Stop polling when this is done, partial_failed, or failed."},"total":{"type":"integer","minimum":0,"description":"Total original processing groups; variants do not increase this count."},"doneCount":{"type":"integer","minimum":0,"description":"Original groups completed successfully, including their automatic-removal outcome; variants are not counted separately."},"failedCount":{"type":"integer","minimum":0,"description":"Requests that ended in failure."},"processingCount":{"type":"integer","minimum":0,"description":"Requests not yet in a terminal state."},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string","description":"Trace ID for this processing request."},"status":{"type":"string","enum":["processing","done","failed"],"description":"Current request status. All nonterminal internal states are reported as processing."},"inputUrls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Input source URLs associated with this processing request."},"outputUrl":{"type":"string","nullable":true,"format":"uri","description":"Original processed image URL when available, including while automatic removal is pending; null before the original is ready or after original processing failure."},"autoRemoveStatus":{"type":"string","nullable":true,"enum":["pending","detecting","removing","clean","complete","skipped","error"],"description":"Automatic removal state, or null for legacy photos without this stage. Clean/skipped means no extra image; unsuccessful removal silently retains the successful original."},"autoRemoveError":{"type":"string","nullable":true,"description":"Retained for compatibility; null. Automatic-removal diagnostics are internal only."},"autoRemoveSkipReason":{"type":"string","nullable":true,"description":"Why automatic removal was skipped, such as no fill passing quality checks. The successful original remains available; null when not applicable."},"variants":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string","description":"Trace ID for the automatic Remove result."},"type":{"type":"string","enum":["auto_removed"]},"filename":{"type":"string","description":"Stable original-based JPEG filename with an _r suffix."},"outputUrl":{"type":"string","format":"uri","description":"Download URL for the auto-removed version."}},"required":["requestId","type","filename","outputUrl"]},"description":"Available automatic-removal versions of this original. Empty when none was produced. Manual Remove and AI Prompt Edits are not included."}},"required":["requestId","status","inputUrls","outputUrl","autoRemoveStatus","autoRemoveError","variants"]},"description":"Current per-request results in filename-sorted bracket order."},"outputUrls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Full flattened list of currently available downloads, each original followed by its variants. Originals may be available while automatic removal is pending. Its length can exceed doneCount."},"requestIds":{"type":"array","items":{"type":"string"},"description":"All request IDs in filename-sorted bracket order."},"callbackStatus":{"type":"string","enum":["pending","sending","sent","failed","skipped"],"description":"Current completion-webhook delivery state."},"callbackAttempts":{"type":"integer","minimum":0,"description":"Stored webhook delivery runs. Each run can include internal HTTP retries."},"createdAt":{"type":"string","nullable":true,"format":"date-time","description":"Batch creation time, when available."},"updatedAt":{"type":"string","nullable":true,"format":"date-time","description":"Last batch metadata update, when available."}},"required":["ok","batchId","listingId","listingName","status","total","doneCount","failedCount","processingCount","results","outputUrls","requestIds","callbackStatus","callbackAttempts","createdAt","updatedAt"]},"CreditsResponse":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true],"description":"Indicates that the balance was retrieved."},"availableCredits":{"type":"number","description":"Current credit balance of the API-key owner. This is a point-in-time snapshot, not a reservation."}},"required":["ok","availableCredits"]}},"parameters":{},"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key"}}},"paths":{"/api/v1/process-listing":{"post":{"tags":["API"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Optional namespace for clientRequestId. Trimmed and truncated to 100 characters; omitted or empty values use default. Keep this header unchanged when retrying an idempotent submission.","example":"my-crm"},"required":false,"name":"x-api-client-id","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"listingName":{"type":"string","minLength":1,"maxLength":200,"description":"Listing display name. This is used as the listing name in RealtyEdit listings.","example":"123 Main St, San Diego, CA"},"photoUrls":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":1000,"description":"Source image URLs to ingest and process for this listing (1–1000 URLs, at most 200 MiB per photo). URLs must be directly fetchable without additional authentication headers or cookies and remain valid until ingestion finishes. Downloads happen asynchronously after acceptance; ingestion failures appear in batch results. URLs are sorted by filename derived from each URL path before processing; submitted array order is not preserved. Name files so filename sorting keeps each bracket's exposures together. In bracket modes, the number of URLs must be a multiple of settings.hdrMode."},"settings":{"type":"object","properties":{"hdrMode":{"type":"integer","enum":[1,3,5,7],"default":1,"description":"Fixed number of consecutive photos per output, after filename sorting. Use 1, 3, 5, or 7. Use 1 to process each photo individually. Bracket modes use fixed-size groups, not image-based automatic grouping.","example":1},"skyOption":{"type":"string","enum":["original-sky","smart-sky","fluffy-clouds","wispy-clouds","clear-sky"],"default":"smart-sky","description":"Sky replacement mode. Supported values: original-sky, smart-sky, fluffy-clouds, wispy-clouds, clear-sky.","examples":["original-sky","smart-sky","fluffy-clouds","wispy-clouds","clear-sky"]},"applyVerticalCorrection":{"type":"boolean","default":true,"description":"Applies perspective/vertical alignment correction to reduce converging vertical lines."},"applyLensCorrection":{"type":"boolean","default":true,"description":"Applies lens profile correction for distortion and optics-related artifacts."}},"default":{"hdrMode":1,"skyOption":"smart-sky","applyVerticalCorrection":true,"applyLensCorrection":true}},"webhookUrl":{"type":"string","format":"uri","description":"Optional HTTPS endpoint that receives one completion callback per batch. RealtyEdit treats any 2xx response as success. Accounts with webhook signing enabled receive an X-RealtyEdit-Signature header.","example":"https://example.com/realtyedit-webhook"},"clientRequestId":{"type":"string","minLength":1,"maxLength":120,"description":"Optional idempotency key from your system, scoped by x-api-client-id (default: default). Reusing both values returns the existing batch unchanged, even if photos, settings, or webhookUrl differ. Use a new key for new work. Omitting this field creates a new batch on every submission.","example":"crm-listing-123-main-st-v1"}},"required":["listingName","photoUrls"]},"example":{"listingName":"123 Main St, San Diego, CA","photoUrls":["https://cdn.example.com/listings/123-main/IMG_0001.jpg","https://cdn.example.com/listings/123-main/IMG_0002.jpg"],"settings":{"hdrMode":1,"skyOption":"smart-sky","applyVerticalCorrection":true,"applyLensCorrection":true},"webhookUrl":"https://example.com/realtyedit-webhook","clientRequestId":"crm-listing-123-main-st-v1"}}}},"responses":{"202":{"description":"Accepted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true],"description":"Indicates the request was accepted."},"idempotent":{"type":"boolean","description":"True when this response reuses an existing batch for the same clientRequestId."},"batchId":{"type":"string","description":"Unique ID for the processing batch."},"listingId":{"type":"string","description":"RealtyEdit listing ID associated with the batch."},"queuedCount":{"type":"integer","minimum":0,"description":"Number of processing requests queued from the submitted photos."},"requestIds":{"type":"array","items":{"type":"string"},"description":"Trace IDs for queued processing requests (one per grouped input set)."}},"required":["ok","idempotent","batchId","listingId","queuedCount","requestIds"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"405":{"description":"Method not allowed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"409":{"description":"Requested processing mode is not enabled","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"500":{"description":"Failed to enqueue processing batch","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}}},"summary":"Submit a listing processing batch","description":"Returns immediately with queued request IDs. If webhookUrl is provided, RealtyEdit sends one completion callback when the batch is complete. Webhook delivery uses up to 3 attempts with a 15000ms timeout per attempt and exponential backoff (1s, 2s). Use GET /api/v1/batch-status/{batchId} to poll progress or recover results if webhook delivery fails. The listing can also be monitored and accessed from the RealtyEdit listings page at realtyedit.app/listings","x-codeSamples":[{"lang":"Shell","label":"cURL","source":"curl -X POST 'https://realtyedit.app/api/v1/process-listing' \\\n  -H 'Content-Type: application/json' \\\n  -H 'x-api-key: YOUR_API_KEY' \\\n  -d '{\n  \"listingName\": \"123 Main St, San Diego, CA\",\n  \"photoUrls\": [\n    \"https://realtyedit.app/api-docs/brackets/IMG_0001.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0002.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0003.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0004.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0005.jpg\",\n    \"https://realtyedit.app/api-docs/brackets/IMG_0006.jpg\"\n  ],\n  \"settings\": {\n    \"hdrMode\": 3,\n    \"skyOption\": \"smart-sky\",\n    \"applyVerticalCorrection\": true,\n    \"applyLensCorrection\": true\n  },\n  \"webhookUrl\": \"https://partner.example.com/webhooks/realtyedit\",\n  \"clientRequestId\": \"crm-listing-123-main-st-v1\"\n}'"}],"callbacks":{"processingComplete":{"{$request.body#/webhookUrl}":{"post":{"summary":"Batch completion callback","description":"Sent once when all requests in the submitted batch reach terminal state. Delivery retries up to 3 attempts with a 15000ms timeout per attempt and exponential backoff (1s, 2s). Respond with any HTTP 2xx status to acknowledge receipt. When webhook signing is enabled, X-RealtyEdit-Signature contains a Unix timestamp and one or more HMAC-SHA256 v1 signatures over <timestamp>.<exact raw body>. Existing accounts remain unsigned until they opt in.","parameters":[{"name":"X-RealtyEdit-Signature","in":"header","required":false,"description":"Present when webhook signing is enabled. Format: t=<unix timestamp>,v1=<HMAC-SHA256 hex digest>. Rotation may add a second v1 digest for 24 hours.","schema":{"type":"string"},"example":"t=1789632000,v1=8e3a0d7ef5d31f31f9a76ab01e4ec04fd9a25edca0bfdbd945d36bdb49c80167"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessListingWebhookPayload"},"examples":{"done":{"summary":"All requests succeeded","value":{"batchId":"b_3b2d9e4f8f2745f7a55f","listingId":"l_5f5bf8d6","listingName":"123 Main St, San Diego, CA","status":"done","total":2,"doneCount":2,"failedCount":0,"results":[{"requestId":"req_a","status":"done","inputUrls":["https://realtyedit.app/api-docs/brackets/IMG_0001.jpg","https://realtyedit.app/api-docs/brackets/IMG_0002.jpg","https://realtyedit.app/api-docs/brackets/IMG_0003.jpg"],"outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg","autoRemoveStatus":"complete","autoRemoveError":null,"variants":[{"requestId":"remove_auto_example","type":"auto_removed","filename":"IMG_0001_r.jpg","outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001_r.jpg"}]},{"requestId":"req_b","status":"done","inputUrls":["https://realtyedit.app/api-docs/brackets/IMG_0004.jpg","https://realtyedit.app/api-docs/brackets/IMG_0005.jpg","https://realtyedit.app/api-docs/brackets/IMG_0006.jpg"],"outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0004.jpg","autoRemoveStatus":"clean","autoRemoveError":null,"variants":[]}],"outputUrls":["https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg","https://realtyedit.app/download/l_5f5bf8d6/IMG_0001_r.jpg","https://realtyedit.app/download/l_5f5bf8d6/IMG_0004.jpg"],"requestIds":["req_a","req_b"],"completedAt":"2026-09-07T18:42:11.129Z"}},"partial_failed":{"summary":"Some requests failed","value":{"batchId":"b_3b2d9e4f8f2745f7a55f","listingId":"l_5f5bf8d6","listingName":"123 Main St","status":"partial_failed","total":4,"doneCount":3,"failedCount":1,"results":[{"requestId":"req_a","status":"done","inputUrls":["https://cdn.example.com/source/IMG_0001.jpg","https://cdn.example.com/source/IMG_0002.jpg","https://cdn.example.com/source/IMG_0003.jpg"],"outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg","autoRemoveStatus":"complete","autoRemoveError":null,"variants":[{"requestId":"remove_auto_example","type":"auto_removed","filename":"IMG_0001_r.jpg","outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001_r.jpg"}]},{"requestId":"req_b","status":"done","inputUrls":["https://cdn.example.com/source/IMG_0004.jpg","https://cdn.example.com/source/IMG_0005.jpg","https://cdn.example.com/source/IMG_0006.jpg"],"outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0004.jpg","autoRemoveStatus":"clean","autoRemoveError":null,"variants":[]},{"requestId":"req_c","status":"failed","inputUrls":["https://cdn.example.com/source/IMG_0007.jpg","https://cdn.example.com/source/IMG_0008.jpg","https://cdn.example.com/source/IMG_0009.jpg"],"outputUrl":null,"autoRemoveStatus":"skipped","autoRemoveError":null,"variants":[]},{"requestId":"req_d","status":"done","inputUrls":["https://cdn.example.com/source/IMG_0010.jpg","https://cdn.example.com/source/IMG_0011.jpg","https://cdn.example.com/source/IMG_0012.jpg"],"outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0010.jpg","autoRemoveStatus":"skipped","autoRemoveError":null,"variants":[]}],"outputUrls":["https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg","https://realtyedit.app/download/l_5f5bf8d6/IMG_0001_r.jpg","https://realtyedit.app/download/l_5f5bf8d6/IMG_0004.jpg","https://realtyedit.app/download/l_5f5bf8d6/IMG_0010.jpg"],"requestIds":["req_a","req_b","req_c","req_d"],"completedAt":"2026-06-30T03:42:11.129Z"}}}}}},"responses":{"200":{"description":"Callback accepted"},"400":{"description":"Callback rejected"}}}}}}}},"/api/v1/credits":{"get":{"tags":["API"],"security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Current available credits","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true],"description":"Indicates that the balance was retrieved."},"availableCredits":{"type":"number","description":"Current credit balance of the API-key owner. This is a point-in-time snapshot, not a reservation."}},"required":["ok","availableCredits"]},"example":{"ok":true,"availableCredits":42}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"405":{"description":"Method not allowed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"500":{"description":"Credit balance lookup failed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}}},"summary":"Get available credits","description":"Returns the API-key owner's current credit balance. This is a point-in-time snapshot, not a credit reservation; listing submission checks the balance again.","x-codeSamples":[{"lang":"Shell","label":"cURL","source":"curl 'https://realtyedit.app/api/v1/credits' \\\n  -H 'x-api-key: YOUR_API_KEY'"}]}},"/api/v1/batch-status/{batchId}":{"get":{"tags":["API"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Batch ID returned by POST /api/v1/process-listing.","example":"batch_03a12e3d-20a1-4ca5-90cd-d21fa36553ce"},"required":true,"name":"batchId","in":"path"}],"responses":{"200":{"description":"Current batch progress and available results","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true],"description":"Indicates that the batch was found."},"batchId":{"type":"string","description":"Unique ID for the submitted batch."},"listingId":{"type":"string","nullable":true,"description":"RealtyEdit listing ID associated with the batch."},"listingName":{"type":"string","nullable":true,"description":"Listing name associated with the batch."},"status":{"type":"string","enum":["processing","done","partial_failed","failed"],"description":"Current aggregate status. Stop polling when this is done, partial_failed, or failed."},"total":{"type":"integer","minimum":0,"description":"Total original processing groups; variants do not increase this count."},"doneCount":{"type":"integer","minimum":0,"description":"Original groups completed successfully, including their automatic-removal outcome; variants are not counted separately."},"failedCount":{"type":"integer","minimum":0,"description":"Requests that ended in failure."},"processingCount":{"type":"integer","minimum":0,"description":"Requests not yet in a terminal state."},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string","description":"Trace ID for this processing request."},"status":{"type":"string","enum":["processing","done","failed"],"description":"Current request status. All nonterminal internal states are reported as processing."},"inputUrls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Input source URLs associated with this processing request."},"outputUrl":{"type":"string","nullable":true,"format":"uri","description":"Original processed image URL when available, including while automatic removal is pending; null before the original is ready or after original processing failure."},"autoRemoveStatus":{"type":"string","nullable":true,"enum":["pending","detecting","removing","clean","complete","skipped","error"],"description":"Automatic removal state, or null for legacy photos without this stage. Clean/skipped means no extra image; unsuccessful removal silently retains the successful original."},"autoRemoveError":{"type":"string","nullable":true,"description":"Retained for compatibility; null. Automatic-removal diagnostics are internal only."},"autoRemoveSkipReason":{"type":"string","nullable":true,"description":"Why automatic removal was skipped, such as no fill passing quality checks. The successful original remains available; null when not applicable."},"variants":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string","description":"Trace ID for the automatic Remove result."},"type":{"type":"string","enum":["auto_removed"]},"filename":{"type":"string","description":"Stable original-based JPEG filename with an _r suffix."},"outputUrl":{"type":"string","format":"uri","description":"Download URL for the auto-removed version."}},"required":["requestId","type","filename","outputUrl"]},"description":"Available automatic-removal versions of this original. Empty when none was produced. Manual Remove and AI Prompt Edits are not included."}},"required":["requestId","status","inputUrls","outputUrl","autoRemoveStatus","autoRemoveError","variants"]},"description":"Current per-request results in filename-sorted bracket order."},"outputUrls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Full flattened list of currently available downloads, each original followed by its variants. Originals may be available while automatic removal is pending. Its length can exceed doneCount."},"requestIds":{"type":"array","items":{"type":"string"},"description":"All request IDs in filename-sorted bracket order."},"callbackStatus":{"type":"string","enum":["pending","sending","sent","failed","skipped"],"description":"Current completion-webhook delivery state."},"callbackAttempts":{"type":"integer","minimum":0,"description":"Stored webhook delivery runs. Each run can include internal HTTP retries."},"createdAt":{"type":"string","nullable":true,"format":"date-time","description":"Batch creation time, when available."},"updatedAt":{"type":"string","nullable":true,"format":"date-time","description":"Last batch metadata update, when available."}},"required":["ok","batchId","listingId","listingName","status","total","doneCount","failedCount","processingCount","results","outputUrls","requestIds","callbackStatus","callbackAttempts","createdAt","updatedAt"]},"examples":{"processing":{"summary":"Batch still processing","value":{"ok":true,"batchId":"batch_03a12e3d-20a1-4ca5-90cd-d21fa36553ce","listingId":"l_5f5bf8d6","listingName":"123 Main St, San Diego, CA","status":"processing","total":2,"doneCount":1,"failedCount":0,"processingCount":1,"results":[{"requestId":"req_a","status":"done","inputUrls":["https://cdn.example.com/source/IMG_0001.jpg"],"outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg","autoRemoveStatus":"clean","autoRemoveError":null,"variants":[]},{"requestId":"req_b","status":"processing","inputUrls":["https://cdn.example.com/source/IMG_0002.jpg"],"outputUrl":null,"autoRemoveStatus":"pending","autoRemoveError":null,"variants":[]}],"outputUrls":["https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg"],"requestIds":["req_a","req_b"],"callbackStatus":"pending","callbackAttempts":0,"createdAt":"2026-09-17T18:40:00.000Z","updatedAt":"2026-09-17T18:41:00.000Z"}},"done":{"summary":"Batch complete","value":{"ok":true,"batchId":"batch_03a12e3d-20a1-4ca5-90cd-d21fa36553ce","listingId":"l_5f5bf8d6","listingName":"123 Main St, San Diego, CA","status":"done","total":2,"doneCount":2,"failedCount":0,"processingCount":0,"results":[{"requestId":"req_a","status":"done","inputUrls":["https://cdn.example.com/source/IMG_0001.jpg"],"outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg","autoRemoveStatus":"complete","autoRemoveError":null,"variants":[{"requestId":"remove_auto_example","type":"auto_removed","filename":"IMG_0001_r.jpg","outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0001_r.jpg"}]},{"requestId":"req_b","status":"done","inputUrls":["https://cdn.example.com/source/IMG_0002.jpg"],"outputUrl":"https://realtyedit.app/download/l_5f5bf8d6/IMG_0002.jpg","autoRemoveStatus":"clean","autoRemoveError":null,"variants":[]}],"outputUrls":["https://realtyedit.app/download/l_5f5bf8d6/IMG_0001.jpg","https://realtyedit.app/download/l_5f5bf8d6/IMG_0001_r.jpg","https://realtyedit.app/download/l_5f5bf8d6/IMG_0002.jpg"],"requestIds":["req_a","req_b"],"callbackStatus":"sent","callbackAttempts":1,"createdAt":"2026-09-17T18:40:00.000Z","updatedAt":"2026-09-17T18:44:00.000Z"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"404":{"description":"Batch not found","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"405":{"description":"Method not allowed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}},"500":{"description":"Status lookup failed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false],"description":"Indicates the request failed."},"error":{"type":"string","description":"Human-readable error message describing why the request was rejected."}},"required":["ok","error"]}}}}},"summary":"Get batch status and results","description":"Returns live per-request progress and all output URLs currently available. Use this endpoint for polling or as a fallback when the completion webhook is delayed or fails. Stop polling when status is done, partial_failed, or failed. callbackAttempts counts stored delivery runs; each run can perform multiple internal HTTP retries.","x-codeSamples":[{"lang":"Shell","label":"cURL","source":"curl 'https://realtyedit.app/api/v1/batch-status/batch_03a12e3d-20a1-4ca5-90cd-d21fa36553ce' \\\n  -H 'x-api-key: YOUR_API_KEY'"}]}}}}