# Locations

> Create locations, generate establishing shots, approve one, and add time-of-day, weather, season, angle and motion variants from TypeScript.

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

**`client.locations`** scripts everything Location Studio does: it creates and edits locations, generates establishing-shot candidates, approves one as the main image, and adds variants for time of day, weather, seasons, angles and lighting, plus atmosphere clips. A location keeps its main image, its variant collections, its reference photos and a caption, so every later shot can show the same place. The methods call the [Locations REST API](https://nodaro.ai/docs/developers/api/locations). See [Locations](https://nodaro.ai/docs/guides/locations) for the editor view.

## Methods

| Method | What it does |
| --- | --- |
| [`list(params?)`](#listparams) | List your locations |
| [`listArchived(params?)`](#listarchivedparams) | List your archived locations |
| [`get(id)`](#getid) | Read one location, with its jobs in progress |
| [`create(input)`](#createinput) | Create a location |
| [`update(id, input)`](#updateid-input) | Change a location |
| [`delete(id)` and `restore(id)`](#deleteid-and-restoreid) | Archive a location, or bring it back |
| [`generate(input)`](#generateinput) | Generate establishing-shot candidates |
| [`generateAsset(input)`](#generateassetinput) | Generate a time-of-day, weather, season, angle or lighting variant |
| [`generateSurroundContinuation(input)`](#generatesurroundcontinuationinput) | Generate the next view of a 360-degree ring |
| [`generateMotion(input)`](#generatemotioninput) | Animate the main image into an atmosphere clip |
| [`removeAsset(id, data)`](#removeassetid-data) | Remove one take from a variant collection |
| [`approveMainImage(id, candidateJobId)`](#approvemainimageid-candidatejobid) | Make a candidate the main image |
| [`recaption(id)`](#recaptionid) | Write the location's description again |

## client.locations

### list(params?)

Lists your locations. By default it returns active locations only. Paging is optional: without `limit` you get the whole list and no cursor; with `limit`, at most 500, you get one page and a `nextCursor`.

```ts
list(params?: { archived?: boolean; limit?: number; cursor?: string }): Promise<{
locations: Location[]
nextCursor?: string | null
}>
```

<TypeTable
type={{
archived: { type: 'boolean', default: 'false', description: "true lists archived locations instead." },
limit: { type: 'number', description: "The page size, at most 500. Omit it for the whole list." },
cursor: { type: 'string', description: "The nextCursor of the previous page." },
}}
/>

```ts
const { locations } = await client.locations.list()

const page = await client.locations.list({ limit: 100 })
const next = await client.locations.list({ limit: 100, cursor: page.nextCursor ?? undefined })
```

Keep paging until `nextCursor` is `null`.

### listArchived(params?)

Lists your archived locations. It is a shortcut for `list({ archived: true })`.

```ts
listArchived(params?: { limit?: number; cursor?: string }): Promise<{ locations: Location[]; nextCursor?: string | null }>
```

<TypeTable
type={{
limit: { type: 'number', description: "The page size, at most 500." },
cursor: { type: 'string', description: "The nextCursor of the previous page." },
}}
/>

```ts
const { locations: archived } = await client.locations.listArchived()
```

### get(id)

Reads one location, with `pendingJobs`, the variants still generating, and `previousCandidates`, up to 5 earlier main-image candidates, newest first. Promote one of those with `approveMainImage()`.

```ts
get(id: string): Promise<LocationDetail>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The location id." },
}}
/>

```ts
const location = await client.locations.get(locationId)
console.log(location.sourceImageUrl, location.weather, location.atmosphereMotions)
```

An archived location is still returned by id, so workflow nodes that point at it keep loading. A `Location` has its main image in `sourceImageUrl`, six collections (`timeOfDay`, `weather`, `seasons`, `angles`, `lighting` and `atmosphereMotions`, each a list of `{ name, url }`), `boards`, `referencePhotos`, `canonicalDescription`, `styleLock` and `updatedAt`.

### create(input)

Creates a location. `name` and `nodeId` are required. A script without a canvas node can pass `"mcp-managed"` as `nodeId`.

```ts
create(input: CreateLocationInput): Promise<{ id: string }>
```

<TypeTable
type={{
nodeId: { type: 'string', required: true, description: "The canvas node the location belongs to, or mcp-managed." },
name: { type: 'string', required: true, description: "The name." },
description: { type: 'string', description: "A free-text description." },
category: { type: 'string', description: "The category, such as urban or nature." },
style: { type: 'string', description: "The visual style, such as realistic or anime." },
projectId: { type: 'string', description: "The project to file it under." },
workflowId: { type: 'string', description: "The workflow it comes from." },
sourceImageUrl: { type: 'string', description: "The main image URL." },
imageProvider: { type: 'string | null', description: "The image model the main image was made with." },
referencePhotos: { type: 'LocationReferencePhoto[]', description: "Mood-board photos, at most 20, each { url, kind }. kind is wide, interior, exterior, detail, moodBoard or other." },
canonicalDescription: { type: 'string', description: "The description used in prompts." },
styleLock: { type: 'boolean', description: "Keep variants in the location's approved style." },
}}
/>

```ts
const { id: locationId } = await client.locations.create({
nodeId: "mcp-managed",
name: "Rainy Tokyo Alley",
description: "Neon-soaked alley with vending machines",
category: "urban",
style: "realistic",
})
```

### update(id, input)

Changes a location. Only the fields you send are written. The variant collections are not part of this call, because generation jobs add to them while you work. Use `generateAsset()` and `removeAsset()` for them.

```ts
update(id: string, input: UpdateLocationInput): Promise<{ id: string; updatedAt: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The location id." },
name: { type: 'string', description: "The name." },
description: { type: 'string', description: "A free-text description." },
category: { type: 'string', description: "The category." },
style: { type: 'string', description: "The visual style." },
sourceImageUrl: { type: 'string', description: "The main image URL." },
imageProvider: { type: 'string | null', description: "The image model of the main image." },
referencePhotos: { type: 'LocationReferencePhoto[]', description: "Mood-board photos, at most 20." },
canonicalDescription: { type: 'string', description: "The description used in prompts." },
styleLock: { type: 'boolean', description: "Keep variants in the approved style." },
boards: { type: 'Array<{ name, url, type?, sourceImages? }>', description: "Reference boards of the location." },
selectedAssetByVariant: { type: 'Record<string, string>', description: "The chosen take of each variant." },
piiConsentAt: { type: 'string', description: "An ISO 8601 time that records when the user confirmed they have the rights to the reference photos. Set it when you first attach referencePhotos." },
expectedUpdatedAt: { type: 'string', description: "The updatedAt value you read. When the location changed since, the update fails with 409 concurrent_modification." },
}}
/>

```ts
await client.locations.update(locationId, {
canonicalDescription: "A narrow, rain-soaked alley lit by neon signs",
styleLock: false,
piiConsentAt: new Date().toISOString(),
expectedUpdatedAt: location.updatedAt,
})
```

The `409 concurrent_modification` arrives as a plain `NodaroError` with that `code`. Read the location again, merge your change, and retry.

### delete(id) and restore(id)

`delete()` archives a location, and `restore()` brings it back. Permanent deletion is only available in the Nodaro app. When a restored name matches an active location's name, ignoring case, the server adds `(restored)` and returns the name it used.

```ts
delete(id: string): Promise<{ success: true; archived: true }>
restore(id: string): Promise<{ id: string; name: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The location id." },
}}
/>

```ts
await client.locations.delete(locationId)
const { name } = await client.locations.restore(locationId)
```

### generate(input)

Generates establishing-shot candidates (`POST /v1/generate-location`). With `count` above 1, every job is reserved before any starts, so a failure part way through rolls the batch back.

```ts
generate(input: GenerateLocationInput): Promise<{ jobIds: string[]; jobId?: string }>
```

<TypeTable
type={{
name: { type: 'string', required: true, description: "The location's name." },
description: { type: 'string', description: "What the place looks like." },
userPrompt: { type: 'string', description: "Extra instructions." },
category: { type: '"indoor" | "outdoor" | "urban" | "nature" | "fantasy" | "sci-fi" | "historical" | "futuristic" | "other"', description: "The category." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "The visual style." },
sourceImageUrl: { type: 'string', description: "A photo to base the shot on." },
provider: { type: 'string', description: "The image model id." },
count: { type: 'number', description: "How many candidates to generate." },
quality: { type: 'string', description: "medium, high or basic, on models with a quality setting. It changes the price." },
resolution: { type: 'string', description: "1K, 2K, 4K, 0.5 MP, 1 MP, 2 MP or 4 MP, on models that support it. It changes the price." },
attachToLocationId: { type: 'string', description: "The location to write a single result to." },
}}
/>

```ts
// One candidate, written to the location when it completes
const { jobIds: [jobId] } = await client.locations.generate({
name: "Rainy Tokyo Alley",
description: "Neon-soaked alley with vending machines",
attachToLocationId: locationId,
})

// Four candidates to choose from
const { jobIds } = await client.locations.generate({ name: "Rainy Tokyo Alley", count: 4 })
```

With `attachToLocationId` and `count` of 1, the result becomes the main image when the job completes. Otherwise, pick a candidate with `approveMainImage()`. `quality` and `resolution` are priced as in [Generate Image](https://nodaro.ai/docs/nodes/image/generate-image). A value the model does not support is ignored.

### generateAsset(input)

Generates one variant of the location (`POST /v1/generate-location-asset`). With `attachToLocationId`, `attachToColumn` and `attachName`, the result is added to that collection when the job completes.

```ts
generateAsset(input: GenerateLocationAssetInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
assetType: { type: '"timeOfDay" | "weather" | "seasons" | "angles" | "lighting" | "custom"', required: true, description: "The kind of variant." },
variant: { type: 'string', required: true, description: "The variant, such as golden hour, storm, winter, aerial or neon." },
name: { type: 'string', required: true, description: "The location's name." },
description: { type: 'string', description: "The location's description." },
userPrompt: { type: 'string', description: "Extra instructions." },
sourceImageUrl: { type: 'string', description: "The image to start from. Usually the main image." },
provider: { type: 'string', description: "The image model id." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "The image shape." },
quality: { type: 'string', description: "As in generate(). It changes the price." },
resolution: { type: 'string', description: "As in generate(). It changes the price." },
attachToLocationId: { type: 'string', description: "The location to add the result to." },
attachToColumn: { type: 'string', description: "The collection: time_of_day, weather, seasons, angles, lighting, atmosphere_motions, sheets or detail_closeups." },
attachName: { type: 'string', description: "The name of the new entry." },
}}
/>

```ts
const { jobId } = await client.locations.generateAsset({
name: "Rainy Tokyo Alley",
assetType: "weather",
variant: "storm",
attachToLocationId: locationId,
attachToColumn: "weather",
attachName: "storm",
})
```

### generateSurroundContinuation(input)

Generates the next view of a 360-degree ring (`POST /v1/generate-surround-continuation`), continuing from the previous view. The server keeps half of the previous view exactly as it is, paints the other half, and matches its colors, so neighboring views join without a seam. This method works on Nodaro Cloud; other editions answer `403 edition_required` before any processing, which the SDK throws as `ForbiddenError`.

```ts
generateSurroundContinuation(input: GenerateSurroundContinuationInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
referenceImageUrl: { type: 'string', required: true, description: "The previous view of the ring." },
direction: { type: '"right" | "up" | "down"', required: true, description: "Which way the ring continues." },
degrees: { type: 'number', description: "How far the view turns, such as 45." },
carriedFraction: { type: 'number', default: '0.5', description: "How much of the previous view is carried over unchanged." },
userPrompt: { type: 'string', description: "Extra instructions." },
provider: { type: 'string', description: "The image model id." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "The image shape." },
attachToLocationId: { type: 'string', description: "The location to add the view to." },
attachToColumn: { type: 'string', description: "The collection. The studio uses angles." },
attachName: { type: 'string', description: "The name of the new entry, such as Surround 45°." },
}}
/>

```ts
const { jobId } = await client.locations.generateSurroundContinuation({
referenceImageUrl: previousRingView,
direction: "right",
degrees: 45,
provider: "nano-banana-pro",
aspectRatio: "16:9",
attachToLocationId: locationId,
attachToColumn: "angles",
attachName: "Surround 45°",
})
```

### generateMotion(input)

Animates the location's image into an atmosphere clip (`POST /v1/generate-location-motion`), with [Generate Video](https://nodaro.ai/docs/nodes/video/generate-video) in image-to-video mode. A location has one motion collection, so the clip always goes to `atmosphereMotions` and there is no `attachToColumn`.

```ts
generateMotion(input: GenerateLocationMotionInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
motionPrompt: { type: 'string', required: true, description: "What moves in the scene." },
sourceImageUrl: { type: 'string', required: true, description: "The image to animate, usually the main image." },
name: { type: 'string', required: true, description: "The location's name." },
provider: { type: 'string', description: "The video model id, such as kling." },
category: { type: 'string', description: "The category." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "The visual style." },
canonicalDescription: { type: 'string', description: "The location's description." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "The clip shape." },
attachToLocationId: { type: 'string', description: "The location to add the clip to." },
attachName: { type: 'string', description: "The name of the new clip." },
}}
/>

```ts
const { jobId } = await client.locations.generateMotion({
name: "Rainy Tokyo Alley",
motionPrompt: "Slow dolly-in, neon signs flicker, light rain falling",
sourceImageUrl: location.sourceImageUrl!,
provider: "kling",
attachToLocationId: locationId,
attachName: "neon dolly-in",
})
```

### removeAsset(id, data)

Removes one take from a variant collection (`POST /v1/locations/:id/remove-asset`). Every entry with that `url` is removed in one step. Use it, for example, before you regenerate a 360-degree view.

```ts
removeAsset(id: string, data: { column: LocationAttachColumn; url: string }): Promise<{ removed: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The location id." },
column: { type: 'LocationAttachColumn', required: true, description: "The collection, such as angles or weather." },
url: { type: 'string', required: true, description: "The URL of the take to remove." },
}}
/>

```ts
await client.locations.removeAsset(locationId, { column: "angles", url: oldViewUrl })
```

Throws `NotFoundError` when the URL is not in that collection or the location is not yours.

### approveMainImage(id, candidateJobId)

Makes a completed candidate from `generate()` the location's main image. A vision model then writes the location's description, and the method returns both.

```ts
approveMainImage(id: string, candidateJobId: string): Promise<{ sourceImageUrl: string; canonicalDescription: string | null }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The location id." },
candidateJobId: { type: 'string', required: true, description: "The job id of the completed candidate." },
}}
/>

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

`canonicalDescription` is `null` when the description could not be written. The main image is still set; call `recaption()` to try again.

### recaption(id)

Writes the location's description again from its current main image.

```ts
recaption(id: string): Promise<{ canonicalDescription: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The location id." },
}}
/>

```ts
const { canonicalDescription } = await client.locations.recaption(locationId)
```

Fails with `400 no_source_image` when the location has no main image, and with a 502 when the vision model fails.

## Frequently asked questions

### How do I create a location with the Nodaro SDK?

Call client.locations.create with a nodeId and a name, generate establishing shots with client.locations.generate, and approve one with client.locations.approveMainImage.

### Which variants can I generate for a location?

Time of day, weather, seasons, angles and lighting, plus custom variants, with generateAsset, and atmosphere clips with generateMotion. On Nodaro Cloud, generateSurroundContinuation adds 360-degree views.

### How do I avoid overwriting a location someone else changed?

Pass expectedUpdatedAt, the updatedAt value you read, to client.locations.update. When the location changed, the call fails with 409 concurrent_modification. Read it again, merge, and retry.

### Can I delete a location permanently with the SDK?

No. delete archives the location, and restore brings it back. Permanent deletion is available only in the Nodaro app.
