Easy Room AI

Developers

The EasyRoomAI API

Redesign rooms, stage listings, build 360° panoramas, restyle exteriors, remove furniture and edit a region of a photo — from your own code. One Bearer key, JSON in and out, and results by polling.

The API is included with the Ultra plan ($99.90/month, 10,000 credits — about 1,000 standard images). Credits are shared with the web tools.

Quickstart

Create a key in Settings → API, export it as EASYROOMAI_API_KEY, then create a task and poll it.

curl

# 1. Create a task (returns 202 and an id)
curl https://www.easyroomai.com/api/v1/generations \
  -H "Authorization: Bearer $EASYROOMAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "type": "redesign",
    "image": {
      "url": "https://example.com/my-living-room.jpg"
    },
    "style": "japandi",
    "room_type": "living_room"
  }'

# 2. Poll until status is "succeeded" or "failed"
curl https://www.easyroomai.com/api/v1/generations/gen_… \
  -H "Authorization: Bearer $EASYROOMAI_API_KEY"

Node.js 18+ (ESM)

import { randomUUID } from "node:crypto";

const API = "https://www.easyroomai.com/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.EASYROOMAI_API_KEY}`,
  "Content-Type": "application/json",
};

async function call(path, options = {}) {
  const res = await fetch(`${API}${path}`, {
    ...options,
    headers: { ...headers, ...options.headers },
  });
  const body = await res.json();
  if (!res.ok) {
    const { code, message, request_id } = body.error;
    throw new Error(`${code}: ${message} (${request_id})`);
  }
  return body;
}

// 1. Create the task. The Idempotency-Key makes a retry safe.
const created = await call("/generations", {
  method: "POST",
  headers: { "Idempotency-Key": randomUUID() },
  body: JSON.stringify({
    "type": "redesign",
    "image": {
      "url": "https://example.com/my-living-room.jpg"
    },
    "style": "japandi",
    "room_type": "living_room"
  }),
});

// 2. Poll until it finishes.
let generation = created;
while (generation.status === "pending") {
  await new Promise((resolve) => setTimeout(resolve, 4000));
  generation = await call(`/generations/${created.id}`);
}

console.log(generation.status, generation.output?.url);

Python 3

import os
import time
import uuid

import requests

API = "https://www.easyroomai.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['EASYROOMAI_API_KEY']}"}


def call(method, path, **kwargs):
    res = requests.request(method, API + path, headers={**HEADERS, **kwargs.pop("headers", {})}, **kwargs)
    body = res.json()
    if not res.ok:
        err = body["error"]
        raise RuntimeError(f"{err['code']}: {err['message']} ({err['request_id']})")
    return body


# 1. Create the task. The Idempotency-Key makes a retry safe.
created = call(
    "POST",
    "/generations",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={
      "type": "redesign",
      "image": {
        "url": "https://example.com/my-living-room.jpg"
      },
      "style": "japandi",
      "room_type": "living_room"
    },
)

# 2. Poll until it finishes.
generation = created
while generation["status"] == "pending":
    time.sleep(4)
    generation = call("GET", f"/generations/{created['id']}")

print(generation["status"], generation.get("output", {}).get("url"))

A create call returns 202 straight away with the task id. The result URL appears on the task once status is succeeded.

Authentication

Base URL: https://www.easyroomai.com/api/v1. Send your key on every request:

Authorization: Bearer rai_live_…
  • A key is shown once, when you create it. We store only a hash, so a lost key cannot be recovered — revoke it and create another.
  • You can have up to 5 active keys, for example one per environment. Revoking a key takes effect immediately.
  • Keep keys on your server. The API does not allow browser (CORS) calls, so a key in front-end code is a leaked key.
  • If your Ultra subscription lapses the keys stop working (403 subscription_required) and resume when it is active again.

How it works

  1. POST /generations checks your request, charges the credits and starts the task. You get 202 and an id like gen_….
  2. GET /generations/{id} returns pending, then succeeded with output.url, or failed.

Most tasks finish in a minute or two; 360° panoramas can take longer. Poll every 3–5 seconds. A task that is still pending after an hour is marked failed and refunded automatically. There are no webhooks yet.

Create a generation

POST /generations with a JSON body. Every request has a type; the other fields depend on it (see below). Fields we do not recognise are rejected with 400 rather than ignored, so a typo never silently changes what you are charged for.

FieldDescription
typeRequired. One of redesign, staging, panorama360, exterior, furniture_removal, region_edit.
imageThe photo — see Images and masks.
promptOptional extra direction, up to 500 characters.
metadataOptional object of your own (up to 512 bytes). Echoed back on every read — handy for your order or listing id.

Valid style, room_type and subject ids come from GET /pricing, so you never have to hard-code them.

Generation types

Room redesign · redesign · 10 credits

Re-style an existing interior photo in one of our design styles.

Required: image, style, room_type
Optional: prompt, aspect_ratio, metadata (aspect_ratio: 1:1, 3:2, 2:3, 4:3, 3:4, 16:9 or 9:16)

  • Get the valid style and room_type ids from GET /v1/pricing.

Example body

{
  "type": "redesign",
  "image": {
    "url": "https://example.com/photos/living-room.jpg"
  },
  "style": "japandi",
  "room_type": "living_room",
  "aspect_ratio": "3:2"
}

Virtual staging · staging · 15 credits

Furnish an empty room with a staging preset.

Required: image, style, room_type
Optional: prompt, aspect_ratio, metadata (aspect_ratio: 1:1, 3:2, 2:3, 4:3, 3:4, 16:9 or 9:16)

  • style is a staging preset id and room_type a staging room type id — a different list from redesign. Both are in GET /v1/pricing.

Example body

{
  "type": "staging",
  "image": {
    "url": "https://example.com/photos/living-room.jpg"
  },
  "style": "modern_neutral",
  "room_type": "living_room"
}

360° panorama staging · panorama360 · 38 credits

Stage a full 360° panorama and keep it a valid panorama.

Required: image, style, room_type
Optional: prompt, metadata

  • image must be an equirectangular panorama with a 2:1 aspect ratio. Anything else is rejected with invalid_image (reason: not_equirectangular).
  • The result is a 2048×1024 panorama. Only the standard quality tier is available through the API.

Example body

{
  "type": "panorama360",
  "image": {
    "url": "https://example.com/photos/panorama.jpg"
  },
  "style": "modern_neutral",
  "room_type": "living_room"
}

Exterior design · exterior · 10 credits

Restyle the outside of a building or garden.

Required: image, style, subject
Optional: prompt, metadata

  • subject says what is in the photo — the facade, the front yard, the backyard or a patio/deck. The valid ids are in GET /v1/pricing.

Example body

{
  "type": "exterior",
  "image": {
    "url": "https://example.com/photos/living-room.jpg"
  },
  "style": "modern_farmhouse",
  "subject": "facade"
}

Furniture removal · furniture_removal · 10 credits

Empty a room automatically, or remove only what you paint over.

Required: image, mode, mask (when mode is "brush")
Optional: floor_material, prompt, metadata

  • mode: "auto_empty" removes everything and takes no mask. mode: "brush" removes only the masked area and requires mask.
  • floor_material (up to 100 characters) describes the floor to reveal. Leave it out and the floor is continued to match what is visible.

Example body

{
  "type": "furniture_removal",
  "image": {
    "url": "https://example.com/photos/living-room.jpg"
  },
  "mode": "brush",
  "mask": {
    "base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
  }
}

Region edit · region_edit · 10 credits

Change only a masked area; every pixel outside it stays exactly as it was.

Required: mask, image or source_generation_id, prompt or reference_image
Optional: reference_image, metadata

  • Send image, or continue an earlier edit by passing its id as source_generation_id (it must be one of your own finished region_edit results).
  • Describe the change in prompt (up to 400 characters), provide a reference_image, or both.

Example body

{
  "type": "region_edit",
  "image": {
    "url": "https://example.com/photos/living-room.jpg"
  },
  "mask": {
    "base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
  },
  "prompt": "Replace the sofa with a green velvet sofa"
}

Images and masks

  • Give either { "url": "https://…" } or { "base64": "…" } (plain base64 or a data: URI), never both.
  • JPEG, PNG or WebP, up to 15 MB, 40 megapixels, shortest side at least 256 px. Animated images are rejected.
  • URLs must be public https on port 443. We fetch them from our servers, follow at most 3 redirects, and give up after 10 seconds. Private, loopback and link-local addresses are refused.
  • The request body is limited to about 22 MB, so a single image plus a mask fits as base64.

Masks

  • PNG with the same aspect ratio as the image (3% tolerance). It does not have to be the same pixel size.
  • Transparent pixels (alpha below 128) mark the area to change; opaque pixels stay as they are.
  • A fully opaque mask selects nothing and is rejected with empty_mask.

Image failures come back as 400 invalid_image with a details.reason such as unsupported_format, too_large, too_small, too_many_pixels, animated, blocked_address, unreachable or not_equirectangular.

Get a generation

GET /generations/{id} returns the task. You can only read your own tasks; anything else is a 404.

Succeeded

{
  "id": "gen_3f2b8c1e-9d4a-4e6b-8f10-a1b2c3d4e5f6",
  "type": "redesign",
  "status": "succeeded",
  "credits_charged": 10,
  "created_at": "2026-10-09T12:00:00.000Z",
  "completed_at": "2026-10-09T12:01:05.000Z",
  "metadata": { "listing": "A-1042" },
  "output": { "url": "https://cdn.easyroomai.com/…" }
}

Failed

{
  "id": "gen_…",
  "status": "failed",
  "error": { "code": "generation_failed", "message": "The image could not be generated." },
  "refunded": true
}

GET /generations?limit=20&cursor=… lists your API tasks, newest first, as { data, has_more, next_cursor } (limit 1–50). GET /credits returns your balance.

Credits and pricing

API requests spend the same credit balance as the web tools, at API rates. Credits are charged when the task is created and returned automatically if it fails or times out.

TypeCredits
redesign10
staging15
panorama36038
exterior10
furniture_removal10
region_edit10

Only the standard quality tier is available through the API. GET /pricing returns this table, the accepted options and the limits as JSON.

Limits

LimitValue
Tasks running at once3 per account (429 too_many_inflight)
Create requests30 per minute per account
Read requests120 per minute per account
Invalid API keysRepeated failures from one address are throttled for a minute
Request bodyabout 22 MB
Active keys5

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 or 503 includes Retry-After. Each response also has an X-Request-Id — quote it if you contact support. The platform can briefly refuse new tasks when it is at capacity (503 capacity_exceeded); you are not charged.

Idempotency

Send an Idempotency-Key header (1–64 characters of letters, digits, _ - : .) on create calls so a network retry never double-charges.

  • Same key and same body: you get the original task back with 200 and Idempotent-Replayed: true.
  • Same key, different body: 422 idempotency_key_reuse.
  • Same key while the first call is still being created: 409 request_in_progress — retry shortly.
  • A call that failed on our side (502) does not hold the key, so retrying with it really retries.
  • Keys are remembered for 24 hours.

Errors

Every error has the same shape:

{
  "error": {
    "code": "insufficient_credits",
    "message": "Not enough credits.",
    "request_id": "req_9f8e7d6c5b4a3210",
    "details": { "required": 10, "available": 4 }
  }
}
StatusCodeMeaning
400invalid_requestThe body is malformed or a field is missing, unknown or out of range. details.field names it. Also used for 413 (body too large) and 415 (not JSON).
400invalid_imageAn image could not be used. details.reason says why.
400invalid_maskThe mask could not be used (wrong aspect ratio, not a PNG, …).
400empty_maskThe mask selects nothing.
401invalid_api_keyThe key is missing, malformed, unknown or revoked.
402insufficient_creditsNot enough credits. details.required and details.available say by how much.
403subscription_requiredThe key is fine, but its owner does not have an active Ultra subscription.
404not_foundNo such generation for this account.
409request_in_progressA request with that Idempotency-Key is still being created. Retry in a moment.
422idempotency_key_reuseThat Idempotency-Key was already used with a different request body.
429rate_limitedToo many requests. Wait Retry-After seconds.
429too_many_inflightYou already have 3 tasks running. Wait for one to finish.
500internal_errorSomething went wrong on our side. Quote the request_id if you contact support.
502upstream_errorOur image provider could not take the task. You were not charged; retrying is safe.
503capacity_exceededThe platform is at capacity. Retry after Retry-After seconds; you were not charged.
503api_disabledThe API is temporarily switched off. Retry later.

Changelog

  • 2026-10 — v1: six generation types, polling, idempotency, credits and pricing endpoints.

Use of the API is covered by our Terms of Service (section 14, API access). Questions or a use case we should know about? Write to [email protected].