Table of Contents(12)
What changed in this update
- Updated the API section: free API images now carry a Roomagen watermark until a pack is bought, and every tool is documented
- Added the warnings list that names the options a tool ignores
Earlier updates(1)
- Corrected every code sample to match Roomagen's live API: camelCase options such as roomType, UUID job IDs, the Idempotency-Key header and the v1= webhook signature computed over the timestamp and raw body
- Updated access and pricing: self-serve keys since August 30, 2026 instead of a waitlist, 50 free images once on the first key, packs that never expire, and six documented tools instead of 40+
- Rewrote the disclosure section around AB 723's statement-plus-link-to-original rule, California's AB 2025 for rental ads from January 2027, conflicting MLS label rules and EU AI Act Article 50
- Added an error-code table, rate limits, a section on what the API does not do and a pre-launch checklist; removed an unsupported market-size figure and the free-regeneration and disclosure-label claims
- Corrected job timing to the documented 20–60 seconds and the polling interval to 2–5 seconds, with measured median API times for staging, day to dusk and floor plan to 3D
To add virtual staging to an app, your backend sends each room photo to a virtual staging API as an asynchronous job, receives the result by signed webhook or polling, then stores it beside the original with disclosure data.
Free AI Virtual Staging — Stage Any Empty Room in Under a Minute
Stage vacant rooms with AI, usually in under a minute. 13 design styles, 15 room types, listing-ready photos for real estate, rentals and hospitality. 6 free credits, no credit card required.
Adding virtual staging to an app comes down to one loop: your backend sends a room photo to a virtual staging API as a job, the provider renders it in the background, and your app collects the result, stores it next to the original and shows it with whatever disclosure the destination needs. This tutorial builds that loop against the Roomagen API, with code in cURL, Node.js and Python, and keeps the design portable to other providers.
The integration at a glance
- Send each photo from your server as a job:
POST /v1/jobswith anIdempotency-Key. - Store the returned
job_idbefore doing anything else. - Receive the finished job on a signed webhook, and reconcile misses with
GET /v1/jobs/{id}. - Re-host the result, keep the original beside it, and carry disclosure data to every export.
What You're Building: Architecture of a Staging Feature
By the end of this tutorial, your app will accept a room photo and show a furnished version of the same room, typically 20 to 60 seconds later according to Roomagen's documentation. Everything else here exists to make that loop reliable at production volume. The same loop serves listing portals, photography delivery platforms, property management software advertising vacant units, interior design apps pitching a concept on a client's own room, and furniture retailers showing a room dressed in a chosen style.
A render takes tens of seconds, far too long to hold an HTTP request open, so the design is asynchronous: submit the job, get a job ID immediately, collect the result later. Staging runs a multi-step pipeline (scene analysis, then prompt, then generation), so it is slower than tools such as day to dusk: in Roomagen's production data for the 30 days to October 1, 2026, the median API staging job took about 45 seconds, against about 25 seconds for day to dusk and for floor plan to 3D.
| Stage | Where it runs | What to get right |
|---|---|---|
| Submit the job | Your backend → POST /v1/jobs |
Key stays server-side; Idempotency-Key; store job_id |
| Render | Roomagen | Typically 20–60 seconds; staging always renders at 2K |
| Learn that it finished | Webhook (push) plus polling (pull) | Verify the signature; de-duplicate on job_id |
| Store and show | Your storage and UI | Re-host the result; keep the original; disclose |
Roomagen is an AI photo editing and virtual staging platform with 40+ tools in its web app; the API documents every tool it accepts (65 as of October 2026) behind one job endpoint. Its image tools run on Google's Gemini image models, and the API adds the staging pipeline, job handling, automatic refunds for failed renders and signed webhooks.
Before You Start: Keys, Environments, and Image Requirements
Getting a key. Roomagen's API has been self-serve since its public launch on August 30, 2026; there is no waitlist. Sign in to the developer portal with a Roomagen account, verify your email and create a key, as the API quickstart describes. Your first key adds 50 free images, once per account, which is enough to build and test everything in this guide. Until the account buys a pack, every render carries a Roomagen watermark, so use them for building and testing, not for publishing.
Keys are rmg_live_ plus 64 hexadecimal characters, sent in an X-Api-Key header, and the portal shows each key only once. Keep it in a server-side secret store; never ship it in browser JavaScript or a mobile app, where anyone can extract it and spend your balance.
Environments. An account can hold up to 10 active keys, so create one per environment and revoke either without touching the other. There is no test mode: every key is live, and an account's keys share one image balance, one portal webhook and one signing secret, so give development jobs their own per-job webhook_url. There is no rotate button, by design: create the new key, deploy it, then revoke the old one.
Image inputs.
| Requirement | Roomagen API |
|---|---|
| Formats and size | JPEG, PNG or WebP, up to 20 MB; exactly one of image_url or image_base64 |
image_url (preferred) |
Public https URL, fetched while your POST is handled: 10-second timeout, up to 3 redirects, private hosts refused |
image_base64 |
Optional data-URI prefix; the request body may reach 30 MB, but only when X-Api-Key is present (1 MB without it) |
Prefer image_url: requests stay small, you skip base64's 33% overhead, and because the fetch happens during your POST, a signed URL only has to be valid for that request. Send the full-size export, one room per frame, shot level and reasonably lit; the model can only keep detail it can see.
Check the balance in code. GET /v1/account returns image_credits, the images your balance can still render. Wire a low-balance alert to it now: when the balance can't cover a job, POST /v1/jobs answers 402 insufficient_credits and no job is created.
Step 1: Submit a Staging Job
One POST creates the job: the tool, the image, options and, optionally, a per-job webhook_url (without one, Roomagen uses the account webhook saved in the portal, or you poll).
curl -X POST https://api.roomagen.com/api/v1/jobs \
-H "X-Api-Key: $ROOMAGEN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: photo-4821-3-stage" \
-d '{
"tool": "virtual-staging",
"image_url": "https://cdn.example.com/rooms/4821-3.jpg",
"options": { "style": "scandinavian", "roomType": "living-room", "mode": "empty-room" },
"webhook_url": "https://app.example.com/hooks/roomagen"
}'
The response arrives with status 201, before rendering finishes:
{ "job_id": "3f1c9d4e-5b2a-4c8e-9f10-7d6a2b3c4e5f", "status": "processing", "images_charged": 1 }
The same call from a Node.js 18+ backend:
async function submitStagingJob(photo) {
const res = await fetch("https://api.roomagen.com/api/v1/jobs", {
method: "POST",
headers: {
"X-Api-Key": process.env.ROOMAGEN_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": `photo-${photo.id}-stage`, // derived from your record, never random
},
body: JSON.stringify({
tool: "virtual-staging",
image_url: photo.signedUrl,
options: { style: photo.style, roomType: photo.roomType, mode: "empty-room" },
webhook_url: "https://app.example.com/hooks/roomagen",
}),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
await db.query(
"INSERT INTO staging_jobs (photo_id, job_id, status, images_charged) VALUES ($1, $2, 'processing', $3)",
[photo.id, body.job_id, body.images_charged],
);
return body.job_id;
}
Staging is stricter than the other tools. style is required, options are camelCase, and any key outside the list below, including room_type, resolution, tier and customPrompt, is rejected with invalid_request. Staging always renders at 2K and costs one image.
| Option | Values | Default |
|---|---|---|
style (required) |
modern, scandinavian, industrial, traditional, minimalist, bohemian, mid-century-modern, coastal, farmhouse, luxury, japandi, art-deco, mediterranean, or custom to let the model choose |
none |
roomType |
15 types, such as living-room, bedroom, kitchen, home-office, kids-room and terrace |
inferred from the photo |
mode |
empty-room furnishes a bare room; restyle re-dresses a furnished one; recreate rebuilds it as a different room type |
empty-room |
furnitureMode, furniture |
ai, or manual with a list such as "sectional sofa, floor lamp, rug" |
ai |
changeWalls, changeFloor |
true lets the model repaint walls or replace flooring |
false |
The three modes correspond to the staging modes in Roomagen's web app, so your product team can try them before you write code: AI virtual staging for empty rooms, redesign a furnished room for restyle and Room Repurpose for recreate.
Persist before anything else. Save job_id and images_charged against your own record. The Idempotency-Key header (up to 128 characters, scoped to your API key) makes retries safe: the same key with the same body returns the original job, with no second charge. Derive keys from your records so you can rebuild them after a crash, and resend the exact same body: the same key with a different body, including a re-signed image_url, returns 409 idempotency_conflict.
Step 2: Handle Completion — Webhooks vs Polling
There are two ways to learn that a job finished, and production integrations use both.
| Dimension | Webhooks (push) | Polling (pull) |
|---|---|---|
| Latency | Sent after the job finishes, with no requests from you | Up to one interval; Roomagen suggests 2–5 seconds |
| Infrastructure | Public https endpoint that answers 2xx within 10 seconds | A worker or scheduler |
| Reliability | At-least-once; 5 attempts over about 36 minutes, then abandoned | You control the loop |
| Best for | Production at volume | Development, interactive waits, reconciliation |
Use webhooks first and polling as the safety net. Roomagen's documentation calls webhooks "an optimisation, not a system of record", because a delivery is abandoned once the retry window ends. Polling alone does not scale either: at a 3-second interval each job costs 20 requests a minute, so the 300-per-minute limit on GET /v1/jobs/{id} covers about 15 jobs polling at once.
Set up the endpoint. Save an https endpoint in the portal's Webhook card, using the final URL because redirects are not followed. Store the whsec_ signing secret like a password, and fire a job.test delivery with Send test event while you get verification right. A per-job webhook_url uses the same secret.
const crypto = require("crypto");
const express = require("express");
const app = express();
// Raw body on this route, mounted before any global express.json()
app.post("/hooks/roomagen", express.raw({ type: "application/json" }), async (req, res) => {
const ts = req.get("X-Roomagen-Timestamp");
const sig = req.get("X-Roomagen-Signature");
if (!ts || !sig) return res.sendStatus(401);
const expected = "v1=" + crypto
.createHmac("sha256", process.env.ROOMAGEN_WEBHOOK_SECRET) // whsec_...
.update(`${ts}.`)
.update(req.body) // the raw bytes, never re-serialized JSON
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (sig.length !== expected.length || !fresh ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.sendStatus(401);
}
const job = JSON.parse(req.body);
if (job.event === "job.test") return res.sendStatus(200);
// Idempotent: only the first delivery (or poll) moves the row out of "processing"
const { rowCount } = await db.query(
"UPDATE staging_jobs SET status = $1, result_urls = $2, error = $3 WHERE job_id = $4 AND status = 'processing'",
[job.status, job.result_urls, job.error, job.job_id],
);
if (rowCount === 1 && job.status === "completed") {
await queue.add("store-results", { jobId: job.job_id }); // download off the request path
}
res.sendStatus(200);
});
Four details matter:
- Verify over the raw bytes. The header is
v1=plus a hex HMAC-SHA256 of{X-Roomagen-Timestamp}.{raw body}; re-serialized JSON won't match. - Compare in constant time, then check freshness. Node's
timingSafeEqualthrows on unequal lengths, so compare lengths first. Rejecting timestamps older than about five minutes blocks replays, the same defence Stripe's webhook documentation describes; keep server clocks synced. - Answer fast. Roomagen waits 10 seconds for a 2xx; anything else, including a timeout, is a failed attempt and is retried.
- Be idempotent. Delivery is at-least-once and unordered, and your poller may have finished the job already, so de-duplicate on
job_id.
For polling, during development or as a reconciler, the Python version is short:
import os, time, requests
API = "https://api.roomagen.com/api/v1"
HEADERS = {"X-Api-Key": os.environ["ROOMAGEN_API_KEY"]}
def wait_for_job(job_id, interval=3, max_wait=300):
deadline = time.time() + max_wait
while time.time() < deadline:
r = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS, timeout=30)
r.raise_for_status()
job = r.json()
if job["status"] != "processing":
return job # "completed" or "failed"; both are final
time.sleep(interval)
return None # still processing: leave it to the reconciler
A job ends as completed, with result_urls, or failed, with a reason in error and an automatic refund; log processing_ms to watch latency. Run a reconciler every minute or two for jobs still processing after a few minutes. Don't mark a slow job as failed just because a timer ran out: it may still complete, and it was charged when created.
Step 3: Deliver Results to Your Users
A completed job returns result_urls, an array of rendered JPEG images. Don't hotlink them.
Re-host the results. Roomagen serves results from its own storage, subject to the account's retention, so a background job should download each image as soon as the job completes, write it to your own bucket (S3, R2, GCS or similar) and serve it from your CDN. Your images then never depend on a provider's retention policy.
Keep the original beside the output. Store the source photo and the staged image as a linked pair. The pair powers a before/after slider, lets users revert, and is what listing disclosure rules ask for (Step 4).
Plan for variance. Generative output varies, and sometimes the sofa is wrong. A re-run is a new job that costs another image, because Roomagen refunds renders that fail, not results a user dislikes. Give each re-run its own Idempotency-Key, such as photo-4821-stage-2: reusing the first key returns the original job, or a 409 if the body changed. Offer a "Try again" button, and decide whether that cost is visible to users or built into your pricing.
Design the waiting and failure states. Staging usually lands within a minute, which is long in interface terms. Design the progress state, the failure state ("the render failed and was refunded") and the retry action before launch.
Step 4: Build Disclosure Into Your Data Model
If your users put staged photos in property ads, disclosure is their legal duty, and your data model decides whether it is easy. Roomagen's API adds no disclosure label or original-and-edited pairing, and Roomagen's terms make disclosure the user's responsibility, so your integration has to carry it.
- California AB 723, in force since January 1, 2026. A broker or salesperson, or someone acting for them, who uses a digitally altered image in an ad for a property sale must state, on or next to the image, that it was altered, and add a link, URL or QR code to a public site showing the original, unaltered image. California's real estate regulator adds that when an AI tool produces misleading advertising, responsibility rests "with the licensee and their responsible broker, not the technology provider."
- California rental ads from January 1, 2027. AB 2025 extends similar disclosure to rental advertising, covering housing providers and others, not only licensees.
- MLS rules disagree on placement. Canopy MLS requires the label on the image itself, not only in captions, while NWMLS will not accept photos with superimposed graphics or text.
- EU-facing products. Since 2 August 2026, Article 50 of the EU AI Act requires deployers of AI systems that generate or manipulate "deep fake" images (content resembling existing places or objects that would falsely appear authentic) to disclose it, a definition AI-edited photos of a real property can plausibly meet (see our EU AI Act Article 50 guide).
So store disclosure as data, not pixels: a public URL for the original photo, an altered flag with the tool and options used, and a disclosure text and label placement for each destination. Render the label when you export, not when you store.
| Tool | How AB 723's definition of a digitally altered image applies |
|---|---|
virtual-staging |
Adds furniture, a named element: disclose and link the original |
item-removal |
Removes elements from the image: disclose |
virtual-renovation |
Changes fixtures, flooring, walls or paint color, all named elements: disclose |
day-to-dusk |
The lighting exclusion covers only edits that don't change how the property is represented, and a new viewType replaces the view through windows, a named element: the cautious route is to disclose |
image-enhancement |
Lighting, white balance, color correction and exposure are excluded if they don't change how the property is represented |
floor-plan-to-3d |
Floor plans are a named element; present the render as an illustration, not a photo |
This is a summary for builders, not legal advice; our AB 723 guide and MLS photo requirements guide go deeper.
Production Concerns: Rate Limits, Retries, and Credit Budgeting
Rate limits. Per key, in a sliding 60-second window: 60 requests a minute for POST /v1/jobs, 300 for GET /v1/jobs/{id} and 120 for GET /v1/tools and GET /v1/account, plus 600 a minute per client IP, shared by every server behind one NAT address. Successful responses carry X-RateLimit-Limit and X-RateLimit-Remaining; a 429 rate_limited carries Retry-After in seconds, so honour it rather than inventing your own backoff. For bulk work, queue submissions: at 60 jobs a minute, 500 photos take just over eight minutes to submit.
Retries and error codes. Every error has the shape { "error": { "code", "message", "doc_url" } }. Branch on code, never on message:
| Response | What your code should do |
|---|---|
400 invalid_request, invalid_tool |
Fix the request (option names, values, or a website name used instead of the API slug); don't retry unchanged |
400 invalid_image, image_fetch_failed |
Send exactly one image source, from a public https URL that serves the image directly |
401 invalid_api_key, key_revoked |
Alert a human; don't retry |
402 insufficient_credits |
No job was created: top up, then retry |
409 idempotency_conflict |
Resend the original body, or use a new key for a genuinely new request |
413 payload_too_large |
Switch to image_url, and check that the X-Api-Key header was sent |
429 rate_limited, 500 internal_error |
Wait (Retry-After for a 429), then retry with the same Idempotency-Key |
Job status: "failed" |
Refunded automatically; retry once, and if it fails again, check the photo |
Failure economics. A job that fails during generation is refunded automatically, but images_charged on the job is not rewritten, so treat GET /v1/account as the source of truth for your balance. When comparing providers, check failure policies next to price.
Credit budgeting. Roomagen bills the API in images, bought up front in packs from the developer portal:
| Pack | Images | Price | Per image |
|---|---|---|---|
| Free grant (first key, once) | 50 | $0 | — |
| Pilot | 500 | $125 | $0.25 |
| Growth | 2,500 | $550 | $0.22 |
| Scale | 10,000 | $2,000 | $0.20 |
| Volume | 50,000+ | By arrangement | — |
Packs never expire and stack. Each documented tool costs one image per render at 2K, resolution: "4K" doubles that on tools that accept it, and staging is always one image. To budget, include re-runs: a platform staging 1,200 photos a month that expects one in ten to be re-run needs about 1,320 images, so a Growth pack lasts almost two months.
Published prices vary widely (checked October 2026): ModelsLab lists $0.02 per interior render, Decor8 $0.20 per image for all API operations, and AI HomeDesign $0.24 per photo with yearly billing or about $0.49 with monthly billing. Compare tool scope, webhooks, failure policy and output quality as well as unit price; our virtual staging API pricing guide and comparison of staging APIs go provider by provider.
An honest caveat on maturity. Roomagen's API is young: version 1 launched on August 30, 2026, with one job endpoint, and its docs have covered every tool it accepts only since October 3, 2026, so it has current conventions but not years of public track record. Breaking changes are announced by email to accounts with an active key before they ship; additive changes can land at any time, so ignore response fields you don't recognise.
Extending the Integration: Other Documented Tools
Once the loop works for staging, a "twilight version" or "remove clutter" button changes only tool and options:
| Tool | Slug | Options |
|---|---|---|
| Virtual renovation | virtual-renovation |
style (10 values), roomType (8 values) |
| AI photo enhancement | image-enhancement |
intensity: light, medium (default) or heavy |
| Day to dusk photo editing | day-to-dusk |
timeShift (hour 0–23, default 20), viewType, season, hasWindows |
| Object & item removal | item-removal |
None: omit options to clear obvious clutter, or send tier: "custom" with a customPrompt naming the item |
| Floor plan to 3D | floor-plan-to-3d |
outputStyle, interiorStyle, furnitureMode, fp3dOutdoor, fp3dLabels, inputType |
These tools also accept resolution (2K or 4K), tier (auto, guided or custom) and customPrompt (up to 500 characters). Unlike staging, they ignore unknown keys, naming each one in the response's warnings list, but still reject out-of-range values. For windowless rooms, send hasWindows: false to day to dusk so the prompt drops window and view instructions.
GET /v1/tools returns every slug your key can call, with its cost in images, and every slug it returns is documented. Read the list instead of hardcoding it, and remember that tool names on roomagen.com are not API slugs.
What the API Does Not Do
- No batch endpoint. One job is one image through one tool; the web app's batch processing is not part of the API, so queue jobs within the rate limits.
- No disclosure labels. Outputs carry no disclosure label or before/after pair. Renders are watermarked until the account buys a pack, and carry no Roomagen branding after that.
- No free regeneration. A re-run is a new job and is charged like one.
- No measurements or CAD files. Outputs are JPEG images, not dimensions, vectors or DWG files. If you need room measurements from photos, a generative image API is the wrong tool.
- No guaranteed output. Generative models can produce artifacts or inaccuracies, so review output before it reaches a listing.
- No app-only features. Refine, the Library and the Design Companion chat live in Roomagen's web app.
Common Mistakes in Staging API Integrations
- Blocking the request thread. Holding a user's request open while a job renders runs into load-balancer timeouts. Return
202 Acceptedfrom your own API and push updates to the client. - Wrong option names.
room_typeorresolutionon a staging job returnsinvalid_request; staging wants camelCase keys from its own list, and alwaysstyle. - Trusting webhooks alone. After about 36 minutes of retries, Roomagen stops trying. Without a reconciler, that job sits in "processing" forever.
- Verifying the signature wrongly. Hashing parsed JSON, dropping the timestamp prefix, comparing with
===or skipping the freshness check all weaken verification. - A webhook URL that redirects. Plain http and private hosts are refused up front with
webhook_url_rejected, and redirects are not followed, so a trailing-slash or www redirect fails every delivery. - Random idempotency keys or re-signed URLs. A key you didn't store can't be replayed, so the retry charges again; a re-signed
image_urlunder the same key returns409. - Hotlinking result URLs. Results are subject to the provider's retention. Re-host them on completion.
- One watermark for every destination. A burned-in "Virtually Staged" label can break MLS rules against superimposed text, and without a link to the original it does not meet AB 723 on its own.
Pre-Launch Checklist
- The API key lives only on your servers, one per environment.
- Staging requests always send
styleand nothing outside the staging option list. - Every job has an
Idempotency-Keyderived from your records, and retries resend the original body. - The webhook endpoint is the final https URL, verifies
v1=signatures over the raw body, rejects stale timestamps and answers within 10 seconds. - A
job.testdelivery from the portal passes verification. - A reconciler checks jobs still processing after a few minutes.
- Results are re-hosted, each with its original alongside.
- Disclosure data (original URL, altered flag, per-destination label) reaches every export path.
- A low-balance alert reads
image_credits, and every error code maps to a clear message for your users.
The Bottom Line: Ship the Loop, Then Extend It
Adding virtual staging to an app is a small integration: one POST with an idempotency key, one verified webhook handler backed by a reconciler, and a storage step that keeps the original beside the result. The happy path is a few dozen lines; most of the work is production hardening and disclosure data. None of it is tied to one vendor: asynchronous submission, push-plus-pull completion, re-hosted results and an original/output pair fit any staging API.
To build against the worked example, get a Roomagen API key: the first key includes 50 free images, and the quickstart, webhook guide and error reference cover the details. The same job loop then runs virtual staging, day to dusk, item removal, photo enhancement, virtual renovation and floor plan to 3D behind one integration.
Ready to transform your listings?
Try Roomagen's AI virtual staging for free. Upload your first photo and see the result, usually in under a minute.
Start FreeSources & References
- 1.California Legislature – AB 723 bill text (Business and Professions Code §10140.8)
- 2.California DRE – Artificial Intelligence in California Real Estate (advisory, March 17, 2026)
- 3.California Apartment Association – AB 2025 disclosure rule for altered rental ads (from Jan. 1, 2027)
- 4.Canopy MLS – Listing images: instructions and common questions
- 5.NWMLS – Real Estate Photography 101
- 6.EU AI Act – Article 50: Transparency obligations for providers and deployers
- 7.Stripe Docs – Receive webhook events (signature checks and replay protection)
- 8.ModelsLab – Interior Design API ($0.02 per render)
- 9.Decor8 AI – Pricing (API: $0.20 per image)
- 10.AI HomeDesign – B2B and API plans
Frequently Asked Questions
Written by
Roomagen Team
The Roomagen team creates in-depth guides about AI virtual staging, real estate photography, and property marketing strategies to help agents and professionals stay ahead.






