Reference
Jobs
Create a job, read its state, delete it, check the account balance, and how long images are kept.
A job is one image through one tool. Creating a job charges immediately and returns before generation finishes; you collect the result by polling or by webhook.
Create a job
POST /v1/jobs
Headers
| Header | Type | Required | Description |
|---|---|---|---|
X-Api-Key | string | required | Key issued in the developer portal, prefixed |
Idempotency-Key | stringmax 128 characters | optional | Client-generated key that makes a retry safe. Repeating a request with the same key and the same body returns the original job — no second charge, no second render — and still answers |
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
tool | stringmax 100 characters | required | Tool slug, exactly as returned by |
image_url | string · urimax 2000 characters | optional | Public https URL of the input image (JPEG, PNG or WebP, max 20 MB). Provide exactly one of |
image_base64 | string | optional | The image inline, base64-encoded, optionally with a data-URI prefix ( |
options | object | optional | Tool-specific options. For tool handlers, an unknown key is ignored — and named in the response's Some tools require options: See |
webhook_url | string · urimax 2000 characters | optional | Overrides the account webhook for this job. Must be a public https URL; private and loopback hosts are rejected with |
Response
201 with the job id and what it cost.
| Parameter | Type | Required | Description |
|---|---|---|---|
job_id | string · uuid | required | Pass this to |
status | string | required | Always processing |
images_charged | integer | required | Images charged for this job. |
warnings | array<object> | required | Options in the request that the tool will not use: a key it does not have, a |
For a new job the status is always processing — the job is accepted, generation has not
finished. A replayed Idempotency-Key also answers 201, but with the job as it stands now:
the same job object GET /v1/jobs/{id} returns, plus warnings. See
Idempotency.
Warnings
An option the tool will not use never fails the request — the job runs as it always has —
but the response names it in warnings, so a typo does not cost you a render in silence:
- a key the tool does not have, such as
stlyeforstyle(the message suggests the key you probably meant); customPrompton a tool that does not read one, or sent withouttier: "custom";- a
resolutionother than2Kor4K, in which case the default size is used; - on floor plan to 3D, a
vieworstylealias that lost to its canonical key, or astylethat is not one of the interior styles.
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | required | Stable machine-readable code. Branch on this, never on option_ignored |
option | string | required | The |
message | string | required | Human-readable detail, often with the key you probably meant. May change without notice. |
{
"job_id": "3f1c9d4e-5b2a-4c8e-9f10-7d6a2b3c4e5f",
"status": "processing",
"images_charged": 1,
"warnings": [
{
"code": "option_ignored",
"option": "stlye",
"message": "\"stlye\" is not an option of virtual-renovation, so it was ignored. Did you mean \"style\"?"
}
]
}warnings is always present and usually empty. Log it while you build the integration, and
branch on code, never on message. Virtual staging is the exception: it rejects an unknown
key with invalid_request instead, so its warnings is always empty.
Choosing an image source
Provide exactly one of image_url or image_base64. Neither or both is invalid_image.
image_url
A public https URL serving JPEG, PNG or WebP, up to 20 MB.
Roomagen fetches it server-side with a 10-second timeout. Private, loopback and link-local
hosts are refused. At most 3 redirects are followed, and each hop is re-checked against the
same rules. A non-image Content-Type or a non-2xx status comes back as image_fetch_failed.
image_base64
The image inline, base64-encoded, optionally with a data-URI prefix
(data:image/png;base64,…). The prefix also selects the stored format; without one the image
is treated as JPEG.
The decoded image may be up to 20 MB, and the whole request body must stay under 30 MB —
but that larger body limit applies only when X-Api-Key is present. Anonymous requests
are capped at 1 MB and rejected with payload_too_large, which is what you see if the header
is missing or misspelled on a large upload.
For anything close to the limit, prefer image_url: it keeps your request small and avoids
base64's 33% size overhead.
Idempotency
Send an Idempotency-Key header to make a retry safe after a timeout or a dropped
connection.
- Repeating a request with the same key and the same body returns the original job — no
second charge, no second render — and still answers
201. The body is that job as it stands now, the same job objectGET /v1/jobs/{id}returns:processingwhile it renders,completedwithresult_urls, orfailedwitherror. A retry that lands after the render finished therefore gets the result straight away. It also carries the original request'swarnings. - If the job that key created no longer exists, the replay answers
404 job_not_found. The key stays spent on that job: send a new key to render again. - The same key with a different body is rejected with
409 idempotency_conflict. Send the original body to replay, or a new key for a genuinely new request. - Keys are scoped to your API key and may be at most 128 characters. Longer is rejected with
400 invalid_requestrather than truncated.
Derive the key from your own records
Use a value you can reconstruct, such as order-8842-image-1. A random key that you do not
store cannot be replayed, which defeats the point — on retry it looks like a new request and
charges again.
Read a job
GET /v1/jobs/{id}, where id is the job_id returned when the job was created. Poll about
once every 2–5 seconds; result_urls is empty until status is completed.
curl https://api.roomagen.com/api/v1/jobs/3f1c9d4e-5b2a-4c8e-9f10-7d6a2b3c4e5f \
-H "X-Api-Key: rmg_live_YOUR_KEY"{
"job_id": "3f1c9d4e-5b2a-4c8e-9f10-7d6a2b3c4e5f",
"tool": "virtual-staging",
"status": "completed",
"images_charged": 1,
"result_urls": [
"https://uploads.roomagen.com/6f0c2b9e-4a1d-4c3b-9e7f-2d8a1b5c3e90/9c2f7a10-5d4e-4f3a-8b2c-1e6d7f8a9b0c-virtual-staging-result.jpg"
],
"error": null,
"created_at": "2026-08-24T09:14:02.114Z",
"completed_at": "2026-08-24T09:14:39.902Z",
"processing_ms": 37788
}A job id belonging to another account is reported as job_not_found — the API never confirms
that someone else's id exists. A job that was deleted answers job_not_found too.
The job object
| Parameter | Type | Required | Description |
|---|---|---|---|
job_id | string · uuid | required | |
tool | string | required | The slug that ran. |
status | string | required | Terminal states are processingcompletedfailed |
images_charged | integer | required | What this job cost at creation time. A |
result_urls | array<string · uri> | required | Rendered images, empty until |
error | string | null | required | Human-readable failure reason when |
created_at | string · date-time | required | |
completed_at | string | null · date-time | required | Set when the job reaches |
processing_ms | integer | null | required | Wall-clock generation time, available once the job is no longer processing. |
completed and failed are terminal; stop polling on either. A failed job is refunded
automatically, and images_charged is not rewritten — treat it as what was charged at
creation, not the net effect on your balance.
Delete a job
DELETE /v1/jobs/{id} deletes a finished job — completed or failed — together with the
images stored for it: the rendered results and the input image. It answers 204 with an empty
body.
curl -X DELETE https://api.roomagen.com/api/v1/jobs/3f1c9d4e-5b2a-4c8e-9f10-7d6a2b3c4e5f \
-H "X-Api-Key: rmg_live_YOUR_KEY"- A job that is still
processinganswers409 job_in_progress. Send theDELETEagain onceGET /v1/jobs/{id}reportscompletedorfailed. - A job id that belongs to another account, an unknown id, or a job that was already deleted
answers
404 job_not_found. After a delete,GET /v1/jobs/{id}, a secondDELETEand a replay of the job'sIdempotency-Keyanswer404 job_not_foundas well. - A webhook for the job that has not been delivered yet is not sent.
- Deleting a job does not refund it, and it still counts in your usage.
- The job's record — its id, tool, status, images charged and timestamps — is kept for billing. The options sent with it are deleted.
- If a stored image cannot be removed at that moment, the delete still answers
204and the image is removed by a retry that runs once a day.
Deletes have a rate limit of their own: 60 per minute per key, separate from creating and reading jobs.
Storage and retention
Result images are served from public URLs of this form:
https://uploads.roomagen.com/<account-id>/<file-id>-<name>.<ext>The URLs are not signed and do not expire: anyone who has a URL can download the image until
the image is deleted. <account-id> identifies the account behind your API keys, and <name>
is an internal label that can differ from the job's tool. Use the URLs exactly as the API
returns them; do not build or parse them.
| When | What is deleted |
|---|---|
You call DELETE /v1/jobs/{id} | The job's results and its input image, at once. |
The key has a retention_days | Each job created with that key once it is older than that many days, counted from its created_at: results and input image. A daily run at 04:20 UTC applies it; a large backlog can take more than one run. |
| The account holds more than 2,000 completed jobs | The oldest completed jobs, results and input image, when a job of a tool other than virtual-staging completes. Jobs of every tool count toward the limit, and it counts all keys of the account together; completing a virtual-staging job does not apply it. |
| A job failed | Its input image, 30 days after the job was created. The job itself can still be read. |
A request is rejected without creating a job, for example with 402 insufficient_credits | The image it sent, at once. |
Without a retention_days, nothing else deletes a job: it stays until you delete it or the
2,000-job limit removes it. An account that only runs virtual-staging therefore keeps its jobs
until it deletes them.
retention_days is set by Roomagen on request. Email
[email protected] with the prefix of the key (rmg_live_…) and a
number of days from 1 to 365. GET /v1/account returns the value for the key that calls it,
null when the key has none.
The input image of a completed job is stored with the job and deleted with it; the API does not return its URL.
Deleting removes the images from storage. A copy that the content delivery network in front of
uploads.roomagen.com has already cached can still be served until that cache expires. Download
and store the images you need on your side.
Check your balance
GET /v1/account returns the identity of the key and how many images it can still render.
Useful as a health check, and for a low-balance alert before a batch run.
curl https://api.roomagen.com/api/v1/account -H "X-Api-Key: rmg_live_YOUR_KEY"{
"name": "Example Realty Ltd",
"status": "active",
"image_credits": 486,
"retention_days": null
}| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | required | Client name recorded when the key was issued. |
status | string | required | A disabled key cannot authenticate at all — it is rejected with activedisabled |
image_credits | integer | required | Images still renderable with the balance behind this key. |
retention_days | integer | null1–365 | required | Days after which jobs created with this key are deleted, images included — counted from each job's |
Rate limits
| Endpoint | Per minute |
|---|---|
POST /v1/jobs | 60 req/min |
GET /v1/jobs/{id} | 300 req/min |
DELETE /v1/jobs/{id} | 60 req/min |
GET /v1/tools, GET /v1/account | 120 req/min |
Per API key, in a sliding 60-second window, counted separately per route group. Counters live in the API process, so they reset on deploy.
IP backstop
600 req/min
Per client IP across all /v1 routes, applied before the key is identified. A fleet behind one NAT egress IP shares this budget; contact support if you need it raised.
| Header | Description |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window for this route and key. |
X-RateLimit-Remaining | Requests left in the current window. |
Retry-After | Seconds to wait, sent only with a 429. |
When exceeded
429 · rate_limited
Retry after Retry-After seconds. The window slides rather than resetting on a fixed boundary, so a caller that keeps a steady rate under the limit never sees a 429.
Next step
Tools — the documented slugs and the options each one accepts.