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.
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.
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.
Quickstart
Stage a photo end to end, in two calls.
# 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 project | 1–4, and they must be angles of the same room |
| Runs per project | 18 |
| Runs per request | 4 (task_count) |
| Request timeout | 60 seconds |
| Charged | Once, 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": {
"code": 403,
"status": "PERMISSION_DENIED",
"message": "insufficient credits",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "INSUFFICIENT_CREDITS"
}
]
}
}| Reason | HTTP | Meaning |
|---|---|---|
| UNAUTHENTICATED | 401 | Missing, malformed, revoked, or unknown API key. |
| INSUFFICIENT_CREDITS | 403 | The account behind the key cannot pay for the project. Nothing was created. |
| — | 403 | PERMISSION_DENIED with no reason: the project belongs to another account. |
| — | 404 | NOT_FOUND: no such project or task. |
| MAX_GENERATION_TASKS_EXCEEDED | 400 | The project is at its eighteen-run ceiling. Create a new project. |
| GPU_PROCESSING_FAILED | 503 | The model failed and produced nothing. Usually worth retrying. |
Create a project
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.
{
"sources": [
{
"url": "https://example.com/living-room.jpg"
}
],
"task": {
"type": "staging",
"config": {
"remove_furniture": {
"mode": "auto"
},
"add_furniture": {
"style": "modern",
"room_type": "living_room"
}
}
}
}{
"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
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.
{
"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
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.
{
"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
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.
(no body)Run a generation
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.
{
"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
}{
"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
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? }.
{
"type": "removal",
"payload": {
"image_url": "https://example.com/living-room.jpg"
}
}{
"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.
| pending | Accepted, not started. Staging tasks come back like this. |
| processing | Running. `progress` moves; `results` may already hold finished views. |
| completed | Done. Read each result as `current_url || url`. |
| failed | `error.reason` says why. The project and its photos are untouched. |
| expired | The 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.
| type | What it does | config |
|---|---|---|
| staging | Furnish a room, empty it first, or both. The workhorse. | remove_furniture { mode: on | off | auto, mask_url? }, add_furniture? { style, room_type } |
| multi-view-staging | The same, kept consistent across several angles of one room. | Same as staging. Which one runs follows from how many photos the project has. |
| 3d-staging | The relight pass behind the 3D studio. Takes composited views as `sources` and never reads the room’s own photos. | style?, room_type? |
| day-to-dusk | Turn a daytime exterior into a dusk shot. | {} — no options |
| floor-plan | Redraw a floor plan. | style? (base | lit), quality? (low | medium | high) |
| renovation | Renovate a room in a given style. | roomType, style |
| enhancement | Photo cleanup: exposure, lens, clutter. | colorAndExposureCorrection, geometricAndLensCorrection, objectRemovalAndCleanup, naturalElementEnhancement — all booleans |
| vacant-lot-house | Put a house on an empty lot. | buildingType, architecturalStyle, userPrompt? |
| masked-editing | Edit only what a mask covers, guided by a prompt. | maskUrl, userPrompt, enableHD? |
| reference-editing | Replace what a mask covers with an object from a reference photo. | maskUrl, referenceImageUrl, referenceImageClassName, enableHD? |
| change-season | Move the scene to another season. | season, userPrompt? |
| lawn-replacement | Replace the lawn. | style, userPrompt? |
| fill-empty-pool | Fill an empty pool. | style, userPrompt? |
| pool-water-enhancement | Clean up pool water. | style, userPrompt? |
| add-pool | Add a pool to a yard. | style, userPrompt? |
| change-weather | Change the weather. | weather, userPrompt? |
| sky-replacement | Replace the sky. | sky, userPrompt? |
| window-view-replacement | Replace what is out the window. | view, userPrompt? |
| repaint-wall | Repaint walls. | scene, style, userPrompt? |
| exterior-rendering | Render an exterior from a prompt. | userPrompt? |
| landscape-design | Design the front landscaping. | style, userPrompt? |
| backyard-design | Design the backyard. | style, userPrompt? |
| aerial-enhancement | Clean up a drone shot. | style, dehaze?, correctLensDistortion?, removeGroundClutter?, userPrompt? |
| flooring-replacement | Replace the flooring. | style, userPrompt? |
Vocabulary
The values a config field accepts, by name.
Something missing, or you need a callback instead of polling? Tell us