Locations
Script locations over REST: create and archive places, generate establishing shots, time-of-day, weather and angle variants, 360-degree views and motion clips.
The Locations API scripts everything Location Studio does. You create a location, generate candidate establishing shots, approve one, and add variants for time of day, weather, season, camera angle and lighting, plus looping atmosphere clips. Image and video nodes then reuse the location, so the same alley or library looks the same in every shot.
The routes work on every edition, except the 360-degree view route, which needs Nodaro Cloud. They take a bearer token: a personal API token (ndr_…), an OAuth app token (ndr_app_…), or your session token on Community edition. See Authentication.
Endpoints
| Method | Path | What it does |
|---|---|---|
GET | /v1/locations | List your locations. |
GET | /v1/locations/:id | Get one location with its jobs in progress and recent candidates. |
POST | /v1/locations | Create a location, or update one when the body has an id. |
DELETE | /v1/locations/:id | Archive a location. It can be restored. |
DELETE | /v1/locations/:id?permanent=true | Delete an archived location and its files for good. |
POST | /v1/locations/:id/restore | Restore an archived location. |
POST | /v1/generate-location | Generate 1 to 10 candidate establishing shots. |
POST | /v1/generate-location-asset | Generate one time-of-day, weather, season, angle, lighting or custom variant. |
POST | /v1/generate-surround-continuation | Nodaro Cloud. Generate the next view of a 360-degree look-around. |
POST | /v1/generate-location-motion | Animate the establishing shot into an atmosphere clip. |
POST | /v1/locations/:id/approve-main-image | Approve a candidate as the main image and write the location's description. |
POST | /v1/locations/:id/llm-caption | Write the description again from the current main image. |
What a location holds
| Field | What it holds |
|---|---|
id, name, description | The identifier, the display name and identity notes. |
category | indoor, outdoor, urban, nature, fantasy, sci-fi, historical, futuristic or other. |
style | realistic, anime, 3d-pixar or illustration. |
sourceImageUrl | The anchor establishing shot, set when you approve a candidate. |
canonicalDescription | A visual description of about 80 to 120 words, written by Nodaro when the main image is approved. It is an empty string until then. |
styleLock | Whether variants are generated from the main image. true by default. |
timeOfDay, weather, seasons, angles, lighting, atmosphereMotions | The asset buckets. Each entry is { name, url }; atmosphereMotions holds videos. |
referencePhotos | Up to 20 mood-board photos, each { kind, url }. |
piiConsentAt | When you confirmed consent for the reference photos, or null. |
pendingJobs, previousCandidates | On GET /v1/locations/:id only: the variant jobs still running, and up to 5 recent candidate main images, newest first. |
The asset buckets
| Bucket | What it shows | Preset variants |
|---|---|---|
timeOfDay | The same frame at another time | dawn, morning, noon, afternoon, golden hour, dusk, blue hour, night, midnight |
weather | The same frame in other weather | clear, cloudy, light rain, heavy rain, storm, snow, blizzard, fog, mist |
seasons | The same frame in another season | spring, summer, autumn, winter |
angles | The place from another camera angle | wide, medium, closeup, aerial, low-angle, eye-level, bird's-eye, dutch tilt |
lighting | Another lighting setup | soft natural, harsh sunlight, golden, blue hour, neon, candlelit, cinematic, dramatic chiaroscuro |
atmosphereMotions | Looping ambient camera moves | slow dolly-in, slow pan-left, slow pan-right, push up, drone fly-over, gentle drift, parallax, static atmospheric |
Reference photos and consent
A mood board travels with the location. Every node that uses the location receives these photos as extra references. Each photo's kind tells the model what the photo is for:
kind | What it is for |
|---|---|
wide | A wider view of the same place. |
interior, exterior | The inside when the main image shows the outside, or the reverse. |
detail | A defining detail, such as a statue, a sign or a material. |
moodBoard | The palette or the feel. |
other | Anything else. |
You can add up to 20 photos, and any number of each kind.
Reference photos can show people's faces. When you first attach photos to a location, also set piiConsentAt to the current time. It records that you have the rights and the consent to use the photos. While it is null, the editor asks for consent the next time someone opens the location.
List and read locations
GET /v1/locations returns your active locations. Add archived=true for the archive. Without limit, the route returns the full list; with limit (at most 500), it returns one page and a nextCursor to pass back as cursor until it is null.
GET /v1/locations/:id returns one location, archived or not, so workflows that use an archived location keep working.
curl "https://app.nodaro.ai/v1/locations?limit=100" \
-H "Authorization: Bearer $NODARO_API_KEY"
curl https://app.nodaro.ai/v1/locations/9a1d3f5b-7c2e-4a8d-b6f1-3e5c7a9b2d4f \
-H "Authorization: Bearer $NODARO_API_KEY"import { createClient, StaticTokenAuth } from '@nodaro/sdk'
const client = createClient({
baseUrl: 'https://app.nodaro.ai',
auth: new StaticTokenAuth(process.env.NODARO_API_KEY!),
})
const { locations } = await client.locations.list()
const alley = await client.locations.get(locations[0].id)
console.log(alley.previousCandidates)nodaro locations list --json
nodaro locations get <id> --jsonCreate or update a location
POST /v1/locations creates a location when the body has no id, and updates it when it has one. A create needs nodeId and name; use any label for nodeId, such as "scripted", when there is no canvas node.
On an update, only the fields you send are written, and the asset buckets are never written. Send expectedUpdatedAt, the location's current updatedAt, to refuse the update with 409 concurrent_modification when someone changed it since you read it.
curl -X POST https://app.nodaro.ai/v1/locations \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"nodeId": "scripted",
"name": "Rainy Tokyo Alley",
"description": "Neon-soaked alley with vending machines and wet pavement",
"category": "urban",
"style": "realistic"
}'const { id } = await client.locations.create({
nodeId: 'scripted',
name: 'Rainy Tokyo Alley',
description: 'Neon-soaked alley with vending machines and wet pavement',
category: 'urban',
style: 'realistic',
})
const location = await client.locations.get(id)
await client.locations.update(id, {
referencePhotos: [{ kind: 'wide', url: 'https://cdn.nodaro.ai/uploads/alley-wide.jpg' }],
piiConsentAt: new Date().toISOString(),
expectedUpdatedAt: location.updatedAt,
})nodaro locations create "Rainy Tokyo Alley" --node-id scripted \
--description "Neon-soaked alley with vending machines and wet pavement" \
--category urban --style realistic
nodaro locations update <id> --style-lock falseProp
Type
A create returns { id }.
Style Lock decides how variants are made. With Style Lock on, the default, every variant is generated from the approved main image, so the building, the materials and the composition stay the same. With Style Lock off, variants are generated from text only and may reinterpret the place.
Generate candidate establishing shots
POST /v1/generate-location starts one job per candidate and returns jobIds. A single-candidate request also returns jobId.
- One candidate, attached. With
attachToLocationIdand acountof 1, the result becomes the main image when the job completes. - Several candidates. Nothing is attached. The current main image stays, and completed candidates appear in
previousCandidatesonGET /v1/locations/:id. Approve the one you prefer. - Edit the current shot.
userPromptis a one-off instruction, for example "add rain and puddles". WithsourceImageUrl, the source image is edited toward the instruction. The instruction is never saved to the location.
curl -X POST https://app.nodaro.ai/v1/generate-location \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Rainy Tokyo Alley",
"count": 1,
"attachToLocationId": "9a1d3f5b-7c2e-4a8d-b6f1-3e5c7a9b2d4f"
}'const { jobIds } = await client.locations.generate({
name: 'Rainy Tokyo Alley',
description: 'Neon-soaked alley with vending machines',
count: 4,
})nodaro locations generate --name "Rainy Tokyo Alley" --count 1 \
--attach-to-location-id <id> --watch{ "jobId": "4d6f8b1a-3c5e-4f7a-9b2d-6e8a1c3f5b7d", "jobIds": ["4d6f8b1a-3c5e-4f7a-9b2d-6e8a1c3f5b7d"] }| Field | What it does |
|---|---|
name | Required. The location name. |
description, category, style | The location's identity. |
count | Candidates to generate, 1 to 10. The default is 1. |
userPrompt | A one-off instruction that drives this generation. Never saved. |
sourceImageUrl | An image to edit or start from. |
provider | The image model id. Omit it for the default model. |
quality, resolution | The image model's output tier, priced as on Generate Image. |
attachToLocationId | Attach a single candidate to this location. |
quality and resolution work as they do for characters: a value the model does not support is changed to the nearest supported one, and the credits follow the changed value. The job's input_data shows the value that ran.
Generate a variant
POST /v1/generate-location-asset generates one variant and returns { jobId }. Send attachToLocationId, attachToColumn and attachName to append { name: attachName, url } to the bucket when the job completes.
curl -X POST https://app.nodaro.ai/v1/generate-location-asset \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Rainy Tokyo Alley",
"assetType": "weather",
"variant": "storm",
"attachToLocationId": "9a1d3f5b-7c2e-4a8d-b6f1-3e5c7a9b2d4f",
"attachToColumn": "weather",
"attachName": "storm"
}'await client.locations.generateAsset({
name: 'Rainy Tokyo Alley',
assetType: 'timeOfDay',
variant: 'blue hour',
attachToLocationId: id,
attachToColumn: 'time_of_day',
attachName: 'blue hour',
})nodaro locations generate-asset <id> --asset-type weather --variant storm --watchassetType is timeOfDay, weather, seasons, angles, lighting or custom. attachToColumn is time_of_day, weather, seasons, angles or lighting, and a custom variant must name it. The route also takes provider, quality, resolution and sourceImageUrl.
Build a 360-degree view
POST /v1/generate-surround-continuation builds a look-around one view at a time, for example every 45 degrees. Each call continues the previous view. Nodaro carries the edge of that view into the new frame and paints only the rest. It then matches the painted part's exposure and color to the carried part. The carried part stays pixel-exact, so adjacent views line up in a panorama viewer. This route needs Nodaro Cloud; other editions answer 403 edition_required.
curl -X POST https://app.nodaro.ai/v1/generate-surround-continuation \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"referenceImageUrl": "https://cdn.nodaro.ai/locations/alley-main.png",
"direction": "right",
"degrees": 45,
"provider": "nano-banana-pro",
"aspectRatio": "16:9",
"attachToLocationId": "9a1d3f5b-7c2e-4a8d-b6f1-3e5c7a9b2d4f",
"attachToColumn": "angles",
"attachName": "Surround 45°"
}'const { jobId } = await client.locations.generateSurroundContinuation({
referenceImageUrl: previousView,
direction: 'right',
degrees: 45,
provider: 'nano-banana-pro',
aspectRatio: '16:9',
attachToLocationId: id,
attachToColumn: 'angles',
attachName: 'Surround 45°',
})| Field | What it does |
|---|---|
referenceImageUrl | Required. The previous view to continue from. |
direction | Required. right or left to turn, up or down to tilt. |
degrees | The angle of this view, 0 to 360, stored with the result. |
carriedFraction | How much of the frame is carried from the previous view, 0.1 to 0.9. The default is 0.5 for a turn and a thin strip for a tilt. |
provider, aspectRatio | The image model and the frame. The editor uses nano-banana-pro and 16:9 so every view matches the establishing shot. |
attachToLocationId, attachToColumn, attachName | Attach the view, usually to the angles bucket. |
Each view costs one generation on the chosen image model. The carrying and the color matching are not charged separately.
Animate the establishing shot
POST /v1/generate-location-motion turns the establishing shot into an ambient clip: drifting fog, a slow dolly, a drone fly-over. The route returns { jobId }. sourceImageUrl is required; pass the approved main image. With attachToLocationId and attachName, the clip is appended to atmosphereMotions; you do not send a column.
curl -X POST https://app.nodaro.ai/v1/generate-location-motion \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Rainy Tokyo Alley",
"motionPrompt": "slow dolly-in, neon signs flicker, light rain falling",
"sourceImageUrl": "https://cdn.nodaro.ai/locations/alley-main.png",
"provider": "kling",
"attachToLocationId": "9a1d3f5b-7c2e-4a8d-b6f1-3e5c7a9b2d4f",
"attachName": "neon dolly-in"
}'await client.locations.generateMotion({
name: 'Rainy Tokyo Alley',
motionPrompt: 'slow dolly-in, neon signs flicker, light rain falling',
sourceImageUrl: location.sourceImageUrl!,
provider: 'kling',
attachToLocationId: id,
attachName: 'neon dolly-in',
})nodaro locations generate-motion --name "Rainy Tokyo Alley" \
--motion-prompt "slow dolly-in, neon signs flicker, light rain falling" \
--source-image-url "https://cdn.nodaro.ai/locations/alley-main.png" \
--provider kling --attach-to-location-id <id> --attach-name "neon dolly-in" --watch| Field | What it does |
|---|---|
name, motionPrompt | Required. The location name and the movement to create. |
sourceImageUrl | Required. The start frame. |
provider | kling (the default), kling-turbo, kling-3.0, wan-i2v, wan-2.7-i2v or seedance-2. |
aspectRatio | 16:9 (the default), 1:1, 3:4 or 9:16. |
refineFromVideoUrl | An existing clip to refine with the new prompt, for example to turn fog into rain without moving the camera. Use a model that supports video-to-video, such as wan-i2v. |
attachToLocationId, attachName | The location and the clip's name in atmosphereMotions. |
| Model | Maker | Modes | Credits | Details |
|---|---|---|---|---|
| Kling 2.6 | Kuaishou | Image to video, Text to video | from 138 | Kling 2.6 I2V — strong motion realism. 5s/10s, optional native audio. |
| Kling 2.5 Turbo Pro | Kuaishou | Image to video, Text to video | from 110 | Faster Kling — good quality at lower cost. Supports end frame. |
| Kling 3.0 | Kuaishou | Image to video, Text to video | from 270 | Premium Kling 3.0 — variable 3-15s duration, native audio, 720P/1080P. |
| Wan 2.6 I2V | Alibaba | Image to video | from 175 | Wan 2.6 image-to-video — 5/10/15s at 720p/1080p. |
| Wan 2.7 I2V | Alibaba | Image to video | 188 | Wan 2.7 image-to-video — 2–15s at 720p/1080p, supports start+end frame. |
| Seedance 2 | Bytedance | Image to video, Text to video | from 230 | Seedance 2 — premium tier with native audio. Per-second pricing by resolution. |
Approve a main image
POST /v1/locations/:id/approve-main-image with { candidateJobId } sets a completed candidate as the main image and writes canonicalDescription in the same call. It returns { sourceImageUrl, canonicalDescription }.
When writing the description fails, the main image is still set and canonicalDescription is an empty string; the SDK returns null. Call POST /v1/locations/:id/llm-caption to try again. It returns { canonicalDescription }, answers 502 when the description cannot be written, and 400 no_source_image when there is no main image yet. Both routes are free and safe to repeat.
curl -X POST https://app.nodaro.ai/v1/locations/9a1d3f5b-7c2e-4a8d-b6f1-3e5c7a9b2d4f/approve-main-image \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "candidateJobId": "4d6f8b1a-3c5e-4f7a-9b2d-6e8a1c3f5b7d" }'const { sourceImageUrl, canonicalDescription } =
await client.locations.approveMainImage(id, jobIds[2])nodaro locations approve-main-image <id> --candidate-job-id <jobId>
nodaro locations recaption <id>Archive, restore and delete a location
| Action | curl | TypeScript SDK | CLI |
|---|---|---|---|
| Archive | DELETE /v1/locations/:id | client.locations.delete(id) | nodaro locations delete <id> |
| Restore | POST /v1/locations/:id/restore | client.locations.restore(id) | nodaro locations restore <id> |
| Delete for good | DELETE /v1/locations/:id?permanent=true | Not available | Not available |
- Archive returns
{ success: true, archived: true }. The location leaves the default list but still loads by id. - Restore returns
{ id, name }, with a(restored)suffix when an active location has the same name, ignoring case. - Delete for good works only on an archived location (
400 not_archivedotherwise) and removes the location and every file it references. The SDK and the CLI do not offer it; the editor's archive view asks you to type the name to confirm.
Pick a variant when you run an app
When a workflow with a Location Asset node is published as an app, the location becomes one of the app's inputs. Pass "<bucket>/<variant>", for example "weather/light-rain", to use that variant as the location's main image for the run. Write the variant name in lowercase with hyphens for spaces. An unknown bucket or variant falls back to the main image.
Use the location in other generations
Pass the location's asset URLs as reference images to Generate Image or Generate Video; explicit URLs are the simplest choice for code. In a workflow, wire the location node into the image node, or mention a variant in the prompt, for example @oldlibrary:1:weather/rain for a location named Old Library. Without a mention, Nodaro looks for variant names in your prompt: "at sunset" selects a dusk variant when you have one. See Locations.
Use it from MCP
| Tool | What it does |
|---|---|
list_locations, get_location | Find a location and read its variant URLs. |
create_location, update_location | Create a location or change its identity fields. |
generate_location | Generate a main image (kind: "main") or a variant (kind: "asset"). |
generate_location_motion | Animate the main image. |
approve_main_image, recaption_location | Approve a main image, or write its description again. |
Archive and restore are deliberately not available over MCP. See the MCP tools reference.
Credits
| Route | Price on Nodaro Cloud |
|---|---|
POST /v1/generate-location | The image model's price times count, reserved for every candidate before the first job starts. |
POST /v1/generate-location-asset | The image model's price, per variant. |
POST /v1/generate-surround-continuation | The image model's price, per view. |
POST /v1/generate-location-motion | The video model's image-to-video price, per clip. |
approve-main-image, llm-caption | Free. |
Errors
| Status | Code | Meaning |
|---|---|---|
400 | validation_error | A field is missing or invalid. |
400 | not_archived | A permanent delete was sent for a location that is not archived. |
400 | no_source_image | llm-caption was called before the location has a main image. |
401 | unauthorized | The token is missing, invalid or revoked. |
402 | insufficient_credits | Nodaro Cloud only. The account cannot cover the reservation. |
403 | edition_required | The 360-degree view route was called on Community or Business edition. |
404 | not_found | No location with that id belongs to you. |
409 | concurrent_modification | expectedUpdatedAt no longer matches. Read the location again, merge and retry. |
502 | — | The canonical description could not be written. Try again. |
Frequently asked questions
Related
Locations
Location Asset
Characters
Objects
Jobs
Last updated on
Objects
Script props and products over REST: create and archive objects, generate main images, material and angle variants and motion clips, and approve the main image.
Creatures
Create and manage animals and creatures over REST: generate main images, angle, pose and variation variants and motion clips, and give a creature a voice.