# Content rights

> Before Nodaro imports a video link, the person confirms a content rights declaration. Read it, confirm it per link, or authorize keys and agents once.

Source: https://nodaro.ai/docs/developers/api/content-rights

**Content rights** are the declaration a person confirms before Nodaro imports a link to someone's video, picture or recording. The person confirms that they hold the rights to the content, or the rights holder's permission, and that they are responsible for how they use it. Nodaro keeps a record of every confirmation: who confirmed, for which link, which text, when and from where, and the download made under it.

## Which imports need a confirmation

Every route that fetches a link runs the same check:

- The video import, `POST /v1/download-video`.
- The audio imports, `POST /v1/youtube-audio` and `POST /v1/extract-youtube-audio`.
- The picture and recording imports, `POST /v1/media/import-url` and `POST /v1/media/import-recording`.
- `POST /v1/trim-audio` when it is given a link to a video's page.
- A run's own fetch of a [Video URL](https://nodaro.ai/docs/nodes/automate/video-url) node, for the video and for its sound.
- A transcription or a Suno cover given a link to a video's page: `POST /v1/transcribe` (its `audioUrl`), `POST /v1/suno/cover` and `POST /v1/suno/upload-extend` (their `uploadUrl`), and the MCP audio tool.
- A dub given a link, `POST /v1/dubbing` with a `sourceUrl`, and a run's Dubbing node given a link.

These routes take the confirmation as `confirmationId`. A file uploaded from your own device is not an import of a link, so it needs no confirmation.

**Transcription and Suno.** A link to a video's page has its sound fetched first, so the route takes the confirmation the same way. The confirmation is checked before the job is created: a refusal is a `403` with the code that says why, and nothing is charged. The job spends the confirmation right before the sound is fetched. A direct file link needs none. A link is a video's page when its host is one of the platforms' own hosts. A host that merely contains a platform's name is a plain file.

**Dubbing.** `POST /v1/dubbing` hands a `sourceUrl` to the dubbing service, which fetches it itself. That is an import of the link, for a video's page and for a direct file alike. Every `sourceUrl` that is not a file of the deployment's own storage takes `confirmationId`. The confirmation is read before anything touches the link, the length check included, and a refusal is a `403` before the job is created. An `audioUrl` or a `videoUrl` is a media input like any node's, and needs none.

**Links that are refused outright.** These routes refuse a link with a backslash or a control character, such as a tab or a line break, with `400 validation_error`, before anything reads it. No real link contains one.

Whether an import without a confirmation is refused is a setting of the deployment. While the setting is off, the import is accepted. Once it is on, the import answers `403 rights_confirmation_required`. Send a confirmation every time, so your client keeps working when the setting changes.

## The routes

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/content-rights/declaration` | Returns the current declaration: `{ data: { version, text } }`. It is public, so a client can show it before anyone signs in. |
| `POST` | `/v1/content-rights/confirm` | Records that the signed-in account confirmed the declaration for one link. Body: `{ url, declarationVersion }`. Answers `201 { data: { id, declarationVersion, expiresAt } }`. Any `http(s)` link that an import takes can be confirmed: a video's page, a file, a picture, or a page that the dubbing service reads. A link with a backslash or a control character is a `400`. |
| `GET` | `/v1/content-rights/authorization` | Returns the account's standing authorization: `{ data: { id, declarationVersion, createdAt } }`, or `{ data: null }` when there is none. |
| `POST` | `/v1/content-rights/authorization` | Records a standing authorization for the current declaration. Body: `{ declarationVersion }`. Answers `201 { data: { id, declarationVersion, createdAt } }`. Your own signed-in session only. |
| `DELETE` | `/v1/content-rights/authorization` | Revokes the standing authorization. The record is kept. Answers `{ data: { revoked } }`. Your own signed-in session only. |

`POST /v1/content-rights/confirm` and `POST /v1/content-rights/authorization` answer `409 rights_confirmation_stale` when the declaration changed after the client read it. Read the declaration again and ask the person again.

## Confirm a link, then import it
### Read the declaration

Call `GET /v1/content-rights/declaration`. Show its `text` as it is, with a checkbox that starts unticked.

### Confirm the link

Once the person ticks the box, call `POST /v1/content-rights/confirm` with the link and the declaration's `version`.

### Import the link

Send the answered `id` as `confirmationId` on the import, for example on `POST /v1/download-video`.

**curl**

```bash
VERSION=$(curl -s https://app.nodaro.ai/v1/content-rights/declaration | jq -r .data.version)

CONFIRMATION=$(curl -s -X POST https://app.nodaro.ai/v1/content-rights/confirm \
  -H "Authorization: Bearer $NODARO_API_KEY" -H 'Content-Type: application/json' \
  -d "{ \"url\": \"https://example.com/watch/VIDEO_ID\", \"declarationVersion\": $VERSION }" | jq -r .data.id)

curl -s -X POST https://app.nodaro.ai/v1/download-video \
  -H "Authorization: Bearer $NODARO_API_KEY" -H 'Content-Type: application/json' \
  -d "{ \"url\": \"https://example.com/watch/VIDEO_ID\", \"confirmationId\": \"$CONFIRMATION\" }"
```

**TypeScript SDK**

```ts
const { version, text } = await client.contentRights.declaration()
// Show `text` with a checkbox. Once it is ticked:
const { id } = await client.contentRights.confirm({ url, declarationVersion: version })
const { downloadId } = await client.media.downloadVideo({ url, maxHeight: 1080, confirmationId: id })
```

One confirmation opens one download of that link. The download must be made by the same account, under the current declaration, within one hour. The link is compared after it is normalized: the same video with other query parameters matches, and another video never does. The download then carries the confirmation on the stored file.

The confirmation is checked before anything else can refuse the import, and it is spent only when the download is about to start. A `429 too_many_downloads` or a refused address never uses a confirmation up. A refused import never starts a fetch.

## Imports through API keys, connected apps and AI agents
An import made through a personal API key, a connected app or an AI agent over MCP has nobody to tick a box for each link. The person authorizes those imports once, in their own signed-in session:

- In the app, in **Settings**, under **Imports through API keys, connected apps and AI agents**: tick the box and click **Authorize**. **Revoke** ends the authorization.
- From code, with `POST /v1/content-rights/authorization` from the person's own session. A key, an app or an agent that calls the route is refused with `403 forbidden`.

With a standing authorization, every import through the account's keys, apps and agents is recorded as a confirmation derived from it, one for each download, and the import passes without a `confirmationId`. A `confirmationId` you send is still used first. Runs that a key, an app or an agent started carry the same cover to every import the run makes.

The authorization names the declaration version it was given for. A new declaration needs a new authorization, as it needs a new tick. Until then, the account counts as not authorized.

A connected app imports, and confirms, only with a scope that lets it write for the account, that is any `:write` or `:execute` scope. An app with read scopes only is refused with `403 insufficient_scope` before the content rights check.

While the deployment requires a confirmation, an import through a key, an app or an agent with neither a confirmation nor a standing authorization is refused with `403 rights_authorization_required`. The remedy is the person authorizing in their settings. A run with no way in at all, such as a scheduled run or a webhook run, is covered by no authorization, and is refused like a person who did not tick the box.

## A Video URL node in a run
A run's own fetch of a [Video URL](https://nodaro.ai/docs/nodes/automate/video-url) node takes its confirmation from the run request. Set `rightsConfirmationId` on the node in `inputOverrides`, on `POST /v1/workflows/:id/run` or `POST /v1/app/:slug/run`:

```json
{
"inputOverrides": {
"<video url node id>": { "youtubeUrl": "https://example.com/watch/VIDEO_ID", "rightsConfirmationId": "<confirmation id>" }
}
}
```

A run's Dubbing node given a link is held the same way before its job is created. Its confirmation comes from the run request, as `rightsConfirmationId` on the Dubbing node in `inputOverrides`, or from the account's standing authorization for a run started through a key or an agent. A repeated node dubs its one link in every iteration, all under that one confirmation for that run.

The confirmation must be for that same link, made for that run. It is a field of the run only. A confirmation saved on a node is dropped before the run reads the workflow, because a confirmation is good for one download within an hour. When `inputOverrides` sets another link, the old link's confirmation is dropped with the rest of its fields.

## In the editor
Every node that fetches a link in the editor shows the content rights box under the link, and waits for the box ticked for that link:

- **Dubbing.** A run stops until the box under the **Source Link** is ticked, and each run confirms the link again. A run whose request puts another link in place of the ticked one does not start. A frozen Dubbing node is never run, so it does not ask. A published app's run has no box for a Dubbing node's own link, and neither does a Dubbing node inside a sub-workflow, so with the setting on, either is refused. See [Dubbing](https://nodaro.ai/docs/nodes/audio/dubbing#content-rights).
- **Transcribe and Suno Cover.** Both take a link to where a video is through a [Video URL](https://nodaro.ai/docs/nodes/automate/video-url) node. That node asks for the declaration and fetches the sound, and a run reads that track. A run in the editor stops before anything starts when one of these nodes is given such a link directly: in its own link field, or from a Text node mapped into Suno Cover's link. The message names the node and says to connect a Video URL node instead. A value made during a run, a link written into Suno Upload Extend, and a request through the API or an agent are the server's to answer.
- **Generate Music and Reference Audio.** Both fetch the sound of a link in the editor: Generate Music when you click **Get**, and [Reference Audio](https://nodaro.ai/docs/nodes/audio/reference-audio) as soon as its box is ticked, and again when you click **Extract Audio**. Each fetch confirms the declaration for that one fetch, with the `confirmationId` of `POST /v1/youtube-audio` or `POST /v1/extract-youtube-audio`. A run and a published app read the stored file and never fetch these links.
- **A picture added from a link.** [Upload Image](https://nodaro.ai/docs/nodes/image/upload-image) and a published app's picture input copy a pasted picture link into storage with `POST /v1/media/import-url`. The copy waits for the box ticked for that link. In the editor, ticking the box starts the copy. On an app's page, **Set** waits for it. A tick covers one copy, so the next link asks again. A link that the copy refuses with `403` is not added. A link that cannot be copied for another reason, such as an unreachable or too large file, is still added as it is, with a notice.

When the page finds that the declaration changed, every box on the page is unticked, not only the box of one node, because each box was ticked under the old text. An uploaded file needs no box.

## Errors
An import answers `403` with one of these codes when the confirmation does not fit. Nothing was fetched.

| Code | Meaning |
| --- | --- |
| `rights_confirmation_required` | The deployment requires a confirmation, and none was sent. |
| `rights_authorization_required` | The deployment requires a confirmation, the import came through an API key, a connected app or an AI agent, and the account holds no standing authorization. |
| `rights_confirmation_not_found` | No confirmation has that id. |
| `rights_confirmation_mismatch` | The confirmation is for another link or another account. |
| `rights_confirmation_stale` | The declaration changed since the confirmation. Read it again and confirm again. |
| `rights_confirmation_expired` | The confirmation is older than one hour. |
| `rights_confirmation_used` | An import was already made under the confirmation. An import that failed after its fetch started counts too. |
| `imports_blocked` | Importing links is blocked on the account, or the account is closed, under the deployment's repeat-infringer policy. The message says until when: a date in UTC, or until an administrator lifts the block. See [Repeat infringers](#repeat-infringers). |
| `import_source_disabled` | The deployment does not allow imports from the link's site. The message names no site. |

For every `rights_confirmation_*` code, confirm the link again and send the new id. In the TypeScript SDK, these refusals arrive as [`ImportRefusedError`](https://nodaro.ai/docs/developers/sdk/errors#importrefusederror).

## Restricted content
After a complaint, Nodaro can restrict a stored file or the result of a job. A restricted file reads, for every caller including its owner, as one that does not exist: it is out of the library, every listing, the gallery and every share.

- `GET /v1/download?url=` and `GET /v1/image-proxy?url=` answer `451` with the code `content_restricted` for the address of a restricted file.
- A request that names a restricted file as an input is refused with `422 job_blocked`. A workflow node that would read it fails with `content_restricted`.
- A file held as evidence for a complaint cannot be deleted. `DELETE /v1/library/:id` answers `409 content_held` with the message "This file cannot be deleted right now." A request that deletes several files skips it.

When a restriction from a copyright complaint covers your content, Nodaro can email you about it. The email names your restricted file, the link it was imported from, the reason, the case's reference, and the page where you can send a counter-notification. A person who sent a complaint and proved their email can receive an update with the case's status and decision. A person at Nodaro sends each of these emails. None is sent automatically.

To report content that you believe infringes your rights, use the complaints form at `app.nodaro.ai/copyright-complaints`. The app links to the form from the sign-in page, the sidebar and the account menu. Every complaint is handled by a person.

## Counter-notices
When your content was restricted after a copyright notice, you can answer it with a **counter-notification** under 17 U.S.C. § 512(g)(3). Open **Settings › Content restrictions** in the app. The page lists the restrictions that a complaint led to on your own content: what was restricted and when, the complaint's reference, and the kind of complaint. It never shows who complained. The Settings page shows the way there only when you have such a restriction.

A counter-notification answers one restriction. You can send one only for a restriction that a copyright complaint led to, that still stands, and whose complaint is open. It gives:

- Your full name, physical address, phone number and email.
- What was restricted and where it was before. The app fills these in for you, in English.
- Your signature, which is your typed full name.
- The declaration, shown word for word, with each statement confirmed on its own. You state under penalty of perjury that you believe in good faith that the material was removed or disabled by mistake or misidentification. You also consent to the jurisdiction of the applicable United States Federal District Court, and to service of process from the person who complained.

Before you send it, the page tells you that your name, address, phone number and email are sent to the person who complained, as the law requires. You send it yourself, signed in to the app. An API key, a connected app or an AI agent is refused with `403 in_app_only`. Your details are stored encrypted, and nothing you sent changes afterwards.

**When content can be restored.** The law allows a restore not less than 10, nor more than 14, business days after the counter-notification is received. Nodaro uses the days where every reading of that rule agrees. The window opens on the 11th business day after receipt, with US federal holidays not counted. Its last day is the 14th business day, counting every weekday. Days are New York calendar dates, and day 1 is the first business day after the day of receipt. For example, a counter-notification received on Monday 23 November 2026 can be restored from Wednesday 9 December 2026 to Friday 11 December 2026, because Thanksgiving on 26 November is not counted for the opening.

**What happens next.** A person at Nodaro handles every counter-notification:

- It is sent to the person who complained. Nothing is sent automatically.
- If the person who complained reports a court action, the content stays restricted until the court action ends.
- A counter-notification that is not valid, for example one that misses an element the law requires, is declined with a reason that you are shown. The content stays restricted.
- Otherwise, the content is restored once the window opens, unless another restriction or another open complaint concerns the same content.

## Repeat infringers
A complaint alone is not an infringement. When Nodaro finds a complaint substantiated, it records an **infringement** against the account. An infringement can be withdrawn, for example when an appeal is upheld or the content is restored after a counter-notification. A withdrawn infringement no longer counts.

The repeat-infringer policy sets a **measure** for each count of an account's standing infringements within a rolling window. The default policy:

| Standing infringements within the last 24 months | Measure |
| --- | --- |
| 1 | A warning |
| 2 | Importing links blocked for 30 days |
| 3 | The account closed |

A deployment can change the window and the steps. A step can also suspend the account for a number of days. A measure is never taken automatically: a person decides each one.

- **A warning** is a record, not a restriction. It is shown to you while it is within the policy's window.
- **Importing links blocked** refuses every import of a link by the account with `403 imports_blocked`, for the step's number of days or until it is lifted. A file uploaded from your own device is not an import and is not refused.
- **The account suspended** refuses sign-in and every request until its end, with `403 access_blocked`. Scheduled runs and published apps stop meanwhile. A suspension always has an end.
- **The account closed** blocks sign-in and every request, imports included.

The account is checked again right before a download starts. A block placed while a download waited for its turn refuses it too, and a lifted block takes effect at once.

**What you see.** **Settings › Content restrictions** shows the measures in force on your account, what you were told and until when. It also shows the warnings and infringements recorded within the policy's window, each by its case's reference, and the window's length. While an account is suspended, its owner cannot sign in, so the app answers as it does for a blocked account.

## Frequently asked questions

### What is the content rights declaration?

A short text that the person importing a link confirms. It says that they hold the rights to the content, or the rights holder's permission, and that they are responsible for how they use it. GET /v1/content-rights/declaration returns the current text and its version.

### How do I import a video link with a confirmation?

Read the declaration, show its text with a checkbox that starts unticked, and call POST /v1/content-rights/confirm with the link once the box is ticked. Send the answered id as confirmationId on POST /v1/download-video.

### How long is a confirmation valid?

One confirmation opens one download of that link, by the same account, under the current declaration, within one hour. Confirm again for every new import.

### How do API keys and AI agents import links?

The person authorizes those imports once, in their own signed-in session, under Settings or with POST /v1/content-rights/authorization. Every import through their API keys, connected apps and AI agents is then recorded as a confirmation derived from that authorization.

### Is a confirmation required on every import today?

A deployment decides. While its setting is off, an import without a confirmation is still accepted. Once it is on, the import is refused with 403 rights_confirmation_required. Send a confirmation every time, so your client keeps working when the setting changes.

### What can I do if my content was restricted after a copyright notice?

Open Settings › Content restrictions in the app and send a counter-notification for that restriction. If you send a valid one and the person who complained reports no court action, the content can be restored 10 to 14 business days after Nodaro receives it.
