Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
REST API

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

MethodPathWhat it does
GET/v1/locationsList your locations.
GET/v1/locations/:idGet one location with its jobs in progress and recent candidates.
POST/v1/locationsCreate a location, or update one when the body has an id.
DELETE/v1/locations/:idArchive a location. It can be restored.
DELETE/v1/locations/:id?permanent=trueDelete an archived location and its files for good.
POST/v1/locations/:id/restoreRestore an archived location.
POST/v1/generate-locationGenerate 1 to 10 candidate establishing shots.
POST/v1/generate-location-assetGenerate one time-of-day, weather, season, angle, lighting or custom variant.
POST/v1/generate-surround-continuationNodaro Cloud. Generate the next view of a 360-degree look-around.
POST/v1/generate-location-motionAnimate the establishing shot into an atmosphere clip.
POST/v1/locations/:id/approve-main-imageApprove a candidate as the main image and write the location's description.
POST/v1/locations/:id/llm-captionWrite the description again from the current main image.

What a location holds

FieldWhat it holds
id, name, descriptionThe identifier, the display name and identity notes.
categoryindoor, outdoor, urban, nature, fantasy, sci-fi, historical, futuristic or other.
stylerealistic, anime, 3d-pixar or illustration.
sourceImageUrlThe anchor establishing shot, set when you approve a candidate.
canonicalDescriptionA visual description of about 80 to 120 words, written by Nodaro when the main image is approved. It is an empty string until then.
styleLockWhether variants are generated from the main image. true by default.
timeOfDay, weather, seasons, angles, lighting, atmosphereMotionsThe asset buckets. Each entry is { name, url }; atmosphereMotions holds videos.
referencePhotosUp to 20 mood-board photos, each { kind, url }.
piiConsentAtWhen you confirmed consent for the reference photos, or null.
pendingJobs, previousCandidatesOn GET /v1/locations/:id only: the variant jobs still running, and up to 5 recent candidate main images, newest first.

The asset buckets

BucketWhat it showsPreset variants
timeOfDayThe same frame at another timedawn, morning, noon, afternoon, golden hour, dusk, blue hour, night, midnight
weatherThe same frame in other weatherclear, cloudy, light rain, heavy rain, storm, snow, blizzard, fog, mist
seasonsThe same frame in another seasonspring, summer, autumn, winter
anglesThe place from another camera anglewide, medium, closeup, aerial, low-angle, eye-level, bird's-eye, dutch tilt
lightingAnother lighting setupsoft natural, harsh sunlight, golden, blue hour, neon, candlelit, cinematic, dramatic chiaroscuro
atmosphereMotionsLooping ambient camera movesslow dolly-in, slow pan-left, slow pan-right, push up, drone fly-over, gentle drift, parallax, static atmospheric

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:

kindWhat it is for
wideA wider view of the same place.
interior, exteriorThe inside when the main image shows the outside, or the reverse.
detailA defining detail, such as a statue, a sign or a material.
moodBoardThe palette or the feel.
otherAnything 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> --json

Create 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 false

Prop

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 attachToLocationId and a count of 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 previousCandidates on GET /v1/locations/:id. Approve the one you prefer.
  • Edit the current shot. userPrompt is a one-off instruction, for example "add rain and puddles". With sourceImageUrl, 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"] }
FieldWhat it does
nameRequired. The location name.
description, category, styleThe location's identity.
countCandidates to generate, 1 to 10. The default is 1.
userPromptA one-off instruction that drives this generation. Never saved.
sourceImageUrlAn image to edit or start from.
providerThe image model id. Omit it for the default model.
quality, resolutionThe image model's output tier, priced as on Generate Image.
attachToLocationIdAttach 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 --watch

assetType 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°',
})
FieldWhat it does
referenceImageUrlRequired. The previous view to continue from.
directionRequired. right or left to turn, up or down to tilt.
degreesThe angle of this view, 0 to 360, stored with the result.
carriedFractionHow 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, aspectRatioThe image model and the frame. The editor uses nano-banana-pro and 16:9 so every view matches the establishing shot.
attachToLocationId, attachToColumn, attachNameAttach 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
FieldWhat it does
name, motionPromptRequired. The location name and the movement to create.
sourceImageUrlRequired. The start frame.
providerkling (the default), kling-turbo, kling-3.0, wan-i2v, wan-2.7-i2v or seedance-2.
aspectRatio16:9 (the default), 1:1, 3:4 or 9:16.
refineFromVideoUrlAn 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, attachNameThe location and the clip's name in atmosphereMotions.
ModelMakerModesCreditsDetails
Kling 2.6KuaishouImage to video, Text to videofrom 138Kling 2.6 I2V — strong motion realism. 5s/10s, optional native audio.
Kling 2.5 Turbo ProKuaishouImage to video, Text to videofrom 110Faster Kling — good quality at lower cost. Supports end frame.
Kling 3.0KuaishouImage to video, Text to videofrom 270Premium Kling 3.0 — variable 3-15s duration, native audio, 720P/1080P.
Wan 2.6 I2VAlibabaImage to videofrom 175Wan 2.6 image-to-video — 5/10/15s at 720p/1080p.
Wan 2.7 I2VAlibabaImage to video188Wan 2.7 image-to-video — 2–15s at 720p/1080p, supports start+end frame.
Seedance 2BytedanceImage to video, Text to videofrom 230Seedance 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

ActioncurlTypeScript SDKCLI
ArchiveDELETE /v1/locations/:idclient.locations.delete(id)nodaro locations delete <id>
RestorePOST /v1/locations/:id/restoreclient.locations.restore(id)nodaro locations restore <id>
Delete for goodDELETE /v1/locations/:id?permanent=trueNot availableNot 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_archived otherwise) 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

ToolWhat it does
list_locations, get_locationFind a location and read its variant URLs.
create_location, update_locationCreate a location or change its identity fields.
generate_locationGenerate a main image (kind: "main") or a variant (kind: "asset").
generate_location_motionAnimate the main image.
approve_main_image, recaption_locationApprove a main image, or write its description again.

Archive and restore are deliberately not available over MCP. See the MCP tools reference.

Credits

RoutePrice on Nodaro Cloud
POST /v1/generate-locationThe image model's price times count, reserved for every candidate before the first job starts.
POST /v1/generate-location-assetThe image model's price, per variant.
POST /v1/generate-surround-continuationThe image model's price, per view.
POST /v1/generate-location-motionThe video model's image-to-video price, per clip.
approve-main-image, llm-captionFree.

Errors

StatusCodeMeaning
400validation_errorA field is missing or invalid.
400not_archivedA permanent delete was sent for a location that is not archived.
400no_source_imagellm-caption was called before the location has a main image.
401unauthorizedThe token is missing, invalid or revoked.
402insufficient_creditsNodaro Cloud only. The account cannot cover the reservation.
403edition_requiredThe 360-degree view route was called on Community or Business edition.
404not_foundNo location with that id belongs to you.
409concurrent_modificationexpectedUpdatedAt no longer matches. Read the location again, merge and retry.
502—The canonical description could not be written. Try again.

Frequently asked questions

Last updated on

On this page