# Trends

> Browse the Nodaro trend catalog, fill a trend's slots with your own pictures, quote the run for free and start it over REST, the SDK, the CLI or MCP.

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

The **Trends API** runs ready-made [Cast Swap](https://nodaro.ai/docs/developers/api/cast-swap) recipes. A trend is a short clip with slots: the people, pets, products and place you can replace. Fill the slots with your own pictures and run the trend to get the clip back with them in it.

Trends are part of Nodaro Cloud. On a self-hosted install, every route answers `404`. The routes take a bearer token. See [Authentication](https://nodaro.ai/docs/developers/api/authentication).

Only **published** trends exist here. A draft, an unpublished or an unknown slug is a `404` on every route.

## Endpoints

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/trends` | The catalog, in its own order: `{ data: TrendCard[], next }`. Query: `q` (search), `tag`, `favorites=1` (only yours), `locale`, `cursor`, `limit` (1 to 60, default 30). |
| `GET` | `/v1/trends/:slug` | One trend: `{ data: TrendDetail }`, with its slots, examples, preview length and resolutions. Query: `locale`. |
| `POST` | `/v1/trends/:slug/estimate` | Quote a run. Free. Answers `{ data: { expectedCredits, holdCredits, breakdown } }`. |
| `POST` | `/v1/trends/:slug/runs` | Start a run. Answers `202 { data: { jobId } }`. Follow the job with `GET /v1/jobs/:id`. |
| `GET` | `/v1/trends/runs` | Your trend runs, newest first: `{ data: TrendRun[], next }`. Query: `trend` (a slug), `cursor`, `limit` (1 to 60, default 30). |
| `PUT` | `/v1/trends/:slug/favorite` | Add the trend to your favorites. Answers `204`. |
| `DELETE` | `/v1/trends/:slug/favorite` | Remove the trend from your favorites. Answers `204`. |

Names, descriptions, slot labels and hints come in the language of `?locale=` when it is a platform language. Otherwise they come in your profile's language, and then in English.

A run stays readable in `GET /v1/trends/runs` after its trend moves to a new version or is unpublished.

## What a trend holds

`GET /v1/trends/:slug` returns:

- `slots`: each slot's `key`, `label`, `kind` (`person`, `pet`, `product` or `location`), `required`, an optional `hint` and its default chips.
- `examples`: each example's result video and the pictures every slot used.
- `previewSec`: `4` when the trend offers a 4-second preview run, else `null`.
- `resolutions`: the resolutions the trend can run at.

A catalog card in `GET /v1/trends` carries the slug, name, tags, `needs` (how many slots of each kind), the length in seconds and `isFavorite`.

## The run body

`POST /v1/trends/:slug/estimate` and `POST /v1/trends/:slug/runs` take the same body: `{ slots, example?, details?, preview?, resolution?, draft?, sessionId?, projectId? }`.

| Field | What it does |
| --- | --- |
| `slots` | Required. Slot key → `{ images?, …chips }`, with 1 to 3 images of one subject per slot. Every required slot needs images. |
| `example` | Optional. An example number, from 1. Its pictures fill the optional slots you leave out. The default is 1. |
| `details` | Optional. Your own words, up to 1,000 characters. Mention a slot as `{slot:<key>}`. |
| `preview` | Optional. `true` runs 4 seconds, when the trend's `previewSec` is 4. |
| `resolution` | Optional. `"480p"`, the default, `"720p"` or `"1080p"`. |
| `draft` | Optional. The default is `true`. A draft is available at 480p only. |
| `sessionId` | Optional. Accepted and checked, but not applied yet: trend runs are not filed into sessions yet. |
| `projectId` | Optional. Accepted, but not used by the run today. |

The chips are the slot kind's own traits. Each chip is `"new"`, made from your pictures, or `"original"`, kept as in the clip:

| Slot kind | Chips |
| --- | --- |
| `person` | `body`, `outfit`, `hair` |
| `pet` | `body`, `coat` |
| `product` | `shape`, `finish` |
| `location` | None |

```json
{
"slots": {
"dancer": { "images": ["https://example.com/me.jpg"], "outfit": "original" }
},
"preview": true,
"resolution": "480p"
}
```

## The quote

The estimate answers `expectedCredits`, what the run is expected to be charged, and `holdCredits`, what is reserved when the run starts. `breakdown` lists the parts of the quote.

## Repeats

`POST /v1/trends/:slug/runs` takes an `Idempotency-Key` header, 8 to 200 characters. A repeat with the same key answers with the first run's job. Nothing new is started or charged.

## Permissions

An OAuth app needs:

- `jobs:read` to read the catalog, a trend and your runs;
- `workflows:execute` to estimate or start a run, plus `assets:write` to pass a `sessionId`;
- `assets:write` for favorites.

## Errors

The body of a refusal is `{ error: { code, message } }`. The slot errors also carry the slot's key as `slot`. The `400` refusals of the estimate and run routes come before anything is charged.

| Status | `code` | When |
| --- | --- | --- |
| `400` | `validation_error` | The body or a query is not valid, the `cursor` is not one this list gave, or the `Idempotency-Key` is not 8 to 200 characters. |
| `400` | `missing_slot` | A required slot has no images, or an optional slot is left empty and the chosen example has no pictures for it. |
| `400` | `unknown_slot` | `slots` names a key the trend does not have. |
| `400` | `invalid_choice` | A chip that the slot's kind does not take, for example a `coat` on a person or any chip on a location. |
| `400` | `example_out_of_range` | `example` names an example the trend does not have, or an optional slot is left empty and the trend has no example to fill it. |
| `400` | `preview_unavailable` | `preview: true` on a trend that offers no preview. |
| `400` | `invalid_slot_mention` | `details` mentions `{slot:<key>}` with a key the trend does not have. |
| `400` | `prompt_too_long` | `details` is too long for this trend. Shorten it. |
| `401` | `unauthorized` | No authenticated user. |
| `404` | `not_found` | The trend is not published, or `sessionId` names a session you cannot use. |
| `502` | `engine_error` | The video engine answered in an unexpected way. |
| `503` | `engine_unsupported` | This platform version cannot run trends yet. |
| `503` | `trend_unavailable` | The trend cannot be run right now. |
| `503` | `not_available` | This server has no trend catalog yet. Every route except `GET /v1/trends/runs`. |

When the video engine refuses an estimate or a run, the trend routes answer with their own codes and fixed messages:

| Status | `code` | When |
| --- | --- | --- |
| `400` | `trend_run_rejected` | The engine refused these inputs, for a reason not listed below. |
| `400` | `clip_out_of_range`, `preview_out_of_range`, `video_unreadable` | The trend's own clip cannot be run as it is. |
| `400` | `unsafe_url` | A picture is not a public http or https address. |
| `400` | `raw_reference_token` | `details` contains `{image:`, `{video:` or `{audio:`. |
| `402` | `insufficient_credits` | Your credits cannot cover the run. In a workspace, the limit that refused it can answer instead: `budget_exceeded`, `member_cap_exceeded` or `user_allowance_exceeded`. |
| `413` | `storage_limit_exceeded` | Your storage is full. |
| `422` | `job_blocked` | The deployment's job policy blocked the run. |
| `429` | `rate_limited` | Too many requests. Try again shortly. |
| `503` | `engine_unsupported`, `estimate_unavailable` | The run cannot be run or quoted right now. Try again shortly. |
| `502` | `engine_error` | Anything else. |

## Runs that did not finish

A run in `GET /v1/trends/runs` that did not finish has `error: { code, message }`. The message is always the same fixed sentence, so branch on the code:

| `code` | Meaning |
| --- | --- |
| `safety-block`, `policy-block` | The job was blocked. |
| `credits` | The run ran out of credits partway. It was refunded. Top up, then run it again. |
| `cancelled` | The run was cancelled. |
| `failed` | Any other failure. |

A run that did not fail and was not cancelled has `error: null`. A completed run carries `videoUrl`, `posterUrl` and `creditsCharged`. Each slot of the run says whether its pictures came from the example (`fromExample`).

## From the SDK

`client.trends` wraps the routes. It is also importable as `@nodaro/sdk/trends`.

```ts
list(params?: ListTrendsParams): Promise<TrendPage<TrendCard>>
get(slug: string, params?: { locale?: string }): Promise<TrendDetail>
estimate(slug: string, request: TrendRunRequest): Promise<TrendEstimate>
run(slug: string, request: TrendRunRequest, options?: { idempotencyKey?: string }): Promise<{ jobId: string }>
runs(params?: ListTrendRunsParams): Promise<TrendPage<TrendRun>>
favorite(slug: string): Promise<void>
unfavorite(slug: string): Promise<void>
```

`list()` and `runs()` resolve to the page as it arrives, `{ data, next }`. Pass `next` back as `cursor`; it is `null` on the last page. `get()`, `estimate()` and `run()` resolve to the payload, without the `{ data }` envelope. `"runs"` is never a slug: the methods that take a slug throw a `TypeError` for it before any request.

```ts

const { data: cards } = await client.trends.list({ q: "dance" })
const trend = await client.trends.get(cards[0].slug)

const request: TrendRunRequest = {
// Every required slot needs 1–3 pictures; optional slots left out take the example's.
slots: Object.fromEntries(
trend.slots.filter((s) => s.required).map((s) => [s.key, { images: [myPhotoUrl] }]),
),
}
const quote = await client.trends.estimate(trend.slug, request)
console.log(`about ${quote.expectedCredits} credits, ${quote.holdCredits} held while it runs`)

const { jobId } = await client.trends.run(trend.slug, request, { idempotencyKey: crypto.randomUUID() })
const job = await client.jobs.get(jobId)
```

## From the CLI

```bash
nodaro trends list --search dance
nodaro trends get <slug>
nodaro trends estimate <slug> --slot dancer=https://example.com/me.jpg --choice dancer.outfit=original
nodaro trends run <slug> --slot dancer=https://example.com/me.jpg --idempotency-key <key>
nodaro trends runs --trend <slug>
nodaro trends favorite <slug>
```

`--slot` repeats, with 1 to 3 pictures per slot. `trends run` spends credits, so run `trends estimate` first. See [CLI commands](https://nodaro.ai/docs/developers/cli/commands).

## From MCP

Four MCP tools cover trends: `list_trends`, `get_trend`, `estimate_trend` and `run_trend`. See [MCP › Video](https://nodaro.ai/docs/mcp/tools/video#trends). `run_trend` without `confirm` returns the quote and charges nothing. With `confirm: true`, after the user accepts the price, it starts the run and returns a job id.

## Frequently asked questions

### What is a trend in Nodaro?

A trend is a ready-made Cast Swap recipe. It is a short clip with slots for the people, pets, products and place you can replace. You fill the slots with your own pictures and get the clip back with them in it.

### How do I know what a trend run costs?

Call POST /v1/trends/:slug/estimate with the same body you will run. It is free. It returns expectedCredits, what the run is expected to be charged, and holdCredits, what is reserved while it runs.

### Do I have to fill every slot?

No. Every required slot needs 1 to 3 pictures. An optional slot that you leave out takes the pictures of the trend's example, the first one by default.

### Can I retry a trend run safely?

Yes. Send the same Idempotency-Key header, 8 to 200 characters. A repeat with the same key answers with the first run's job, and nothing new is started or charged.

### Do trends work on a self-hosted install?

No. Trends are part of Nodaro Cloud. On a self-hosted install, every trend route answers 404.
