# 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.

Source: https://nodaro.ai/docs/developers/api/locations

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](https://nodaro.ai/docs/developers/api/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**

```bash
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"
```

**TypeScript SDK**

```ts

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)
```

**CLI**

```bash
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**

```bash
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"
}'
```

**TypeScript SDK**

```ts
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,
})
```

**CLI**

```bash
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
```

<TypeTable
type={{
id: { type: 'string (uuid)', description: 'The location to update. Omit it to create a location.' },
nodeId: { type: 'string', description: 'Required on create. The canvas node the location belongs to, or any label when there is none.' },
name: { type: 'string', description: 'Required on create.' },
description: { type: 'string', description: 'What makes the place distinctive, in one to three sentences.' },
category: { type: 'string', description: 'indoor, outdoor, urban, nature, fantasy, sci-fi, historical, futuristic or other.' },
style: { type: 'string', description: 'realistic, anime, 3d-pixar or illustration.' },
styleLock: { type: 'boolean', description: 'Generate variants from the approved main image.', default: 'true' },
referencePhotos: { type: 'array', description: 'Up to 20 { kind, url } mood-board photos.' },
piiConsentAt: { type: 'string (ISO 8601)', description: 'When you confirmed the rights and consent for the reference photos. Set it when you first attach photos.' },
canonicalDescription: { type: 'string', description: 'Replace the written description.' },
sourceImageUrl: { type: 'string', description: 'Set the main image directly.' },
projectId: { type: 'string (uuid)', description: 'The project to file the location in.' },
expectedUpdatedAt: { type: 'string (ISO 8601)', description: 'On update: refuse the write with 409 when the location changed since this timestamp.' },
}}
/>

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**

```bash
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"
}'
```

**TypeScript SDK**

```ts
const { jobIds } = await client.locations.generate({
name: 'Rainy Tokyo Alley',
description: 'Neon-soaked alley with vending machines',
count: 4,
})
```

**CLI**

```bash
nodaro locations generate --name "Rainy Tokyo Alley" --count 1 \
  --attach-to-location-id <id> --watch
```

```json
{ "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](https://nodaro.ai/docs/nodes/image/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**

```bash
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"
}'
```

**TypeScript SDK**

```ts
await client.locations.generateAsset({
name: 'Rainy Tokyo Alley',
assetType: 'timeOfDay',
variant: 'blue hour',
attachToLocationId: id,
attachToColumn: 'time_of_day',
attachName: 'blue hour',
})
```

**CLI**

```bash
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**

```bash
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°"
}'
```

**TypeScript SDK**

```ts
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**

```bash
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"
}'
```

**TypeScript SDK**

```ts
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',
})
```

**CLI**

```bash
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](https://nodaro.ai/docs/models/video/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](https://nodaro.ai/docs/models/video/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](https://nodaro.ai/docs/models/video/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](https://nodaro.ai/docs/models/video/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](https://nodaro.ai/docs/models/video/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](https://nodaro.ai/docs/models/video/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**

```bash
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" }'
```

**TypeScript SDK**

```ts
const { sourceImageUrl, canonicalDescription } =
await client.locations.approveMainImage(id, jobIds[2])
```

**CLI**

```bash
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_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](https://nodaro.ai/docs/nodes/assets/location) 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](https://nodaro.ai/docs/nodes/image/generate-image) or [Generate Video](https://nodaro.ai/docs/nodes/video/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](https://nodaro.ai/docs/guides/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](https://nodaro.ai/docs/mcp/tools).

## 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

### What is a location in the Nodaro API?

A location is a saved place with an approved establishing shot, a written description and variant images, such as the same street at dusk or in the rain. Image and video nodes reuse it so the place looks the same in every shot.

### How do I try new establishing shots without losing the current one?

Generate several candidates without attaching them. The current main image stays until you approve a candidate, and GET /v1/locations/:id lists up to 5 recent candidates in previousCandidates.

### Do I need consent to add reference photos to a location?

Yes. Reference photos can show people, so set piiConsentAt, an ISO timestamp, when you first attach photos. It records that you have the rights and consent to use them.

### Can I build a 360-degree view of a location?

On Nodaro Cloud, POST /v1/generate-surround-continuation generates the next view of a panorama from the previous one, and keeps the shared half pixel-exact so the views line up. Each view costs one image generation.

### Which models animate a location?

Six video models, by provider id kling (the default, Kling 2.6), kling-turbo, kling-3.0, wan-i2v, wan-2.7-i2v and seedance-2. Each clip costs that model's image-to-video price.
