API keys

API reference

Virtual staging over HTTP — create a room, run generations against it, read the results back.

Introduction

One HTTP API, the same one the Edensign app itself runs on. Everything is JSON over HTTPS, and every endpoint below lives under https://api.edensign.io.

Two nouns are worth learning before anything else, because the whole API is built out of them:

  • A project is a room — one to four photos of the same space. You create it once, and it is the thing you pay for.
  • A task is one run against that room: stage it, empty it, change the season, redraw the floor plan. A project holds up to eighteen of them, and every run after the project exists is free.

That split is the point. Restaging a room in four styles is one charge and four runs, not four charges — so an integration should create a project per room and keep regenerating inside it rather than starting over.

Requests are answered through a CDN with a 60-second ceiling. Staging runs are dispatched asynchronously and return immediately; the other generation types run inline, so keep an eye on that limit and retry a timeout rather than assuming it failed.

Authentication

Every request carries an API key as a bearer token. Create one on the API keys page — it is shown once, at creation, and stored only as a hash on our side.

Header
Authorization: Bearer sk_live_...

A key belongs to the account that made it and acts as that account: same credits, same projects, same team billing. Keys come in sk_live_ and sk_test_ flavours, and both spend real credits — the prefix is there to label your own environments, not to give you a sandbox.

Anything created through the API shows up in the studio like any other project, and what a key spends is recorded against that key, so API usage can be told apart from work done in the app.

Treat a key like a password: server-side only. Anyone holding it can spend the account’s credits. Delete a leaked key on the API keys page — revocation takes effect immediately.

Quickstart

Stage a photo end to end, in two calls.

Stage a room
# 1. Create the room and start a staging run in one call.
curl -X POST 'https://api.edensign.io/v1/projects' \
  -H "Authorization: Bearer $EDENSIGN_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "sources": [{ "url": "https://example.com/living-room.jpg" }],
    "task": {
      "type": "staging",
      "config": {
        "remove_furniture": { "mode": "auto" },
        "add_furniture": { "style": "modern", "room_type": "living_room" }
      }
    }
  }'

# 2. Poll the project until the task reports a terminal status.
curl 'https://api.edensign.io/v1/projects/{projectId}' \
  -H "Authorization: Bearer $EDENSIGN_API_KEY"

The first call answers with the project and a task in pending. The second is the one you repeat — a couple of seconds apart is plenty — until that task’s status is completed and its results hold the staged images.

Credits & limits

Credits are taken once, when a project is created, and the price follows the room: one photo’s worth for a single angle, more for several. Runs inside the project cost nothing, however many you start. A call from an API key costs exactly what the same work costs in the app.

A caller who cannot pay gets INSUFFICIENT_CREDITS and no project is created. A project whose runs all failed is refunded automatically within a few minutes — nothing to call, and nothing to reconcile.

Photos per project1–4, and they must be angles of the same room
Runs per project18
Runs per request4 (task_count)
Request timeout60 seconds
ChargedOnce, at project creation

Errors

Errors use standard status codes and a single envelope. The field to branch on is details[].reason — it is stable, where message is not.

Error body
{
  "error": {
    "code": 403,
    "status": "PERMISSION_DENIED",
    "message": "insufficient credits",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "INSUFFICIENT_CREDITS"
      }
    ]
  }
}
ReasonHTTPMeaning
UNAUTHENTICATED401Missing, malformed, revoked, or unknown API key.
INSUFFICIENT_CREDITS403The account behind the key cannot pay for the project. Nothing was created.
403PERMISSION_DENIED with no reason: the project belongs to another account.
404NOT_FOUND: no such project or task.
MAX_GENERATION_TASKS_EXCEEDED400The project is at its eighteen-run ceiling. Create a new project.
GPU_PROCESSING_FAILED503The model failed and produced nothing. Usually worth retrying.

Create a project

POST/v1/projects

Creates a room from one to four photos, and optionally starts the first run against it in the same call.

This is the only call that takes credits. What it costs depends on the room — one photo’s worth, more for a room with several angles — and every run inside the project afterwards is free.

The project survives a first task that fails: you get the room back with the failure recorded on the task, so a retry costs nothing and does not re-upload. What does throw is a request that never became a task at all — no credits, or a bad config.

Body

sourcesobject[]required
The room’s photos, 1–4 of them. Each is { url, view_index?, removal_url? }. Give several only when they are angles of the same room — a second room is a second project. `view_index` is filled from position when omitted.
taskobjectoptional
An optional first run: { type, config, task_count?, sources? }. Same shape as the body of POST /v1/generationTasks, minus project_id — the project it belongs to is the one being created.
Example request
{
  "sources": [
    {
      "url": "https://example.com/living-room.jpg"
    }
  ],
  "task": {
    "type": "staging",
    "config": {
      "remove_furniture": {
        "mode": "auto"
      },
      "add_furniture": {
        "style": "modern",
        "room_type": "living_room"
      }
    }
  }
}
Example response · 201
{
  "id": "01997c3e-6a10-7000-8a1f-2c0d4a9d1e77",
  "user_id": "019935ba-0f21-7000-bb0e-6a5f1d2c7a41",
  "sources": [
    {
      "url": "https://example.com/living-room.jpg",
      "view_index": 0
    }
  ],
  "created_at": "2026-08-26T09:12:00.000Z",
  "settled_at": null,
  "deleted_at": null,
  "tasks": [
    {
      "id": "01997c3e-6b42-7000-9d55-8f1b60a2c934",
      "user_id": "019935ba-0f21-7000-bb0e-6a5f1d2c7a41",
      "type": "staging",
      "status": "pending",
      "progress": 0,
      "created_at": "2026-08-26T09:12:00.000Z",
      "updated_at": "2026-08-26T09:12:00.000Z",
      "started_at": null,
      "finished_at": null,
      "error": null,
      "project_id": "01997c3e-6a10-7000-8a1f-2c0d4a9d1e77",
      "config": {
        "remove_furniture": {
          "mode": "auto"
        },
        "add_furniture": {
          "style": "modern",
          "room_type": "living_room"
        }
      },
      "sources": [
        {
          "url": "https://example.com/living-room.jpg",
          "view_index": 0
        }
      ],
      "results": []
    }
  ]
}

Retrieve a project

GET/v1/projects/{projectId}

The room and every run inside it, newest state. This is what you poll.

`status` is the only authority on whether a run is done; `progress` is advisory and exists for a waiting UI. `results` grows as views land, so a multi-view task can be worth showing before it ends.

Path parameters

projectIduuidrequired
The project’s id.
Example response · 200
{
  "id": "01997c3e-6a10-7000-8a1f-2c0d4a9d1e77",
  "user_id": "019935ba-0f21-7000-bb0e-6a5f1d2c7a41",
  "sources": [
    {
      "url": "https://example.com/living-room.jpg",
      "view_index": 0
    }
  ],
  "created_at": "2026-08-26T09:12:00.000Z",
  "settled_at": null,
  "deleted_at": null,
  "tasks": [
    {
      "id": "01997c3e-6b42-7000-9d55-8f1b60a2c934",
      "user_id": "019935ba-0f21-7000-bb0e-6a5f1d2c7a41",
      "type": "staging",
      "status": "pending",
      "progress": 0,
      "created_at": "2026-08-26T09:12:00.000Z",
      "updated_at": "2026-08-26T09:12:00.000Z",
      "started_at": null,
      "finished_at": null,
      "error": null,
      "project_id": "01997c3e-6a10-7000-8a1f-2c0d4a9d1e77",
      "config": {
        "remove_furniture": {
          "mode": "auto"
        },
        "add_furniture": {
          "style": "modern",
          "room_type": "living_room"
        }
      },
      "sources": [
        {
          "url": "https://example.com/living-room.jpg",
          "view_index": 0
        }
      ],
      "results": []
    }
  ]
}

List projects

GET/v1/projects

Every project the key’s account owns, newest first, cursor-paginated.

An empty `paging.next` is how you are told to stop — there is no total, and no page count.

Query parameters

limitintegeroptional
Rows per page. Defaults to 20.
nextstringoptional
Cursor from the previous response.
prevstringoptional
Cursor for the page before this one.
Example response · 200
{
  "data": [
    {
      "id": "01997c3e-6a10-7000-8a1f-2c0d4a9d1e77",
      "user_id": "019935ba-0f21-7000-bb0e-6a5f1d2c7a41",
      "sources": [
        {
          "url": "https://example.com/living-room.jpg",
          "view_index": 0
        }
      ],
      "created_at": "2026-08-26T09:12:00.000Z",
      "settled_at": null,
      "deleted_at": null,
      "tasks": [
        {
          "id": "01997c3e-6b42-7000-9d55-8f1b60a2c934",
          "user_id": "019935ba-0f21-7000-bb0e-6a5f1d2c7a41",
          "type": "staging",
          "status": "pending",
          "progress": 0,
          "created_at": "2026-08-26T09:12:00.000Z",
          "updated_at": "2026-08-26T09:12:00.000Z",
          "started_at": null,
          "finished_at": null,
          "error": null,
          "project_id": "01997c3e-6a10-7000-8a1f-2c0d4a9d1e77",
          "config": {
            "remove_furniture": {
              "mode": "auto"
            },
            "add_furniture": {
              "style": "modern",
              "room_type": "living_room"
            }
          },
          "sources": [
            {
              "url": "https://example.com/living-room.jpg",
              "view_index": 0
            }
          ],
          "results": []
        }
      ]
    }
  ],
  "paging": {
    "next": "MDE5OTdjM2U2YTEw",
    "prev": ""
  }
}

Delete a project

DELETE/v1/projects/{projectId}

Soft delete. Repeating it is a no-op rather than an error, and credits already spent are not returned.

Path parameters

projectIduuidrequired
The project’s id.
Example response · 204
(no body)

Run a generation

POST/v1/generationTasks

Adds a run to a project that already exists — a restage, a different style, another type entirely.

Free, and capped at eighteen runs per project. Which is why the flow is: create the room once, then regenerate here until the result is right.

Staging types are dispatched to a GPU and come back `pending` — poll the project. Every other type runs inline and comes back already `completed` or `failed`, which is also why those requests can take most of a minute.

A batch where every run failed is an error (`GPU_PROCESSING_FAILED`); one where some succeeded comes back as it is, failures included.

Body

project_iduuidrequired
The project to add the run to.
typestringrequired
What to run. See the type reference below for the config each one takes.
configobjectrequired
The options for that type. Its shape is decided by `type`.
task_countintegeroptional
How many runs of this config to start at once, 1–4. Defaults to 1. Free: the project is charged once however many runs happen in it.
sourcesobject[]optional
What this run should eat, when that is not the room’s own photos — a result from an earlier run, say. Omit for the normal case. Required for 3d-staging, which never reads the room’s photos.
Example request
{
  "project_id": "01997c3e-6a10-7000-8a1f-2c0d4a9d1e77",
  "type": "staging",
  "config": {
    "remove_furniture": {
      "mode": "off"
    },
    "add_furniture": {
      "style": "scandinavian",
      "room_type": "living_room"
    }
  },
  "task_count": 2
}
Example response · 200
{
  "data": [
    {
      "id": "01997c3e-6b42-7000-9d55-8f1b60a2c934",
      "user_id": "019935ba-0f21-7000-bb0e-6a5f1d2c7a41",
      "type": "staging",
      "status": "pending",
      "progress": 0,
      "created_at": "2026-08-26T09:12:00.000Z",
      "updated_at": "2026-08-26T09:12:00.000Z",
      "started_at": null,
      "finished_at": null,
      "error": null,
      "project_id": "01997c3e-6a10-7000-8a1f-2c0d4a9d1e77",
      "config": {
        "remove_furniture": {
          "mode": "auto"
        },
        "add_furniture": {
          "style": "modern",
          "room_type": "living_room"
        }
      },
      "sources": [
        {
          "url": "https://example.com/living-room.jpg",
          "view_index": 0
        }
      ],
      "results": []
    }
  ]
}

Run a processing step

POST/v1/processingTasks

Intermediate work a flow needs before it can carry on — an empty-room plate, a mask, an upscale.

These belong to no project and cost nothing; the charge sits on the project they feed. They always block until the worker answers, so what comes back is finished.

The one most integrations want is `removal`: the same photo with the furniture taken out. Pass the result as `sources[].removal_url` when you create the project and every run there will start from the empty room.

Body

typestringrequired
Which step to run.
removalinterior-segmentationprompt-editingreference-editingupscale
payloadobjectrequired
The step’s input, decided by `type`. `removal` and `interior-segmentation` take { image_url }; the editing steps add { mask_url, … }; `upscale` takes { image_url, scale?, tile_size?, overlap? }.
Example request
{
  "type": "removal",
  "payload": {
    "image_url": "https://example.com/living-room.jpg"
  }
}
Example response · 200
{
  "id": "01997c3e-7c19-7000-a3d2-4b7e9f0a1c56",
  "user_id": "019935ba-0f21-7000-bb0e-6a5f1d2c7a41",
  "type": "removal",
  "status": "completed",
  "progress": 100,
  "created_at": "2026-08-26T09:10:00.000Z",
  "updated_at": "2026-08-26T09:10:22.000Z",
  "started_at": "2026-08-26T09:10:00.000Z",
  "finished_at": "2026-08-26T09:10:22.000Z",
  "error": null,
  "payload": {
    "image_url": "https://example.com/living-room.jpg"
  },
  "result": {
    "url": "https://content.edensign.io/images/019a4f1c-2f7d-7a11-b0c4-9a3e5d7f1b22"
  }
}

Waiting for results

There are no webhooks yet. Poll GET /v1/projects/{projectId} — it returns the room and every run in it, so one call covers a whole batch.

pendingAccepted, not started. Staging tasks come back like this.
processingRunning. `progress` moves; `results` may already hold finished views.
completedDone. Read each result as `current_url || url`.
failed`error.reason` says why. The project and its photos are untouched.
expiredThe worker went quiet and a sweep closed it out.

Poll every two to three seconds. A staging run usually lands well inside a minute, and a multi-view run fills results one view at a time, so there is something to show before the task ends.

Generation types

Every value type accepts, and the config that goes with it. Field names inside a config are camelCase — that is the vocabulary the models take, and it is deliberate that the surrounding envelope is snake_case.

typeWhat it doesconfig
stagingFurnish a room, empty it first, or both. The workhorse.remove_furniture { mode: on | off | auto, mask_url? }, add_furniture? { style, room_type }
multi-view-stagingThe same, kept consistent across several angles of one room.Same as staging. Which one runs follows from how many photos the project has.
3d-stagingThe relight pass behind the 3D studio. Takes composited views as `sources` and never reads the room’s own photos.style?, room_type?
day-to-duskTurn a daytime exterior into a dusk shot.{} — no options
floor-planRedraw a floor plan.style? (base | lit), quality? (low | medium | high)
renovationRenovate a room in a given style.roomType, style
enhancementPhoto cleanup: exposure, lens, clutter.colorAndExposureCorrection, geometricAndLensCorrection, objectRemovalAndCleanup, naturalElementEnhancement — all booleans
vacant-lot-housePut a house on an empty lot.buildingType, architecturalStyle, userPrompt?
masked-editingEdit only what a mask covers, guided by a prompt.maskUrl, userPrompt, enableHD?
reference-editingReplace what a mask covers with an object from a reference photo.maskUrl, referenceImageUrl, referenceImageClassName, enableHD?
change-seasonMove the scene to another season.season, userPrompt?
lawn-replacementReplace the lawn.style, userPrompt?
fill-empty-poolFill an empty pool.style, userPrompt?
pool-water-enhancementClean up pool water.style, userPrompt?
add-poolAdd a pool to a yard.style, userPrompt?
change-weatherChange the weather.weather, userPrompt?
sky-replacementReplace the sky.sky, userPrompt?
window-view-replacementReplace what is out the window.view, userPrompt?
repaint-wallRepaint walls.scene, style, userPrompt?
exterior-renderingRender an exterior from a prompt.userPrompt?
landscape-designDesign the front landscaping.style, userPrompt?
backyard-designDesign the backyard.style, userPrompt?
aerial-enhancementClean up a drone shot.style, dehaze?, correctLensDistortion?, removeGroundClutter?, userPrompt?
flooring-replacementReplace the flooring.style, userPrompt?

Vocabulary

The values a config field accepts, by name.

room_type / roomType
living_roombedroomkitchendining_roombathroomhome_officeoutdoorkids_roomhallwayhome_theaterliving_bedroomliving_diningbalcony
style (furniture)
standardscandinavianmodernmid_centuryluxuryfarmhousecoastalindustrialtransitional
season
springsummerautumnwinter
weather
sunnycloudyrainysnowyfoggysunset
sky
clear_blueblue_with_cloudsdramatic_cloudssunset_glowgolden_hourtwilight
view (window)
clear_blue_skyoceancity_skylinegreen_gardenmountainslakeforestsunset
style (lawn)
lush_greenmanicuredtropical_lushwildflower
style (pool water)
crystal_bluetropical_turquoisenatural_lagoonmodern_dark
style (landscape)
modernenglish_cottagejapanese_zentropicalmediterraneandesert_xeriscape
style (backyard)
modern_loungeresort_poolfirepit_patiooutdoor_kitchenzen_retreatfamily_play
style (aerial)
naturalenhancedvivid
style (flooring)
light_oak_plankwalnut_plankherringbone_oakwide_plank_white_oakmarble_whiteceramic_neutrallvp_modern_graypolished_concrete
scene (wall)
interioraccentexterior
style (wall)
pure_whitewarm_whitesoft_beigelight_graysage_greennavy_bluecharcoalterracotta
style (floor plan)
baselit
quality (floor plan)
lowmediumhigh
buildingType
single_family_detachedtownhousecondominiumduplextriplex
architecturalStyle
colonial_revivalranchcraftsmanvictoriancape_codmid_century_moderncontemporaryfarmhouse

Something missing, or you need a callback instead of polling? Tell us